diff --git a/README.md b/README.md index a537f75..bfaec4f 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ initialization, including seed and password generation. - [`gen-seed`](#gen-seed) - [`load-secret`](#load-secret) - [`store-secret`](#store-secret) + - [`store-configmap`](#store-configmap) - [`init-wallet`](#init-wallet) - [`wait-ready`](#wait-ready) - [Example usage](#example-usage) @@ -48,6 +49,9 @@ No `lnd` needed, but seed will be in `lnd`-specific [`aezeed` format](https://gi ### store-secret `store-secret` interacts with kubernetes to write to secrets (no `lnd` needed) +### store-configmap +`store-configmap` interacts with kubernetes to write to configmaps (no `lnd` needed) + ### init-wallet `init-wallet` has two modes: - `--init-type=file` creates an `lnd` specific `wallet.db` file diff --git a/cmd_gen_seed.go b/cmd_gen_seed.go index 15babe6..8fe7988 100644 --- a/cmd_gen_seed.go +++ b/cmd_gen_seed.go @@ -68,7 +68,15 @@ func (x *genSeedCommand) Execute(_ []string) error { // Read passphrase from Kubernetes secret. case x.PassphraseK8s.AnySet(): - passPhrase, _, err = readK8s(x.PassphraseK8s) + k8sSecret := &k8sObjectOptions{ + Namespace: x.PassphraseK8s.Namespace, + Name: x.PassphraseK8s.SecretName, + KeyName: x.PassphraseK8s.SecretKeyName, + Base64: x.PassphraseK8s.Base64, + ObjectType: ObjectTypeSecret, + } + + passPhrase, _, err = readK8s(k8sSecret) } if err != nil { diff --git a/cmd_init_wallet.go b/cmd_init_wallet.go index 21781c5..40f8055 100644 --- a/cmd_init_wallet.go +++ b/cmd_init_wallet.go @@ -239,12 +239,13 @@ func (x *initWalletCommand) readInput(requireSeed bool) (string, string, string, // Read passphrase from Kubernetes secret. case storageK8s: - k8sSecret := &k8sSecretOptions{ + k8sSecret := &k8sObjectOptions{ Namespace: x.K8s.Namespace, - SecretName: x.K8s.SecretName, + Name: x.K8s.SecretName, + KeyName: x.K8s.SeedKeyName, Base64: x.K8s.Base64, + ObjectType: ObjectTypeSecret, } - k8sSecret.SecretKeyName = x.K8s.SeedKeyName if requireSeed { log("Reading seed from k8s secret %s (namespace %s)", @@ -260,7 +261,7 @@ func (x *initWalletCommand) readInput(requireSeed bool) (string, string, string, log("Reading seed passphrase from k8s secret %s "+ "(namespace %s)", x.K8s.SecretName, x.K8s.Namespace) - k8sSecret.SecretKeyName = x.K8s.SeedPassphraseKeyName + k8sSecret.KeyName = x.K8s.SeedPassphraseKeyName seedPassPhrase, _, err = readK8s(k8sSecret) if err != nil { return "", "", "", err @@ -269,7 +270,7 @@ func (x *initWalletCommand) readInput(requireSeed bool) (string, string, string, log("Reading wallet password from k8s secret %s (namespace %s)", x.K8s.SecretName, x.K8s.Namespace) - k8sSecret.SecretKeyName = x.K8s.WalletPasswordKeyName + k8sSecret.KeyName = x.K8s.WalletPasswordKeyName walletPassword, _, err = readK8s(k8sSecret) if err != nil { return "", "", "", err diff --git a/cmd_load_secret.go b/cmd_load_secret.go index 2273a3a..db018c9 100644 --- a/cmd_load_secret.go +++ b/cmd_load_secret.go @@ -37,7 +37,15 @@ func (x *loadSecretCommand) Register(parser *flags.Parser) error { func (x *loadSecretCommand) Execute(_ []string) error { switch x.Source { case storageK8s: - content, secret, err := readK8s(x.K8s) + objectOpts := &k8sObjectOptions{ + Namespace: x.K8s.Namespace, + Name: x.K8s.SecretName, + KeyName: x.K8s.SecretKeyName, + Base64: x.K8s.Base64, + ObjectType: ObjectTypeSecret, + } + + content, secret, err := readK8s(objectOpts) if err != nil { return fmt.Errorf("error reading secret %s in "+ "namespace %s: %v", x.K8s.SecretName, diff --git a/cmd_store_configmap.go b/cmd_store_configmap.go new file mode 100644 index 0000000..a7ba094 --- /dev/null +++ b/cmd_store_configmap.go @@ -0,0 +1,130 @@ +package main + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/jessevdk/go-flags" +) + +type targetK8sConfigmap struct { + k8sConfigmapOptions + + Helm *helmOptions `group:"Flags for configuring the Helm annotations (use when --target=k8s)" namespace:"helm"` +} + +type storeConfigmapCommand struct { + Batch bool `long:"batch" description:"Instead of reading one configmap from stdin, read all files of the argument list and store them as entries in the configmap"` + Overwrite bool `long:"overwrite" description:"Overwrite existing configmap entries instead of aborting"` + Target string `long:"target" short:"t" description:"Configmap storage target" choice:"k8s"` + K8s *targetK8sConfigmap `group:"Flags for storing the key/value pair inside a Kubernetes Configmap (use when --target=k8s)" namespace:"k8s"` +} + +func newStoreConfigmapCommand() *storeConfigmapCommand { + return &storeConfigmapCommand{ + Target: storageK8s, + K8s: &targetK8sConfigmap{ + k8sConfigmapOptions: k8sConfigmapOptions{ + Namespace: defaultK8sNamespace, + }, + }, + } +} + +func (x *storeConfigmapCommand) Register(parser *flags.Parser) error { + _, err := parser.AddCommand( + "store-configmap", + "Write key/value pairs to a Kubernetes configmap", + "Read a configmap from stdin and store it to the "+ + "external configmaps storage indicated by the --target "+ + "flag; if the --batch flag is used, instead of "+ + "reading a single configmap entry from stdin, each command "+ + "line argument is treated as a file and each file's "+ + "content is added to the configmap with the file's name "+ + "as the key name for the configmap entry", + x, + ) + return err +} + +func (x *storeConfigmapCommand) Execute(args []string) error { + var entries []*entry + + switch { + case x.Batch && len(args) == 0: + return fmt.Errorf("at least one command line argument is " + + "required when using --batch flag") + + case x.Batch: + for _, file := range args { + log("Reading value/entry from file %s", file) + content, err := readFile(file) + if err != nil { + return fmt.Errorf("cannot read file %s: %v", + file, err) + } + + entries = append(entries, &entry{ + key: filepath.Base(file), + value: content, + }) + } + + default: + log("Reading value/entry from stdin") + value, err := io.ReadAll(os.Stdin) + if err != nil { + return fmt.Errorf("error reading entry from stdin: %v", err) + } + entries = append(entries, &entry{value: string(value)}) + } + + switch x.Target { + case storageK8s: + // Take the actual entry key from the options if we aren't in + // batch mode. + if len(entries) == 1 && entries[0].key == "" { + entries[0].key = x.K8s.KeyName + } + + return storeConfigmapsK8s(entries, x.K8s, x.Overwrite) + + default: + return fmt.Errorf("invalid configmap storage target %s", x.Target) + } +} + +func storeConfigmapsK8s(entries []*entry, opts *targetK8sConfigmap, + overwrite bool) error { + + if opts.Name == "" { + return fmt.Errorf("configmap name is required") + } + + for _, entry := range entries { + if entry.key == "" { + return fmt.Errorf("configmap entry key name is required") + } + + entryOpts := &k8sObjectOptions{ + Namespace: opts.Namespace, + Name: opts.Name, + KeyName: entry.key, + ObjectType: ObjectTypeConfigMap, + } + + log("Storing key with name %s to configmap %s in namespace %s", + entryOpts.KeyName, entryOpts.Name, + entryOpts.Namespace) + + err := saveK8s(entry.value, entryOpts, overwrite, opts.Helm) + if err != nil { + return fmt.Errorf("error storing key %s in configmap %s: "+ + "%v", entry.key, opts.Name, err) + } + } + + return nil +} diff --git a/cmd_store_secret.go b/cmd_store_secret.go index b9d3a47..14f4e55 100644 --- a/cmd_store_secret.go +++ b/cmd_store_secret.go @@ -10,28 +10,28 @@ import ( "github.com/jessevdk/go-flags" ) -type targetK8s struct { +type targetK8sSecret struct { k8sSecretOptions Helm *helmOptions `group:"Flags for configuring the Helm annotations (use when --target=k8s)" namespace:"helm"` } -type secretEntry struct { +type entry struct { key string value string } type storeSecretCommand struct { - Batch bool `long:"batch" description:"Instead of reading one secret from stdin, read all files of the argument list and store them as entries in the secret"` - Overwrite bool `long:"overwrite" description:"Overwrite existing secret entries instead of aborting"` - Target string `long:"target" short:"t" description:"Secret storage target" choice:"k8s"` - K8s *targetK8s `group:"Flags for storing the secret as a value inside a Kubernetes Secret (use when --target=k8s)" namespace:"k8s"` + Batch bool `long:"batch" description:"Instead of reading one secret from stdin, read all files of the argument list and store them as entries in the secret"` + Overwrite bool `long:"overwrite" description:"Overwrite existing secret entries instead of aborting"` + Target string `long:"target" short:"t" description:"Secret storage target" choice:"k8s"` + K8s *targetK8sSecret `group:"Flags for storing the secret as a value inside a Kubernetes Secret (use when --target=k8s)" namespace:"k8s"` } func newStoreSecretCommand() *storeSecretCommand { return &storeSecretCommand{ Target: storageK8s, - K8s: &targetK8s{ + K8s: &targetK8sSecret{ k8sSecretOptions: k8sSecretOptions{ Namespace: defaultK8sNamespace, }, @@ -59,7 +59,7 @@ func (x *storeSecretCommand) Register(parser *flags.Parser) error { } func (x *storeSecretCommand) Execute(args []string) error { - var entries []*secretEntry + var entries []*entry switch { case x.Batch && len(args) == 0: @@ -75,7 +75,7 @@ func (x *storeSecretCommand) Execute(args []string) error { file, err) } - entries = append(entries, &secretEntry{ + entries = append(entries, &entry{ key: filepath.Base(file), value: content, }) @@ -88,7 +88,7 @@ func (x *storeSecretCommand) Execute(args []string) error { return fmt.Errorf("error reading secret from stdin: %v", err) } - entries = append(entries, &secretEntry{value: secret}) + entries = append(entries, &entry{value: secret}) } switch x.Target { @@ -106,7 +106,7 @@ func (x *storeSecretCommand) Execute(args []string) error { } } -func storeSecretsK8s(entries []*secretEntry, opts *targetK8s, +func storeSecretsK8s(entries []*entry, opts *targetK8sSecret, overwrite bool) error { if opts.SecretName == "" { @@ -118,15 +118,16 @@ func storeSecretsK8s(entries []*secretEntry, opts *targetK8s, return fmt.Errorf("secret key name is required") } - entryOpts := &k8sSecretOptions{ - Namespace: opts.Namespace, - SecretName: opts.SecretName, - SecretKeyName: entry.key, - Base64: opts.Base64, + entryOpts := &k8sObjectOptions{ + Namespace: opts.Namespace, + Name: opts.SecretName, + KeyName: entry.key, + Base64: opts.Base64, + ObjectType: ObjectTypeSecret, } log("Storing key with name %s to secret %s in namespace %s", - entryOpts.SecretKeyName, entryOpts.SecretName, + entryOpts.KeyName, entryOpts.Name, entryOpts.Namespace) err := saveK8s(entry.value, entryOpts, overwrite, opts.Helm) if err != nil { diff --git a/k8s.go b/k8s.go index 45dceb4..1f92adc 100644 --- a/k8s.go +++ b/k8s.go @@ -17,6 +17,13 @@ const ( defaultK8sResourcePolicy = "keep" ) +type k8sObjectType string + +const ( + ObjectTypeSecret k8sObjectType = "Secret" + ObjectTypeConfigMap k8sObjectType = "ConfigMap" +) + type k8sSecretOptions struct { Namespace string `long:"namespace" description:"The Kubernetes namespace the secret is located in"` SecretName string `long:"secret-name" description:"The name of the Kubernetes secret"` @@ -29,6 +36,20 @@ func (s *k8sSecretOptions) AnySet() bool { s.SecretKeyName != "" } +type k8sConfigmapOptions struct { + Namespace string `long:"namespace" description:"The Kubernetes namespace the configmap is located in"` + Name string `long:"configmap-name" description:"The name of the Kubernetes configmap"` + KeyName string `long:"configmap-key-name" description:"The name of the key/entry within the configmap"` +} + +type k8sObjectOptions struct { + Namespace string + Name string + KeyName string + Base64 bool + ObjectType k8sObjectType +} + type helmOptions struct { Annotate bool `long:"annotate" description:"Whether Helm annotations should be added to the created secret"` ReleaseName string `long:"release-name" description:"The value for the meta.helm.sh/release-name annotation"` @@ -40,17 +61,28 @@ type jsonK8sObject struct { metav1.ObjectMeta `json:"metadata,omitempty"` } -func saveK8s(content string, opts *k8sSecretOptions, overwrite bool, - helm *helmOptions) error { +func saveK8s(content string, opts *k8sObjectOptions, + overwrite bool, helm *helmOptions) error { client, err := getClientK8s() if err != nil { return err } - secret, exists, err := getSecretK8s( - client, opts.Namespace, opts.SecretName, - ) + switch opts.ObjectType { + case ObjectTypeSecret: + return saveSecretK8s(client, content, opts, overwrite, helm) + case ObjectTypeConfigMap: + return saveConfigMapK8s(client, content, opts, overwrite, helm) + default: + return fmt.Errorf("unsupported object type: %s", opts.ObjectType) + } +} + +func saveSecretK8s(client *kubernetes.Clientset, content string, + opts *k8sObjectOptions, overwrite bool, helm *helmOptions) error { + + secret, exists, err := getSecretK8s(client, opts.Namespace, opts.Name) if err != nil { return err } @@ -64,14 +96,46 @@ func saveK8s(content string, opts *k8sSecretOptions, overwrite bool, return createSecretK8s(client, opts, helm, content) } -func readK8s(opts *k8sSecretOptions) (string, *jsonK8sObject, error) { +func saveConfigMapK8s(client *kubernetes.Clientset, content string, + opts *k8sObjectOptions, overwrite bool, helm *helmOptions) error { + + configMap, exists, err := getConfigMapK8s( + client, opts.Namespace, opts.Name, + ) + if err != nil { + return err + } + + if exists { + return updateConfigMapValueK8s( + client, configMap, opts, overwrite, content, + ) + } + + return createConfigMapK8s(client, opts, helm, content) +} + +func readK8s(opts *k8sObjectOptions) (string, *jsonK8sObject, error) { client, err := getClientK8s() if err != nil { return "", nil, err } + switch opts.ObjectType { + case ObjectTypeSecret: + return readSecretK8s(client, opts) + default: + return "", nil, fmt.Errorf("unsupported object type: %s", + opts.ObjectType) + } +} + +func readSecretK8s(client *kubernetes.Clientset, + opts *k8sObjectOptions) (string, *jsonK8sObject, error) { + + // Existing logic to read a secret secret, exists, err := getSecretK8s( - client, opts.Namespace, opts.SecretName, + client, opts.Namespace, opts.Name, ) if err != nil { return "", nil, err @@ -79,28 +143,28 @@ func readK8s(opts *k8sSecretOptions) (string, *jsonK8sObject, error) { if !exists { return "", nil, fmt.Errorf("secret %s does not exist in "+ - "namespace %s", opts.SecretName, opts.Namespace) + "namespace %s", opts.Name, opts.Namespace) } if len(secret.Data) == 0 { return "", nil, fmt.Errorf("secret %s exists but contains no "+ - "data", opts.SecretName) + "data", opts.Name) } - if len(secret.Data[opts.SecretKeyName]) == 0 { + if len(secret.Data[opts.KeyName]) == 0 { return "", nil, fmt.Errorf("secret %s exists but does not "+ - "contain the key %s", opts.SecretName, - opts.SecretKeyName) + "contain the key %s", opts.Name, + opts.KeyName) } // There is an additional layer of base64 encoding applied to each of // the secrets. Try to de-code it now. content, err := secretToString( - secret.Data[opts.SecretKeyName], opts.Base64, + secret.Data[opts.KeyName], opts.Base64, ) if err != nil { return "", nil, fmt.Errorf("failed to decode raw secret %s "+ - "key %s: %v", opts.SecretName, opts.SecretKeyName, err) + "key %s: %v", opts.Name, opts.KeyName, err) } return content, &jsonK8sObject{ @@ -150,16 +214,16 @@ func getSecretK8s(client *kubernetes.Clientset, namespace, } func updateSecretValueK8s(client *kubernetes.Clientset, secret *api.Secret, - opts *k8sSecretOptions, overwrite bool, content string) error { + opts *k8sObjectOptions, overwrite bool, content string) error { if len(secret.Data) == 0 { - log("Data of secret %s is empty, initializing", opts.SecretName) + log("Data of secret %s is empty, initializing", opts.Name) secret.Data = make(map[string][]byte) } - if len(secret.Data[opts.SecretKeyName]) > 0 && !overwrite { + if len(secret.Data[opts.KeyName]) > 0 && !overwrite { return fmt.Errorf("key %s in secret %s already exists: %v", - opts.SecretKeyName, opts.SecretName, + opts.KeyName, opts.Name, errTargetExists) } @@ -167,16 +231,16 @@ func updateSecretValueK8s(client *kubernetes.Clientset, secret *api.Secret, if opts.Base64 { content = base64.StdEncoding.EncodeToString([]byte(content)) } - secret.Data[opts.SecretKeyName] = []byte(content) + secret.Data[opts.KeyName] = []byte(content) log("Attempting to update key %s of secret %s in namespace %s", - opts.SecretKeyName, opts.SecretName, opts.Namespace) + opts.KeyName, opts.Name, opts.Namespace) updatedSecret, err := client.CoreV1().Secrets(opts.Namespace).Update( context.Background(), secret, metav1.UpdateOptions{}, ) if err != nil { return fmt.Errorf("error updating secret %s in namespace %s: "+ - "%v", opts.SecretName, opts.Namespace, err) + "%v", opts.Name, opts.Namespace, err) } jsonSecret, _ := asJSON(jsonK8sObject{ @@ -188,11 +252,11 @@ func updateSecretValueK8s(client *kubernetes.Clientset, secret *api.Secret, return nil } -func createSecretK8s(client *kubernetes.Clientset, opts *k8sSecretOptions, +func createSecretK8s(client *kubernetes.Clientset, opts *k8sObjectOptions, helm *helmOptions, content string) error { meta := metav1.ObjectMeta{ - Name: opts.SecretName, + Name: opts.Name, } if helm != nil && helm.Annotate { @@ -215,7 +279,7 @@ func createSecretK8s(client *kubernetes.Clientset, opts *k8sSecretOptions, Type: api.SecretTypeOpaque, ObjectMeta: meta, Data: map[string][]byte{ - opts.SecretKeyName: []byte(content), + opts.KeyName: []byte(content), }, } @@ -224,7 +288,7 @@ func createSecretK8s(client *kubernetes.Clientset, opts *k8sSecretOptions, ) if err != nil { return fmt.Errorf("error creating secret %s in namespace %s: "+ - "%v", opts.SecretName, opts.Namespace, err) + "%v", opts.Name, opts.Namespace, err) } jsonSecret, _ := asJSON(jsonK8sObject{ @@ -252,3 +316,103 @@ func secretToString(rawSecret []byte, doubleBase64 bool) (string, error) { return content, nil } + +func getConfigMapK8s(client *kubernetes.Clientset, + namespace, name string) (*api.ConfigMap, bool, error) { + + log("Attempting to load configmap %s from namespace %s", name, namespace) + configMap, err := client.CoreV1().ConfigMaps(namespace).Get( + context.Background(), name, metav1.GetOptions{}, + ) + + switch { + case err == nil: + log("ConfigMap %s loaded successfully", name) + return configMap, true, nil + + case errors.IsNotFound(err): + log("ConfigMap %s not found in namespace %s", name, namespace) + return nil, false, nil + + default: + return nil, false, fmt.Errorf("error querying configmap "+ + "existence: %v", err) + } +} + +func updateConfigMapValueK8s(client *kubernetes.Clientset, + configMap *api.ConfigMap, opts *k8sObjectOptions, + overwrite bool, content string) error { + + if configMap.Data == nil { + log("Data of configmap %s is empty, initializing", opts.Name) + configMap.Data = make(map[string]string) + } + + if _, exists := configMap.Data[opts.KeyName]; exists && !overwrite { + return fmt.Errorf("key %s in configmap %s already exists", + opts.KeyName, opts.Name) + } + + log("Attempting to update key %s of configmap %s in namespace %s", + opts.KeyName, opts.Name, opts.Namespace) + + configMap.Data[opts.KeyName] = content + updatedConfigMap, err := client.CoreV1().ConfigMaps(opts.Namespace).Update( + context.Background(), configMap, metav1.UpdateOptions{}, + ) + if err != nil { + return fmt.Errorf("error updating configmap %s in namespace %s: %v", + opts.Name, opts.Namespace, err) + } + + jsonConfigMap, _ := asJSON(jsonK8sObject{ + TypeMeta: updatedConfigMap.TypeMeta, + ObjectMeta: updatedConfigMap.ObjectMeta, + }) + log("Updated configmap: %s", jsonConfigMap) + + return nil +} + +func createConfigMapK8s(client *kubernetes.Clientset, + opts *k8sObjectOptions, helm *helmOptions, content string) error { + + meta := metav1.ObjectMeta{ + Name: opts.Name, + } + + if helm != nil && helm.Annotate { + meta.Labels = map[string]string{ + "app.kubernetes.io/managed-by": "Helm", + } + meta.Annotations = map[string]string{ + "helm.sh/resource-policy": helm.ResourcePolicy, + "meta.helm.sh/release-name": helm.ReleaseName, + "meta.helm.sh/release-namespace": opts.Namespace, + } + } + + newConfigMap := &api.ConfigMap{ + ObjectMeta: meta, + Data: map[string]string{ + opts.KeyName: content, + }, + } + + updatedConfigMap, err := client.CoreV1().ConfigMaps(opts.Namespace).Create( + context.Background(), newConfigMap, metav1.CreateOptions{}, + ) + if err != nil { + return fmt.Errorf("error creating configmap %s in namespace %s: %v", + opts.Name, opts.Namespace, err) + } + + jsonConfigMap, _ := asJSON(jsonK8sObject{ + TypeMeta: updatedConfigMap.TypeMeta, + ObjectMeta: updatedConfigMap.ObjectMeta, + }) + log("Created configmap: %s", jsonConfigMap) + + return nil +} diff --git a/main.go b/main.go index 92d8168..97ca0f1 100644 --- a/main.go +++ b/main.go @@ -115,6 +115,7 @@ func registerCommands(parser *flags.Parser) error { newLoadSecretCommand(), newInitWalletCommand(), newStoreSecretCommand(), + newStoreConfigmapCommand(), newWaitReadyCommand(), } diff --git a/version.go b/version.go index 784c1bf..1159be1 100644 --- a/version.go +++ b/version.go @@ -32,7 +32,7 @@ const ( AppMinor uint = 1 // AppPatch defines the application patch for this binary. - AppPatch uint = 17 + AppPatch uint = 18 // AppPreRelease MUST only contain characters from semanticAlphabet // per the semantic versioning spec.