Skip to content
46 changes: 26 additions & 20 deletions cli/cmd/mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func GetRootCmd() *cobra.Command {
like downloading new versions.`),
}
AddVersionCmd(rootCmd)
AddUpdateCmd(rootCmd)
AddUpdateCmd(rootCmd, opts)
AddListCmd(rootCmd, opts)
AddDownloadCmd(rootCmd, opts)

Expand Down
101 changes: 12 additions & 89 deletions cli/cmd/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,111 +5,34 @@ package cmd

import (
"fmt"
"io"
"strings"

"golang.org/x/sync/errgroup"

"github.com/blang/semver"
"github.com/inconshreveable/go-update"
"github.com/spf13/cobra"

"github.com/codesphere-cloud/oms/internal/portal"
"github.com/codesphere-cloud/oms/internal/util"
"github.com/codesphere-cloud/oms/internal/version"
)

type UpdateCmd struct {
cmd *cobra.Command
Version version.Version
Updater Updater
cmd *cobra.Command
}

func (c *UpdateCmd) RunE(_ *cobra.Command, args []string) error {
fmt.Printf("running %s", c.cmd.Use)

p := portal.NewPortalClient()

return c.SelfUpdate(p)
return nil
}

func AddUpdateCmd(rootCmd *cobra.Command) {
update := UpdateCmd{
func AddUpdateCmd(rootCmd *cobra.Command, opts GlobalOptions) {
updateCmd := UpdateCmd{
cmd: &cobra.Command{
Use: "update",
Short: "Update Codesphere OMS",
Long: `Updates the OMS to the latest release from OMS Portal.`,
Short: "Update various resources",
Long: `Updates various resources such as the OMS CLI or API keys.`,
},
Version: &version.Build{},
Updater: &SelfUpdater{},
}
rootCmd.AddCommand(update.cmd)
update.cmd.RunE = update.RunE
}

func (c *UpdateCmd) SelfUpdate(p portal.Portal) error {
currentVersion := semver.MustParse(c.Version.Version())

latest, err := p.GetBuild(portal.OmsProduct, "", "")
if err != nil {
return fmt.Errorf("failed to query OMS Portal for latest version: %w", err)
}
latestVersion := semver.MustParse(strings.TrimPrefix(latest.Version, "oms-v"))

fmt.Printf("current version: %v\n", currentVersion)
fmt.Printf("latest version: %v\n", latestVersion)
if latestVersion.Equals(currentVersion) {
fmt.Println("Current OMS CLI is already the latest version", c.Version.Version())
return nil
}

// Need a build with a single artifact to download it
download, err := latest.GetBuildForDownload(fmt.Sprintf("%s_%s.tar.gz", c.Version.Os(), c.Version.Arch()))
if err != nil {
return fmt.Errorf("failed to find OMS CLI in package: %w", err)
}

// Use a pipe to unzip the file while downloading without storing on the filesystem
reader, writer := io.Pipe()
defer func() { _ = reader.Close() }()

eg := errgroup.Group{}
eg.Go(func() error {
defer func() { _ = writer.Close() }()
err = p.DownloadBuildArtifact(portal.OmsProduct, download, writer)
if err != nil {
return fmt.Errorf("failed to download latest OMS package: %w", err)
}
return nil
})

cliReader, err := util.StreamFileFromGzip(reader, "oms-cli")
if err != nil {
return fmt.Errorf("failed to extract binary from archive: %w", err)
}

err = c.Updater.Apply(cliReader)
if err != nil {
return fmt.Errorf("failed to apply update: %w", err)
}

_, _ = io.Copy(io.Discard, reader)

// Wait for download to finish and handle any error from the go routine
err = eg.Wait()
if err != nil {
return err
}

fmt.Println("Update finished successfully.")
return nil
}

type Updater interface {
Apply(update io.Reader) error
}
updateCmd.cmd.RunE = updateCmd.RunE

type SelfUpdater struct{}
AddDownloadPackageCmd(updateCmd.cmd, opts)
addOmsUpdateCmd(updateCmd.cmd)
addApiKeyUpdateCmd(updateCmd.cmd)

func (s *SelfUpdater) Apply(r io.Reader) error {
return update.Apply(r, update.Options{})
rootCmd.AddCommand(updateCmd.cmd)
}
61 changes: 61 additions & 0 deletions cli/cmd/update_api_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) Codesphere Inc.
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"fmt"
"time"

"github.com/codesphere-cloud/oms/internal/portal"
"github.com/spf13/cobra"
)

type UpdateAPIKeyCmd struct {
Opts UpdateAPIKeyOpts
}

type UpdateAPIKeyOpts struct {
GlobalOptions
APIKeyID string
ExpiresAtStr string
}

func addApiKeyUpdateCmd(parentCmd *cobra.Command) {
cmdState := &UpdateAPIKeyCmd{
Opts: UpdateAPIKeyOpts{},
}

apiKeyCmd := &cobra.Command{
Use: "api-key",
Short: "Update an API key's expiration date",
Long: `Updates the expiration date for a given API key using the --id and --valid-to flags. The expiration date must be provided in RFC3339 format (e.g., "2025-12-31T23:59:59Z").`,
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
p := portal.NewPortalClient()
return cmdState.UpdateAPIKey(p)
},
}

apiKeyCmd.Flags().StringVarP(&cmdState.Opts.APIKeyID, "id", "i", "", "The ID of the API key to update")
apiKeyCmd.Flags().StringVarP(&cmdState.Opts.ExpiresAtStr, "valid-to", "v", "", "The new expiration date (RFC3339 format)")

_ = apiKeyCmd.MarkFlagRequired("id")
_ = apiKeyCmd.MarkFlagRequired("valid-to")

parentCmd.AddCommand(apiKeyCmd)
}

func (c *UpdateAPIKeyCmd) UpdateAPIKey(p portal.Portal) error {
expiresAt, err := time.Parse(time.RFC3339, c.Opts.ExpiresAtStr)
if err != nil {
return fmt.Errorf("invalid date format for <valid-to>: %w. Please use RFC3339 format (e.g., \"2025-12-31T23:59:59Z\")", err)
}

if err := p.UpdateAPIKey(c.Opts.APIKeyID, expiresAt); err != nil {
return fmt.Errorf("failed to update API key: %w", err)
}

fmt.Printf("Successfully updated API key '%s' with new expiration date %s.\n", c.Opts.APIKeyID, expiresAt.Format(time.RFC1123))
return nil
}
71 changes: 71 additions & 0 deletions cli/cmd/update_api_key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) Codesphere Inc.
// SPDX-License-Identifier: Apache-2.0

package cmd_test

import (
"fmt"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"github.com/codesphere-cloud/oms/cli/cmd"
"github.com/codesphere-cloud/oms/internal/portal"
)

var _ = Describe("UpdateAPIKey", func() {

var (
mockPortal *portal.MockPortal
c cmd.UpdateAPIKeyCmd
)

BeforeEach(func() {
mockPortal = portal.NewMockPortal(GinkgoT())
c = cmd.UpdateAPIKeyCmd{}
})

Describe("Run", func() {
It("successfully updates the API key when given valid input", func() {
apiKeyID := "aaaaaaaaaaaaaaaaaaaaaa"
expiresAtStr := "2027-12-31T23:59:59Z"
expectedExpiresAt, err := time.Parse(time.RFC3339, expiresAtStr)
Expect(err).NotTo(HaveOccurred())

c.Opts.APIKeyID = apiKeyID
c.Opts.ExpiresAtStr = expiresAtStr

mockPortal.EXPECT().UpdateAPIKey(apiKeyID, expectedExpiresAt).Return(nil)

err = c.UpdateAPIKey(mockPortal)
Expect(err).NotTo(HaveOccurred())
})

It("returns an error for an invalid api key id format", func() {
apiKeyID := "not-a-valid-id"
expiresAtStr := "2027-12-31T23:59:59Z"
expectedExpiresAt, err := time.Parse(time.RFC3339, expiresAtStr)
Expect(err).NotTo(HaveOccurred())

c.Opts.APIKeyID = apiKeyID
c.Opts.ExpiresAtStr = expiresAtStr

mockPortal.EXPECT().UpdateAPIKey(apiKeyID, expectedExpiresAt).Return(fmt.Errorf("invalid api key id format"))

err = c.UpdateAPIKey(mockPortal)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("invalid api key id format"))
})

It("returns an error for an invalid date format", func() {
c.Opts.APIKeyID = "valid id"
c.Opts.ExpiresAtStr = "2025/123/123"

err := c.UpdateAPIKey(mockPortal)

Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("invalid date format"))
})
})
})
Loading