Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .replicated.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
profile: "prod"
appId: ""
appSlug: ""
promoteToChannelIds: []
Expand Down
49 changes: 49 additions & 0 deletions cli/cmd/profile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package cmd

import (
"github.com/spf13/cobra"
)

func (r *runners) InitProfileCommand(parent *cobra.Command) *cobra.Command {
cmd := &cobra.Command{
Use: "profile",
Short: "Manage authentication profiles",
Long: `The profile command allows you to manage authentication profiles for the Replicated CLI.

Profiles let you store multiple sets of credentials and easily switch between them.
This is useful when working with different Replicated accounts (production, development, etc.)
or different API endpoints.

Credentials are stored in ~/.replicated/config.yaml with file permissions set to 600 (owner read/write only).
You can reference profiles in your .replicated.yaml files using the 'profile' field.

Authentication priority:
1. REPLICATED_API_TOKEN environment variable (highest priority)
2. Profile specified in .replicated.yaml
3. Default profile from ~/.replicated/config.yaml
4. Legacy single token (backward compatibility)

Use the various subcommands to:
- Add new profiles
- List all profiles
- Remove profiles
- Set the default profile`,
Example: `# Add a production profile
replicated profile add prod --token=your-prod-token

# Add a development profile with custom API origin
replicated profile add dev --token=your-dev-token --api-origin=https://vendor-api-dev.com

# List all profiles
replicated profile ls

# Set default profile
replicated profile set-default prod

# Remove a profile
replicated profile rm dev`,
}
parent.AddCommand(cmd)

return cmd
}
79 changes: 79 additions & 0 deletions cli/cmd/profile_add.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package cmd

import (
"fmt"

"github.com/pkg/errors"
"github.com/replicatedhq/replicated/pkg/credentials"
"github.com/replicatedhq/replicated/pkg/credentials/types"
"github.com/spf13/cobra"
)

func (r *runners) InitProfileAddCommand(parent *cobra.Command) *cobra.Command {
cmd := &cobra.Command{
Use: "add [profile-name]",
Short: "Add a new authentication profile",
Long: `Add a new authentication profile with the specified name.

You must provide an API token. Optionally, you can specify custom API and registry origins.
If a profile with the same name already exists, it will be updated.

The profile will be stored in ~/.replicated/config.yaml with file permissions 600 (owner read/write only).`,
Example: `# Add a production profile
replicated profile add prod --token=your-prod-token

# Add a development profile with custom origins
replicated profile add dev \
--token=your-dev-token \
--api-origin=https://vendor-api-noahecampbell.okteto.repldev.com \
--registry-origin=vendor-registry-v2-noahecampbell.okteto.repldev.com`,
Args: cobra.ExactArgs(1),
SilenceUsage: true,
RunE: r.profileAdd,
}
parent.AddCommand(cmd)

cmd.Flags().StringVar(&r.args.profileAddToken, "token", "", "API token for this profile (required)")
cmd.Flags().StringVar(&r.args.profileAddAPIOrigin, "api-origin", "", "API origin (optional, e.g., https://api.replicated.com/vendor)")
cmd.Flags().StringVar(&r.args.profileAddRegistryOrigin, "registry-origin", "", "Registry origin (optional, e.g., registry.replicated.com)")

cmd.MarkFlagRequired("token")

return cmd
}

func (r *runners) profileAdd(_ *cobra.Command, args []string) error {
profileName := args[0]

if profileName == "" {
return errors.New("profile name cannot be empty")
}

if r.args.profileAddToken == "" {
return errors.New("token is required")
}

profile := types.Profile{
APIToken: r.args.profileAddToken,
APIOrigin: r.args.profileAddAPIOrigin,
RegistryOrigin: r.args.profileAddRegistryOrigin,
}

if err := credentials.AddProfile(profileName, profile); err != nil {
return errors.Wrap(err, "failed to add profile")
}

fmt.Printf("Profile '%s' added successfully\n", profileName)

// Check if this is the only profile - if so, it's now the default
_, defaultProfile, err := credentials.ListProfiles()
if err != nil {
return errors.Wrap(err, "failed to check default profile")
}

if defaultProfile == profileName {
fmt.Printf("Profile '%s' set as default\n", profileName)
}

return nil
}
87 changes: 87 additions & 0 deletions cli/cmd/profile_ls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package cmd

import (
"fmt"
"os"
"text/tabwriter"

"github.com/pkg/errors"
"github.com/replicatedhq/replicated/pkg/credentials"
"github.com/spf13/cobra"
)

func (r *runners) InitProfileLsCommand(parent *cobra.Command) *cobra.Command {
cmd := &cobra.Command{
Use: "ls",
Short: "List all authentication profiles",
Long: `List all authentication profiles configured in ~/.replicated/config.yaml.

The default profile is indicated with an asterisk (*). API tokens are masked for security.`,
Example: `# List all profiles
replicated profile ls`,
SilenceUsage: true,
RunE: r.profileLs,
}
parent.AddCommand(cmd)

return cmd
}

func (r *runners) profileLs(_ *cobra.Command, _ []string) error {
profiles, defaultProfile, err := credentials.ListProfiles()
if err != nil {
return errors.Wrap(err, "failed to list profiles")
}

if len(profiles) == 0 {
fmt.Println("No profiles configured")
fmt.Println("")
fmt.Println("To add a profile, run:")
fmt.Println(" replicated profile add <name> --token=<your-token>")
return nil
}

w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "DEFAULT\tNAME\tAPI ORIGIN\tREGISTRY ORIGIN\tTOKEN")

for name, profile := range profiles {
isDefault := ""
if name == defaultProfile {
isDefault = "*"
}

apiOrigin := profile.APIOrigin
if apiOrigin == "" {
apiOrigin = "<default>"
}

registryOrigin := profile.RegistryOrigin
if registryOrigin == "" {
registryOrigin = "<default>"
}

// Mask the token for security (show first 8 chars)
maskedToken := maskToken(profile.APIToken)

fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
isDefault,
name,
apiOrigin,
registryOrigin,
maskedToken,
)
}

w.Flush()
return nil
}

func maskToken(token string) string {
if token == "" {
return ""
}
if len(token) <= 8 {
return token[:len(token)/2] + "..."
}
return token[:8] + "..." + token[len(token)-4:]
}
66 changes: 66 additions & 0 deletions cli/cmd/profile_rm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package cmd

import (
"fmt"

"github.com/pkg/errors"
"github.com/replicatedhq/replicated/pkg/credentials"
"github.com/spf13/cobra"
)

func (r *runners) InitProfileRmCommand(parent *cobra.Command) *cobra.Command {
cmd := &cobra.Command{
Use: "rm [profile-name]",
Short: "Remove an authentication profile",
Long: `Remove an authentication profile by name.

If the removed profile was the default profile, the default will be automatically
set to another available profile (if any exist).`,
Example: `# Remove a profile
replicated profile rm dev`,
Args: cobra.ExactArgs(1),
SilenceUsage: true,
RunE: r.profileRm,
}
parent.AddCommand(cmd)

return cmd
}

func (r *runners) profileRm(_ *cobra.Command, args []string) error {
profileName := args[0]

if profileName == "" {
return errors.New("profile name cannot be empty")
}

// Check if profile exists before removing
_, err := credentials.GetProfile(profileName)
if err == credentials.ErrProfileNotFound {
return errors.Errorf("profile '%s' not found", profileName)
}
if err != nil {
return errors.Wrap(err, "failed to get profile")
}

// Remove the profile
if err := credentials.RemoveProfile(profileName); err != nil {
return errors.Wrap(err, "failed to remove profile")
}

fmt.Printf("Profile '%s' removed successfully\n", profileName)

// Check if there's a new default
_, newDefault, err := credentials.ListProfiles()
if err != nil {
return errors.Wrap(err, "failed to check new default profile")
}

if newDefault != "" {
fmt.Printf("Default profile is now '%s'\n", newDefault)
} else {
fmt.Println("No profiles remaining")
}

return nil
}
51 changes: 51 additions & 0 deletions cli/cmd/profile_set_default.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package cmd

import (
"fmt"

"github.com/pkg/errors"
"github.com/replicatedhq/replicated/pkg/credentials"
"github.com/spf13/cobra"
)

func (r *runners) InitProfileSetDefaultCommand(parent *cobra.Command) *cobra.Command {
cmd := &cobra.Command{
Use: "set-default [profile-name]",
Short: "Set the default authentication profile",
Long: `Set the default authentication profile that will be used when no profile is specified
in .replicated.yaml and no environment variables are set.`,
Example: `# Set production as the default profile
replicated profile set-default prod`,
Args: cobra.ExactArgs(1),
SilenceUsage: true,
RunE: r.profileSetDefault,
}
parent.AddCommand(cmd)

return cmd
}

func (r *runners) profileSetDefault(_ *cobra.Command, args []string) error {
profileName := args[0]

if profileName == "" {
return errors.New("profile name cannot be empty")
}

// Check if profile exists
_, err := credentials.GetProfile(profileName)
if err == credentials.ErrProfileNotFound {
return errors.Errorf("profile '%s' not found", profileName)
}
if err != nil {
return errors.Wrap(err, "failed to get profile")
}

// Set as default
if err := credentials.SetDefaultProfile(profileName); err != nil {
return errors.Wrap(err, "failed to set default profile")
}

fmt.Printf("Default profile set to '%s'\n", profileName)
return nil
}
Loading