Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (
golang.org/x/oauth2 v0.30.0
golang.org/x/tools v0.34.0
gopkg.in/inf.v0 v0.9.1
gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.24.3
k8s.io/code-generator v0.17.2
sigs.k8s.io/yaml v1.4.0
Expand Down Expand Up @@ -103,7 +104,6 @@ require (
golang.org/x/text v0.26.0 // indirect
gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c // indirect
k8s.io/klog v1.0.0 // indirect
k8s.io/klog/v2 v2.70.1 // indirect
Expand Down
241 changes: 241 additions & 0 deletions pkg/vendir/cmd/baseline.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
// Copyright 2024 The Carvel Authors.
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"fmt"
"os"
"path"

"github.com/cppforlife/go-cli-ui/ui"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"

ctlconf "carvel.dev/vendir/pkg/vendir/config"
ctldir "carvel.dev/vendir/pkg/vendir/directory"
ctlcache "carvel.dev/vendir/pkg/vendir/fetch/cache"
)

func NewBaselineOptions(ui ui.UI) *BaselineOptions { //nolint:revive
return &BaselineOptions{ui: ui}
}

func NewBaselineCmd(o *BaselineOptions) *cobra.Command {
cmd := cobra.Command{
Use: "baseline",
Short: "Update git/hg repositories 'ref' to match the current commits",
RunE: func(_ *cobra.Command, _ []string) error { return o.Run() },
}

cmd.Flags().BoolVarP(
&o.Yes, "yes", "y", false,
"If true, automatically answer 'yes' to all the questions")

cmd.Flags().StringSliceVarP(
&o.Files, "file", "f", []string{defaultConfigName},
"Set configuration file")
cmd.Flags().StringVar(
&o.LockFile, "lock-file", defaultLockName, "Set lock file")
cmd.Flags().StringVar(
&o.Chdir, "chdir", "", "Set current directory for process")
cmd.Flags().BoolVar(
&o.PreferSHA, "prefer-sha", false, "Prefer sha instead of tags")
cmd.Flags().BoolVar(&o.DryRun, "dry-run", false, "List what would be done")

return &cmd
}

type BaselineOptions struct {
ui ui.UI

Files []string
LockFile string

Chdir string
ExitCode bool

PreferSHA bool
Yes bool
DryRun bool
}

func (o *BaselineOptions) Run() error {
if len(o.Chdir) > 0 { //nolint:revive
err := os.Chdir(o.Chdir)
if err != nil {
return fmt.Errorf("Running chdir: %s", err)
}
}

conf, secrets, configMaps, err := ctlconf.NewConfigFromFiles(o.Files)
if err != nil {
return (*SyncOptions)(nil).configReadHintErrMsg(err, o.Files)
}

existingLockConfig, err := ctlconf.NewLockConfigFromFile(o.LockFile)
if err != nil {
return err
}

cache, err := ctlcache.NewCache(os.Getenv("VENDIR_CACHE_DIR"), "0Mi")
if err != nil {
return fmt.Errorf("Unable to create cache: %s", err)
}
syncOpts := ctldir.SyncOpts{
RefFetcher: ctldir.NewNamedRefFetcher(secrets, configMaps),
GithubAPIToken: os.Getenv("VENDIR_GITHUB_API_TOKEN"),
HelmBinary: os.Getenv("VENDIR_HELM_BINARY"),
Cache: cache,
Lazy: false,
Partial: false,
}

statusMap, err := fullStatus(conf, syncOpts, existingLockConfig, o.ui)
if err != nil {
return err
}

newRefs := make(map[string]string)

for _, status := range statusMap {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have uncommitted changes in a particular directory I think you should not allow rebasing because you are setting the state to something that cannot be reproducible.

if !status.MatchTarget() ||
o.PreferSHA && !status.MatchTargetSHA() ||
!o.PreferSHA && !status.MatchTargetTag() && len(status.Ref.Tags) != 0 { //nolint:revive
var newRef string
if o.PreferSHA || len(status.Ref.Tags) == 0 { //nolint:revive
newRef = status.Ref.SHA
} else {
newRef = status.Ref.Tags[0] //nolint:revive
}
newRefs[status.Path()] = newRef
}
}

if len(newRefs) == 0 { //nolint:revive
o.ui.PrintLinef(
"All references already match current state, no update needed")

return nil
}

block := "New baseline:\n"
for _, status := range statusMap {
newRef := newRefs[status.Path()]
if newRef != "" {
newRef = " -> " + newRef
}
block += fmt.Sprintf(
"- %s/%s: %s%s\n",
status.DirectoryPath, status.ContentPath, status.TargetRef, newRef)
}
o.ui.PrintBlock([]byte(block))

if !o.DryRun {
if o.Yes || o.ui.AskForConfirmation() == nil {
for _, fname := range o.Files {
if err := updateRefs(fname, newRefs); err != nil {
return err
}
o.ui.PrintLinef("Updated '%s'", fname)
}
}
}

return nil
}

func loadFile(fname string) (*yaml.Node, error) {
f, err := os.Open(fname)
if err != nil {
return nil, err
}

defer f.Close()

var node yaml.Node

if err := yaml.NewDecoder(f).Decode(&node); err != nil {
return nil, err
}

return &node, err
}

func saveFile(fname string, doc *yaml.Node) error {
f, err := os.Create(fname)
if err != nil {
return err
}

enc := yaml.NewEncoder(f)
enc.SetIndent(2) //nolint:revive
if err := enc.Encode(doc); err != nil {
_ = f.Close()

return err
}

return f.Close()
}

func updateRefs(fname string, newRefs map[string]string) error {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feels like we should use the same mechanism here to generate the lock and config file that we use in the sync commands. This "snipping" changes in the configuration can make future changes more complicated.

doc, err := loadFile(fname)
if err != nil {
return err
}

if doc.Kind != yaml.DocumentNode {
panic("expects the root node")
}

top := doc.Content[0] //nolint:revive
if top.Kind != yaml.MappingNode {
panic("top content must be a mapping")
}

directories := getMappingNodeChild(top, "directories")

for _, d := range directories.Content {
dirPath := getMappingNodeChild(d, "path").Value

contents := getMappingNodeChild(d, "contents")

for _, content := range contents.Content {
contentPath := getMappingNodeChild(content, "path").Value

fullPath := path.Join(dirPath, contentPath)

if newRef, ok := newRefs[fullPath]; ok {
if hg := getMappingNodeChild(content, "hg"); hg != nil {
ref := getMappingNodeChild(hg, "ref")
if ref == nil {
return fmt.Errorf(
"could not find 'ref' for '%s'", fullPath)
}
ref.Value = newRef
}
if git := getMappingNodeChild(content, "git"); git != nil {
ref := getMappingNodeChild(git, "ref")
if ref == nil {
return fmt.Errorf(
"could not find 'ref' for '%s'", fullPath)
}
ref.Value = newRef
}
}
}
}

return saveFile(fname, doc)
}

func getMappingNodeChild(node *yaml.Node, name string) *yaml.Node {
for i := 0; i < len(node.Content); i += 2 { //nolint:revive
if node.Content[i].Value == name {
return node.Content[i+1] //nolint:revive
}
}

return nil
}
119 changes: 119 additions & 0 deletions pkg/vendir/cmd/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright 2025 The Carvel Authors.
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"fmt"
"os"

"github.com/cppforlife/go-cli-ui/ui"
"github.com/spf13/cobra"

ctlconf "carvel.dev/vendir/pkg/vendir/config"
ctldir "carvel.dev/vendir/pkg/vendir/directory"
ctlcache "carvel.dev/vendir/pkg/vendir/fetch/cache"
ctlstatus "carvel.dev/vendir/pkg/vendir/status"
)

type StatusOptions struct {
ui ui.UI

Files []string
LockFile string

Chdir string
ExitCode bool
}

func NewStatusOptions(ui ui.UI) *StatusOptions { //nolint:revive
return &StatusOptions{ui: ui}
}

func NewStatusCmd(o *StatusOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "status",
Short: "Check local repositories status",
RunE: func(_ *cobra.Command, _ []string) error { return o.Run() },
}

cmd.Flags().StringSliceVarP(
&o.Files, "file", "f",
[]string{defaultConfigName}, "Set configuration file")
cmd.Flags().StringVar(
&o.LockFile, "lock-file", defaultLockName, "Set lock file")
cmd.Flags().StringVar(
&o.Chdir, "chdir", "", "Set current directory for process")
cmd.Flags().BoolVar(
&o.ExitCode, "exit-code", false,
"Set to 'true', it exits with a non-0 code if any "+
"subproject is not clean")

return cmd
}

func (o *StatusOptions) Run() error {
if len(o.Chdir) > 0 { //nolint:revive
err := os.Chdir(o.Chdir)
if err != nil {
return fmt.Errorf("Running chdir: %s", err)
}
}

conf, secrets, configMaps, err := ctlconf.NewConfigFromFiles(o.Files)
if err != nil {
return (*SyncOptions)(nil).configReadHintErrMsg(err, o.Files)
}

existingLockConfig, err := ctlconf.NewLockConfigFromFile(o.LockFile)
if err != nil {
return err
}

cache, err := ctlcache.NewCache(os.Getenv("VENDIR_CACHE_DIR"), "0Mi")
if err != nil {
return fmt.Errorf("Unable to create cache: %s", err)
}
syncOpts := ctldir.SyncOpts{
RefFetcher: ctldir.NewNamedRefFetcher(secrets, configMaps),
GithubAPIToken: os.Getenv("VENDIR_GITHUB_API_TOKEN"),
HelmBinary: os.Getenv("VENDIR_HELM_BINARY"),
Cache: cache,
Lazy: false,
Partial: false,
}

status, err := fullStatus(conf, syncOpts, existingLockConfig, o.ui)
if err != nil {
return err
}

o.ui.PrintBlock([]byte("---------------\n\n"))

o.ui.PrintTable(status.Table())

return nil
}

func fullStatus(
conf ctlconf.Config,
syncOpts ctldir.SyncOpts,
existingLockConfig ctlconf.LockConfig,
ui ui.UI, //nolint:revive
) (ctlstatus.StatusList, error) {
status := ctlstatus.StatusList{}
for _, dirConf := range conf.Directories {
dirExistingLockConf, _ := existingLockConfig.FindDirectory(dirConf.Path)
directory := ctldir.NewDirectory(dirConf, dirExistingLockConf, ui)

dirStatus, err := directory.Status(syncOpts)
if err != nil {
return nil, fmt.Errorf(
"Reading directory '%s': %s", dirConf.Path, err)
}

status = append(status, dirStatus...)
}

return status, nil
}
Loading
Loading