diff --git a/cmd/cu/secrets.go b/cmd/cu/secrets.go new file mode 100644 index 00000000..5702ebda --- /dev/null +++ b/cmd/cu/secrets.go @@ -0,0 +1,131 @@ +package main + +import ( + "errors" + "fmt" + "os" + + "github.com/dagger/container-use/environment" + "github.com/dagger/container-use/repository" + "github.com/spf13/cobra" +) + +var secretsCmd = &cobra.Command{ + Use: "secret", + Short: "Manage environment secrets", + Long: `Add, remove, and list secrets for container-use environments`, +} + +var secretAddCmd = &cobra.Command{ + Use: "add ", + Short: "Add a secret to an environment", + Long: `Add a secret to a container-use environment. +Supported schemas: +- file://PATH: local file path +- env://NAME: environment variable +- op:////[section-name/]: 1Password secret`, + Args: cobra.ExactArgs(2), + ValidArgsFunction: suggestEnvironments, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + secretName := args[0] + secretSpec := args[1] + + repo, err := repository.Open(ctx, ".") + if err != nil { + return fmt.Errorf("failed to open repository: %w", err) + } + + var cfg environment.EnvironmentConfig + if err := cfg.Load(repo.SourcePath()); err != nil { + if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("failed to load environment config: %w", err) + } + } + + if err := cfg.Secrets.AddSecret(secretName, secretSpec); err != nil { + return fmt.Errorf("failed to add secret: %w", err) + } + + if err := cfg.Save(repo.SourcePath()); err != nil { + return fmt.Errorf("failed to update environment config: %w", err) + } + + fmt.Printf("Secret %s successfully added to environment\n", secretName) + return nil + }, +} + +var secretDeleteCmd = &cobra.Command{ + Use: "delete [SECRET_NAME]", + Short: "Delete a secret from an environment", + Args: cobra.ExactArgs(1), + ValidArgsFunction: suggestEnvironments, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + secretName := args[0] + + repo, err := repository.Open(ctx, ".") + if err != nil { + return fmt.Errorf("failed to open repository: %w", err) + } + + var cfg environment.EnvironmentConfig + if err := cfg.Load(repo.SourcePath()); err != nil { + if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("failed to load environment config: %w", err) + } + } + + if err := cfg.Secrets.DeleteSecret(secretName); err != nil { + return fmt.Errorf("failed to delete secret: %w", err) + } + + if err := cfg.Save(repo.SourcePath()); err != nil { + return fmt.Errorf("failed to update environment config: %w", err) + } + + fmt.Printf("Secret %s successfully deleted\n", secretName) + return nil + }, +} + +var secretListCmd = &cobra.Command{ + Use: "list", + Short: "List all secrets in an environment", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + repo, err := repository.Open(ctx, ".") + if err != nil { + return fmt.Errorf("failed to open repository: %w", err) + } + + var cfg environment.EnvironmentConfig + if err := cfg.Load(repo.SourcePath()); err != nil { + if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("failed to load environment config: %w", err) + } + } + + secretNames := cfg.Secrets.List() + if len(secretNames) == 0 { + fmt.Printf("No secrets found\n") + return nil + } + + fmt.Printf("Secrets:\n") + for _, name := range secretNames { + fmt.Printf("- %s\n", name) + } + return nil + }, +} + +func init() { + secretsCmd.AddCommand(secretAddCmd) + secretsCmd.AddCommand(secretDeleteCmd) + secretsCmd.AddCommand(secretListCmd) + rootCmd.AddCommand(secretsCmd) +} diff --git a/environment/config.go b/environment/config.go index 263359e6..ecefcc30 100644 --- a/environment/config.go +++ b/environment/config.go @@ -29,7 +29,7 @@ type EnvironmentConfig struct { BaseImage string `json:"base_image,omitempty"` SetupCommands []string `json:"setup_commands,omitempty"` Env []string `json:"env,omitempty"` - Secrets []string `json:"secrets,omitempty"` + Secrets Secrets `json:"secrets,omitempty"` Services ServiceConfigs `json:"services,omitempty"` } diff --git a/environment/secret.go b/environment/secret.go new file mode 100644 index 00000000..c5793334 --- /dev/null +++ b/environment/secret.go @@ -0,0 +1,90 @@ +package environment + +import ( + "fmt" + "strings" +) + +// Secrets represents a list of secret specifications in the format NAME=schema://location +type Secrets []string + +// AddSecret adds a new secret to the list +func (s *Secrets) AddSecret(name, spec string) error { + // Validate secret format + if err := validateSecretSpec(spec); err != nil { + return err + } + + // Check if secret already exists + if s.Get(name) != "" { + return fmt.Errorf("secret %s already exists", name) + } + + *s = append(*s, fmt.Sprintf("%s=%s", name, spec)) + return nil +} + +// DeleteSecret removes a secret by name +func (s *Secrets) DeleteSecret(name string) error { + if s.Get(name) == "" { + return fmt.Errorf("secret %s not found", name) + } + + newSecrets := make([]string, 0, len(*s)) + for _, secret := range *s { + secretName, _ := parseSecretSpec(secret) + if secretName != name { + newSecrets = append(newSecrets, secret) + } + } + *s = newSecrets + return nil +} + +// Get returns the full secret spec for a given secret name, or empty string if not found +func (s Secrets) Get(name string) string { + for _, secret := range s { + secretName, _ := parseSecretSpec(secret) + if secretName == name { + return secret + } + } + return "" +} + +// List returns all secret names +func (s Secrets) List() []string { + names := make([]string, 0, len(s)) + for _, secret := range s { + name, _ := parseSecretSpec(secret) + names = append(names, name) + } + return names +} + +// parseSecretSpec splits a secret specification into name and value +func parseSecretSpec(spec string) (name, value string) { + parts := strings.SplitN(spec, "=", 2) + if len(parts) != 2 { + return spec, "" + } + return parts[0], parts[1] +} + +// validateSecretSpec ensures the secret specification is valid +func validateSecretSpec(value string) error { + schemaParts := strings.SplitN(value, "://", 2) + if len(schemaParts) != 2 { + return fmt.Errorf("invalid secret value format: %s (expected schema://value)", value) + } + + schema := schemaParts[0] + switch schema { + case "file", "env", "op": + // Valid schemas + default: + return fmt.Errorf("unsupported secret schema: %s", schema) + } + + return nil +}