Skip to content

Commit

Permalink
Merge pull request #20 from stacklok/yaml-container-replace
Browse files Browse the repository at this point in the history
Add utility to replace image references in YAML files
  • Loading branch information
JAORMX authored Nov 30, 2023
2 parents 022aed2 + 05ed3a5 commit 6b87dfc
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 6b87dfc

Please sign in to comment.