package cli import ( "fmt" "os" "path/filepath" "strings" "text/tabwriter" "git.eeqj.de/sneak/secret/internal/secret" "git.eeqj.de/sneak/secret/internal/vault" "github.com/spf13/afero" "github.com/spf13/cobra" ) // newVersionCmd returns the version management command func newVersionCmd() *cobra.Command { cli := NewCLIInstance() return VersionCommands(cli) } // VersionCommands returns the version management commands func VersionCommands(cli *CLIInstance) *cobra.Command { versionCmd := &cobra.Command{ Use: "version", Short: "Manage secret versions", Long: "Commands for managing secret versions including listing, promoting, and retrieving specific versions", } // List versions command listCmd := &cobra.Command{ Use: "list ", Short: "List all versions of a secret", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { return cli.ListVersions(args[0]) }, } // Promote version command promoteCmd := &cobra.Command{ Use: "promote ", Short: "Promote a specific version to current", Long: "Updates the current symlink to point to the specified version without modifying timestamps", Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { return cli.PromoteVersion(args[0], args[1]) }, } versionCmd.AddCommand(listCmd, promoteCmd) return versionCmd } // ListVersions lists all versions of a secret func (cli *CLIInstance) ListVersions(secretName string) error { secret.Debug("Listing versions for secret", "secret_name", secretName) // Get current vault vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir) if err != nil { return fmt.Errorf("failed to get current vault: %w", err) } // Get vault directory vaultDir, err := vlt.GetDirectory() if err != nil { return fmt.Errorf("failed to get vault directory: %w", err) } // Convert secret name to storage name storageName := strings.ReplaceAll(secretName, "/", "%") secretDir := filepath.Join(vaultDir, "secrets.d", storageName) // Check if secret exists exists, err := afero.DirExists(cli.fs, secretDir) if err != nil { return fmt.Errorf("failed to check if secret exists: %w", err) } if !exists { return fmt.Errorf("secret %s not found", secretName) } // Get all versions versions, err := secret.ListVersions(cli.fs, secretDir) if err != nil { return fmt.Errorf("failed to list versions: %w", err) } if len(versions) == 0 { fmt.Println("No versions found") return nil } // Get current version currentVersion, err := secret.GetCurrentVersion(cli.fs, secretDir) if err != nil { secret.Debug("Failed to get current version", "error", err) currentVersion = "" } // Get long-term key for decrypting metadata ltIdentity, err := vlt.GetOrDeriveLongTermKey() if err != nil { return fmt.Errorf("failed to get long-term key: %w", err) } // Create table writer w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "VERSION\tCREATED\tSTATUS\tNOT_BEFORE\tNOT_AFTER") // Load and display each version's metadata for _, version := range versions { sv := secret.NewSecretVersion(vlt, secretName, version) // Load metadata if err := sv.LoadMetadata(ltIdentity); err != nil { secret.Debug("Failed to load version metadata", "version", version, "error", err) // Display version with error status := "error" if version == currentVersion { status = "current (error)" } fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", version, "-", status, "-", "-") continue } // Determine status status := "expired" if version == currentVersion { status = "current" } // Format timestamps createdAt := "-" if sv.Metadata.CreatedAt != nil { createdAt = sv.Metadata.CreatedAt.Format("2006-01-02 15:04:05") } notBefore := "-" if sv.Metadata.NotBefore != nil { notBefore = sv.Metadata.NotBefore.Format("2006-01-02 15:04:05") } notAfter := "-" if sv.Metadata.NotAfter != nil { notAfter = sv.Metadata.NotAfter.Format("2006-01-02 15:04:05") } fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", version, createdAt, status, notBefore, notAfter) } w.Flush() return nil } // PromoteVersion promotes a specific version to current func (cli *CLIInstance) PromoteVersion(secretName string, version string) error { secret.Debug("Promoting version", "secret_name", secretName, "version", version) // Get current vault vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir) if err != nil { return fmt.Errorf("failed to get current vault: %w", err) } // Get vault directory vaultDir, err := vlt.GetDirectory() if err != nil { return fmt.Errorf("failed to get vault directory: %w", err) } // Convert secret name to storage name storageName := strings.ReplaceAll(secretName, "/", "%") secretDir := filepath.Join(vaultDir, "secrets.d", storageName) // Check if secret exists exists, err := afero.DirExists(cli.fs, secretDir) if err != nil { return fmt.Errorf("failed to check if secret exists: %w", err) } if !exists { return fmt.Errorf("secret %s not found", secretName) } // Check if version exists versionPath := filepath.Join(secretDir, "versions", version) exists, err = afero.DirExists(cli.fs, versionPath) if err != nil { return fmt.Errorf("failed to check if version exists: %w", err) } if !exists { return fmt.Errorf("version %s not found for secret %s", version, secretName) } // Update current symlink if err := secret.SetCurrentVersion(cli.fs, secretDir, version); err != nil { return fmt.Errorf("failed to promote version: %w", err) } fmt.Printf("Promoted version %s to current for secret '%s'\n", version, secretName) return nil }