Skip to content

Commit da28d61

Browse files
committed
New command: 'vendir baseline'
This command updates the git & hg 'ref' field based on the actual checked-out commit of the corresponding content paths
1 parent 85088db commit da28d61

5 files changed

Lines changed: 243 additions & 4 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ require (
2222
golang.org/x/oauth2 v0.30.0
2323
golang.org/x/tools v0.34.0
2424
gopkg.in/inf.v0 v0.9.1
25+
gopkg.in/yaml.v3 v3.0.1
2526
k8s.io/apimachinery v0.24.3
2627
k8s.io/code-generator v0.17.2
2728
sigs.k8s.io/yaml v1.4.0
@@ -103,7 +104,6 @@ require (
103104
golang.org/x/text v0.26.0 // indirect
104105
gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485 // indirect
105106
gopkg.in/yaml.v2 v2.4.0 // indirect
106-
gopkg.in/yaml.v3 v3.0.1 // indirect
107107
k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c // indirect
108108
k8s.io/klog v1.0.0 // indirect
109109
k8s.io/klog/v2 v2.70.1 // indirect

pkg/vendir/cmd/baseline.go

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path"
7+
8+
"github.com/cppforlife/go-cli-ui/ui"
9+
"github.com/spf13/cobra"
10+
"gopkg.in/yaml.v3"
11+
12+
ctlconf "carvel.dev/vendir/pkg/vendir/config"
13+
ctldir "carvel.dev/vendir/pkg/vendir/directory"
14+
ctlcache "carvel.dev/vendir/pkg/vendir/fetch/cache"
15+
)
16+
17+
func NewBaselineOptions(ui ui.UI) *BaselineOptions {
18+
return &BaselineOptions{ui: ui}
19+
}
20+
21+
func NewBaselineCmd(o *BaselineOptions) *cobra.Command {
22+
cmd := cobra.Command{
23+
Use: "baseline",
24+
Short: "Update git/hg repositories 'ref' to match the current commits",
25+
RunE: func(_ *cobra.Command, _ []string) error { return o.Run() },
26+
}
27+
28+
cmd.Flags().BoolVarP(
29+
&o.Yes, "yes", "y", false,
30+
"If true, automatically answer 'yes' to all the questions")
31+
32+
cmd.Flags().StringSliceVarP(&o.Files, "file", "f", []string{defaultConfigName}, "Set configuration file")
33+
cmd.Flags().StringVar(&o.LockFile, "lock-file", defaultLockName, "Set lock file")
34+
cmd.Flags().StringVar(&o.Chdir, "chdir", "", "Set current directory for process")
35+
cmd.Flags().BoolVar(&o.PreferSHA, "prefer-sha", false, "Prefer sha instead of tags")
36+
cmd.Flags().BoolVar(&o.DryRun, "dry-run", false, "List what would be done")
37+
38+
return &cmd
39+
}
40+
41+
type BaselineOptions struct {
42+
ui ui.UI
43+
44+
Files []string
45+
LockFile string
46+
47+
Chdir string
48+
ExitCode bool
49+
50+
PreferSHA bool
51+
Yes bool
52+
DryRun bool
53+
}
54+
55+
func (o *BaselineOptions) Run() error {
56+
if len(o.Chdir) > 0 {
57+
err := os.Chdir(o.Chdir)
58+
if err != nil {
59+
return fmt.Errorf("Running chdir: %s", err)
60+
}
61+
}
62+
63+
conf, secrets, configMaps, err := ctlconf.NewConfigFromFiles(o.Files)
64+
if err != nil {
65+
return (*SyncOptions)(nil).configReadHintErrMsg(err, o.Files)
66+
}
67+
68+
existingLockConfig, err := ctlconf.NewLockConfigFromFile(o.LockFile)
69+
if err != nil {
70+
return err
71+
}
72+
73+
cache, err := ctlcache.NewCache(os.Getenv("VENDIR_CACHE_DIR"), "0Mi")
74+
if err != nil {
75+
return fmt.Errorf("Unable to create cache: %s", err)
76+
}
77+
syncOpts := ctldir.SyncOpts{
78+
RefFetcher: ctldir.NewNamedRefFetcher(secrets, configMaps),
79+
GithubAPIToken: os.Getenv("VENDIR_GITHUB_API_TOKEN"),
80+
HelmBinary: os.Getenv("VENDIR_HELM_BINARY"),
81+
Cache: cache,
82+
Lazy: false,
83+
Partial: false,
84+
}
85+
86+
statusMap, err := fullStatus(conf, syncOpts, existingLockConfig, o.ui)
87+
if err != nil {
88+
return err
89+
}
90+
91+
newRefs := make(map[string]string)
92+
93+
for _, status := range statusMap {
94+
if !status.MatchTarget() || !o.PreferSHA && !status.MatchTargetTag() && len(status.Ref.Tags) != 0 {
95+
var newRef string
96+
if o.PreferSHA || len(status.Ref.Tags) == 0 {
97+
newRef = status.Ref.SHA
98+
} else {
99+
newRef = status.Ref.Tags[0]
100+
}
101+
newRefs[status.Path()] = newRef
102+
}
103+
}
104+
105+
if len(newRefs) == 0 {
106+
o.ui.PrintLinef("All references already match current state, no update needed")
107+
108+
return nil
109+
}
110+
111+
o.ui.PrintLinef("New baseline:")
112+
for _, status := range statusMap {
113+
newRef := newRefs[status.Path()]
114+
if newRef != "" {
115+
newRef = " -> " + newRef
116+
}
117+
o.ui.PrintLinef("%s/%s: %s%s", status.DirectoryPath, status.ContentPath, status.TargetRef, newRef)
118+
}
119+
120+
if !o.DryRun {
121+
for _, fname := range o.Files {
122+
if err := updateRefs(fname, newRefs); err != nil {
123+
return err
124+
}
125+
}
126+
}
127+
128+
return nil
129+
}
130+
131+
func loadFile(fname string) (*yaml.Node, error) {
132+
f, err := os.Open(fname)
133+
if err != nil {
134+
return nil, err
135+
}
136+
137+
defer f.Close()
138+
139+
var node yaml.Node
140+
141+
if err := yaml.NewDecoder(f).Decode(&node); err != nil {
142+
return nil, err
143+
}
144+
145+
return &node, err
146+
}
147+
148+
func saveFile(fname string, doc *yaml.Node) error {
149+
f, err := os.Create(fname)
150+
if err != nil {
151+
return err
152+
}
153+
154+
enc := yaml.NewEncoder(f)
155+
enc.SetIndent(2)
156+
if err := enc.Encode(doc); err != nil {
157+
_ = f.Close()
158+
159+
return err
160+
}
161+
162+
return f.Close()
163+
}
164+
165+
func updateRefs(fname string, newRefs map[string]string) error {
166+
doc, err := loadFile(fname)
167+
if err != nil {
168+
return err
169+
}
170+
171+
if doc.Kind != yaml.DocumentNode {
172+
panic("expects the root node")
173+
}
174+
175+
top := doc.Content[0]
176+
if top.Kind != yaml.MappingNode {
177+
panic("top content must be a mapping")
178+
}
179+
180+
directories := getMappingNodeChild(top, "directories")
181+
182+
for _, d := range directories.Content {
183+
dirPath := getMappingNodeChild(d, "path").Value
184+
185+
contents := getMappingNodeChild(d, "contents")
186+
187+
for _, content := range contents.Content {
188+
contentPath := getMappingNodeChild(content, "path").Value
189+
190+
fullPath := path.Join(dirPath, contentPath)
191+
192+
if newRef, ok := newRefs[fullPath]; ok {
193+
if hg := getMappingNodeChild(content, "hg"); hg != nil {
194+
ref := getMappingNodeChild(hg, "ref")
195+
if ref == nil {
196+
return fmt.Errorf("could not find 'ref' for '%s'", fullPath)
197+
}
198+
ref.Value = newRef
199+
}
200+
if git := getMappingNodeChild(content, "git"); git != nil {
201+
ref := getMappingNodeChild(git, "ref")
202+
if ref == nil {
203+
return fmt.Errorf("could not find 'ref' for '%s'", fullPath)
204+
}
205+
ref.Value = newRef
206+
}
207+
}
208+
}
209+
}
210+
211+
return saveFile(fname, doc)
212+
}
213+
214+
func getMappingNodeChild(node *yaml.Node, name string) *yaml.Node {
215+
for i := 0; i < len(node.Content); i += 2 {
216+
if node.Content[i].Value == name {
217+
return node.Content[i+1]
218+
}
219+
}
220+
221+
return nil
222+
}

pkg/vendir/cmd/vendir.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func NewVendirCmd(o *VendirOptions) *cobra.Command {
4444
cmd.AddCommand(NewSyncCmd(NewSyncOptions(o.ui)))
4545
cmd.AddCommand(NewStatusCmd(NewStatusOptions(o.ui)))
4646
cmd.AddCommand(NewVersionCmd(NewVersionOptions(o.ui)))
47+
cmd.AddCommand(NewBaselineCmd(NewBaselineOptions(o.ui)))
4748

4849
toolsCmd := NewToolsCmd()
4950
toolsCmd.AddCommand(NewSortSemverCmd(NewSortSemverOptions(o.ui)))

pkg/vendir/fetch/hg/status.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"os/exec"
88
"path"
9+
"slices"
910
"strings"
1011

1112
ctlstatus "carvel.dev/vendir/pkg/vendir/status"
@@ -50,6 +51,8 @@ func (d Sync) Status(target string) (*ctlstatus.Status, error) {
5051
}
5152
if tags != "" {
5253
status.Ref.Tags = strings.Split(tags, " ")
54+
status.Ref.Tags = slices.DeleteFunc(
55+
status.Ref.Tags, func(t string) bool { return t == "tip" })
5356
}
5457
if branch != "" {
5558
status.Ref.Others = append(status.Ref.Others, branch)

pkg/vendir/status/status.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package status
55

66
import (
77
"fmt"
8+
"path"
89
"slices"
910
"strings"
1011
)
@@ -24,10 +25,24 @@ type Status struct {
2425
LocalCsets []string
2526
}
2627

28+
func (s Status) Path() string {
29+
return path.Join(s.DirectoryPath, s.ContentPath)
30+
}
31+
2732
func (s Status) IsSafe() bool {
2833
return len(s.UncommitedChanges) == 0 && len(s.LocalCsets) == 0
2934
}
3035

36+
func (s Status) MatchTarget() bool {
37+
return strings.HasPrefix(s.Ref.SHA, s.TargetRef) ||
38+
slices.Contains(s.Ref.Tags, s.TargetRef) ||
39+
slices.Contains(s.Ref.Others, s.TargetRef)
40+
}
41+
42+
func (s Status) MatchTargetTag() bool {
43+
return slices.Contains(s.Ref.Tags, s.TargetRef)
44+
}
45+
3146
func (s Status) String() string {
3247
messages := make([]string, 0, 3)
3348

@@ -42,9 +57,7 @@ func (s Status) String() string {
4257
messages = append(messages, fmt.Sprintf("%d unpushed commits", len(s.LocalCsets)))
4358
}
4459

45-
if !strings.HasPrefix(s.Ref.SHA, s.TargetRef) &&
46-
!slices.Contains(s.Ref.Tags, s.TargetRef) &&
47-
!slices.Contains(s.Ref.Others, s.TargetRef) {
60+
if !s.MatchTarget() {
4861
messages = append(messages, "ref mismatch")
4962
}
5063

0 commit comments

Comments
 (0)