diff --git a/.frizbee.yml b/.frizbee.yml new file mode 100644 index 0000000..efbd775 --- /dev/null +++ b/.frizbee.yml @@ -0,0 +1,5 @@ +ghactions: + exclude: + # Exclude the SLSA GitHub Generator workflow. + # See https://github.com/slsa-framework/slsa-github-generator/issues/2993 + - slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.9.0 \ No newline at end of file diff --git a/.github/workflows/releaser.yml b/.github/workflows/releaser.yml index 50b1a05..3ff9ad7 100644 --- a/.github/workflows/releaser.yml +++ b/.github/workflows/releaser.yml @@ -89,7 +89,7 @@ jobs: actions: read # To read the workflow path. id-token: write # To sign the provenance. contents: write # To add assets to a release. - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@07e64b653f10a80b6510f4568f685f8b7b9ea830 # v1.9.0 + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.9.0 with: base64-subjects: "${{ needs.release.outputs.hashes }}" upload-assets: true # upload to a new release diff --git a/cmd/ghactions/ghactions.go b/cmd/ghactions/ghactions.go index dda22d3..8bb9d0d 100644 --- a/cmd/ghactions/ghactions.go +++ b/cmd/ghactions/ghactions.go @@ -22,6 +22,8 @@ import ( "github.com/google/go-github/v56/github" "github.com/spf13/cobra" + + "github.com/stacklok/frizbee/pkg/config" ) // CmdGHActions represents the ghactions command @@ -70,6 +72,10 @@ func replace(cmd *cobra.Command, _ []string) error { 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) + } // remove trailing / from dir. This doesn't play well with // the go-billy filesystem and walker we use. @@ -94,5 +100,5 @@ func replace(cmd *cobra.Command, _ []string) error { errOnModified: errOnModified, } - return replacer.do(ctx, cmd) + return replacer.do(ctx, cmd, cfg) } diff --git a/cmd/ghactions/replacer.go b/cmd/ghactions/replacer.go index 906f8d5..f3b8d54 100644 --- a/cmd/ghactions/replacer.go +++ b/cmd/ghactions/replacer.go @@ -29,6 +29,7 @@ import ( "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" ) @@ -41,7 +42,7 @@ type replacer struct { errOnModified bool } -func (r *replacer) do(ctx context.Context, cmd *cobra.Command) error { +func (r *replacer) do(ctx context.Context, cmd *cobra.Command, cfg *config.Config) error { basedir := filepath.Dir(r.dir) base := filepath.Base(r.dir) bfs := osfs.New(basedir, osfs.WithBoundOS()) @@ -51,7 +52,7 @@ func (r *replacer) do(ctx context.Context, cmd *cobra.Command) error { err := ghactions.TraverseGitHubActionWorkflows(bfs, base, func(path string, wflow *yaml.Node) error { r.logf(cmd, "Processing %s\n", path) - m, err := ghactions.ModifyReferencesInYAML(ctx, r.ghcli, wflow) + 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) } diff --git a/cmd/root.go b/cmd/root.go index 09fc5ab..b27ddd4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -18,23 +18,52 @@ package cmd import ( "context" + "fmt" "os" "github.com/spf13/cobra" "github.com/stacklok/frizbee/cmd/ghactions" + "github.com/stacklok/frizbee/pkg/config" ) // Execute runs the root command. func Execute() { var rootCmd = &cobra.Command{ - Use: "frizbee", - Short: "frizbee is a tool you may throw a tag at and it comes back with a checksum", + Use: "frizbee", + Short: "frizbee is a tool you may throw a tag at and it comes back with a checksum", + PersistentPreRunE: prerun, } + rootCmd.PersistentFlags().StringP("config", "c", ".frizbee.yml", "config file (default is .frizbee.yml)") + rootCmd.AddCommand(ghactions.CmdGHActions()) - err := rootCmd.ExecuteContext(context.Background()) - if err != nil { + + if err := rootCmd.ExecuteContext(context.Background()); err != nil { os.Exit(1) } } + +func prerun(cmd *cobra.Command, _ []string) error { + ctx := cmd.Context() + + cfg, err := readConfig(cmd) + if err != nil { + return fmt.Errorf("failed to read config: %w", err) + } + + ctx = context.WithValue(ctx, config.ContextConfigKey, cfg) + + cmd.SetContext(ctx) + + return nil +} + +func readConfig(cmd *cobra.Command) (*config.Config, error) { + configFile, err := cmd.Flags().GetString("config") + if err != nil { + return nil, fmt.Errorf("failed to get config file: %w", err) + } + + return config.ParseConfigFile(configFile) +} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..158ba4c --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,82 @@ +// +// 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 config provides the frizbee configuration. +package config + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "gopkg.in/yaml.v3" +) + +type contextConfigKey struct{} + +// ContextConfigKey is the context key for the configuration. +// nolint:gochecknoglobals // this is a context key +var ContextConfigKey = contextConfigKey{} + +// FromContext returns the configuration from the context. +func FromContext(ctx context.Context) (*Config, error) { + cfg, ok := ctx.Value(ContextConfigKey).(*Config) + if !ok { + return nil, fmt.Errorf("failed to get config from context") + } + + return cfg, nil +} + +// Config is the frizbee configuration. +type Config struct { + GHActions GHActions `yaml:"ghactions" mapstructure:"ghactions"` +} + +// GHActions is the GitHub Actions configuration. +type GHActions struct { + Filter `yaml:",inline" mapstructure:",inline"` +} + +// Filter is a common configuration for filtering out patterns. +type Filter struct { + // Exclude is a list of patterns to exclude. + Exclude []string `yaml:"exclude" mapstructure:"exclude"` +} + +// ParseConfigFile parses a configuration file. +func ParseConfigFile(configfile string) (*Config, error) { + cfg := &Config{} + cleancfgfile := filepath.Clean(configfile) + cfgF, err := os.Open(cleancfgfile) + if err != nil { + if os.IsNotExist(err) { + return cfg, nil + } + + return nil, fmt.Errorf("failed to open config file: %w", err) + } + // nolint:errcheck // we don't care about the error here + defer cfgF.Close() + + dec := yaml.NewDecoder(cfgF) + + if err := dec.Decode(cfg); err != nil { + return nil, fmt.Errorf("failed to decode config file: %w", err) + } + + return cfg, nil +} diff --git a/pkg/ghactions/ghactions.go b/pkg/ghactions/ghactions.go index 08f5016..9bbf9a9 100644 --- a/pkg/ghactions/ghactions.go +++ b/pkg/ghactions/ghactions.go @@ -24,6 +24,8 @@ import ( "github.com/google/go-github/v56/github" "gopkg.in/yaml.v3" + + "github.com/stacklok/frizbee/pkg/config" ) // IsLocal returns true if the input is a local path. @@ -75,7 +77,7 @@ func GetChecksum(ctx context.Context, ghcli *github.Client, action, ref string) // all references to tags with the checksum of the tag. // Note that the given YAML structure is modified in-place. // The function returns true if any references were modified. -func ModifyReferencesInYAML(ctx context.Context, ghcli *github.Client, node *yaml.Node) (bool, error) { +func ModifyReferencesInYAML(ctx context.Context, ghcli *github.Client, node *yaml.Node, cfg *config.GHActions) (bool, error) { // `uses` will be immediately before the action // name in the YAML `Content` array. We use a toggle // to track if we've found `uses` and then look for @@ -97,6 +99,10 @@ func ModifyReferencesInYAML(ctx context.Context, ghcli *github.Client, node *yam continue } + if shouldExclude(cfg, v.Value) { + continue + } + act, ref, err := ParseActionReference(v.Value) if err != nil { return modified, fmt.Errorf("failed to parse action reference '%s': %w", v.Value, err) @@ -116,7 +122,7 @@ func ModifyReferencesInYAML(ctx context.Context, ghcli *github.Client, node *yam } // Otherwise recursively look more - m, err := ModifyReferencesInYAML(ctx, ghcli, v) + m, err := ModifyReferencesInYAML(ctx, ghcli, v, cfg) if err != nil { return m, err } @@ -182,3 +188,12 @@ func parseValue(val string) (*Action, error) { Ref: ref, }, nil } + +func shouldExclude(cfg *config.GHActions, input string) bool { + for _, e := range cfg.Exclude { + if e == input { + return true + } + } + return false +}