Skip to content

Commit a4fd7b0

Browse files
authored
Digestabotctl (#243)
This is a Go cli to do what digestabot does. It will bump digest versions of images in files (maybe other stuff later?). You can override the file types to check. You can pass a flag to create a PR, and It currently works with GitHub and GitLab.
1 parent f1ac277 commit a4fd7b0

34 files changed

+1534
-0
lines changed

digestabotctl/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.DS_Store
2+
digestabotctl

digestabotctl/Makefile

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
PROJECT_NAME := digestabot
2+
BINARY := $(PROJECT_NAME)ctl
3+
PKG := "github.com/chainguard-dev/platform-examples/$(PROJECT_NAME)"
4+
VERSION := $(shell if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then git describe --exact-match --tags HEAD 2>/dev/null || echo "dev-$(shell git rev-parse --short HEAD)"; else echo "dev"; fi)
5+
GOOS=$(shell go env GOOS)
6+
GOARCH=$(shell go env GOARCH)
7+
8+
.PHONY: deps lint test coverage
9+
10+
deps: ## Get dependencies
11+
go install github.com/fzipp/gocyclo/cmd/gocyclo@latest
12+
13+
lint: deps ## Lint the files
14+
go vet
15+
gocyclo -over 15 -ignore "generated" ./
16+
17+
test: lint ## Run unittests
18+
go test -v ./...
19+
20+
coverage: ## Create test coverage report
21+
go test -cover ./...
22+
go test ./... -coverprofile=cover.out && go tool cover -html=cover.out -o coverage.html
23+
24+
build: ## Builds the binary on the current platform
25+
go build -a -ldflags "-w -X '$(PKG)/cmd.Version=$(VERSION)'" -o $(BINARY)
26+
27+
docs: build ## Builds the cli documentation
28+
mkdir -p docs
29+
./digestabotctl docs
30+
31+
clean: ## Reset everything
32+
docker run --rm -v ./output:/out alpine rm -rf /out/*
33+
git clean -fd
34+
git clean -fx
35+
git reset --hard
36+
37+
help: ## Display this help screen
38+
@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

digestabotctl/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# digestabotctl
2+
3+
## Docs are [here](./docs)

digestabotctl/cmd/docs.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package cmd
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
"github.com/spf13/cobra/doc"
6+
)
7+
8+
var docsCmd = &cobra.Command{
9+
Use: "docs",
10+
Short: "Generate cli documentation",
11+
RunE: func(cmd *cobra.Command, args []string) error {
12+
return doc.GenMarkdownTree(rootCmd, "./docs")
13+
},
14+
}
15+
16+
func init() {
17+
rootCmd.AddCommand(docsCmd)
18+
}

digestabotctl/cmd/files.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package cmd
2+
3+
import (
4+
"time"
5+
6+
"github.com/chainguard-dev/platform-examples/digestabotctl/digestabot"
7+
"github.com/chainguard-dev/platform-examples/digestabotctl/platforms"
8+
"github.com/chainguard-dev/platform-examples/digestabotctl/versioncontrol"
9+
"github.com/spf13/cobra"
10+
"github.com/spf13/viper"
11+
)
12+
13+
var filesCmd = &cobra.Command{
14+
Use: "files",
15+
Short: "Update digest hashes in files",
16+
RunE: files,
17+
PreRunE: validateFiles,
18+
SilenceUsage: true,
19+
}
20+
21+
var requiredPRFlags = []string{
22+
"owner",
23+
"repo",
24+
"branch",
25+
"token",
26+
"platform",
27+
}
28+
29+
func init() {
30+
updateCmd.AddCommand(filesCmd)
31+
fileFlags(filesCmd)
32+
bindFileFlags(filesCmd)
33+
}
34+
35+
func validateFiles(cmd *cobra.Command, args []string) error {
36+
if !viper.GetBool("create_pr") {
37+
return nil
38+
}
39+
40+
if err := validateEnvs(requiredPRFlags...); err != nil {
41+
return err
42+
}
43+
44+
_, ok := platforms.ValidPlatforms[platforms.GitPlatform(viper.GetString("platform"))]
45+
if !ok {
46+
return platforms.ErrInvalidPlatform
47+
}
48+
49+
return nil
50+
}
51+
52+
func files(cmd *cobra.Command, args []string) error {
53+
opts := versioncontrol.CommitOptions{
54+
Directory: ".",
55+
Message: viper.GetString("title"),
56+
When: time.Now(),
57+
Branch: viper.GetString("branch"),
58+
Token: viper.GetString("token"),
59+
}
60+
61+
checkout, err := versioncontrol.Checkout(opts)
62+
if err != nil {
63+
return err
64+
}
65+
66+
fileTypes := viper.GetStringSlice("file_types")
67+
dir := viper.GetString("directory")
68+
69+
files, err := digestabot.FindFiles(fileTypes, dir)
70+
if err != nil {
71+
return err
72+
}
73+
74+
if err := digestabot.UpdateFiles(files, cfg.Logger); err != nil {
75+
return err
76+
}
77+
78+
if viper.GetBool("create_pr") {
79+
platform := viper.GetString("platform")
80+
81+
return handlePRForPlatform(platform, checkout, opts)
82+
}
83+
84+
return nil
85+
}
86+
87+
func handlePRForPlatform(platform string, checkout versioncontrol.CheckoutResponse, opts versioncontrol.CommitOptions) error {
88+
commit, err := versioncontrol.CommitAndPush(checkout.Repo, checkout.Worktree, opts)
89+
if err != nil {
90+
return err
91+
}
92+
93+
pr := platforms.PullRequest{
94+
Description: viper.GetString("description"),
95+
Title: viper.GetString("title"),
96+
Diff: commit,
97+
Base: viper.GetString("base"),
98+
Head: viper.GetString("branch"),
99+
RepoData: platforms.RepoData{
100+
Repo: viper.GetString("repo"),
101+
Owner: viper.GetString("owner"),
102+
Token: viper.GetString("token"),
103+
},
104+
}
105+
106+
platformFunc := platforms.ValidPlatforms[platforms.GitPlatform(platform)]
107+
if platformFunc == nil {
108+
return platforms.ErrInvalidPlatform
109+
}
110+
111+
creator, err := platformFunc(pr)
112+
if err != nil {
113+
return err
114+
}
115+
116+
return creator.CreatePR()
117+
}

digestabotctl/cmd/flags.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package cmd
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"maps"
7+
"slices"
8+
9+
"github.com/chainguard-dev/platform-examples/digestabotctl/digestabot"
10+
"github.com/chainguard-dev/platform-examples/digestabotctl/platforms"
11+
"github.com/spf13/cobra"
12+
"github.com/spf13/viper"
13+
)
14+
15+
func validateEnvs(vals ...string) error {
16+
var errs []error
17+
for _, v := range vals {
18+
if !viper.IsSet(v) {
19+
errs = append(errs, fmt.Errorf("%v must be set", v))
20+
}
21+
}
22+
23+
return errors.Join(errs...)
24+
}
25+
26+
// Flags are defined here. Because of the way Viper binds values, if the same flag name is called
27+
// with viper.BindPFlag multiple times during init() the value will be overwritten. For example if
28+
// two subcommands each have a flag called name but they each have their own default values,
29+
// viper can overwrite any value passed in for one subcommand with the default value of the other subcommand.
30+
// The answer here is to not use init() and instead use something like PersistentPreRun to bind the
31+
// viper values. Using init for the cobra flags is ok, they are only in here to limit duplication of names.
32+
33+
// bindFileFlags binds the file flag values to viper
34+
func bindFileFlags(cmd *cobra.Command) {
35+
viper.BindPFlag("file_types", cmd.Flags().Lookup("file-types"))
36+
viper.BindPFlag("directory", cmd.Flags().Lookup("directory"))
37+
}
38+
39+
// fileFlags adds the file flags to the passed in command
40+
func fileFlags(cmd *cobra.Command) {
41+
cmd.Flags().StringSliceP("file-types", "f", digestabot.DefaultFileTypes, "Files to update")
42+
cmd.Flags().StringP("directory", "d", ".", "Directory to update files")
43+
}
44+
45+
// bindFileFlags binds the pr flag values to viper
46+
func bindPRFlags(cmd *cobra.Command) {
47+
viper.BindPFlag("create_pr", cmd.Flags().Lookup("create-pr"))
48+
viper.BindPFlag("owner", cmd.Flags().Lookup("owner"))
49+
viper.BindPFlag("repo", cmd.Flags().Lookup("repo"))
50+
viper.BindPFlag("branch", cmd.Flags().Lookup("branch"))
51+
viper.BindPFlag("base", cmd.Flags().Lookup("base"))
52+
viper.BindPFlag("title", cmd.Flags().Lookup("title"))
53+
viper.BindPFlag("token", cmd.Flags().Lookup("token"))
54+
viper.BindPFlag("description", cmd.Flags().Lookup("description"))
55+
viper.BindPFlag("platform", cmd.Flags().Lookup("platform"))
56+
}
57+
58+
// prFlags adds the pr flags to the passed in command
59+
func prFlags(cmd *cobra.Command) {
60+
cmd.PersistentFlags().Bool("create-pr", false, "Create a PR")
61+
cmd.PersistentFlags().String("owner", "", "Repo owner/organization")
62+
cmd.PersistentFlags().String("repo", "", "Repo name")
63+
cmd.PersistentFlags().String("branch", "", "branch for commit")
64+
cmd.PersistentFlags().String("base", "main", "branch for PR to merge into")
65+
cmd.PersistentFlags().String("title", "Updating image digests", "PR title")
66+
cmd.PersistentFlags().String("token", "", "API token")
67+
cmd.PersistentFlags().String("description", "Updating image digests", "PR description")
68+
cmd.PersistentFlags().String("platform", "", fmt.Sprintf("Platform to create the PR. Options are %s", slices.Collect(maps.Keys(platforms.ValidPlatforms))))
69+
}

digestabotctl/cmd/root.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"log/slog"
6+
"os"
7+
"strings"
8+
9+
"github.com/spf13/cobra"
10+
"github.com/spf13/viper"
11+
)
12+
13+
var cfgFile string
14+
var cfg Config
15+
var replacer = strings.NewReplacer("-", "_")
16+
17+
var rootCmd = &cobra.Command{
18+
Use: "digestabotctl",
19+
Short: "Update image hashes in your files",
20+
}
21+
22+
type Config struct {
23+
FlagTypes []string `json:"flag_types" mapstructure:"flag_types"`
24+
Directory string `json:"directory" mapstructure:"directory"`
25+
CreatePR bool `json:"create_pr" mapstructure:"create_pr"`
26+
Logger *slog.Logger
27+
}
28+
29+
func Execute() {
30+
err := rootCmd.Execute()
31+
if err != nil {
32+
os.Exit(1)
33+
}
34+
}
35+
36+
func init() {
37+
cobra.OnInitialize(initConfig)
38+
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.digestabot.json)")
39+
}
40+
41+
func initConfig() {
42+
43+
if cfgFile != "" {
44+
viper.SetConfigFile(cfgFile)
45+
} else {
46+
home, err := os.UserHomeDir()
47+
cobra.CheckErr(err)
48+
49+
viper.AddConfigPath(home)
50+
viper.SetConfigType("json")
51+
viper.SetConfigName(".digestabot")
52+
}
53+
54+
viper.SetEnvPrefix("digestabot")
55+
viper.AutomaticEnv()
56+
// replace - with _ in env vars
57+
viper.SetEnvKeyReplacer(replacer)
58+
59+
// If a config file is found, read it in.
60+
cfg.Logger = slog.New(slog.NewTextHandler(os.Stdout, nil))
61+
if err := viper.ReadInConfig(); err == nil {
62+
cfg.Logger.Debug(fmt.Sprintf("using config %s", viper.ConfigFileUsed()))
63+
}
64+
65+
if err := viper.Unmarshal(&cfg); err != nil {
66+
cobra.CheckErr(err)
67+
}
68+
}

digestabotctl/cmd/update.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package cmd
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
)
6+
7+
// updateCmd represents the update command
8+
var updateCmd = &cobra.Command{
9+
Use: "update",
10+
Short: "Command to control updates to digests",
11+
PersistentPreRunE: updatePreRunE,
12+
}
13+
14+
var requiredUpdateFlags = []string{
15+
"branch",
16+
}
17+
18+
func init() {
19+
rootCmd.AddCommand(updateCmd)
20+
prFlags(updateCmd)
21+
22+
}
23+
24+
func updatePreRunE(cmd *cobra.Command, args []string) error {
25+
//bind flags for Viper
26+
bindPRFlags(cmd)
27+
28+
if err := validateEnvs(requiredUpdateFlags...); err != nil {
29+
return err
30+
}
31+
32+
return nil
33+
}

digestabotctl/cmd/version.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
)
8+
9+
var Version = "dev"
10+
11+
var versionCmd = &cobra.Command{
12+
Use: "version",
13+
Short: "Prints the version",
14+
Run: func(cmd *cobra.Command, args []string) {
15+
fmt.Println(Version)
16+
},
17+
}
18+
19+
func init() {
20+
rootCmd.AddCommand(versionCmd)
21+
}

0 commit comments

Comments
 (0)