Skip to content

Commit 5bcb981

Browse files
Add support for incremental stamping (#175)
* Add support for incremental stamping * Update docs with stamp parameter
1 parent 6c99281 commit 5bcb981

File tree

6 files changed

+182
-0
lines changed

6 files changed

+182
-0
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,16 @@ As a result of the setup above the `create_gitops_prs` tool will open up to 2 po
464464

465465
The GitOps pull request is only created (or new commits added) if the `gitops` target changes the state for the target deployment branch. The source pull request will remain open (and keep accumulation GitOps results) until the pull request is merged and source branch is deleted.
466466

467+
The `--stamp` parameter allows for the replacement of certain placeholders, but only when the `gitops` target changes the output's digest compared to the one already saved. The new digest of the unstamped data is also saved with the manifest. The digest is kept in a file in the same location as the YAML file, with a `.digest` extension added to its name. This is helpful when the manifests have volatile information that shouldn't be the only factor causing changes in the target deployment branch.
468+
469+
Here are the placeholders that can be replaced:
470+
471+
| Placeholder | Replacement |
472+
|------------------|-------------------------------------------------|
473+
| `{{GIT_REVISION}}` | Result of `git rev-parse HEAD` |
474+
| `{{UTC_DATE}}` | Result of `date -u` |
475+
| `{{GIT_BRANCH}}` | The `branch_name` argument given to `create_gitops_prs` |
476+
467477
`--dry_run` parameter can be used to test the tool without creating any pull requests. The tool will print the list of the potential pull requests. It is recommended to run the tool in the dry run mode as a part of the CI test suite to verify that the tool is configured correctly.
468478

469479
<a name="multiple-release-branches-gitops-workflow"></a>

gitops/digester/BUILD

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright 2024 Adobe. All rights reserved.
2+
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License. You may obtain a copy
4+
# of the License at http://www.apache.org/licenses/LICENSE-2.0
5+
6+
# Unless required by applicable law or agreed to in writing, software distributed under
7+
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
8+
# OF ANY KIND, either express or implied. See the License for the specific language
9+
# governing permissions and limitations under the License.
10+
11+
load("@io_bazel_rules_go//go:def.bzl", "go_library")
12+
13+
licenses(["notice"]) # Apache 2.0
14+
15+
go_library(
16+
name = "go_default_library",
17+
srcs = ["digester.go"],
18+
importpath = "github.com/adobe/rules_gitops/gitops/digester",
19+
visibility = ["//visibility:public"],
20+
)

gitops/digester/digester.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
Copyright 2024 Adobe. All rights reserved.
3+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License. You may obtain a copy
5+
of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
7+
Unless required by applicable law or agreed to in writing, software distributed under
8+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
OF ANY KIND, either express or implied. See the License for the specific language
10+
governing permissions and limitations under the License.
11+
*/
12+
package digester
13+
14+
import (
15+
"crypto/sha256"
16+
"encoding/hex"
17+
"errors"
18+
"io"
19+
"log"
20+
"os"
21+
)
22+
23+
// CalculateDigest calculates the SHA256 digest of a file specified by the given path
24+
func CalculateDigest(path string) string {
25+
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
26+
return ""
27+
}
28+
29+
fi, err := os.Open(path)
30+
if err != nil {
31+
log.Fatal(err)
32+
}
33+
defer fi.Close()
34+
35+
h := sha256.New()
36+
if _, err := io.Copy(h, fi); err != nil {
37+
log.Fatal(err)
38+
}
39+
40+
return hex.EncodeToString(h.Sum(nil))
41+
}
42+
43+
// GetDigest retrieves the digest of a file from a file with the same name but with a ".digest" extension
44+
func GetDigest(path string) string {
45+
digestPath := path + ".digest"
46+
47+
if _, err := os.Stat(digestPath); errors.Is(err, os.ErrNotExist) {
48+
return ""
49+
}
50+
51+
digest, err := os.ReadFile(digestPath)
52+
if err != nil {
53+
log.Fatal(err)
54+
}
55+
56+
return string(digest)
57+
}
58+
59+
// VerifyDigest verifies the integrity of a file by comparing its calculated digest with the stored digest
60+
func VerifyDigest(path string) bool {
61+
return CalculateDigest(path) == GetDigest(path)
62+
}
63+
64+
// SaveDigest calculates the digest of a file at the given path and saves it to a file with the same name but with a ".digest" extension.
65+
func SaveDigest(path string) {
66+
digest := CalculateDigest(path)
67+
68+
digestPath := path + ".digest"
69+
70+
err := os.WriteFile(digestPath, []byte(digest), 0666)
71+
if err != nil {
72+
log.Fatal(err)
73+
}
74+
}

gitops/git/git.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ governing permissions and limitations under the License.
1212
package git
1313

1414
import (
15+
"bufio"
1516
"fmt"
1617
"io/ioutil"
1718
"log"
1819
"os"
1920
oe "os/exec"
2021
"path/filepath"
22+
"strings"
2123

2224
"github.com/adobe/rules_gitops/gitops/exec"
2325
)
@@ -114,6 +116,28 @@ func (r *Repo) Commit(message, gitopsPath string) bool {
114116
return true
115117
}
116118

119+
// RestoreFile restores the specified file in the repository to its original state
120+
func (r *Repo) RestoreFile(fileName string) {
121+
exec.Mustex(r.Dir, "git", "checkout", "--", fileName)
122+
}
123+
124+
// GetChangedFiles returns a list of files that have been changed in the repository
125+
func (r *Repo) GetChangedFiles() []string {
126+
s, err := exec.Ex(r.Dir, "git", "diff", "--name-only")
127+
if err != nil {
128+
log.Fatalf("ERROR: %s", err)
129+
}
130+
var files []string
131+
sc := bufio.NewScanner(strings.NewReader(s))
132+
for sc.Scan() {
133+
files = append(files, sc.Text())
134+
}
135+
if err := sc.Err(); err != nil {
136+
log.Fatalf("ERROR: %s", err)
137+
}
138+
return files
139+
}
140+
117141
// IsClean returns true if there is no local changes (nothing to commit)
118142
func (r *Repo) IsClean() bool {
119143
cmd := oe.Command("git", "status", "--porcelain")

gitops/prer/BUILD

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ go_library(
2121
"//gitops/analysis:go_default_library",
2222
"//gitops/bazel:go_default_library",
2323
"//gitops/commitmsg:go_default_library",
24+
"//gitops/digester:go_default_library",
2425
"//gitops/exec:go_default_library",
2526
"//gitops/git:go_default_library",
2627
"//gitops/git/bitbucket:go_default_library",
2728
"//gitops/git/github:go_default_library",
2829
"//gitops/git/gitlab:go_default_library",
30+
"//templating/fasttemplate:go_default_library",
2931
"//vendor/github.com/golang/protobuf/proto:go_default_library",
3032
],
3133
)

gitops/prer/create_gitops_prs.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ import (
2525
"github.com/adobe/rules_gitops/gitops/analysis"
2626
"github.com/adobe/rules_gitops/gitops/bazel"
2727
"github.com/adobe/rules_gitops/gitops/commitmsg"
28+
"github.com/adobe/rules_gitops/gitops/digester"
2829
"github.com/adobe/rules_gitops/gitops/exec"
2930
"github.com/adobe/rules_gitops/gitops/git"
3031
"github.com/adobe/rules_gitops/gitops/git/bitbucket"
3132
"github.com/adobe/rules_gitops/gitops/git/github"
3233
"github.com/adobe/rules_gitops/gitops/git/gitlab"
34+
"github.com/adobe/rules_gitops/templating/fasttemplate"
3335

3436
proto "github.com/golang/protobuf/proto"
3537
)
@@ -71,6 +73,7 @@ var (
7173
gitopsKind SliceFlags
7274
gitopsRuleName SliceFlags
7375
gitopsRuleAttr SliceFlags
76+
stamp = flag.Bool("stamp", false, "Stamp results of gitops targets with volatile information")
7477
dryRun = flag.Bool("dry_run", false, "Do not create PRs, just print what would be done")
7578
)
7679

@@ -101,6 +104,40 @@ func bazelQuery(query string) *analysis.CqueryResult {
101104
return qr
102105
}
103106

107+
func getGitStatusDict(workdir *git.Repo, gitCommit, branchName string) map[string]interface{} {
108+
utcDate, err := exec.Ex("", "date", "-u")
109+
if err != nil {
110+
log.Fatal(err)
111+
}
112+
utcDate = strings.TrimSpace(utcDate)
113+
114+
ctx := map[string]interface{}{
115+
"GIT_REVISION": gitCommit,
116+
"UTC_DATE": utcDate,
117+
"GIT_BRANCH": branchName,
118+
}
119+
120+
return ctx
121+
}
122+
123+
func stampFile(fullPath string, ctx map[string]interface{}) {
124+
template, err := os.ReadFile(fullPath)
125+
if err != nil {
126+
log.Fatal(err)
127+
}
128+
129+
outf, err := os.OpenFile(fullPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
130+
if err != nil {
131+
log.Fatal(err)
132+
}
133+
defer outf.Close()
134+
135+
_, err = fasttemplate.Execute(string(template), "{{", "}}", outf, ctx)
136+
if err != nil {
137+
log.Fatal(err)
138+
}
139+
}
140+
104141
func main() {
105142
flag.Parse()
106143
if *workspace != "" {
@@ -187,6 +224,21 @@ func main() {
187224
bin := bazel.TargetToExecutable(target)
188225
exec.Mustex("", bin, "--nopush", "--nobazel", "--deployment_root", gitopsdir)
189226
}
227+
if *stamp {
228+
changedFiles := workdir.GetChangedFiles()
229+
if len(changedFiles) > 0 {
230+
ctx := getGitStatusDict(workdir, *gitCommit, *branchName)
231+
for _, filePath := range changedFiles {
232+
fullPath := gitopsdir + "/" + filePath
233+
if digester.VerifyDigest(fullPath) {
234+
workdir.RestoreFile(fullPath)
235+
} else {
236+
digester.SaveDigest(fullPath)
237+
stampFile(fullPath, ctx)
238+
}
239+
}
240+
}
241+
}
190242
if workdir.Commit(fmt.Sprintf("GitOps for release branch %s from %s commit %s\n%s", *releaseBranch, *branchName, *gitCommit, commitmsg.Generate(targets)), *gitopsPath) {
191243
log.Println("branch", branch, "has changes, push is required")
192244
updatedGitopsTargets = append(updatedGitopsTargets, targets...)

0 commit comments

Comments
 (0)