From 05ed3a56a70aa194e319bebb51b30442ef15ee3a Mon Sep 17 00:00:00 2001 From: Juan Antonio Osorio Date: Wed, 29 Nov 2023 15:41:59 +0200 Subject: [PATCH] Add utility to replace image references in YAML files 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. --- cmd/containerimage/containerimage.go | 2 + cmd/containerimage/yaml.go | 91 ++++++++++++++++++++++++++++ cmd/containerimage/yamlreplacer.go | 91 ++++++++++++++++++++++++++++ cmd/ghactions/ghactions.go | 27 ++++----- cmd/ghactions/replacer.go | 65 +++----------------- pkg/containers/replace.go | 83 +++++++++++++++++++++++++ pkg/containers/replace_test.go | 75 +++++++++++++++++++++++ pkg/ghactions/utils.go | 32 +++------- pkg/utils/cli/billy.go | 29 +++++++++ pkg/utils/cli/replacer.go | 89 +++++++++++++++++++++++++++ pkg/utils/utils.go | 32 ++++++++++ 11 files changed, 522 insertions(+), 94 deletions(-) create mode 100644 cmd/containerimage/yaml.go create mode 100644 cmd/containerimage/yamlreplacer.go create mode 100644 pkg/containers/replace.go create mode 100644 pkg/containers/replace_test.go create mode 100644 pkg/utils/cli/billy.go create mode 100644 pkg/utils/cli/replacer.go diff --git a/cmd/containerimage/containerimage.go b/cmd/containerimage/containerimage.go index d48cf78..d542cf1 100644 --- a/cmd/containerimage/containerimage.go +++ b/cmd/containerimage/containerimage.go @@ -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 } diff --git a/cmd/containerimage/yaml.go b/cmd/containerimage/yaml.go new file mode 100644 index 0000000..1960119 --- /dev/null +++ b/cmd/containerimage/yaml.go @@ -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) +} diff --git a/cmd/containerimage/yamlreplacer.go b/cmd/containerimage/yamlreplacer.go new file mode 100644 index 0000000..cffbdaa --- /dev/null +++ b/cmd/containerimage/yamlreplacer.go @@ -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 +} diff --git a/cmd/ghactions/ghactions.go b/cmd/ghactions/ghactions.go index 8bb9d0d..a78853d 100644 --- a/cmd/ghactions/ghactions.go +++ b/cmd/ghactions/ghactions.go @@ -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 @@ -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()) @@ -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() @@ -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) } diff --git a/cmd/ghactions/replacer.go b/cmd/ghactions/replacer.go index f3b8d54..40bcd12 100644 --- a/cmd/ghactions/replacer.go +++ b/cmd/ghactions/replacer.go @@ -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) @@ -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() } @@ -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 -} diff --git a/pkg/containers/replace.go b/pkg/containers/replace.go new file mode 100644 index 0000000..9b64263 --- /dev/null +++ b/pkg/containers/replace.go @@ -0,0 +1,83 @@ +// +// 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 containers + +import ( + "bufio" + "context" + "fmt" + "io" + "regexp" + + "github.com/google/go-containerregistry/pkg/name" +) + +// ReplaceImageReferenceFromYAML replaces the image reference in the input text with the digest +func ReplaceImageReferenceFromYAML(ctx context.Context, input io.Reader, output io.Writer) (bool, error) { + return ReplaceReferenceFromYAML(ctx, "image", input, output) +} + +// ReplaceReferenceFromYAML replaces the image reference in the input text with the digest +func ReplaceReferenceFromYAML(ctx context.Context, keyRegex string, input io.Reader, output io.Writer) (bool, error) { + scanner := bufio.NewScanner(input) + re, err := regexp.Compile(fmt.Sprintf(`(\s*%s):\s*([^\s]+)`, keyRegex)) + if err != nil { + return false, fmt.Errorf("failed to compile regex: %w", err) + } + + modified := false + + for scanner.Scan() { + line := scanner.Text() + updatedLine := re.ReplaceAllStringFunc(line, func(match string) string { + submatches := re.FindStringSubmatch(match) + if len(submatches) != 3 { + return match + } + + imageReferenceWithTag := submatches[2] + ref, err := name.ParseReference(imageReferenceWithTag) + if err != nil { + return match + } + + digest, err := GetDigestFromRef(ctx, ref) + if err != nil { + return match + } + + imgWithoutTag := ref.Context().Name() + outstr := imgWithoutTag + "@" + digest + + if imageReferenceWithTag != outstr { + modified = true + } + + replacement := fmt.Sprintf("${1}: %s # %s", outstr, ref.Identifier()) + return re.ReplaceAllString(match, replacement) + }) + + if _, err := io.WriteString(output, updatedLine+"\n"); err != nil { + return false, err + } + } + + if err := scanner.Err(); err != nil { + return false, err + } + + return modified, nil +} diff --git a/pkg/containers/replace_test.go b/pkg/containers/replace_test.go new file mode 100644 index 0000000..401d13c --- /dev/null +++ b/pkg/containers/replace_test.go @@ -0,0 +1,75 @@ +// +// 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 containers + +import ( + "context" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReplaceImageReference(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + testCases := []struct { + name string + input string + expectedOutput string + modified bool + }{ + { + name: "Replace image reference", + input: ` +version: v1 +services: + - name: web + image: nginx:latest + - name: localstack + image: localstack/localstack +`, + expectedOutput: ` +version: v1 +services: + - name: web + image: index.docker.io/library/nginx@sha256:10d1f5b58f74683ad34eb29287e07dab1e90f10af243f151bb50aa5dbb4d62ee # latest + - name: localstack + image: index.docker.io/localstack/localstack@sha256:9b89e7d3bd1b0869f58d9aff0bfad30b4e1c2491ece7a00fb0a7515530d69cf2 # latest +`, + modified: true, + }, + // Add more test cases as needed + } + + // Define a regular expression to match YAML tags containing "image" + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + var output strings.Builder + m, err := ReplaceImageReferenceFromYAML(ctx, strings.NewReader(tc.input), &output) + assert.NoError(t, err) + + assert.Equal(t, tc.expectedOutput, output.String()) + assert.Equal(t, tc.modified, m, "modified") + }) + } +} diff --git a/pkg/ghactions/utils.go b/pkg/ghactions/utils.go index a0c74c4..8ee3575 100644 --- a/pkg/ghactions/utils.go +++ b/pkg/ghactions/utils.go @@ -18,26 +18,22 @@ package ghactions import ( "fmt" "io/fs" - "strings" "github.com/go-git/go-billy/v5" - billyutil "github.com/go-git/go-billy/v5/util" "gopkg.in/yaml.v3" + + "github.com/stacklok/frizbee/pkg/utils" ) -// TraverseFunc is a function that gets called with each file in a GitHub Actions workflow +// TraverseGHWFunc is a function that gets called with each file in a GitHub Actions workflow // directory. It receives the path to the file and the parsed workflow. -type TraverseFunc func(path string, wflow *yaml.Node) error +type TraverseGHWFunc func(path string, wflow *yaml.Node) error // TraverseGitHubActionWorkflows traverses the GitHub Actions workflows in the given directory // and calls the given function with each workflow. -func TraverseGitHubActionWorkflows(bfs billy.Filesystem, base string, fun TraverseFunc) error { - return billyutil.Walk(bfs, base, func(path string, info fs.FileInfo, err error) error { - if err != nil { - return nil - } - - if shouldSkipFile(info) { +func TraverseGitHubActionWorkflows(bfs billy.Filesystem, base string, fun TraverseGHWFunc) error { + return utils.Traverse(bfs, base, func(path string, info fs.FileInfo) error { + if !utils.IsYAMLFile(info) { return nil } @@ -63,17 +59,3 @@ func TraverseGitHubActionWorkflows(bfs billy.Filesystem, base string, fun Traver return nil }) } - -func shouldSkipFile(info fs.FileInfo) bool { - // skip if not a file - if info.IsDir() { - return true - } - - // skip if not a .yml or .yaml file - if !strings.HasSuffix(info.Name(), ".yml") && !strings.HasSuffix(info.Name(), ".yaml") { - return true - } - - return false -} diff --git a/pkg/utils/cli/billy.go b/pkg/utils/cli/billy.go new file mode 100644 index 0000000..88d12d7 --- /dev/null +++ b/pkg/utils/cli/billy.go @@ -0,0 +1,29 @@ +// +// 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 cli provides utilities for frizbee's CLI. +package cli + +// ProcessDirNameForBillyFS processes the given directory name for use with +// go-billy filesystems. +func ProcessDirNameForBillyFS(dir string) string { + // remove trailing / from dir. This doesn't play well with + // the go-billy filesystem and walker we use. + if dir[len(dir)-1] == '/' { + return dir[:len(dir)-1] + } + + return dir +} diff --git a/pkg/utils/cli/replacer.go b/pkg/utils/cli/replacer.go new file mode 100644 index 0000000..c2fb34d --- /dev/null +++ b/pkg/utils/cli/replacer.go @@ -0,0 +1,89 @@ +// +// 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 cli provides utilities for frizbee's CLI. +package cli + +import ( + "fmt" + "io" + "os" + + "github.com/go-git/go-billy/v5" + "github.com/spf13/cobra" +) + +// DeclareReplacerFlags declares the flags common to all replacer commands. +// Note that `dir` is not declared here because it is command-specific. +func DeclareReplacerFlags(cmd *cobra.Command) { + 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") +} + +// Replacer is a common struct for implementing a CLI command that replaces +// files. +type Replacer struct { + Dir string + DryRun bool + Quiet bool + ErrOnModified bool + Cmd *cobra.Command +} + +// Logf logs the given message to the given command's stderr if the command is +// not quiet. +func (r *Replacer) Logf(format string, args ...interface{}) { + if !r.Quiet { + fmt.Fprintf(r.Cmd.ErrOrStderr(), format, args...) + } +} + +// ProcessOutput processes the given output files. +// If the command is quiet, the output is discarded. +// If the command is a dry run, the output is written to the command's stdout. +// Otherwise, the output is written to the given filesystem. +func (r *Replacer) ProcessOutput(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 = r.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(r.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 +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index fb44320..c6e91f3 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -18,8 +18,11 @@ package utils import ( "fmt" + "io/fs" "strings" + "github.com/go-git/go-billy/v5" + billyutil "github.com/go-git/go-billy/v5/util" "gopkg.in/yaml.v3" ) @@ -37,3 +40,32 @@ func YAMLToBuffer(wflow *yaml.Node) (fmt.Stringer, error) { return &buf, nil } + +// TraverseFunc is a function that gets called with each file in a directory. +type TraverseFunc func(path string, info fs.FileInfo) error + +// Traverse traverses the given directory and calls the given function with each file. +func Traverse(bfs billy.Filesystem, base string, fun TraverseFunc) error { + return billyutil.Walk(bfs, base, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return nil + } + + return fun(path, info) + }) +} + +// IsYAMLFile returns true if the given file is a YAML file. +func IsYAMLFile(info fs.FileInfo) bool { + // skip if not a file + if info.IsDir() { + return false + } + + // skip if not a .yml or .yaml file + if strings.HasSuffix(info.Name(), ".yml") || strings.HasSuffix(info.Name(), ".yaml") { + return true + } + + return false +}