Skip to content

Commit 42061bb

Browse files
authored
Promote services by name. Only promote files in services/serviceName/base/config (#15)
Promote services by name. Also, only promote files in services/service-name/base/config
1 parent 8690a4c commit 42061bb

File tree

6 files changed

+150
-22
lines changed

6 files changed

+150
-22
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ $ ./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-
This will _copy_ a single file `deployment.txt` from `service-a` in `first-environment` to `service-a` in `second-environment`, commit and push, and open a PR for the change.
26+
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.
2727

2828
## Testing
2929

pkg/avancement/service_manager.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const fromBranch = "master"
5151
//
5252
// It uses a Git cache to checkout the code to, and will copy the environment
5353
// configuration for the `fromURL` to the `toURL` in a named branch.
54-
func (s *ServiceManager) Promote(service, fromURL, toURL, newBranchName string) error {
54+
func (s *ServiceManager) Promote(serviceName, fromURL, toURL, newBranchName string) error {
5555
source, err := s.checkoutSourceRepo(fromURL, fromBranch)
5656
if err != nil {
5757
return err
@@ -61,7 +61,8 @@ func (s *ServiceManager) Promote(service, fromURL, toURL, newBranchName string)
6161
return fmt.Errorf("failed to checkout repo: %w", err)
6262
}
6363

64-
copied, err := git.CopyService(service, source, destination)
64+
copied, err := git.CopyService(serviceName, source, destination)
65+
6566
if err != nil {
6667
return fmt.Errorf("failed to copy service: %w", err)
6768
}

pkg/avancement/service_manager_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,15 @@ func TestPromoteWithSuccess(t *testing.T) {
3131
sm.repoFactory = func(url, _ string) (git.Repo, error) {
3232
return git.Repo(repos[url]), nil
3333
}
34-
devRepo.AddFiles("/my-service/deploy/myfile.yaml")
34+
devRepo.AddFiles("/services/my-service/base/config/myfile.yaml")
3535

3636
err := sm.Promote("my-service", dev, staging, dstBranch)
3737
if err != nil {
3838
t.Fatal(err)
3939
}
4040

4141
stagingRepo.AssertBranchCreated(t, "master", dstBranch)
42-
stagingRepo.AssertFileCopiedInBranch(t, dstBranch, "/dev/my-service/deploy/myfile.yaml", "/staging/my-service/deploy/myfile.yaml")
42+
stagingRepo.AssertFileCopiedInBranch(t, dstBranch, "/dev/services/my-service/base/config/myfile.yaml", "/staging/services/my-service/base/config/myfile.yaml")
4343
stagingRepo.AssertCommit(t, dstBranch, defaultCommitMsg, author)
4444
stagingRepo.AssertPush(t, dstBranch)
4545
}

pkg/git/copy.go

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,48 @@
11
package git
22

3-
import "path"
3+
import (
4+
"path"
5+
"path/filepath"
6+
"strings"
7+
)
48

5-
// CopyService takes a path within a repository, and a source and destination,
6-
// and copies the files from within the source, to the destination.
9+
// CopyService takes the name of a service to copy from a Source to a Destination.
10+
// Source, Destination implement Walk() and are typically Repository objects.
11+
//
12+
// Only files under /services/[serviceName]/base/config/* are copied to the destination
713
//
814
// Returns the list of files that were copied, and possibly an error.
9-
func CopyService(filePath string, source Source, dest Destination) ([]string, error) {
15+
func CopyService(serviceName string, source Source, dest Destination) ([]string, error) {
16+
17+
// filePath defines the root folder for serviceName's config in the repository
18+
filePath := pathForServiceConfig(serviceName)
19+
1020
copied := []string{}
1121
err := source.Walk(filePath, func(prefix, name string) error {
12-
err := dest.CopyFile(path.Join(prefix, name), name)
13-
copied = append(copied, name)
14-
return err
22+
sourcePath := path.Join(prefix, name)
23+
destPath := pathForServiceConfig(name)
24+
if pathValidForPromotion(serviceName, destPath) {
25+
err := dest.CopyFile(sourcePath, destPath)
26+
if err == nil {
27+
copied = append(copied, destPath)
28+
}
29+
return err
30+
}
31+
return nil
1532
})
1633
return copied, err
1734
}
35+
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")
41+
validPath := strings.HasPrefix(filePath, filterPath)
42+
return validPath
43+
}
44+
45+
// 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)
48+
}

pkg/git/copy_test.go

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"io"
66
"path"
7+
"path/filepath"
78
"reflect"
89
"strings"
910
"testing"
@@ -13,7 +14,7 @@ import (
1314

1415
func TestCopyService(t *testing.T) {
1516
s := &mockSource{localPath: "/tmp/testing"}
16-
files := []string{"service-a/my-system/my-file.yaml", "service-a/my-system/this-file.yaml"}
17+
files := []string{"services/service-a/base/config/my-file.yaml", "services/service-a/base/config/this-file.yaml"}
1718
for _, f := range files {
1819
s.addFile(f)
1920
}
@@ -29,18 +30,97 @@ func TestCopyService(t *testing.T) {
2930
}
3031
}
3132

33+
func TestPathValidForPromotion(t *testing.T) {
34+
35+
serviceBeingPromoted := "service-name"
36+
servicesNotBeingPromoted := []string{"svc", "base", "serviceName", "services"}
37+
promoteTheseWhenServiceNameIsRight := []string{
38+
"services/service-name/base/config/kustomization.yaml",
39+
"services/service-name/base/config/deployment.yaml",
40+
"services/service-name/base/config/dir/below/it/may/contain/important.yaml",
41+
}
42+
for _, filePath := range promoteTheseWhenServiceNameIsRight {
43+
if !pathValidForPromotion(serviceBeingPromoted, filePath) {
44+
t.Fatalf("Valid path for promotion for %s incorrectly rejected: %s", serviceBeingPromoted, filePath)
45+
}
46+
for _, wrongService := range servicesNotBeingPromoted {
47+
if pathValidForPromotion(wrongService, filePath) {
48+
t.Fatalf("Path for service %s incorrectly accepted for promotion: %s", wrongService, filePath)
49+
}
50+
}
51+
}
52+
53+
// These paths should never be promoted
54+
badServiceNames := []string{"svc", "badService"}
55+
neverPromoteThese := []string{
56+
"services/svc/overlays/kustomization.yaml",
57+
"services/svc/kustomization.yaml",
58+
"svc/base/config/deployment.yaml",
59+
"services/badService/any/other/path/resource.yaml",
60+
}
61+
for _, badPath := range neverPromoteThese {
62+
for _, badServiceName := range badServiceNames {
63+
if pathValidForPromotion(badServiceName, badPath) {
64+
t.Fatalf("Invalid path %s for promotion of service %s incorrectly accepted", badPath, badServiceName)
65+
}
66+
}
67+
}
68+
}
69+
70+
func TestPathForServiceConfig(t *testing.T) {
71+
serviceName := "usefulService"
72+
correctPath := "services/usefulService"
73+
serviceConfigPath := pathForServiceConfig(serviceName)
74+
if serviceConfigPath != correctPath {
75+
t.Fatalf("Invalid result for pathForServiceConfig(%s): wanted %s got %s", serviceName, correctPath, serviceConfigPath)
76+
}
77+
}
78+
79+
func TestCopyServiceWithFailureCopying(t *testing.T) {
80+
testError := errors.New("this is a test error")
81+
s := &mockSource{localPath: "/tmp/testing"}
82+
files := []string{"services/service-a/base/config/my-file.yaml", "services/service-a/base/config/this-file.yaml"}
83+
for _, f := range files {
84+
s.addFile(f)
85+
}
86+
d := &mockDestination{}
87+
d.copyError = testError
88+
89+
copied, err := CopyService("service-a", s, d)
90+
if err != testError {
91+
t.Fatalf("unexpected error: got %v, wanted %v", err, testError)
92+
}
93+
d.assertFilesWritten(t, []string{})
94+
if !reflect.DeepEqual(copied, []string{}) {
95+
t.Fatalf("failed to copy the files, got %#v, want %#v", copied, []string{})
96+
}
97+
}
98+
3299
type mockSource struct {
33100
files []string
34101
localPath string
35102
}
36103

37-
func (s *mockSource) Walk(filePath string, cb func(string, string) error) error {
104+
// Walk: a mock function to emulate what happens in Repository.Walk()
105+
// The Mock version is different: it iterates over mockSource.files[] and then drives
106+
// the visitor callback in CopyService() as usual.
107+
//
108+
// To preserve the same behaviour, we see that Repository Walk receives /full/path/to/repo/services/service-name
109+
// and then calls filePath.Walk() on /full/path/to/repo/services/ .
110+
// When CopyService() drives Walk(), 'base' is typically services/service-name
111+
// Thus we take each /full/path/to/file/in/mockSource.files[] and split it at 'services/' as happens in the Walk() method we're mocking.
112+
func (s *mockSource) Walk(base string, cb func(string, string) error) error {
38113
if s.files == nil {
39114
return nil
40115
}
116+
41117
for _, f := range s.files {
42-
if strings.HasPrefix(f, path.Join(s.localPath, filePath)) {
43-
err := cb(f, strings.TrimPrefix(f, s.localPath+"/"))
118+
if strings.HasPrefix(f, path.Join(s.localPath, base)) {
119+
splitString := filepath.Dir(base) + "/"
120+
splitPoint := strings.Index(f, splitString) + len(splitString)
121+
prefix := f[:splitPoint]
122+
name := f[splitPoint:]
123+
err := cb(prefix, name)
44124
if err != nil {
45125
return err
46126
}
@@ -57,13 +137,17 @@ func (s *mockSource) addFile(name string) {
57137
}
58138

59139
type mockDestination struct {
60-
written []string
140+
written []string
141+
copyError error
61142
}
62143

63144
func (d *mockDestination) CopyFile(src, dst string) error {
64145
if d.written == nil {
65146
d.written = []string{}
66147
}
148+
if d.copyError != nil {
149+
return d.copyError
150+
}
67151
d.written = append(d.written, dst)
68152
return nil
69153
}

pkg/git/mock/mock.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package mock
33
import (
44
"io"
55
"path"
6+
"path/filepath"
67
"strings"
78
"testing"
89

@@ -100,15 +101,26 @@ func (m *Repository) WriteFile(src io.Reader, dst string) error {
100101
return nil
101102
}
102103

103-
// Walk fulfils the git.Repo interface.
104-
func (m *Repository) Walk(filePath string, cb func(string, string) error) error {
104+
// Walk fulfils the git.Repo interface. It's a mock function to emulate what happens in Repository.Walk()
105+
// The Mock version is different: it iterates over mockSource.files[] and then drives
106+
// the visitor callback in CopyService() as usual.
107+
//
108+
// To preserve the same behaviour, we see that Repository Walk receives /full/path/to/repo/services/service-name
109+
// and then calls filePath.Walk() on /full/path/to/repo/services/ .
110+
// When CopyService() drives Walk(), 'base' is typically services/service-name
111+
// Thus we take each /full/path/to/file/in/mockSource.files[] and split it at 'services/' as happens in the Walk() method we're mocking.
112+
func (m *Repository) Walk(base string, cb func(string, string) error) error {
105113
if m.files == nil {
106114
return nil
107115
}
116+
108117
for _, f := range m.files {
109-
prefix := m.localPath + "/"
110-
if strings.HasPrefix(f, prefix) {
111-
err := cb(prefix, strings.TrimPrefix(f, prefix))
118+
if strings.HasPrefix(f, path.Join(m.localPath, base)) {
119+
splitString := filepath.Dir(base) + "/"
120+
splitPoint := strings.Index(f, splitString) + len(splitString)
121+
prefix := f[:splitPoint]
122+
name := f[splitPoint:]
123+
err := cb(prefix, name)
112124
if err != nil {
113125
return err
114126
}

0 commit comments

Comments
 (0)