Skip to content

Commit 5ff623d

Browse files
Justin Kulikauskasmnuttall
authored andcommitted
Add promotions between environments in a repo
Users will have the ability to specify every facet of the source and destination environments: repo URL, branch name, and environment folder. New tests check for various combinations of those options. Some minor changes were made to some repository implementation details, so that it can checkout specific branches (rather than just the default) and so that calling Clone() multiple times does not result in an error. Implements #92
1 parent 0d953bb commit 5ff623d

File tree

11 files changed

+425
-61
lines changed

11 files changed

+425
-61
lines changed

pkg/git/mock/file_info.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package mock
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"time"
7+
)
8+
9+
// satisfies the os.FileInfo interface for mocking
10+
type mockFileInfo struct {
11+
name string
12+
}
13+
14+
func newFileInfo(name string) mockFileInfo {
15+
return mockFileInfo{name}
16+
}
17+
18+
func (f mockFileInfo) Name() string {
19+
return filepath.Base(f.name)
20+
}
21+
22+
func (f mockFileInfo) Size() int64 {
23+
return 8
24+
}
25+
26+
func (f mockFileInfo) Mode() os.FileMode {
27+
return os.ModeDir
28+
}
29+
30+
func (f mockFileInfo) ModTime() time.Time {
31+
return time.Now()
32+
}
33+
34+
func (f mockFileInfo) IsDir() bool {
35+
return true
36+
}
37+
38+
func (f mockFileInfo) Sys() interface{} {
39+
return nil
40+
}

pkg/git/mock/mock.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,13 @@ func (m *Repository) Commit(msg string, author *git.Author) error {
9696
return m.CommitErr
9797
}
9898

99-
// Not implemented
10099
func (m *Repository) DirectoriesUnderPath(path string) ([]os.FileInfo, error) {
101-
return nil, nil
100+
// For easy mocking, returns all files "added" to this repo.
101+
dirs := make([]os.FileInfo, len(m.files))
102+
for i, f := range m.files {
103+
dirs[i] = newFileInfo(f)
104+
}
105+
return dirs, nil
102106
}
103107

104108
func (m *Repository) GetUniqueEnvironmentFolder() (string, error) {

pkg/git/repository.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type Repository struct {
2424
tlsVerify bool
2525
debug bool
2626
logger func(fmt string, v ...interface{})
27+
noPush bool
2728
}
2829

2930
// NewRepository creates and returns a local cache of an upstream repository.
@@ -48,6 +49,13 @@ func (r *Repository) Clone() error {
4849
if err != nil {
4950
return fmt.Errorf("error creating the cache dir %s: %w", r.LocalPath, err)
5051
}
52+
53+
// Just pull if the repo is already cached
54+
if _, err := os.Stat(r.repoPath()); !os.IsNotExist(err) {
55+
_, err = r.execGit(r.repoPath(), nil, "pull")
56+
return err
57+
}
58+
5159
// Intentionally omit output as it can contain an access token
5260
_, err = r.execGit(r.LocalPath, nil, "clone", r.RepoURL)
5361
return err
@@ -102,15 +110,13 @@ func (r *Repository) WriteFile(src io.Reader, dst string) error {
102110
// Returns an error if there was a problem in doing so (including if more than one folder found)
103111
// string return type for ease of mocking, callers would use .Name() anyway
104112
func (r *Repository) GetUniqueEnvironmentFolder() (string, error) {
105-
lookup := filepath.Join(r.repoPath(), "environments")
106-
107-
foundDirsUnderEnv, err := r.DirectoriesUnderPath(lookup)
113+
foundDirsUnderEnv, err := r.DirectoriesUnderPath("environments")
108114
if err != nil {
109115
return "", err
110116
}
111117
numDirsUnderEnv := len(foundDirsUnderEnv)
112118
if numDirsUnderEnv != 1 {
113-
return "", fmt.Errorf("found %d directories under environments folder, wanted one. Looked under directory %s", numDirsUnderEnv, lookup)
119+
return "", fmt.Errorf("found %d directories under environments folder, wanted one", numDirsUnderEnv)
114120
}
115121
foundEnvDir := foundDirsUnderEnv[0]
116122
return foundEnvDir.Name(), nil
@@ -119,7 +125,8 @@ func (r *Repository) GetUniqueEnvironmentFolder() (string, error) {
119125
// Returns the directory names of those under a certain path (excluding sub-dirs)
120126
// Returns an error if a directory list attempt errored
121127
func (r *Repository) DirectoriesUnderPath(path string) ([]os.FileInfo, error) {
122-
files, err := ioutil.ReadDir(path)
128+
lookup := filepath.Join(r.repoPath(), path)
129+
files, err := ioutil.ReadDir(lookup)
123130
if err != nil {
124131
return nil, err
125132
}
@@ -187,6 +194,9 @@ func (r *Repository) Commit(msg string, author *Author) error {
187194

188195
// Does a git push origin *branch name*
189196
func (r *Repository) Push(branchName string) error {
197+
if r.noPush {
198+
return nil
199+
}
190200
args := []string{"push", "origin", branchName}
191201
_, err := r.execGit(r.repoPath(), nil, args...)
192202
return err
@@ -253,3 +263,7 @@ func (r *Repository) DeleteCache() error {
253263
}
254264
return nil
255265
}
266+
267+
func (r *Repository) DisablePush() {
268+
r.noPush = true
269+
}

pkg/git/repository_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ func TestClone(t *testing.T) {
7070
}
7171
}
7272

73+
func TestCloneIsIdempotent(t *testing.T) {
74+
r, cleanup := cloneTestRepository(t)
75+
defer cleanup()
76+
77+
err := r.Clone()
78+
if err != nil {
79+
t.Fatalf("failed to clone repository after it had already been cloned: %v", err)
80+
}
81+
}
82+
7383
func TestWalk(t *testing.T) {
7484
r, cleanup := cloneTestRepository(t)
7585
defer cleanup()

pkg/promotion/env_location.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package promotion
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
"strings"
7+
8+
"github.com/rhd-gitops-example/services/pkg/util"
9+
)
10+
11+
type EnvLocation struct {
12+
RepoPath string // URL or local path
13+
Branch string
14+
Folder string
15+
}
16+
17+
func (env EnvLocation) IsLocal() (bool, error) {
18+
parsed, err := url.Parse(env.RepoPath)
19+
if err != nil {
20+
return false, fmt.Errorf("failed parsing URL for environment path: %w", err)
21+
}
22+
return parsed.Scheme == "", nil
23+
}
24+
25+
// String implements Stringer so that messages (including logs, commits and PRs)
26+
// can reference EnvLocations consistently and in a more readable way.
27+
// When possible, it will shorten repository paths so that 'github.com/org/repo'
28+
// is just 'repo'. When not possible, it will use the entire URL.
29+
func (env EnvLocation) String() string {
30+
var repoName string
31+
local, err := env.IsLocal()
32+
if err != nil {
33+
repoName = env.RepoPath
34+
} else {
35+
_, repoName, err = util.ExtractUserAndRepo(env.RepoPath)
36+
if err != nil {
37+
repoName = env.RepoPath
38+
}
39+
}
40+
if local {
41+
return "local filesystem directory"
42+
}
43+
44+
var b strings.Builder
45+
fmt.Fprintf(&b, "branch %s in %s", env.Branch, repoName)
46+
if env.Folder != "" {
47+
fmt.Fprintf(&b, " (environment folder %s)", env.Folder)
48+
}
49+
return b.String()
50+
}

pkg/promotion/promote.go

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,6 @@ import (
1414
"github.com/jenkins-x/go-scm/scm"
1515
)
1616

17-
type EnvLocation struct {
18-
RepoPath string // URL or local path
19-
Branch string
20-
}
21-
22-
func (env EnvLocation) IsLocal() bool {
23-
parsed, err := url.Parse(env.RepoPath)
24-
if err != nil {
25-
log.Printf("Error while parsing URL for environment path: '%v', assuming it is local.\n", err)
26-
return true
27-
}
28-
return parsed.Scheme == ""
29-
}
30-
3117
// Promote is the main driver for promoting files between two
3218
// repositories.
3319
//
@@ -39,9 +25,13 @@ func (s *ServiceManager) Promote(serviceName string, from, to EnvLocation, newBr
3925
defer clearCache(&reposToDelete)
4026
}
4127

28+
fromIsLocal, err := from.IsLocal()
29+
if err != nil {
30+
return fmt.Errorf("failed to determine if repository is local: %w", err)
31+
}
32+
4233
var source git.Source
43-
var err error
44-
if from.IsLocal() {
34+
if fromIsLocal {
4535
source = s.localFactory(from.RepoPath, s.debug)
4636
} else {
4737
source, err = s.checkoutSourceRepo(from.RepoPath, from.Branch)
@@ -54,18 +44,18 @@ func (s *ServiceManager) Promote(serviceName string, from, to EnvLocation, newBr
5444
newBranchName = generateBranchName(source)
5545
}
5646

57-
destination, err := s.checkoutDestinationRepo(to.RepoPath, newBranchName)
47+
destination, err := s.checkoutDestinationRepo(to.RepoPath, to.Branch, newBranchName)
5848
if err != nil {
5949
return err
6050
}
6151
reposToDelete = append(reposToDelete, destination)
62-
destinationEnvironment, err := destination.GetUniqueEnvironmentFolder()
52+
destinationEnvironment, err := getEnvironmentFolder(destination, to.Folder)
6353
if err != nil {
64-
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")
54+
return err
6555
}
6656

6757
var copied []string
68-
if from.IsLocal() {
58+
if fromIsLocal {
6959
copied, err = local.CopyConfig(serviceName, source, destination, destinationEnvironment)
7060
if err != nil {
7161
return fmt.Errorf("failed to set up local repository: %w", err)
@@ -76,9 +66,9 @@ func (s *ServiceManager) Promote(serviceName string, from, to EnvLocation, newBr
7666
// should not happen, but just in case
7767
return fmt.Errorf("failed to convert source '%v' to Git Repo", source)
7868
}
79-
sourceEnvironment, err := repo.GetUniqueEnvironmentFolder()
69+
sourceEnvironment, err := getEnvironmentFolder(repo, from.Folder)
8070
if err != nil {
81-
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")
71+
return err
8272
}
8373

8474
copied, err = git.CopyService(serviceName, source, destination, sourceEnvironment, destinationEnvironment)
@@ -141,9 +131,9 @@ func generateDefaultCommitMsg(source git.Source, serviceName string, from EnvLoc
141131
repo, ok := source.(git.Repo)
142132
if ok {
143133
commit := repo.GetCommitID()
144-
return fmt.Sprintf("Promoting service %s at commit %s from branch %s in %s.", serviceName, commit, from.Branch, from.RepoPath)
134+
return fmt.Sprintf("Promote service %s at commit %s from %v", serviceName, commit, from)
145135
} else {
146-
return fmt.Sprintf("Promoting service %s from local filesystem directory %s.", serviceName, from.RepoPath)
136+
return fmt.Sprintf("Promote service %s from local filesystem directory %s", serviceName, from.RepoPath)
147137
}
148138
}
149139

@@ -158,3 +148,27 @@ func createPullRequest(ctx context.Context, from, to EnvLocation, newBranchName,
158148
pr, _, err := client.PullRequests.Create(ctx, pathToUse, prInput)
159149
return pr, err
160150
}
151+
152+
// getEnvironmentFolder returns the name of the folder to use, or an error.
153+
// Will return an error if the specified folder is not present in the repository,
154+
// or if there are multiple folders and one isn't specified in the args here.
155+
func getEnvironmentFolder(r git.Repo, folder string) (string, error) {
156+
if folder == "" {
157+
dir, err := r.GetUniqueEnvironmentFolder()
158+
if err != nil {
159+
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")
160+
}
161+
return dir, nil
162+
}
163+
164+
dirs, err := r.DirectoriesUnderPath("environments")
165+
if err != nil {
166+
return "", err
167+
}
168+
for _, dir := range dirs {
169+
if dir.Name() == folder {
170+
return dir.Name(), nil
171+
}
172+
}
173+
return "", fmt.Errorf("did not find environment folder matching '%v', only found '%v'", folder, dirs)
174+
}

0 commit comments

Comments
 (0)