Skip to content

Commit

Permalink
feat: add ability to specify a different write and base branch (#304)
Browse files Browse the repository at this point in the history
* add ability to specify a different write and base branch

* format/spelling

* add sha1 to template and truncate branch name length

* add hasher error checking

* document

* spelling

* correction to template docs

* use sha256 and add warning to truncation

* condense annotations into one

* add some tests

* add docs on omitting base branch
  • Loading branch information
nkkowa authored Jan 7, 2022
1 parent 8b401e4 commit a374b73
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 26 deletions.
53 changes: 50 additions & 3 deletions docs/configuration/applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,7 @@ The important pieces to this workflow are:
* Credentials configured in Argo CD will not be re-used, you have to supply a
dedicated set of credentials

* Write-back is a commit to the tracking branch of the Application. Currently,
Image Updater does not support creating a new branch or creating pull or
merge requests
* Write-back is a commit to the tracking branch of the Application.

* If `.spec.source.targetRevision` does not reference a *branch*, you will have
to specify the branch to use manually (see below)
Expand Down Expand Up @@ -215,6 +213,55 @@ following would use GitHub's default `main` branch:
argocd-image-updater.argoproj.io/git-branch: main
```

#### Specifying a separate base and commit branch

By default, Argo CD Imager Updater will checkout, commit, and push back to the
same branch specified above. There are many scenarios where this is not
desired or possible, such as when the default branch is protected. You can
add a separate write-branch by modifying `argocd-image-updater.argoproj.io/git-branch`
with additional data, which will create a new branch from the base branch, and
push to this new branch instead:

```yaml
argocd-image-updater.argoproj.io/git-branch: base:target
```

If you want to specify a write-branch but continue to use the target revision from the application
specification, just omit the base branch name:

```yaml
argocd-image-updater.argoproj.io/git-branch: :target
```

A static branch name may not be desired for this value, so a simple template
can be created (evaluating using the `text/template` Golang package) within
the annotation. For example, the following would create a branch named
`image-updater-foo/bar-1.1` based on `main` in the event an image with
the name `foo/bar` was updated to the new tag `1.1`.

```yaml
argocd-image-updater.argoproj.io/git-branch: main:image-updater{{range .Images}}-{{.Name}}-{{.NewTag}}{{end}}
```

Alternatively, to assure unique branch names you could use the SHA1 representation of the changes:

```yaml
argocd-image-updater.argoproj.io/git-branch: main:image-updater-{{.SHA256}}
```

The following variables are provided for this template:

* `.Images` is a list of changes that were performed by the update. Each
entry in this list is a struct providing the following information for
each change:
* `.Name` holds the full name of the image that was updated
* `.Alias` holds the alias of the image that was updated
* `.OldTag` holds the tag name or SHA digest previous to the update
* `.NewTag` holds the tag name or SHA digest that was updated to
* `.SHA256` is a unique SHA256 has representing these changes

Please note that if the output of the template exceeds 255 characters (git branch name limit) it will be truncated.

#### Specifying the user and email address for commits

Each Git commit is associated with an author's name and email address. If not
Expand Down
90 changes: 88 additions & 2 deletions pkg/argocd/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package argocd

import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"io/ioutil"
"os"
Expand Down Expand Up @@ -58,11 +60,75 @@ func TemplateCommitMessage(tpl *template.Template, appName string, changeList []
return cmBuf.String()
}

// TemplateBranchName parses a string to a template, and returns a
// branch name from that new template. If a branch name can not be
// rendered, it returns an empty value.
func TemplateBranchName(branchName string, changeList []ChangeEntry) string {
var cmBuf bytes.Buffer

tpl, err1 := template.New("branchName").Parse(branchName)

if err1 != nil {
log.Errorf("could not create template for Git branch name: %v", err1)
return ""
}

type imageChange struct {
Name string
Alias string
OldTag string
NewTag string
}

type branchNameTemplate struct {
Images []imageChange
SHA256 string
}

// Let's add a unique hash to the template
hasher := sha256.New()

// We need to transform the change list into something more viable for the
// writer of a template.
changes := make([]imageChange, 0)
for _, c := range changeList {
changes = append(changes, imageChange{c.Image.ImageName, c.Image.ImageAlias, c.OldTag.String(), c.NewTag.String()})
id := fmt.Sprintf("%v-%v-%v,", c.Image.ImageName, c.OldTag.String(), c.NewTag.String())
_, hasherErr := hasher.Write([]byte(id))
log.Infof("writing to hasher %v", id)
if hasherErr != nil {
log.Errorf("could not write image string to hasher: %v", hasherErr)
return ""
}
}

tplData := branchNameTemplate{
Images: changes,
SHA256: hex.EncodeToString(hasher.Sum(nil)),
}

err2 := tpl.Execute(&cmBuf, tplData)
if err2 != nil {
log.Errorf("could not execute template for Git branch name: %v", err2)
return ""
}

toReturn := cmBuf.String()

if len(toReturn) > 255 {
trunc := toReturn[:255]
log.Warnf("write-branch name %v exceeded 255 characters and was truncated to %v", toReturn, trunc)
return trunc
} else {
return toReturn
}
}

type changeWriter func(app *v1alpha1.Application, wbc *WriteBackConfig, gitC git.Client) (err error, skip bool)

// commitChanges commits any changes required for updating one or more images
// after the UpdateApplication cycle has finished.
func commitChangesGit(app *v1alpha1.Application, wbc *WriteBackConfig, write changeWriter) error {
func commitChangesGit(app *v1alpha1.Application, wbc *WriteBackConfig, changeList []ChangeEntry, write changeWriter) error {
creds, err := wbc.GetCreds(app)
if err != nil {
return fmt.Errorf("could not get creds for repo '%s': %v", app.Spec.Source.RepoURL, err)
Expand Down Expand Up @@ -125,6 +191,26 @@ func commitChangesGit(app *v1alpha1.Application, wbc *WriteBackConfig, write cha
return err
}

// The push branch is by default the same as the checkout branch, unless
// specified after a : separator git-branch annotation, in which case a
// new branch will be made following a template that can use the list of
// changed images.
pushBranch := checkOutBranch

if wbc.GitWriteBranch != "" {
log.Debugf("Using branch template: %s", wbc.GitWriteBranch)
pushBranch = TemplateBranchName(wbc.GitWriteBranch, changeList)
if pushBranch != "" {
log.Debugf("Creating branch '%s' and using that for push operations", pushBranch)
err = gitC.Branch(checkOutBranch, pushBranch)
if err != nil {
return err
}
} else {
return fmt.Errorf("Git branch name could not be created from the template: %s", wbc.GitWriteBranch)
}
}

if err, skip := write(app, wbc, gitC); err != nil {
return err
} else if skip {
Expand Down Expand Up @@ -152,7 +238,7 @@ func commitChangesGit(app *v1alpha1.Application, wbc *WriteBackConfig, write cha
if err != nil {
return err
}
err = gitC.Push("origin", checkOutBranch, false)
err = gitC.Push("origin", pushBranch, false)
if err != nil {
return err
}
Expand Down
64 changes: 64 additions & 0 deletions pkg/argocd/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,70 @@ updates image bar/baz tag '2.0' to '2.1'
})
}

func Test_TemplateBranchName(t *testing.T) {
t.Run("Template branch name with image name", func(t *testing.T) {
exp := `image-updater-foo/bar-1.1-bar/baz-2.1`
tpl := "image-updater{{range .Images}}-{{.Name}}-{{.NewTag}}{{end}}"
cl := []ChangeEntry{
{
Image: image.NewFromIdentifier("foo/bar"),
OldTag: tag.NewImageTag("1.0", time.Now(), ""),
NewTag: tag.NewImageTag("1.1", time.Now(), ""),
},
{
Image: image.NewFromIdentifier("bar/baz"),
OldTag: tag.NewImageTag("2.0", time.Now(), ""),
NewTag: tag.NewImageTag("2.1", time.Now(), ""),
},
}
r := TemplateBranchName(tpl, cl)
assert.NotEmpty(t, r)
assert.Equal(t, exp, r)
})
t.Run("Template branch name with alias", func(t *testing.T) {
exp := `image-updater-bar-1.1`
tpl := "image-updater{{range .Images}}-{{.Alias}}-{{.NewTag}}{{end}}"
cl := []ChangeEntry{
{
Image: image.NewFromIdentifier("bar=0001.dkr.ecr.us-east-1.amazonaws.com/bar"),
OldTag: tag.NewImageTag("1.0", time.Now(), ""),
NewTag: tag.NewImageTag("1.1", time.Now(), ""),
},
}
r := TemplateBranchName(tpl, cl)
assert.NotEmpty(t, r)
assert.Equal(t, exp, r)
})
t.Run("Template branch name with hash", func(t *testing.T) {
// Expected value generated from https://emn178.github.io/online-tools/sha256.html
exp := `image-updater-0fcc2782543e4bb067c174c21bf44eb947f3e55c0d62c403e359c1c209cbd041`
tpl := "image-updater-{{.SHA256}}"
cl := []ChangeEntry{
{
Image: image.NewFromIdentifier("foo/bar"),
OldTag: tag.NewImageTag("1.0", time.Now(), ""),
NewTag: tag.NewImageTag("1.1", time.Now(), ""),
},
}
r := TemplateBranchName(tpl, cl)
assert.NotEmpty(t, r)
assert.Equal(t, exp, r)
})
t.Run("Template branch over 255 chars", func(t *testing.T) {
tpl := "image-updater-lorem-ipsum-dolor-sit-amet-consectetur-" +
"adipiscing-elit-phasellus-imperdiet-vitae-elit-quis-pulvinar-" +
"suspendisse-pulvinar-lacus-vel-semper-congue-enim-purus-posuere-" +
"orci-ut-vulputate-mi-ipsum-quis-ipsum-quisque-elit-arcu-lobortis-" +
"in-blandit-vel-pharetra-vel-urna-aliquam-euismod-elit-vel-mi"
exp := tpl[:255]
cl := []ChangeEntry{}
r := TemplateBranchName(tpl, cl)
assert.NotEmpty(t, r)
assert.Equal(t, exp, r)
assert.Len(t, r, 255)
})
}

func Test_parseImageOverride(t *testing.T) {
cases := []struct {
name string
Expand Down
22 changes: 15 additions & 7 deletions pkg/argocd/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type WriteBackConfig struct {
GitClient git.Client
GetCreds GitCredsSource
GitBranch string
GitWriteBranch string
GitCommitUser string
GitCommitEmail string
GitCommitMessage string
Expand Down Expand Up @@ -313,7 +314,7 @@ func UpdateApplication(updateConf *UpdateConfiguration, state *SyncIterationStat
log.Debugf("Using commit message: %s", wbc.GitCommitMessage)
if !updateConf.DryRun {
logCtx.Infof("Committing %d parameter update(s) for application %s", result.NumImagesUpdated, app)
err := commitChangesLocked(&updateConf.UpdateApp.Application, wbc, state)
err := commitChangesLocked(&updateConf.UpdateApp.Application, wbc, state, changeList)
if err != nil {
logCtx.Errorf("Could not update application spec: %v", err)
result.NumErrors += 1
Expand Down Expand Up @@ -447,7 +448,14 @@ func parseTarget(target string, sourcePath string) (kustomizeBase string) {
func parseGitConfig(app *v1alpha1.Application, kubeClient *kube.KubernetesClient, wbc *WriteBackConfig, creds string) error {
branch, ok := app.Annotations[common.GitBranchAnnotation]
if ok {
wbc.GitBranch = strings.TrimSpace(branch)
branches := strings.Split(strings.TrimSpace(branch), ":")
if len(branches) > 2 {
return fmt.Errorf("invalid format for git-branch annotation: %v", branch)
}
wbc.GitBranch = branches[0]
if len(branches) == 2 {
wbc.GitWriteBranch = branches[1]
}
}
credsSource, err := getGitCredsSource(creds, kubeClient)
if err != nil {
Expand All @@ -457,19 +465,19 @@ func parseGitConfig(app *v1alpha1.Application, kubeClient *kube.KubernetesClient
return nil
}

func commitChangesLocked(app *v1alpha1.Application, wbc *WriteBackConfig, state *SyncIterationState) error {
func commitChangesLocked(app *v1alpha1.Application, wbc *WriteBackConfig, state *SyncIterationState, changeList []ChangeEntry) error {
if wbc.RequiresLocking() {
lock := state.GetRepositoryLock(app.Spec.Source.RepoURL)
lock.Lock()
defer lock.Unlock()
}

return commitChanges(app, wbc)
return commitChanges(app, wbc, changeList)
}

// commitChanges commits any changes required for updating one or more images
// after the UpdateApplication cycle has finished.
func commitChanges(app *v1alpha1.Application, wbc *WriteBackConfig) error {
func commitChanges(app *v1alpha1.Application, wbc *WriteBackConfig, changeList []ChangeEntry) error {
switch wbc.Method {
case WriteBackApplication:
_, err := wbc.ArgoClient.UpdateSpec(context.TODO(), &application.ApplicationUpdateSpecRequest{
Expand All @@ -482,9 +490,9 @@ func commitChanges(app *v1alpha1.Application, wbc *WriteBackConfig) error {
case WriteBackGit:
// if the kustomize base is set, the target is a kustomization
if wbc.KustomizeBase != "" {
return commitChangesGit(app, wbc, writeKustomization)
return commitChangesGit(app, wbc, changeList, writeKustomization)
}
return commitChangesGit(app, wbc, writeOverrides)
return commitChangesGit(app, wbc, changeList, writeOverrides)
default:
return fmt.Errorf("unknown write back method set: %d", wbc.Method)
}
Expand Down
Loading

0 comments on commit a374b73

Please sign in to comment.