From af2e3a67acc151831697cec0958a5f52cf8c9a8c Mon Sep 17 00:00:00 2001 From: Ayaz <20735482+ayazhafiz@users.noreply.github.com> Date: Thu, 21 Mar 2024 11:58:47 -0500 Subject: [PATCH] Add a tool to update mint leaf versions (#46) Co-authored-by: Pierre Beaucamp Co-authored-by: Pierre Beaucamp Co-authored-by: Tommy Graves --- .tool-versions | 3 +- cmd/mint/leaves.go | 47 +++++ cmd/mint/root.go | 1 + internal/api/client.go | 30 +++ internal/api/config.go | 7 +- internal/cli/config.go | 29 +++ internal/cli/interfaces.go | 1 + internal/cli/service.go | 170 +++++++++++++++- internal/cli/service_test.go | 367 ++++++++++++++++++++++++++++++++++- internal/mocks/api.go | 8 + 10 files changed, 657 insertions(+), 6 deletions(-) create mode 100644 cmd/mint/leaves.go diff --git a/.tool-versions b/.tool-versions index f3ab914..2915e00 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1,2 @@ -golang 1.20 +golang 1.22.1 +golangci-lint 1.57.1 diff --git a/cmd/mint/leaves.go b/cmd/mint/leaves.go new file mode 100644 index 0000000..6b469bc --- /dev/null +++ b/cmd/mint/leaves.go @@ -0,0 +1,47 @@ +package main + +import ( + "os" + + "github.com/rwx-research/mint-cli/internal/cli" + "github.com/spf13/cobra" +) + +var leavesCmd = &cobra.Command{ + Short: "Manage Mint leaves", + Use: "leaves", +} + +var ( + Files []string + AllowMajorVersionChange bool + + leavesUpdateCmd = &cobra.Command{ + PreRunE: func(cmd *cobra.Command, args []string) error { + return requireAccessToken() + }, + RunE: func(cmd *cobra.Command, args []string) error { + replacementVersionPicker := cli.PickLatestMinorVersion + if AllowMajorVersionChange { + replacementVersionPicker = cli.PickLatestMajorVersion + } + + return service.UpdateLeaves(cli.UpdateLeavesConfig{ + Files: args, + DefaultDir: ".mint", + ReplacementVersionPicker: replacementVersionPicker, + Stdout: os.Stdout, + Stderr: os.Stderr, + }) + }, + Short: "Update all leaves to their latest (minor) version", + Long: "Update all leaves to their latest (minor) version.\n" + + "Takes a list of files as arguments, or updates all toplevel YAML files in .mint if no files are given.", + Use: "update [flags] [file...]", + } +) + +func init() { + leavesUpdateCmd.Flags().BoolVar(&AllowMajorVersionChange, "allow-major-version-change", false, "update leaves to the latest major version") + leavesCmd.AddCommand(leavesUpdateCmd) +} diff --git a/cmd/mint/root.go b/cmd/mint/root.go index a51c970..9a41d36 100644 --- a/cmd/mint/root.go +++ b/cmd/mint/root.go @@ -70,4 +70,5 @@ func init() { rootCmd.AddCommand(loginCmd) rootCmd.AddCommand(whoamiCmd) rootCmd.AddCommand(vaultsCmd) + rootCmd.AddCommand(leavesCmd) } diff --git a/internal/api/client.go b/internal/api/client.go index a954d2a..17787ac 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -289,6 +289,36 @@ func (c Client) SetSecretsInVault(cfg SetSecretsInVaultConfig) (*SetSecretsInVau return &respBody, nil } +func (c Client) GetLeafVersions() (*LeafVersionsResult, error) { + endpoint := "/mint/api/leaves" + + req, err := http.NewRequest(http.MethodGet, endpoint, bytes.NewBuffer([]byte{})) + if err != nil { + return nil, errors.Wrap(err, "unable to create new HTTP request") + } + + resp, err := c.RoundTrip(req) + if err != nil { + return nil, errors.Wrap(err, "HTTP request failed") + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + msg := extractErrorMessage(resp.Body) + if msg == "" { + msg = fmt.Sprintf("Unable to call Mint API - %s", resp.Status) + } + return nil, errors.New(msg) + } + + respBody := LeafVersionsResult{} + if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil { + return nil, errors.Wrap(err, "unable to parse API response") + } + + return &respBody, nil +} + // extractErrorMessage is a small helper function for parsing an API error message func extractErrorMessage(reader io.Reader) string { errorStruct := struct { diff --git a/internal/api/config.go b/internal/api/config.go index 6f96b4f..9abb613 100644 --- a/internal/api/config.go +++ b/internal/api/config.go @@ -82,7 +82,7 @@ type WhoamiResult struct { type SetSecretsInVaultConfig struct { Secrets []Secret `json:"secrets"` - VaultName string `json:"vault_name"` + VaultName string `json:"vault_name"` } type Secret struct { @@ -93,3 +93,8 @@ type Secret struct { type SetSecretsInVaultResult struct { SetSecrets []string `json:"set_secrets"` } + +type LeafVersionsResult struct { + LatestMajor map[string]string `json:"latest_major"` + LatestMinor map[string]map[string]string `json:"latest_minor"` +} diff --git a/internal/cli/config.go b/internal/cli/config.go index 5717503..d20962d 100644 --- a/internal/cli/config.go +++ b/internal/cli/config.go @@ -4,6 +4,7 @@ import ( "io" "github.com/rwx-research/mint-cli/internal/accesstoken" + "github.com/rwx-research/mint-cli/internal/api" "github.com/rwx-research/mint-cli/internal/errors" "github.com/rwx-research/mint-cli/internal/fs" ) @@ -98,3 +99,31 @@ func (c SetSecretsInVaultConfig) Validate() error { return nil } + +type UpdateLeavesConfig struct { + DefaultDir string + Files []string + ReplacementVersionPicker func(versions api.LeafVersionsResult, leaf string, major string) (string, error) + Stdout io.Writer + Stderr io.Writer +} + +func (c UpdateLeavesConfig) Validate() error { + if len(c.Files) == 0 && c.DefaultDir == "" { + return errors.New("a default directory must be provided if not specifying files explicitly") + } + + if c.ReplacementVersionPicker == nil { + return errors.New("a replacement version picker must be provided") + } + + if c.Stdout == nil { + return errors.New("a stdout interface needs to be provided") + } + + if c.Stdout == nil { + return errors.New("a stderr interface needs to be provided") + } + + return nil +} diff --git a/internal/cli/interfaces.go b/internal/cli/interfaces.go index 6ccc61b..c8f76fe 100644 --- a/internal/cli/interfaces.go +++ b/internal/cli/interfaces.go @@ -13,6 +13,7 @@ type APIClient interface { AcquireToken(tokenUrl string) (*api.AcquireTokenResult, error) Whoami() (*api.WhoamiResult, error) SetSecretsInVault(api.SetSecretsInVaultConfig) (*api.SetSecretsInVaultResult, error) + GetLeafVersions() (*api.LeafVersionsResult, error) } type SSHClient interface { diff --git a/internal/cli/service.go b/internal/cli/service.go index a80f9a1..f7b5f2d 100644 --- a/internal/cli/service.go +++ b/internal/cli/service.go @@ -6,6 +6,7 @@ import ( "io" "os" "path/filepath" + "regexp" "strings" "time" @@ -296,7 +297,6 @@ func (s Service) Whoami(cfg WhoamiConfig) error { return nil } -// DebugRunConfig will connect to a running task over SSH. Key exchange is facilitated over the Cloud API. func (s Service) SetSecretsInVault(cfg SetSecretsInVaultConfig) error { err := cfg.Validate() if err != nil { @@ -358,6 +358,149 @@ func (s Service) SetSecretsInVault(cfg SetSecretsInVaultConfig) error { return nil } +func (s Service) UpdateLeaves(cfg UpdateLeavesConfig) error { + var files []string + + err := cfg.Validate() + if err != nil { + return errors.Wrap(err, "validation failed") + } + + if len(cfg.Files) > 0 { + files = cfg.Files + } else { + yamlFilePathsInDirectory, err := s.yamlFilePathsInDirectory(cfg.DefaultDir) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("unable to find yaml files in directory %s", cfg.DefaultDir)) + } + files = yamlFilePathsInDirectory + } + + if len(files) == 0 { + return errors.New(fmt.Sprintf("no files provided, and no yaml files found in directory %s", cfg.DefaultDir)) + } + + leafReferences, err := s.findLeafReferences(files) + if err != nil { + return err + } + + leafVersions, err := s.APIClient.GetLeafVersions() + if err != nil { + return errors.Wrap(err, "unable to fetch leaf versions") + } + + replacements := make(map[string]string) + for leaf, majorVersions := range leafReferences { + for majorVersion, references := range majorVersions { + targetLeafVersion, err := cfg.ReplacementVersionPicker(*leafVersions, leaf, majorVersion) + if err != nil { + fmt.Fprintln(cfg.Stderr, err.Error()) + continue + } + + replacement := fmt.Sprintf("%s %s", leaf, targetLeafVersion) + for _, reference := range references { + if reference != replacement { + replacements[reference] = replacement + } + } + } + } + + err = s.replaceInFiles(files, replacements) + if err != nil { + return errors.Wrap(err, "unable to replace leaf references") + } + + if len(replacements) == 0 { + fmt.Fprintln(cfg.Stdout, "No leaves to update.") + } else { + fmt.Fprintln(cfg.Stdout, "Updated the following leaves:") + for original, replacement := range replacements { + fmt.Fprintf(cfg.Stdout, "\t%s -> %s\n", original, replacement) + } + } + + return nil +} + +var reLeaf = regexp.MustCompile(`([a-z0-9-]+\/[a-z0-9-]+) ([0-9]+)\.[0-9]+\.[0-9]+`) + +// findLeafReferences returns a map indexed with the leaf names. Each key is another map, this time indexed by +// the major version number. Finally, the value is an array of version strings as they appeared in the source +// file +func (s Service) findLeafReferences(files []string) (map[string]map[string][]string, error) { + matches := make(map[string]map[string][]string) + + for _, path := range files { + fd, err := s.FileSystem.Open(path) + if err != nil { + return nil, errors.Wrapf(err, "error while opening %q", path) + } + defer fd.Close() + + fileContent, err := io.ReadAll(fd) + if err != nil { + return nil, errors.Wrapf(err, "error while reading %q", path) + } + + for _, match := range reLeaf.FindAllSubmatch(fileContent, -1) { + fullMatch := string(match[0]) + leaf := string(match[1]) + majorVersion := string(match[2]) + + majorVersions, ok := matches[leaf] + if !ok { + majorVersions = make(map[string][]string) + } + + if _, ok := majorVersions[majorVersion]; !ok { + majorVersions[majorVersion] = []string{fullMatch} + } else { + majorVersions[majorVersion] = append(majorVersions[majorVersion], fullMatch) + } + + matches[leaf] = majorVersions + } + } + + return matches, nil +} + +func (s Service) replaceInFiles(files []string, replacements map[string]string) error { + for _, path := range files { + fd, err := s.FileSystem.Open(path) + if err != nil { + return errors.Wrapf(err, "error while opening %q", path) + } + defer fd.Close() + + fileContent, err := io.ReadAll(fd) + if err != nil { + return errors.Wrapf(err, "error while reading %q", path) + } + fileContentStr := string(fileContent) + + for old, new := range replacements { + fileContentStr = strings.ReplaceAll(fileContentStr, old, new) + } + + fd, err = s.FileSystem.Create(path) + if err != nil { + return errors.Wrapf(err, "error while opening %q", path) + } + defer fd.Close() + + _, err = io.WriteString(fd, fileContentStr) + if err != nil { + return errors.Wrapf(err, "error while writing %q", path) + } + } + + return nil +} + // taskDefinitionsFromPaths opens each file specified in `paths` and reads their content as a string. // No validation takes place here. func (s Service) taskDefinitionsFromPaths(paths []string) ([]api.TaskDefinition, error) { @@ -430,7 +573,7 @@ func (s Service) findMintDirectoryPath(configuredDirectory string) (string, erro return filepath.Join(workingDirectory, ".mint"), nil } - if (workingDirectory == string(os.PathSeparator)) { + if workingDirectory == string(os.PathSeparator) { return "", nil } @@ -448,3 +591,26 @@ func validateYAML(body string) error { return nil } + +func PickLatestMajorVersion(versions api.LeafVersionsResult, leaf string, _ string) (string, error) { + latestVersion, ok := versions.LatestMajor[leaf] + if !ok { + return "", fmt.Errorf("Unable to find the leaf %q; skipping it.", leaf) + } + + return latestVersion, nil +} + +func PickLatestMinorVersion(versions api.LeafVersionsResult, leaf string, major string) (string, error) { + majorVersions, ok := versions.LatestMinor[leaf] + if !ok { + return "", fmt.Errorf("Unable to find the leaf %q; skipping it.", leaf) + } + + latestVersion, ok := majorVersions[major] + if !ok { + return "", fmt.Errorf("Unable to find major version %q for leaf %q; skipping it.", major, leaf) + } + + return latestVersion, nil +} diff --git a/internal/cli/service_test.go b/internal/cli/service_test.go index 9cb0d63..7708560 100644 --- a/internal/cli/service_test.go +++ b/internal/cli/service_test.go @@ -1350,7 +1350,7 @@ AAAEC6442PQKevgYgeT0SIu9zwlnEMl6MF59ZgM+i0ByMv4eLJPqG3xnZcEQmktHj/GY2i Describe("setting secrets", func() { var ( - stdout strings.Builder + stdout strings.Builder ) BeforeEach(func() { @@ -1391,7 +1391,7 @@ AAAEC6442PQKevgYgeT0SIu9zwlnEMl6MF59ZgM+i0ByMv4eLJPqG3xnZcEQmktHj/GY2i Expect(ssivc.Secrets[1].Name).To(Equal("DEF")) Expect(ssivc.Secrets[1].Secret).To(Equal("\"xyz\"")) return &api.SetSecretsInVaultResult{ - SetSecrets: []string{"ABC","DEF"}, + SetSecrets: []string{"ABC", "DEF"}, }, nil } }) @@ -1448,4 +1448,367 @@ AAAEC6442PQKevgYgeT0SIu9zwlnEMl6MF59ZgM+i0ByMv4eLJPqG3xnZcEQmktHj/GY2i }) }) }) + + Describe("updating leaves", func() { + var ( + stdout strings.Builder + stderr strings.Builder + ) + + BeforeEach(func() { + stdout = strings.Builder{} + stderr = strings.Builder{} + }) + + Context("when no files provided", func() { + Context("when no yaml files found in the default directory", func() { + BeforeEach(func() { + mockFS.MockReadDir = func(name string) ([]fs.DirEntry, error) { + return []fs.DirEntry{ + mocks.DirEntry{FileName: "foo.txt"}, + mocks.DirEntry{FileName: "bar.json"}, + }, nil + } + }) + + It("returns an error", func() { + err := service.UpdateLeaves(cli.UpdateLeavesConfig{ + Stdout: &stdout, + Stderr: &stderr, + Files: []string{}, + DefaultDir: ".mint", + ReplacementVersionPicker: cli.PickLatestMajorVersion, + }) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no files provided, and no yaml files found in directory .mint")) + }) + }) + + Context("when yaml files are found in the specified directory", func() { + var openedFiles []string + + BeforeEach(func() { + openedFiles = []string{} + + mockFS.MockReadDir = func(name string) ([]fs.DirEntry, error) { + return []fs.DirEntry{ + mocks.DirEntry{FileName: "foo.txt"}, + mocks.DirEntry{FileName: "bar.yaml"}, + mocks.DirEntry{FileName: "baz.yml"}, + }, nil + } + mockFS.MockOpen = func(path string) (fs.File, error) { + openedFiles = append(openedFiles, path) + file := mocks.NewFile("") + return file, nil + } + mockFS.MockCreate = func(path string) (fs.File, error) { + openedFiles = append(openedFiles, path) + file := mocks.NewFile("") + return file, nil + } + + mockAPI.MockGetLeafVersions = func() (*api.LeafVersionsResult, error) { + return &api.LeafVersionsResult{ + LatestMajor: map[string]string{}, + }, nil + } + }) + + It("uses the default directory", func() { + err := service.UpdateLeaves(cli.UpdateLeavesConfig{ + Stdout: &stdout, + Stderr: &stderr, + Files: []string{}, + DefaultDir: ".mint", + ReplacementVersionPicker: cli.PickLatestMajorVersion, + }) + + Expect(err).NotTo(HaveOccurred()) + Expect(openedFiles).To(ContainElement(".mint/bar.yaml")) + Expect(openedFiles).To(ContainElement(".mint/baz.yml")) + }) + }) + }) + + Context("with files", func() { + var originalFiles map[string]string + var writtenFiles map[string]*mocks.File + var majorLeafVersions map[string]string + var minorLeafVersions map[string]map[string]string + var leafError error + + BeforeEach(func() { + originalFiles = make(map[string]string) + writtenFiles = make(map[string]*mocks.File) + majorLeafVersions = make(map[string]string) + minorLeafVersions = make(map[string]map[string]string) + leafError = nil + + mockFS.MockOpen = func(path string) (fs.File, error) { + content, ok := originalFiles[path] + if !ok { + return nil, errors.New("file not found") + } + file := mocks.NewFile(content) + return file, nil + } + mockFS.MockCreate = func(path string) (fs.File, error) { + file := mocks.NewFile("") + writtenFiles[path] = file + return file, nil + } + mockAPI.MockGetLeafVersions = func() (*api.LeafVersionsResult, error) { + return &api.LeafVersionsResult{ + LatestMajor: majorLeafVersions, + LatestMinor: minorLeafVersions, + }, leafError + } + }) + + Context("when the leaf versions cannot be retrieved", func() { + BeforeEach(func() { + leafError = errors.New("cannot get leaf versions") + originalFiles["foo.yaml"] = "" + }) + + It("returns an error", func() { + err := service.UpdateLeaves(cli.UpdateLeavesConfig{ + Stdout: &stdout, + Stderr: &stderr, + Files: []string{"foo.yaml"}, + ReplacementVersionPicker: cli.PickLatestMajorVersion, + }) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("cannot get leaf versions")) + }) + }) + + Context("when all leaves are already up-to-date", func() { + BeforeEach(func() { + majorLeafVersions["mint/setup-node"] = "1.2.3" + originalFiles["foo.yaml"] = ` + tasks: + - key: foo + call: mint/setup-node 1.2.3 + ` + }) + + It("does not change the file content", func() { + err := service.UpdateLeaves(cli.UpdateLeavesConfig{ + Stdout: &stdout, + Stderr: &stderr, + Files: []string{"foo.yaml"}, + ReplacementVersionPicker: cli.PickLatestMajorVersion, + }) + + Expect(err).NotTo(HaveOccurred()) + Expect(writtenFiles["foo.yaml"].Buffer.String()).To(Equal(` + tasks: + - key: foo + call: mint/setup-node 1.2.3 + `)) + }) + + It("indicates no leaves were updated", func() { + err := service.UpdateLeaves(cli.UpdateLeavesConfig{ + Stdout: &stdout, + Stderr: &stderr, + Files: []string{"foo.yaml"}, + ReplacementVersionPicker: cli.PickLatestMajorVersion, + }) + + Expect(err).NotTo(HaveOccurred()) + Expect(stdout.String()).To(ContainSubstring("No leaves to update.")) + }) + }) + + Context("when there are leaves to update across multiple files", func() { + BeforeEach(func() { + majorLeafVersions["mint/setup-node"] = "1.2.3" + majorLeafVersions["mint/setup-ruby"] = "1.0.1" + originalFiles["foo.yaml"] = ` + tasks: + - key: foo + call: mint/setup-node 1.0.1 + - key: bar + call: mint/setup-ruby 0.0.1 + ` + originalFiles["bar.yaml"] = ` + tasks: + - key: foo + call: mint/setup-ruby 1.0.0 + ` + }) + + It("updates all files", func() { + err := service.UpdateLeaves(cli.UpdateLeavesConfig{ + Stdout: &stdout, + Stderr: &stderr, + Files: []string{"foo.yaml", "bar.yaml"}, + ReplacementVersionPicker: cli.PickLatestMajorVersion, + }) + + Expect(err).NotTo(HaveOccurred()) + Expect(writtenFiles["foo.yaml"].Buffer.String()).To(Equal(` + tasks: + - key: foo + call: mint/setup-node 1.2.3 + - key: bar + call: mint/setup-ruby 1.0.1 + `)) + Expect(writtenFiles["bar.yaml"].Buffer.String()).To(Equal(` + tasks: + - key: foo + call: mint/setup-ruby 1.0.1 + `)) + }) + + It("indicates leaves were updated", func() { + err := service.UpdateLeaves(cli.UpdateLeavesConfig{ + Stdout: &stdout, + Stderr: &stderr, + Files: []string{"foo.yaml", "bar.yaml"}, + ReplacementVersionPicker: cli.PickLatestMajorVersion, + }) + + Expect(err).NotTo(HaveOccurred()) + Expect(stdout.String()).To(ContainSubstring("Updated the following leaves:")) + Expect(stdout.String()).To(ContainSubstring("mint/setup-node 1.0.1 -> mint/setup-node 1.2.3")) + Expect(stdout.String()).To(ContainSubstring("mint/setup-ruby 0.0.1 -> mint/setup-ruby 1.0.1")) + Expect(stdout.String()).To(ContainSubstring("mint/setup-ruby 1.0.0 -> mint/setup-ruby 1.0.1")) + }) + }) + + Context("when a leaf cannot be found", func() { + BeforeEach(func() { + originalFiles["foo.yaml"] = ` + tasks: + - key: foo + call: mint/setup-node 1.0.1 + ` + }) + + It("does not modify the file", func() { + err := service.UpdateLeaves(cli.UpdateLeavesConfig{ + Stdout: &stdout, + Stderr: &stderr, + Files: []string{"foo.yaml"}, + ReplacementVersionPicker: cli.PickLatestMajorVersion, + }) + + Expect(err).NotTo(HaveOccurred()) + Expect(writtenFiles["foo.yaml"].Buffer.String()).To(Equal(` + tasks: + - key: foo + call: mint/setup-node 1.0.1 + `)) + }) + + It("indicates a leaf could not be found", func() { + err := service.UpdateLeaves(cli.UpdateLeavesConfig{ + Stdout: &stdout, + Stderr: &stderr, + Files: []string{"foo.yaml"}, + ReplacementVersionPicker: cli.PickLatestMajorVersion, + }) + + Expect(err).NotTo(HaveOccurred()) + Expect(stderr.String()).To(ContainSubstring(`Unable to find the leaf "mint/setup-node"; skipping it.`)) + }) + }) + + Context("when a leaf reference is a later version than the latest major", func() { + BeforeEach(func() { + majorLeafVersions["mint/setup-node"] = "1.0.3" + originalFiles["foo.yaml"] = ` + tasks: + - key: foo + call: mint/setup-node 1.1.1 + ` + }) + + It("updates the leaf", func() { + err := service.UpdateLeaves(cli.UpdateLeavesConfig{ + Stdout: &stdout, + Stderr: &stderr, + Files: []string{"foo.yaml"}, + ReplacementVersionPicker: cli.PickLatestMajorVersion, + }) + + Expect(err).NotTo(HaveOccurred()) + Expect(writtenFiles["foo.yaml"].Buffer.String()).To(Equal(` + tasks: + - key: foo + call: mint/setup-node 1.0.3 + `)) + }) + }) + + Context("when a leaf reference is a major version behind the latest", func() { + BeforeEach(func() { + majorLeafVersions["mint/setup-node"] = "2.0.3" + minorLeafVersions["mint/setup-node"] = make(map[string]string) + minorLeafVersions["mint/setup-node"]["2"] = "2.0.3" + minorLeafVersions["mint/setup-node"]["1"] = "1.1.1" + }) + + JustBeforeEach(func() { + Expect(service.UpdateLeaves(cli.UpdateLeavesConfig{ + Stdout: &stdout, + Stderr: &stderr, + Files: []string{"foo.yaml"}, + ReplacementVersionPicker: cli.PickLatestMinorVersion, + })).To(Succeed()) + }) + + Context("while referencing the latest minor version", func() { + BeforeEach(func() { + originalFiles["foo.yaml"] = ` + tasks: + - key: foo + call: mint/setup-node 1.1.1 + ` + }) + + It("does not modify the file", func() { + Expect(writtenFiles["foo.yaml"].Buffer.String()).To(Equal(` + tasks: + - key: foo + call: mint/setup-node 1.1.1 + `)) + }) + + It("indicates no leaves were updated", func() { + Expect(stdout.String()).To(ContainSubstring("No leaves to update.")) + }) + }) + + Context("while not referencing the latest minor version", func() { + BeforeEach(func() { + originalFiles["foo.yaml"] = ` + tasks: + - key: foo + call: mint/setup-node 1.0.9 + ` + }) + + It("updates the file", func() { + Expect(writtenFiles["foo.yaml"].Buffer.String()).To(Equal(` + tasks: + - key: foo + call: mint/setup-node 1.1.1 + `)) + }) + + It("indicates that a leaf was updated", func() { + Expect(stdout.String()).To(ContainSubstring("Updated the following leaves:")) + Expect(stdout.String()).To(ContainSubstring("mint/setup-node 1.0.9 -> mint/setup-node 1.1.1")) + }) + }) + }) + }) + }) }) diff --git a/internal/mocks/api.go b/internal/mocks/api.go index 0502ff0..edbdca0 100644 --- a/internal/mocks/api.go +++ b/internal/mocks/api.go @@ -12,6 +12,7 @@ type API struct { MockAcquireToken func(tokenUrl string) (*api.AcquireTokenResult, error) MockWhoami func() (*api.WhoamiResult, error) MockSetSecretsInVault func(api.SetSecretsInVaultConfig) (*api.SetSecretsInVaultResult, error) + MockGetLeafVersions func() (*api.LeafVersionsResult, error) } func (c *API) InitiateRun(cfg api.InitiateRunConfig) (*api.InitiateRunResult, error) { @@ -62,3 +63,10 @@ func (c *API) SetSecretsInVault(cfg api.SetSecretsInVaultConfig) (*api.SetSecret return nil, errors.New("MockSetSecretsInVault was not configured") } +func (c *API) GetLeafVersions() (*api.LeafVersionsResult, error) { + if c.MockGetLeafVersions != nil { + return c.MockGetLeafVersions() + } + + return nil, errors.New("MockGetLeafVersions was not configured") +}