Skip to content

Commit ed5c1e2

Browse files
authored
Creating github webhook secrets per pipeline (#283)
* Secrets are now prefixed for pipelines * Creating github webhook secrets per pipeline * Setting header before parsing * Added missing value * Added migrating from the old format to the new * Changed the return value * Closing the request body * Fixed * Saving the pipeline * Saving the new pipeline * Fixed the damn thing. * fix to reset only on unstaged * Fixed nodejs build * Fixed ruby and the build order * Fixed the test * Fixed the ruby test
1 parent 107cfb1 commit ed5c1e2

File tree

9 files changed

+126
-64
lines changed

9 files changed

+126
-64
lines changed

gaia.go

+7
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,13 @@ const (
155155

156156
// StartReasonScheduled label for pipelines which were triggered automated process, i.e. cron job.
157157
StartReasonScheduled = "scheduled"
158+
159+
// SecretNamePrefix defines the prefix for github secrets for pipelines.
160+
SecretNamePrefix = "GITHUB_WEBHOOK_SECRET_"
161+
162+
// LegacySecretName is the old name for a secret that has been created by previous versions.
163+
// Deprecated
164+
LegacySecretName = "GITHUB_WEBHOOK_SECRET"
158165
)
159166

160167
// JwtExpiry is the default JWT expiry.

providers/pipelines/hook.go

+38-22
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"io/ioutil"
1111
"net/http"
12+
"strconv"
1213
"strings"
1314

1415
"github.com/labstack/echo"
@@ -61,7 +62,7 @@ func verifySignature(secret []byte, signature string, body []byte) bool {
6162
return hmac.Equal(expected, actual)
6263
}
6364

64-
func parse(secret []byte, req *http.Request) (Hook, error) {
65+
func checkHeaders(req *http.Request) (Hook, error) {
6566
h := Hook{}
6667

6768
if h.Signature = req.Header.Get("x-hub-signature"); len(h.Signature) == 0 {
@@ -82,19 +83,7 @@ func parse(secret []byte, req *http.Request) (Hook, error) {
8283
if h.ID = req.Header.Get("x-github-delivery"); len(h.ID) == 0 {
8384
return Hook{}, errors.New("no event id")
8485
}
85-
86-
body, err := ioutil.ReadAll(req.Body)
87-
88-
if err != nil {
89-
return Hook{}, err
90-
}
91-
92-
if !verifySignature(secret, h.Signature, body) {
93-
return Hook{}, errors.New("Invalid signature")
94-
}
95-
96-
h.Payload = body
97-
return h, err
86+
return h, nil
9887
}
9988

10089
// GitWebHook handles callbacks from GitHub's webhook system.
@@ -109,13 +98,11 @@ func (pp *PipelineProvider) GitWebHook(c echo.Context) error {
10998
return c.String(http.StatusInternalServerError, "unable to open vault: "+err.Error())
11099
}
111100

112-
secret, err := vault.Get("GITHUB_WEBHOOK_SECRET")
113-
if err != nil {
114-
return c.String(http.StatusBadRequest, err.Error())
115-
}
101+
req := c.Request()
102+
req.Header.Set("Content-type", "application/json")
103+
defer req.Body.Close()
116104

117-
h, err := parse(secret, c.Request())
118-
c.Request().Header.Set("Content-type", "application/json")
105+
h, err := checkHeaders(req)
119106
if err != nil {
120107
return c.String(http.StatusBadRequest, err.Error())
121108
}
@@ -124,10 +111,14 @@ func (pp *PipelineProvider) GitWebHook(c echo.Context) error {
124111
}
125112

126113
p := Payload{}
114+
body, err := ioutil.ReadAll(req.Body)
115+
if err != nil {
116+
return c.String(http.StatusBadRequest, err.Error())
117+
}
118+
h.Payload = body
127119
if err := json.Unmarshal(h.Payload, &p); err != nil {
128120
return c.String(http.StatusBadRequest, "error in unmarshalling json payload")
129121
}
130-
131122
var foundPipeline *gaia.Pipeline
132123
for _, pipe := range pipeline.GlobalActivePipelines.GetAll() {
133124
if pipe.Repo.URL == p.Repo.GitURL || pipe.Repo.URL == p.Repo.HTMLURL || pipe.Repo.URL == p.Repo.SSHURL {
@@ -138,10 +129,35 @@ func (pp *PipelineProvider) GitWebHook(c echo.Context) error {
138129
if foundPipeline == nil {
139130
return c.String(http.StatusInternalServerError, "pipeline not found")
140131
}
132+
id := strconv.Itoa(foundPipeline.ID)
133+
secret, err := vault.Get(gaia.SecretNamePrefix + id)
134+
migrate := false
135+
if err != nil {
136+
// Backwards compatibility, check if there is a secret using the old name.
137+
secret, err = vault.Get(gaia.LegacySecretName)
138+
if err != nil {
139+
return c.String(http.StatusBadRequest, err.Error())
140+
}
141+
migrate = true
142+
err = nil
143+
}
144+
if !verifySignature(secret, h.Signature, h.Payload) {
145+
return c.String(http.StatusBadRequest, "invalid signature")
146+
}
147+
148+
if migrate {
149+
// Migrate the secret to a new value using the new format after verification of the signature succeeded.
150+
// We don't want to migrate an incorrect secret.
151+
vault.Add(gaia.SecretNamePrefix+id, secret)
152+
if err := vault.SaveSecrets(); err != nil {
153+
return c.String(http.StatusBadRequest, err.Error())
154+
}
155+
}
156+
141157
uniqueFolder, err := pipelinehelper.GetLocalDestinationForPipeline(*foundPipeline)
142158
if err != nil {
143159
gaia.Cfg.Logger.Error("Pipeline type invalid", "type", foundPipeline.Type)
144-
return err
160+
return c.String(http.StatusInternalServerError, "pipeline type invalid")
145161
}
146162
foundPipeline.Repo.LocalDest = uniqueFolder
147163
err = pp.deps.PipelineService.UpdateRepository(foundPipeline)

providers/pipelines/pipeline_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,7 @@ func TestHookReceive(t *testing.T) {
641641
})
642642
m := new(MockVaultStorer)
643643
v, _ := services.VaultService(m)
644-
v.Add("GITHUB_WEBHOOK_SECRET", []byte("superawesomesecretgithubpassword"))
644+
v.Add("GITHUB_WEBHOOK_SECRET_1", []byte("superawesomesecretgithubpassword"))
645645
defer func() {
646646
services.MockVaultService(nil)
647647
}()

workers/pipeline/build_ruby.go

+24-13
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ const gemInitFile = "gaia.rb"
2929

3030
// BuildPipelineRuby is the real implementation of BuildPipeline for Ruby
3131
type BuildPipelineRuby struct {
32-
Type gaia.PipelineType
32+
Type gaia.PipelineType
33+
GemfileName string
3334
}
3435

3536
// PrepareEnvironment prepares the environment before we start the build process.
@@ -131,22 +132,31 @@ func (b *BuildPipelineRuby) ExecuteBuild(p *gaia.CreatePipeline) error {
131132
}
132133

133134
// Search for resulting gem file.
134-
gemfile, err := filterPathContentBySuffix(localDest, ".gem")
135+
gemfile, err := findGemFileByGlob(localDest, uuid+"*.gem")
135136
if err != nil {
136137
gaia.Cfg.Logger.Error("cannot find final gem file after build", "path", p.Pipeline.Repo.LocalDest)
137138
return err
138139
}
139140

140-
// if we found more or less than one gem file then we have a problem.
141+
// fallback to the old method because we don't have a uuid based gem file.
142+
if gemfile == nil {
143+
gemfile, err = findGemFileByGlob(localDest, "*.gem")
144+
if err != nil {
145+
gaia.Cfg.Logger.Error("cannot find final gem file after build", "path", p.Pipeline.Repo.LocalDest)
146+
return err
147+
}
148+
}
149+
150+
// if we found more or less than one gem file for the given uuid then we have a problem.
141151
if len(gemfile) != 1 {
142-
gaia.Cfg.Logger.Debug("cannot find gem file in cloned repo", "foundGemFiles", len(gemfile), "gems", gemfile)
152+
gaia.Cfg.Logger.Debug("cannot find gem file in cloned repo", "gems", gemfile)
143153
return errors.New("cannot find gem file in cloned repo")
144154
}
145155

146156
// Build has been finished. Set execution path to the build result archive.
147157
// This will be used during pipeline verification phase which will happen after this step.
148158
p.Pipeline.ExecPath = gemfile[0]
149-
159+
b.GemfileName = gemfile[0]
150160
return nil
151161
}
152162

@@ -170,18 +180,19 @@ func filterPathContentBySuffix(path, suffix string) ([]string, error) {
170180
return filteredFiles, nil
171181
}
172182

183+
// findGemFileByGlob reads the whole directory given by path and
184+
// returns all files with full path which have the same glob like provided.
185+
func findGemFileByGlob(path, glob string) ([]string, error) {
186+
fullPath := filepath.Join(path, glob)
187+
return filepath.Glob(fullPath)
188+
}
189+
173190
// CopyBinary copies the final compiled binary to the
174191
// destination folder.
175192
func (b *BuildPipelineRuby) CopyBinary(p *gaia.CreatePipeline) error {
176-
// Search for resulting gem file.
177-
gemfile, err := filterPathContentBySuffix(p.Pipeline.Repo.LocalDest, ".gem")
178-
if err != nil {
179-
gaia.Cfg.Logger.Error("cannot find final gem file during copy", "path", p.Pipeline.Repo.LocalDest)
180-
return err
181-
}
182-
183193
// Define src and destination
184-
src := gemfile[0]
194+
src := b.GemfileName
195+
gaia.Cfg.Logger.Debug("Copying over ruby gem file", "gem", src)
185196
dest := filepath.Join(gaia.Cfg.PipelinePath, pipelinehelper.AppendTypeToName(p.Pipeline.Name, p.Pipeline.Type))
186197

187198
// Copy binary

workers/pipeline/build_ruby_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ func TestCopyBinaryRuby(t *testing.T) {
150150
p.Pipeline.Type = gaia.PTypeRuby
151151
p.Pipeline.Repo = &gaia.GitRepo{LocalDest: tmp}
152152
src := filepath.Join(tmp, "test.gem")
153+
b.GemfileName = src
153154
dst := pipelinehelper.AppendTypeToName(p.Pipeline.Name, p.Pipeline.Type)
154155
f, _ := os.Create(src)
155156
defer f.Close()
@@ -184,6 +185,7 @@ func TestCopyBinarySrcDoesNotExistRuby(t *testing.T) {
184185
p.Pipeline.Name = "main"
185186
p.Pipeline.Type = gaia.PTypeRuby
186187
p.Pipeline.Repo = &gaia.GitRepo{LocalDest: "/noneexistent"}
188+
b.GemfileName = "/noneexistent"
187189
err := b.CopyBinary(p)
188190
if err == nil {
189191
t.Fatal("error was expected when copying binary but none occurred ")

workers/pipeline/create_pipeline.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package pipeline
33
import (
44
"errors"
55
"fmt"
6+
"strconv"
67
"strings"
78
"unicode"
89

@@ -158,7 +159,8 @@ func (s *GaiaPipelineService) CreatePipeline(p *gaia.CreatePipeline) {
158159

159160
if !gaia.Cfg.Poll && len(gitToken) > 0 {
160161
// if there is a githubtoken provided, that means that a webhook was requested to be added.
161-
err = createGithubWebhook(gitToken, p.Pipeline.Repo, nil)
162+
id := strconv.Itoa(p.Pipeline.ID)
163+
err = createGithubWebhook(gitToken, p.Pipeline.Repo, id, nil)
162164
if err != nil {
163165
gaia.Cfg.Logger.Error("error while creating webhook for repository", "error", err.Error())
164166
return
@@ -192,7 +194,7 @@ func ValidatePipelineName(pName string) error {
192194

193195
// Check if pipeline name is already in use.
194196
for _, activePipeline := range GlobalActivePipelines.GetAll() {
195-
if strings.ToLower(s) == strings.ToLower(activePipeline.Name) {
197+
if strings.EqualFold(s, activePipeline.Name) {
196198
return errPipelineNameInUse
197199
}
198200
}

workers/pipeline/create_pipeline_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"errors"
66
"io/ioutil"
7+
"log"
78
"strings"
89
"testing"
910

@@ -134,13 +135,15 @@ func TestCreatePipeline(t *testing.T) {
134135
defer func() { services.MockStorageService(nil) }()
135136
cp := new(gaia.CreatePipeline)
136137
cp.Pipeline.Name = "test"
138+
cp.Pipeline.ID = 1
137139
cp.Pipeline.Type = gaia.PTypeGolang
138140
cp.Pipeline.Repo = &gaia.GitRepo{URL: "https://github.com/gaia-pipeline/pipeline-test"}
139141
pipelineService := NewGaiaPipelineService(Dependencies{
140142
Scheduler: &mockScheduleService{},
141143
})
142144
pipelineService.CreatePipeline(cp)
143145
if cp.StatusType != gaia.CreatePipelineSuccess {
146+
log.Println("Output: ", cp.Output)
144147
t.Fatal("pipeline status was not success. was: ", cp.StatusType)
145148
}
146149
}

workers/pipeline/git.go

+34-13
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"errors"
88
gohttp "net/http"
99
"path"
10+
"path/filepath"
1011
"regexp"
1112
"strings"
1213
"sync"
@@ -95,6 +96,10 @@ func GitLSRemote(repo *gaia.GitRepo) error {
9596
// UpdateRepository takes a git type repository and updates
9697
// it by pulling in new code if it's available.
9798
func (s *GaiaPipelineService) UpdateRepository(pipe *gaia.Pipeline) error {
99+
gaia.Cfg.Logger.Debug("updating repository for pipeline type", "type", pipe.Type)
100+
if pipe.Type == gaia.PTypeNodeJS {
101+
pipe.Repo.LocalDest = filepath.Join(pipe.Repo.LocalDest, nodeJSInternalCloneFolder)
102+
}
98103
r, err := git.PlainOpen(pipe.Repo.LocalDest)
99104
if err != nil {
100105
// We don't stop gaia working because of an automated update failed.
@@ -110,7 +115,9 @@ func (s *GaiaPipelineService) UpdateRepository(pipe *gaia.Pipeline) error {
110115
gaia.Cfg.Logger.Error("error getting auth info while doing a pull request: ", "error", err.Error())
111116
return err
112117
}
118+
113119
tree, _ := r.Worktree()
120+
114121
o := &git.PullOptions{
115122
ReferenceName: plumbing.ReferenceName(pipe.Repo.SelectedBranch),
116123
SingleBranch: true,
@@ -126,12 +133,20 @@ func (s *GaiaPipelineService) UpdateRepository(pipe *gaia.Pipeline) error {
126133
return err
127134
}
128135
o.Auth = auth
129-
err = tree.Pull(o)
130-
if err != nil {
136+
if err := tree.Pull(o); err != nil {
131137
return err
132138
}
133139
} else if strings.Contains(err.Error(), "worktree contains unstaged changes") {
134-
// ignore this error, the pull overwrote everything anyways.
140+
gaia.Cfg.Logger.Error("worktree contains unstaged changes, resetting", "error", err.Error())
141+
// Clean the worktree. Because of various builds, it can happen that the local folder if polluted.
142+
// For example go build tends to edit the go.mod file.
143+
if err := tree.Reset(&git.ResetOptions{
144+
Mode: git.HardReset,
145+
}); err != nil {
146+
gaia.Cfg.Logger.Error("failed to reset worktree", "error", err.Error())
147+
return err
148+
}
149+
// Success, move on.
135150
err = nil
136151
} else {
137152
// It's also an error if the repo is already up to date so we just move on.
@@ -142,14 +157,19 @@ func (s *GaiaPipelineService) UpdateRepository(pipe *gaia.Pipeline) error {
142157

143158
gaia.Cfg.Logger.Debug("updating pipeline: ", "message", pipe.Name)
144159
b := newBuildPipeline(pipe.Type)
145-
createPipeline := &gaia.CreatePipeline{
146-
Pipeline: gaia.Pipeline{
147-
Repo: &gaia.GitRepo{},
148-
},
160+
createPipeline := &gaia.CreatePipeline{Pipeline: *pipe}
161+
if err := b.ExecuteBuild(createPipeline); err != nil {
162+
gaia.Cfg.Logger.Error("error while executing the build", "error", err.Error())
163+
return err
164+
}
165+
if err := b.SavePipeline(&createPipeline.Pipeline); err != nil {
166+
gaia.Cfg.Logger.Error("failed to save pipeline", "error", err.Error())
167+
return err
168+
}
169+
if err := b.CopyBinary(createPipeline); err != nil {
170+
gaia.Cfg.Logger.Error("error while copying binary to plugin folder", "error", err.Error())
171+
return err
149172
}
150-
createPipeline.Pipeline = *pipe
151-
_ = b.ExecuteBuild(createPipeline)
152-
_ = b.CopyBinary(createPipeline)
153173
gaia.Cfg.Logger.Debug("successfully updated: ", "message", pipe.Name)
154174
return nil
155175
}
@@ -240,7 +260,8 @@ func NewGithubClient(httpClient *gohttp.Client, repoMock GithubRepoService) Gith
240260
}
241261
}
242262

243-
func createGithubWebhook(token string, repo *gaia.GitRepo, gitRepo GithubRepoService) error {
263+
func createGithubWebhook(token string, repo *gaia.GitRepo, id string, gitRepo GithubRepoService) error {
264+
name := gaia.SecretNamePrefix + id
244265
vault, err := services.DefaultVaultService()
245266
if err != nil {
246267
gaia.Cfg.Logger.Error("unable to initialize vault: ", "error", err.Error())
@@ -260,10 +281,10 @@ func createGithubWebhook(token string, repo *gaia.GitRepo, gitRepo GithubRepoSer
260281
tc := oauth2.NewClient(ctx, ts)
261282
config := make(map[string]interface{})
262283
config["url"] = gaia.Cfg.Hostname + "/api/" + gaia.APIVersion + "/pipeline/githook"
263-
secret, err := vault.Get("GITHUB_WEBHOOK_SECRET")
284+
secret, err := vault.Get(name)
264285
if err != nil {
265286
secret = []byte(generateWebhookSecret())
266-
vault.Add("GITHUB_WEBHOOK_SECRET", secret)
287+
vault.Add(name, secret)
267288
err = vault.SaveSecrets()
268289
if err != nil {
269290
return err

0 commit comments

Comments
 (0)