Skip to content

Commit a4da6ab

Browse files
authored
Handle "environments" folders for new GitOps directory structures (#73)
Adjust for /environments/env-name/ at top of GitOps repository structure.
1 parent fec3986 commit a4da6ab

File tree

13 files changed

+517
-76
lines changed

13 files changed

+517
-76
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,35 @@ $ ./services promote --from https://github.com/organisation/first-environment.gi
2323

2424
If the `commit-name` and `commit-email` are not provided, it will attempt to find them in `~/.gitconfig`, otherwise it will fail.
2525

26+
2627
This will _copy_ all files under `/services/service-a/base/config/*` in `first-environment` to `second-environment`, commit and push, and open a PR for the change.
2728

29+
30+
## Using environments
31+
32+
33+
If an `environments` folder exists in the GitOps repository you are promoting into, and that only has one folder, the files will be copied into the destination repository's `/environments/<the only folder>` directory.
34+
35+
Future support is planned for an `--env` like flag which will allow us to promote from/to different repositories with multiple environments.
36+
2837
## Testing
2938

39+
Linting should be done first (this is done on Travis, and what's good locally should be good there too)
40+
41+
Grab the linter if you haven't already:
42+
43+
```shell
44+
GO111MODULE=on go get github.com/golangci/golangci-lint/cmd/[email protected]
45+
```
46+
47+
Then you can do:
48+
49+
```shell
50+
golangci-lint run
51+
```
52+
53+
Run the unit tests:
54+
3055
```shell
3156
$ go test ./...
3257
```

go.mod

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,15 @@ go 1.13
44

55
require (
66
github.com/golang/protobuf v1.3.2 // indirect
7+
github.com/golangci/golangci-lint v1.26.0 // indirect
78
github.com/google/go-cmp v0.3.0
89
github.com/google/uuid v1.1.1
910
github.com/jenkins-x/go-scm v1.5.77
1011
github.com/mitchellh/go-homedir v1.1.0
1112
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 // indirect
1213
github.com/spf13/cobra v1.0.0
13-
github.com/spf13/pflag v1.0.3
14+
github.com/spf13/pflag v1.0.5
1415
github.com/spf13/viper v1.6.3
1516
github.com/tcnksm/go-gitconfig v0.1.2
16-
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 // indirect
1717
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
18-
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e // indirect
1918
)

go.sum

Lines changed: 219 additions & 0 deletions
Large diffs are not rendered by default.

pkg/avancement/service_manager.go

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ func (s *ServiceManager) Promote(serviceName, fromURL, toURL, newBranchName, mes
107107
var localSource git.Source
108108
var errorSource error
109109
isLocal := fromLocalRepo(fromURL)
110+
110111
if isLocal {
111112
localSource = s.localFactory(fromURL, s.debug)
112113
if newBranchName == "" {
@@ -131,16 +132,38 @@ func (s *ServiceManager) Promote(serviceName, fromURL, toURL, newBranchName, mes
131132
// This would be a checkout error as the clone error gives us the above gitError instead
132133
return fmt.Errorf("failed to checkout destination repository, error: %w", err)
133134
}
135+
136+
if destination == nil {
137+
// Should never happen, but if it does...
138+
return fmt.Errorf("destination repository was not initialised despite being no errors")
139+
}
140+
134141
reposToDelete = append(reposToDelete, destination)
135142

136143
var copied []string
144+
137145
if isLocal {
138-
copied, err = local.CopyConfig(serviceName, localSource, destination)
146+
overrideTargetFolder, err := destination.GetUniqueEnvironmentFolder()
147+
if err != nil {
148+
return fmt.Errorf("could not determine unique environment name for destination repository - check that only one directory exists under it and you can write to your cache folder")
149+
}
150+
copied, err = local.CopyConfig(serviceName, localSource, destination, overrideTargetFolder)
139151
if err != nil {
140152
return fmt.Errorf("failed to set up local repository: %w", err)
141153
}
142154
} else {
143-
copied, err = git.CopyService(serviceName, source, destination)
155+
sourceEnvironment, err := source.GetUniqueEnvironmentFolder()
156+
if err != nil {
157+
return fmt.Errorf("could not determine unique environment name for source repository - check that only one directory exists under it and you can write to your cache folder")
158+
}
159+
160+
destinationEnvironment, err := destination.GetUniqueEnvironmentFolder()
161+
162+
if err != nil {
163+
return fmt.Errorf("could not determine unique environment name for destination repository - check that only one directory exists under it and you can write to your cache folder")
164+
}
165+
166+
copied, err = git.CopyService(serviceName, source, destination, sourceEnvironment, destinationEnvironment)
144167
if err != nil {
145168
return fmt.Errorf("failed to copy service: %w", err)
146169
}
@@ -149,14 +172,13 @@ func (s *ServiceManager) Promote(serviceName, fromURL, toURL, newBranchName, mes
149172
commitMsg := message
150173
if commitMsg == "" {
151174
if isLocal {
152-
commitMsg = fmt.Sprintf("Promotion of service `%s` from local filesystem directory `%s`.", serviceName, fromURL)
175+
commitMsg = fmt.Sprintf("Promotion of service %s from local filesystem directory %s.", serviceName, fromURL)
153176
} else {
154177
commitMsg = generateDefaultCommitMsg(source, serviceName, fromURL, fromBranch)
155178
}
156179
}
157-
158180
if err := destination.StageFiles(copied...); err != nil {
159-
return fmt.Errorf("failed to stage files: %w", err)
181+
return fmt.Errorf("failed to stage files %s: %w", copied, err)
160182
}
161183
if err := destination.Commit(commitMsg, s.author); err != nil {
162184
return fmt.Errorf("failed to commit: %w", err)
@@ -198,7 +220,7 @@ func (s *ServiceManager) checkoutDestinationRepo(repoURL, branch string) (git.Re
198220
}
199221
err = repo.CheckoutAndCreate(branch)
200222
if err != nil {
201-
return nil, fmt.Errorf("failed to checkout branch %s: %w", branch, err)
223+
return nil, fmt.Errorf("failed to checkout branch %s, error: %w", branch, err)
202224
}
203225
return repo, nil
204226
}
@@ -280,6 +302,6 @@ func generateBranchForLocalSource(source git.Source) string {
280302
// generateDefaultCommitMsg constructs a default commit message based on the source information.
281303
func generateDefaultCommitMsg(sourceRepo git.Repo, serviceName, from, fromBranch string) string {
282304
commit := sourceRepo.GetCommitID()
283-
msg := fmt.Sprintf("Promoting service `%s` at commit `%s` from branch `%s` in `%s`.", serviceName, commit, fromBranch, from)
305+
msg := fmt.Sprintf("Promoting service %s at commit %s from branch %s in %s.", serviceName, commit, fromBranch, from)
284306
return msg
285307
}

pkg/avancement/service_manager_test.go

Lines changed: 83 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ func TestPromoteWithSuccessCustomMsg(t *testing.T) {
3737
func promoteWithSuccess(t *testing.T, keepCache bool, repoType string, tlsVerify bool, msg string) {
3838
dstBranch := "test-branch"
3939
author := &git.Author{Name: "Testing User", Email: "[email protected]", Token: "test-token"}
40-
devRepo, stagingRepo := mock.New("/dev", "master"), mock.New("/staging", "master")
40+
devRepo, stagingRepo := mock.New("environments/dev", "master"), mock.New("environments/staging", "master")
4141
repos := map[string]*mock.Repository{
4242
mustAddCredentials(t, dev, author): devRepo,
4343
mustAddCredentials(t, staging, author): stagingRepo,
4444
}
4545
sm := New("tmp", author)
4646
sm.repoType = repoType
4747
sm.tlsVerify = tlsVerify
48-
sm.clientFactory = func(s, ty, r string, v bool) *scm.Client {
48+
sm.clientFactory = func(s, ty, r string, v bool) *scm.Client {
4949
client, _ := fakescm.NewDefault()
5050
if r != repoType {
5151
t.Fatalf("repoType doesn't match %s != %s\n", r, repoType)
@@ -61,8 +61,8 @@ func promoteWithSuccess(t *testing.T, keepCache bool, repoType string, tlsVerify
6161
}
6262
return git.Repo(repos[url]), nil
6363
}
64-
devRepo.AddFiles("/services/my-service/base/config/myfile.yaml")
65-
64+
devRepo.AddFiles("services/my-service/base/config/myfile.yaml")
65+
stagingRepo.AddFiles("")
6666
err := sm.Promote("my-service", dev, staging, dstBranch, msg, keepCache)
6767
if err != nil {
6868
t.Fatal(err)
@@ -71,11 +71,11 @@ func promoteWithSuccess(t *testing.T, keepCache bool, repoType string, tlsVerify
7171
expectedCommitMsg := msg
7272
if msg == "" {
7373
commit := devRepo.GetCommitID()
74-
expectedCommitMsg = fmt.Sprintf("Promoting service `my-service` at commit `%s` from branch `master` in `%s`.", commit, dev)
74+
expectedCommitMsg = fmt.Sprintf("Promoting service my-service at commit %s from branch master in %s.", commit, dev)
7575
}
7676

7777
stagingRepo.AssertBranchCreated(t, "master", dstBranch)
78-
stagingRepo.AssertFileCopiedInBranch(t, dstBranch, "/dev/services/my-service/base/config/myfile.yaml", "/staging/services/my-service/base/config/myfile.yaml")
78+
stagingRepo.AssertFileCopiedInBranch(t, dstBranch, "environments/dev/services/my-service/base/config/myfile.yaml", "environments/staging/services/my-service/base/config/myfile.yaml")
7979
stagingRepo.AssertCommit(t, dstBranch, expectedCommitMsg, author)
8080
stagingRepo.AssertPush(t, dstBranch)
8181

@@ -103,11 +103,11 @@ func TestPromoteLocalWithSuccessCustomMsg(t *testing.T) {
103103
func promoteLocalWithSuccess(t *testing.T, keepCache bool, msg string) {
104104
dstBranch := "test-branch"
105105
author := &git.Author{Name: "Testing User", Email: "[email protected]", Token: "test-token"}
106-
stagingRepo := mock.New("/staging", "master")
106+
stagingRepo := mock.New("environments", "master")
107107
devRepo := NewLocal("/dev")
108108

109109
sm := New("tmp", author)
110-
sm.clientFactory = func(s, t, r string, v bool) *scm.Client {
110+
sm.clientFactory = func(s, t, r string, v bool) *scm.Client {
111111
client, _ := fakescm.NewDefault()
112112
return client
113113
}
@@ -118,7 +118,8 @@ func promoteLocalWithSuccess(t *testing.T, keepCache bool, msg string) {
118118
return git.Source(devRepo)
119119
}
120120
sm.debug = true
121-
devRepo.AddFiles("/config/myfile.yaml")
121+
devRepo.AddFiles("config/myfile.yaml")
122+
stagingRepo.AddFiles("staging")
122123

123124
err := sm.Promote("my-service", ldev, staging, dstBranch, msg, keepCache)
124125
if err != nil {
@@ -127,11 +128,11 @@ func promoteLocalWithSuccess(t *testing.T, keepCache bool, msg string) {
127128

128129
expectedCommitMsg := msg
129130
if expectedCommitMsg == "" {
130-
expectedCommitMsg = "Promotion of service `my-service` from local filesystem directory `/root/repo`."
131+
expectedCommitMsg = "Promotion of service my-service from local filesystem directory /root/repo."
131132
}
132133

133134
stagingRepo.AssertBranchCreated(t, "master", dstBranch)
134-
stagingRepo.AssertFileCopiedInBranch(t, dstBranch, "/dev/config/myfile.yaml", "/staging/services/my-service/base/config/myfile.yaml")
135+
stagingRepo.AssertFileCopiedInBranch(t, dstBranch, "/dev/config/myfile.yaml", "environments/staging/services/my-service/base/config/myfile.yaml")
135136
stagingRepo.AssertCommit(t, dstBranch, expectedCommitMsg, author)
136137
stagingRepo.AssertPush(t, dstBranch)
137138

@@ -142,6 +143,70 @@ func promoteLocalWithSuccess(t *testing.T, keepCache bool, msg string) {
142143
}
143144
}
144145

146+
func TestPromoteLocalWithSuccessOneEnvAndIsUsed(t *testing.T) {
147+
// Destination repo (GitOps repo) to have /environments/staging
148+
// Promotion should copy files into that staging directory
149+
dstBranch := "test-branch"
150+
author := &git.Author{Name: "Testing User", Email: "[email protected]", Token: "test-token"}
151+
stagingRepo := mock.New("environments", "master")
152+
devRepo := NewLocal("/dev")
153+
154+
sm := New("tmp", author)
155+
sm.clientFactory = func(s, t, r string, v bool) *scm.Client {
156+
client, _ := fakescm.NewDefault()
157+
return client
158+
}
159+
sm.repoFactory = func(url, _ string, _ bool, _ bool) (git.Repo, error) {
160+
return git.Repo(stagingRepo), nil
161+
}
162+
sm.localFactory = func(path string, _ bool) git.Source {
163+
return git.Source(devRepo)
164+
}
165+
sm.debug = true
166+
devRepo.AddFiles("/config/myfile.yaml")
167+
stagingRepo.AddFiles("/staging")
168+
169+
err := sm.Promote("my-service", ldev, staging, dstBranch, "", false)
170+
if err != nil {
171+
t.Fatal(err)
172+
}
173+
expectedCommitMsg := "Promotion of service my-service from local filesystem directory /root/repo."
174+
175+
stagingRepo.AssertBranchCreated(t, "master", dstBranch)
176+
stagingRepo.AssertFileCopiedInBranch(t, dstBranch, "/dev/config/myfile.yaml", "environments/staging/services/my-service/base/config/myfile.yaml")
177+
stagingRepo.AssertCommit(t, dstBranch, expectedCommitMsg, author)
178+
stagingRepo.AssertPush(t, dstBranch)
179+
}
180+
181+
func TestPromoteErrorsIfMultipleEnvironments(t *testing.T) {
182+
dstBranch := "test-branch"
183+
author := &git.Author{Name: "Testing User", Email: "[email protected]", Token: "test-token"}
184+
devRepo, stagingRepo := mock.New("/", "master"), mock.New("/environments", "master")
185+
186+
stagingRepo.AddFiles("/staging")
187+
stagingRepo.AddFiles("/prod")
188+
189+
repos := map[string]*mock.Repository{
190+
mustAddCredentials(t, dev, author): devRepo,
191+
mustAddCredentials(t, staging, author): stagingRepo,
192+
}
193+
sm := New("tmp", author)
194+
sm.clientFactory = func(s, ty, r string, v bool) *scm.Client {
195+
client, _ := fakescm.NewDefault()
196+
return client
197+
}
198+
sm.repoFactory = func(url, _ string, v bool, _ bool) (git.Repo, error) {
199+
return git.Repo(repos[url]), nil
200+
}
201+
devRepo.AddFiles("services/my-service/base/config/myfile.yaml")
202+
203+
msg := "foo message"
204+
err := sm.Promote("my-service", dev, staging, dstBranch, msg, false)
205+
if err == nil {
206+
t.Fail()
207+
}
208+
}
209+
145210
func TestAddCredentials(t *testing.T) {
146211
testUser := &git.Author{Name: "Test User", Email: "[email protected]", Token: "test-token"}
147212
tests := []struct {
@@ -177,32 +242,33 @@ func mustAddCredentials(t *testing.T, repoURL string, a *git.Author) string {
177242
func TestPromoteWithCacheDeletionFailure(t *testing.T) {
178243
dstBranch := "test-branch"
179244
author := &git.Author{Name: "Testing User", Email: "[email protected]", Token: "test-token"}
180-
devRepo, stagingRepo := mock.New("/dev", "master"), mock.New("/staging", "master")
245+
devRepo, stagingRepo := mock.New("environments", "master"), mock.New("environments", "master")
181246
stagingRepo.DeleteErr = errors.New("failed test delete")
182247
repos := map[string]*mock.Repository{
183248
mustAddCredentials(t, dev, author): devRepo,
184249
mustAddCredentials(t, staging, author): stagingRepo,
185250
}
186251
sm := New("tmp", author)
187-
sm.clientFactory = func(s, t, r string, v bool) *scm.Client {
252+
sm.clientFactory = func(s, t, r string, v bool) *scm.Client {
188253
client, _ := fakescm.NewDefault()
189254
return client
190255
}
191256
sm.repoFactory = func(url, _ string, _ bool, _ bool) (git.Repo, error) {
192257
return git.Repo(repos[url]), nil
193258
}
194-
devRepo.AddFiles("/services/my-service/base/config/myfile.yaml")
259+
devRepo.AddFiles("dev/services/my-service/base/config/myfile.yaml")
260+
stagingRepo.AddFiles("staging")
195261

196262
err := sm.Promote("my-service", dev, staging, dstBranch, "", false)
197263
if err != nil {
198264
t.Fatal(err)
199265
}
200266

201267
commit := devRepo.GetCommitID()
202-
expectedCommitMsg := fmt.Sprintf("Promoting service `my-service` at commit `%s` from branch `master` in `%s`.", commit, dev)
268+
expectedCommitMsg := fmt.Sprintf("Promoting service my-service at commit %s from branch master in %s.", commit, dev)
203269

204270
stagingRepo.AssertBranchCreated(t, "master", dstBranch)
205-
stagingRepo.AssertFileCopiedInBranch(t, dstBranch, "/dev/services/my-service/base/config/myfile.yaml", "/staging/services/my-service/base/config/myfile.yaml")
271+
stagingRepo.AssertFileCopiedInBranch(t, dstBranch, "environments/dev/services/my-service/base/config/myfile.yaml", "environments/staging/services/my-service/base/config/myfile.yaml")
206272
stagingRepo.AssertCommit(t, dstBranch, expectedCommitMsg, author)
207273
stagingRepo.AssertPush(t, dstBranch)
208274

@@ -298,7 +364,7 @@ func TestRepositoryCloneErrorOmitsToken(t *testing.T) {
298364
dstBranch := "test-branch"
299365
author := &git.Author{Name: "Testing User", Email: "[email protected]", Token: "test-token"}
300366
client, _ := fakescm.NewDefault()
301-
fakeClientFactory := func(s, t, r string, v bool) *scm.Client {
367+
fakeClientFactory := func(s, t, r string, v bool) *scm.Client {
302368
return client
303369
}
304370
sm := New("tmp", author)

pkg/cmd/root.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ func Execute() {
6666
logIfError(cobra.MarkFlagRequired(rootCmd.PersistentFlags(), githubTokenFlag))
6767
logIfError(viper.BindPFlag(githubTokenFlag, rootCmd.PersistentFlags().Lookup(githubTokenFlag)))
6868
rootCmd.AddCommand(makePromoteCmd())
69-
7069
if err := rootCmd.Execute(); err != nil {
7170
log.Fatal(err)
7271
}

pkg/git/copy.go

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,15 @@ import (
1212
// Only files under /services/[serviceName]/base/config/* are copied to the destination
1313
//
1414
// Returns the list of files that were copied, and possibly an error.
15-
func CopyService(serviceName string, source Source, dest Destination) ([]string, error) {
16-
15+
func CopyService(serviceName string, source Source, dest Destination, sourceEnvironment, destinationEnvironment string) ([]string, error) {
1716
// filePath defines the root folder for serviceName's config in the repository
18-
filePath := pathForServiceConfig(serviceName)
19-
17+
// the lookup is done for the source repository
18+
filePath := pathForServiceConfig(serviceName, sourceEnvironment)
2019
copied := []string{}
2120
err := source.Walk(filePath, func(prefix, name string) error {
2221
sourcePath := path.Join(prefix, name)
23-
destPath := pathForServiceConfig(name)
24-
if pathValidForPromotion(serviceName, destPath) {
22+
destPath := pathForServiceConfig(name, destinationEnvironment)
23+
if pathValidForPromotion(serviceName, destPath, destinationEnvironment) {
2524
err := dest.CopyFile(sourcePath, destPath)
2625
if err == nil {
2726
copied = append(copied, destPath)
@@ -30,19 +29,19 @@ func CopyService(serviceName string, source Source, dest Destination) ([]string,
3029
}
3130
return nil
3231
})
32+
3333
return copied, err
3434
}
3535

36-
// pathValidForPromotion()
37-
// For a given serviceName, only files in services/serviceName/base/config/* are valid for promotion
38-
//
39-
func pathValidForPromotion(serviceName, filePath string) bool {
40-
filterPath := filepath.Join(pathForServiceConfig(serviceName), "base", "config")
36+
// For a given serviceName, only files in environments/envName/services/serviceName/base/config/* are valid for promotion
37+
func pathValidForPromotion(serviceName, filePath, environmentName string) bool {
38+
filterPath := filepath.Join(pathForServiceConfig(serviceName, environmentName), "base", "config")
4139
validPath := strings.HasPrefix(filePath, filterPath)
4240
return validPath
4341
}
4442

4543
// pathForServiceConfig defines where in a 'gitops' repository the config for a given service should live.
46-
func pathForServiceConfig(serviceName string) string {
47-
return filepath.Join("services", serviceName)
44+
func pathForServiceConfig(serviceName, environmentName string) string {
45+
pathForConfig := filepath.Join("environments", environmentName, "services", serviceName)
46+
return pathForConfig
4847
}

0 commit comments

Comments
 (0)