Skip to content
36 changes: 18 additions & 18 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 @@ -27,7 +27,7 @@ func GetRootCmd() *cobra.Command {
like downloading new versions.`),
}
AddVersionCmd(rootCmd)
AddUpdateCmd(rootCmd)
AddUpdateCmd(rootCmd, opts)
AddListCmd(rootCmd, opts)
AddDownloadCmd(rootCmd, opts)
AddBetaCmd(rootCmd, &opts)
Expand Down
103 changes: 12 additions & 91 deletions cli/cmd/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,113 +5,34 @@ package cmd

import (
"fmt"
"io"
"log"
"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 OMS related resources",
Long: `Updates resources, e.g. OMS or OMS 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"))

log.Printf("Current version: %v\n", currentVersion)
log.Printf("Latest version: %v\n", latestVersion)

if latestVersion.Equals(currentVersion) {
log.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
}

log.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)
}
63 changes: 63 additions & 0 deletions cli/cmd/update_api_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) Codesphere Inc.
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"fmt"
"time"

"github.com/codesphere-cloud/oms/internal/portal"
"github.com/codesphere-cloud/oms/internal/util"
"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.`,
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 in RFC3339 format (e.g., \"2025-12-31T23:59:59Z\")")

util.MarkFlagRequired(apiKeyCmd, "id")
util.MarkFlagRequired(apiKeyCmd, "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", 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