maidn-cli/cmd/app_secret.go

217 lines
6.9 KiB
Go

package cmd
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"github.com/Pingu-Studio/MaidnCLI/internal/config"
"github.com/Pingu-Studio/MaidnCLI/internal/openbao"
"github.com/spf13/cobra"
)
var appSecretConfigPath, appSecretFile, appSecretTokenFile, appSecretGrantEnvironment string
var appSecretShared, appSecretDeleteYes bool
var appSecretGrantShared, appSecretGrantSecrets []string
var loadAppSecretConfig = config.Load
var saveAppSecretConfig = config.Save
var storeAppSecret = openbao.StoreManagedSecret
var listAppSecrets = openbao.ListManagedSecrets
var appSecretStatus = openbao.ManagedSecretStatus
var deleteAppSecret = openbao.DeleteManagedSecret
var appSecretCmd = &cobra.Command{
Use: "secret",
Short: "Manage application and shared OpenBao secret values.",
}
var appSecretSetCmd = &cobra.Command{
Use: "set <app-or-group> <secret>",
Short: "Store a value from stdin or --file.",
Args: cobra.ExactArgs(2),
RunE: runAppSecretSet,
}
var appSecretListCmd = &cobra.Command{
Use: "list <app-or-group>",
Short: "List secret names without values.",
Args: cobra.ExactArgs(1),
RunE: runAppSecretList,
}
var appSecretDeleteCmd = &cobra.Command{
Use: "delete <app-or-group> <secret>",
Short: "Permanently delete a secret after explicit confirmation.",
Args: cobra.ExactArgs(2),
RunE: runAppSecretDelete,
}
var appSecretGrantCmd = &cobra.Command{
Use: "grant <app> <build|publish|runtime>",
Short: "Add a declarative SecretGrant to the private bootstrap config.",
Args: cobra.ExactArgs(2),
RunE: runAppSecretGrant,
}
var appSecretStatusCmd = &cobra.Command{
Use: "status <app-or-group> <secret>",
Short: "Report whether a secret exists without reading its value.",
Args: cobra.ExactArgs(2),
RunE: runAppSecretStatus,
}
func init() {
appCmd.AddCommand(appSecretCmd)
appSecretCmd.AddCommand(appSecretSetCmd, appSecretListCmd, appSecretDeleteCmd, appSecretGrantCmd, appSecretStatusCmd)
appSecretCmd.PersistentFlags().StringVar(&appSecretConfigPath, "config", "", "Path to private bootstrap config YAML")
_ = appSecretCmd.MarkPersistentFlagRequired("config")
appSecretCmd.PersistentFlags().StringVar(&appSecretTokenFile, "token-file", "", "Path to restricted OpenBao token file for secret CRUD")
appSecretSetCmd.Flags().StringVar(&appSecretFile, "file", "", "Read the secret value from this file instead of stdin")
for _, command := range []*cobra.Command{appSecretSetCmd, appSecretListCmd, appSecretDeleteCmd, appSecretStatusCmd} {
command.Flags().BoolVar(&appSecretShared, "shared", false, "Use shared/<group>/<secret> instead of apps/<app>/<secret>")
}
appSecretDeleteCmd.Flags().BoolVar(&appSecretDeleteYes, "yes", false, "Confirm permanent deletion")
appSecretGrantCmd.Flags().StringVar(&appSecretGrantEnvironment, "environment", "", "Runtime environment: staging or production")
appSecretGrantCmd.Flags().StringSliceVar(&appSecretGrantSecrets, "secret", nil, "Application secret name granted to this consumer (repeat for each)")
appSecretGrantCmd.Flags().StringSliceVar(&appSecretGrantShared, "shared", nil, "Shared secret group allowed by this grant")
}
func runAppSecretSet(cmd *cobra.Command, args []string) error {
path, kubeconfig, err := appSecretTarget(args)
if err != nil {
return err
}
value, err := readAppSecretValue(cmd)
if err != nil {
return err
}
if err := storeAppSecret(kubeconfig, appSecretTokenFile, path, value); err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "stored %s\n", path)
return nil
}
func runAppSecretList(cmd *cobra.Command, args []string) error {
kubeconfig, err := appSecretKubeconfigFromConfig()
if err != nil {
return err
}
values, err := listAppSecrets(kubeconfig, appSecretTokenFile, appSecretShared, args[0])
if err != nil {
return err
}
for _, value := range values {
fmt.Fprintln(cmd.OutOrStdout(), value)
}
return nil
}
func runAppSecretDelete(cmd *cobra.Command, args []string) error {
if !appSecretDeleteYes {
return errors.New("delete requires --yes")
}
path, kubeconfig, err := appSecretTarget(args)
if err != nil {
return err
}
if err := deleteAppSecret(kubeconfig, appSecretTokenFile, path); err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "deleted %s\n", path)
return nil
}
func runAppSecretGrant(cmd *cobra.Command, args []string) error {
cfg, err := loadRequiredAppSecretConfig()
if err != nil {
return err
}
grant := config.SecretGrant{Application: args[0], Consumer: args[1], Environment: appSecretGrantEnvironment, Secrets: appSecretGrantSecrets, Shared: appSecretGrantShared}
if err := config.ValidateSecretGrants([]config.SecretGrant{grant}); err != nil {
return err
}
for _, existing := range cfg.SecretGrants {
if existing.Application == grant.Application && existing.Consumer == grant.Consumer && existing.Environment == grant.Environment {
if reflect.DeepEqual(existing, grant) {
fmt.Fprintln(cmd.OutOrStdout(), "grant already declared")
return nil
}
return errors.New("secret grant already exists with a different definition")
}
}
cfg.SecretGrants = append(cfg.SecretGrants, grant)
if err := config.ValidateSecretGrants(cfg.SecretGrants); err != nil {
return err
}
if err := saveAppSecretConfig(appSecretConfigPath, cfg); err != nil {
return fmt.Errorf("save declarative secret grant: %w", err)
}
fmt.Fprintln(cmd.OutOrStdout(), "grant declared; reconcile OpenBao through the reviewed bootstrap workflow")
return nil
}
func runAppSecretStatus(cmd *cobra.Command, args []string) error {
path, kubeconfig, err := appSecretTarget(args)
if err != nil {
return err
}
present, err := appSecretStatus(kubeconfig, appSecretTokenFile, path)
if err != nil {
return err
}
if present {
fmt.Fprintf(cmd.OutOrStdout(), "%s: present\n", path)
} else {
fmt.Fprintf(cmd.OutOrStdout(), "%s: absent\n", path)
}
return nil
}
func appSecretTarget(args []string) (string, string, error) {
if len(args) != 2 {
return "", "", errors.New("secret target requires an application or shared group and a secret name")
}
path, err := openbao.ManagedSecretPath(appSecretShared, args[0], args[1])
if err != nil {
return "", "", err
}
kubeconfig, err := appSecretKubeconfigFromConfig()
return path, kubeconfig, err
}
func loadRequiredAppSecretConfig() (config.Config, error) {
if appSecretConfigPath == "" {
return config.Config{}, errors.New("--config is required")
}
return loadAppSecretConfig(appSecretConfigPath)
}
func appSecretKubeconfigFromConfig() (string, error) {
cfg, err := loadRequiredAppSecretConfig()
if err != nil {
return "", err
}
return filepath.Join(cfg.Git.CloneParent, cfg.Talos.RepoDirName, cfg.Talos.GeneratedDir, "kubeconfig"), nil
}
func readAppSecretValue(cmd *cobra.Command) ([]byte, error) {
if appSecretFile != "" {
value, err := os.ReadFile(appSecretFile)
if err != nil {
return nil, fmt.Errorf("read secret file: %w", err)
}
return value, nil
}
value, err := io.ReadAll(cmd.InOrStdin())
if err != nil {
return nil, fmt.Errorf("read secret stdin: %w", err)
}
return value, nil
}