Skip to content

Commit

Permalink
Add utility to replace image references in YAML files
Browse files Browse the repository at this point in the history
This allows us to replace kuberentes or docker-compose files. Note that it
currently does not support authentication to container registries, this is
a TODO item.
  • Loading branch information
JAORMX committed Nov 29, 2023
1 parent 82768f4 commit 05ed3a5
Show file tree
Hide file tree
Showing 11 changed files with 522 additions and 94 deletions.
2 changes: 2 additions & 0 deletions cmd/containerimage/containerimage.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ func CmdContainerImage() *cobra.Command {
cmd := &cobra.Command{
Use: "containerimage",
Short: "Replace container image references with checksums",
RunE: replaceYAML,
SilenceUsage: true,
}

cmd.AddCommand(CmdOne())
cmd.AddCommand(CmdYAML())

return cmd
}
91 changes: 91 additions & 0 deletions cmd/containerimage/yaml.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2023 Stacklok, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package containerimage provides command-line utilities to work with container images.
package containerimage

import (
"fmt"

"github.com/spf13/cobra"

"github.com/stacklok/frizbee/pkg/config"
cliutils "github.com/stacklok/frizbee/pkg/utils/cli"
)

// CmdYAML represents the yaml sub-command
func CmdYAML() *cobra.Command {
cmd := &cobra.Command{
Use: "yaml",
Short: "Replace container image references with checksums in YAML files",
Long: `This utility replaces a tag or branch reference in a container image references
with the digest hash of the referenced tag in YAML files.
Example:
$ frizbee containerimage yaml --dir . --dry-run --quiet --error
`,
RunE: replaceYAML,
SilenceUsage: true,
}

// flags
cmd.Flags().StringP("dir", "d", ".", "workflows directory")
cmd.Flags().StringP("image-regex", "i", "image", "regex to match container image references")

cliutils.DeclareReplacerFlags(cmd)

return cmd
}

func replaceYAML(cmd *cobra.Command, _ []string) error {
dir := cmd.Flag("dir").Value.String()
dryRun, err := cmd.Flags().GetBool("dry-run")
if err != nil {
return fmt.Errorf("failed to get dry-run flag: %w", err)
}
errOnModified, err := cmd.Flags().GetBool("error")
if err != nil {
return fmt.Errorf("failed to get error flag: %w", err)
}
quiet, err := cmd.Flags().GetBool("quiet")
if err != nil {
return fmt.Errorf("failed to get quiet flag: %w", err)
}
cfg, err := config.FromContext(cmd.Context())
if err != nil {
return fmt.Errorf("failed to get config from context: %w", err)
}
ir, err := cmd.Flags().GetString("image-regex")
if err != nil {
return fmt.Errorf("failed to get image-regex flag: %w", err)
}

dir = cliutils.ProcessDirNameForBillyFS(dir)

ctx := cmd.Context()

replacer := &yamlReplacer{
Replacer: cliutils.Replacer{
Dir: dir,
DryRun: dryRun,
Quiet: quiet,
ErrOnModified: errOnModified,
Cmd: cmd,
},
imageRegex: ir,
}

return replacer.do(ctx, cfg)
}
91 changes: 91 additions & 0 deletions cmd/containerimage/yamlreplacer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//
// Copyright 2023 Stacklok, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package containerimage

import (
"bytes"
"context"
"fmt"
"io/fs"
"path/filepath"

"github.com/go-git/go-billy/v5/osfs"

"github.com/stacklok/frizbee/pkg/config"
"github.com/stacklok/frizbee/pkg/containers"
"github.com/stacklok/frizbee/pkg/utils"
cliutils "github.com/stacklok/frizbee/pkg/utils/cli"
)

type yamlReplacer struct {
cliutils.Replacer
imageRegex string
}

func (r *yamlReplacer) do(ctx context.Context, _ *config.Config) error {
basedir := filepath.Dir(r.Dir)
base := filepath.Base(r.Dir)
// NOTE: For some reason using boundfs causes a panic when trying to open a file.
// I instead falled back to chroot which is the default.
bfs := osfs.New(basedir)

outfiles := map[string]string{}
modified := false

err := utils.Traverse(bfs, base, func(path string, info fs.FileInfo) error {
if !utils.IsYAMLFile(info) {
return nil
}

f, err := bfs.Open(path)
if err != nil {
return fmt.Errorf("failed to open file %s: %w", path, err)
}

// nolint:errcheck // ignore error
defer f.Close()

r.Logf("Processing %s\n", path)

buf := bytes.Buffer{}
m, err := containers.ReplaceReferenceFromYAML(ctx, r.imageRegex, f, &buf)
if err != nil {
return fmt.Errorf("failed to process YAML file %s: %w", path, err)
}

modified = modified || m

if m {
r.Logf("Modified %s\n", path)
outfiles[path] = buf.String()
}

return nil
})
if err != nil {
return err
}

if err := r.ProcessOutput(bfs, outfiles); err != nil {
return err
}

if r.ErrOnModified && modified {
return fmt.Errorf("modified files")
}

return nil
}
27 changes: 13 additions & 14 deletions cmd/ghactions/ghactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/spf13/cobra"

"github.com/stacklok/frizbee/pkg/config"
cliutils "github.com/stacklok/frizbee/pkg/utils/cli"
)

// CmdGHActions represents the ghactions command
Expand All @@ -47,9 +48,8 @@ for the given directory.

// flags
cmd.Flags().StringP("dir", "d", ".github/workflows", "workflows directory")
cmd.Flags().BoolP("dry-run", "n", false, "don't modify files")
cmd.Flags().BoolP("quiet", "q", false, "don't print anything")
cmd.Flags().BoolP("error", "e", false, "exit with error code if any file is modified")

cliutils.DeclareReplacerFlags(cmd)

// sub-commands
cmd.AddCommand(CmdOne())
Expand Down Expand Up @@ -77,11 +77,7 @@ func replace(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("failed to get config from context: %w", err)
}

// remove trailing / from dir. This doesn't play well with
// the go-billy filesystem and walker we use.
if dir[len(dir)-1] == '/' {
dir = dir[:len(dir)-1]
}
dir = cliutils.ProcessDirNameForBillyFS(dir)

ctx := cmd.Context()

Expand All @@ -93,12 +89,15 @@ func replace(cmd *cobra.Command, _ []string) error {
}

replacer := &replacer{
ghcli: ghcli,
dir: dir,
dryRun: dryRun,
quiet: quiet,
errOnModified: errOnModified,
Replacer: cliutils.Replacer{
Dir: dir,
DryRun: dryRun,
Quiet: quiet,
ErrOnModified: errOnModified,
Cmd: cmd,
},
ghcli: ghcli,
}

return replacer.do(ctx, cmd, cfg)
return replacer.do(ctx, cfg)
}
65 changes: 10 additions & 55 deletions cmd/ghactions/replacer.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,39 +19,33 @@ package ghactions
import (
"context"
"fmt"
"io"
"os"
"path/filepath"

"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/osfs"
"github.com/google/go-github/v56/github"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"

"github.com/stacklok/frizbee/pkg/config"
"github.com/stacklok/frizbee/pkg/ghactions"
"github.com/stacklok/frizbee/pkg/utils"
cliutils "github.com/stacklok/frizbee/pkg/utils/cli"
)

type replacer struct {
ghcli *github.Client
dir string
dryRun bool
quiet bool
errOnModified bool
cliutils.Replacer
ghcli *github.Client
}

func (r *replacer) do(ctx context.Context, cmd *cobra.Command, cfg *config.Config) error {
basedir := filepath.Dir(r.dir)
base := filepath.Base(r.dir)
func (r *replacer) do(ctx context.Context, cfg *config.Config) error {
basedir := filepath.Dir(r.Dir)
base := filepath.Base(r.Dir)
bfs := osfs.New(basedir, osfs.WithBoundOS())

outfiles := map[string]string{}
modified := false

err := ghactions.TraverseGitHubActionWorkflows(bfs, base, func(path string, wflow *yaml.Node) error {
r.logf(cmd, "Processing %s\n", path)
r.Logf("Processing %s\n", path)
m, err := ghactions.ModifyReferencesInYAML(ctx, r.ghcli, wflow, &cfg.GHActions)
if err != nil {
return fmt.Errorf("failed to process YAML file %s: %w", path, err)
Expand All @@ -65,7 +59,7 @@ func (r *replacer) do(ctx context.Context, cmd *cobra.Command, cfg *config.Confi
}

if m {
r.logf(cmd, "Modified %s\n", path)
r.Logf("Modified %s\n", path)
outfiles[path] = buf.String()
}

Expand All @@ -75,52 +69,13 @@ func (r *replacer) do(ctx context.Context, cmd *cobra.Command, cfg *config.Confi
return err
}

if err := r.processOutput(cmd, bfs, outfiles); err != nil {
if err := r.ProcessOutput(bfs, outfiles); err != nil {
return err
}

if r.errOnModified && modified {
if r.ErrOnModified && modified {
return fmt.Errorf("modified files")
}

return nil
}

func (r *replacer) logf(cmd *cobra.Command, format string, args ...interface{}) {
if !r.quiet {
fmt.Fprintf(cmd.ErrOrStderr(), format, args...)
}
}

func (r *replacer) processOutput(cmd *cobra.Command, bfs billy.Filesystem, outfiles map[string]string) error {

var out io.Writer

for path, content := range outfiles {
if r.quiet {
out = io.Discard
} else if r.dryRun {
out = cmd.OutOrStdout()
} else {
f, err := bfs.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("failed to open file %s: %w", path, err)
}

defer func() {
if err := f.Close(); err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "failed to close file %s: %v", path, err)
}
}()

out = f
}

_, err := fmt.Fprintf(out, "%s", content)
if err != nil {
return fmt.Errorf("failed to write to file %s: %w", path, err)
}
}

return nil
}
Loading

0 comments on commit 05ed3a5

Please sign in to comment.