217 lines
6.9 KiB
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
|
|
}
|