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(cmd, 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(cmd, args[0], args[1]) }, } versionCmd.AddCommand(listCmd, promoteCmd) return versionCmd } // ListVersions lists all versions of a secret func (cli *CLIInstance) ListVersions(cmd *cobra.Command, secretName string) error { secret.Debug("ListVersions called", "secret_name", secretName) // Get current vault vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir) if err != nil { secret.Debug("Failed to get current vault", "error", err) return err } vaultDir, err := vlt.GetDirectory() if err != nil { secret.Debug("Failed to get vault directory", "error", err) return err } // Get the encoded secret name encodedName := strings.ReplaceAll(secretName, "/", "%") secretDir := filepath.Join(vaultDir, "secrets.d", encodedName) // Check if secret exists exists, err := afero.DirExists(cli.fs, secretDir) if err != nil { secret.Debug("Failed to check if secret exists", "error", err) return fmt.Errorf("failed to check if secret exists: %w", err) } if !exists { secret.Debug("Secret not found", "secret_name", secretName) return fmt.Errorf("secret '%s' not found", secretName) } // List all versions versions, err := secret.ListVersions(cli.fs, secretDir) if err != nil { secret.Debug("Failed to list versions", "error", err) return fmt.Errorf("failed to list versions: %w", err) } if len(versions) == 0 { cmd.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(cmd *cobra.Command, secretName string, version string) error { // Get current vault vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir) if err != nil { return err } vaultDir, err := vlt.GetDirectory() if err != nil { return err } // Get the encoded secret name encodedName := strings.ReplaceAll(secretName, "/", "%") secretDir := filepath.Join(vaultDir, "secrets.d", encodedName) // Check if version exists versionDir := filepath.Join(secretDir, "versions", version) exists, err := afero.DirExists(cli.fs, versionDir) 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 the current symlink currentLink := filepath.Join(secretDir, "current") // Remove existing symlink _ = cli.fs.Remove(currentLink) // Create new symlink to the selected version relativePath := filepath.Join("versions", version) if err := afero.WriteFile(cli.fs, currentLink, []byte(relativePath), 0644); err != nil { return fmt.Errorf("failed to update current version: %w", err) } cmd.Printf("Promoted version %s to current for secret '%s'\n", version, secretName) return nil }