From 1c0500193f140b7aa70151837f69e64363744e14 Mon Sep 17 00:00:00 2001 From: Calvin Zachman Date: Mon, 15 Jan 2024 10:53:46 -0600 Subject: [PATCH] lndinit: add ability to write to k8s configmap We'll use this to write connection relateed information for consumption by payment service proxy. --- README.md | 4 + cmd_gen_seed.go | 4 +- cmd_init_wallet.go | 19 ++-- cmd_load_secret.go | 9 +- cmd_store_configmap.go | 127 +++++++++++++++++++++++ cmd_store_secret.go | 36 +++---- example-init-wallet-k8s.sh | 24 ++--- k8s.go | 203 ++++++++++++++++++++++++++++++------- main.go | 1 + version.go | 2 +- 10 files changed, 347 insertions(+), 82 deletions(-) create mode 100644 cmd_store_configmap.go diff --git a/README.md b/README.md index a537f75..0bf41b1 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-configamp` 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..de5c058 100644 --- a/cmd_gen_seed.go +++ b/cmd_gen_seed.go @@ -23,14 +23,14 @@ type jsonSeed struct { type genSeedCommand struct { EntropySourceFile string `long:"entropy-source-file" description:"The file descriptor to read the seed entropy from; if set lndinit will read exactly 16 bytes from the file, otherwise the default crypto/rand source will be used"` PassphraseFile string `long:"passphrase-file" description:"The file to read the seed passphrase from; if not set, no seed passphrase will be used, unless --passhprase-k8s is used"` - PassphraseK8s *k8sSecretOptions `group:"Flags for reading seed passphrase from Kubernetes" namespace:"passphrase-k8s"` + PassphraseK8s *k8sObjectOptions `group:"Flags for reading seed passphrase from Kubernetes" namespace:"passphrase-k8s"` Output string `long:"output" short:"o" description:"Output format" choice:"raw" choice:"json"` } func newGenSeedCommand() *genSeedCommand { return &genSeedCommand{ Output: outputFormatRaw, - PassphraseK8s: &k8sSecretOptions{ + PassphraseK8s: &k8sObjectOptions{ Namespace: defaultK8sNamespace, }, } diff --git a/cmd_init_wallet.go b/cmd_init_wallet.go index 21781c5..6fc195c 100644 --- a/cmd_init_wallet.go +++ b/cmd_init_wallet.go @@ -45,7 +45,7 @@ type secretSourceFile struct { type secretSourceK8s 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"` + Name string `long:"name" description:"The name of the Kubernetes secret"` SeedKeyName string `long:"seed-key-name" description:"The name of the key within the secret that contains the seed"` SeedPassphraseKeyName string `long:"seed-passphrase-key-name" description:"The name of the key within the secret that contains the seed passphrase"` WalletPasswordKeyName string `long:"wallet-password-key-name" description:"The name of the key within the secret that contains the wallet password"` @@ -239,16 +239,17 @@ 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.Name, Base64: x.K8s.Base64, + ObjectType: ObjectTypeSecret, } - k8sSecret.SecretKeyName = x.K8s.SeedKeyName + k8sSecret.KeyName = x.K8s.SeedKeyName if requireSeed { log("Reading seed from k8s secret %s (namespace %s)", - x.K8s.SecretName, x.K8s.Namespace) + x.K8s.Name, x.K8s.Namespace) seed, _, err = readK8s(k8sSecret) if err != nil { return "", "", "", err @@ -258,9 +259,9 @@ func (x *initWalletCommand) readInput(requireSeed bool) (string, string, string, // The seed passphrase is optional. if x.K8s.SeedPassphraseKeyName != "" { log("Reading seed passphrase from k8s secret %s "+ - "(namespace %s)", x.K8s.SecretName, + "(namespace %s)", x.K8s.Name, x.K8s.Namespace) - k8sSecret.SecretKeyName = x.K8s.SeedPassphraseKeyName + k8sSecret.KeyName = x.K8s.SeedPassphraseKeyName seedPassPhrase, _, err = readK8s(k8sSecret) if err != nil { return "", "", "", err @@ -268,8 +269,8 @@ 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 + x.K8s.Name, x.K8s.Namespace) + 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..ed5c9e6 100644 --- a/cmd_load_secret.go +++ b/cmd_load_secret.go @@ -8,15 +8,16 @@ import ( type loadSecretCommand struct { Source string `long:"source" short:"s" description:"Secret storage source" choice:"k8s"` - K8s *k8sSecretOptions `group:"Flags for looking up the secret as a value inside a Kubernetes Secret (use when --source=k8s)" namespace:"k8s"` + K8s *k8sObjectOptions `group:"Flags for looking up the secret as a value inside a Kubernetes Secret (use when --source=k8s)" namespace:"k8s"` Output string `long:"output" short:"o" description:"Output format" choice:"raw" choice:"json"` } func newLoadSecretCommand() *loadSecretCommand { return &loadSecretCommand{ Source: storageK8s, - K8s: &k8sSecretOptions{ - Namespace: defaultK8sNamespace, + K8s: &k8sObjectOptions{ + Namespace: defaultK8sNamespace, + ObjectType: ObjectTypeSecret, }, Output: outputFormatRaw, } @@ -40,7 +41,7 @@ func (x *loadSecretCommand) Execute(_ []string) error { content, secret, err := readK8s(x.K8s) if err != nil { return fmt.Errorf("error reading secret %s in "+ - "namespace %s: %v", x.K8s.SecretName, + "namespace %s: %v", x.K8s.Name, x.K8s.Namespace, err) } diff --git a/cmd_store_configmap.go b/cmd_store_configmap.go new file mode 100644 index 0000000..1f2ac94 --- /dev/null +++ b/cmd_store_configmap.go @@ -0,0 +1,127 @@ +package main + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/jessevdk/go-flags" +) + +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 *targetK8s `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: &targetK8s{ + k8sObjectOptions: k8sObjectOptions{ + Namespace: defaultK8sNamespace, + ObjectType: ObjectTypeConfigMap, + }, + }, + } +} + +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 value/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 *targetK8s, + 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) + + // TODO(calvin): We need to make the Kubernetes write method capable + // of handling multiple different types. + 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..3730297 100644 --- a/cmd_store_secret.go +++ b/cmd_store_secret.go @@ -11,12 +11,12 @@ import ( ) type targetK8s struct { - k8sSecretOptions + k8sObjectOptions 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 } @@ -32,8 +32,9 @@ func newStoreSecretCommand() *storeSecretCommand { return &storeSecretCommand{ Target: storageK8s, K8s: &targetK8s{ - k8sSecretOptions: k8sSecretOptions{ - Namespace: defaultK8sNamespace, + k8sObjectOptions: k8sObjectOptions{ + Namespace: defaultK8sNamespace, + ObjectType: ObjectTypeSecret, }, Helm: &helmOptions{ ResourcePolicy: defaultK8sResourcePolicy, @@ -59,7 +60,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 +76,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 +89,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 { @@ -96,7 +97,7 @@ func (x *storeSecretCommand) Execute(args []string) error { // 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.SecretKeyName + entries[0].key = x.K8s.KeyName } return storeSecretsK8s(entries, x.K8s, x.Overwrite) @@ -106,10 +107,10 @@ func (x *storeSecretCommand) Execute(args []string) error { } } -func storeSecretsK8s(entries []*secretEntry, opts *targetK8s, +func storeSecretsK8s(entries []*entry, opts *targetK8s, overwrite bool) error { - if opts.SecretName == "" { + if opts.Name == "" { return fmt.Errorf("secret name is required") } @@ -118,20 +119,21 @@ 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.Name, + 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 { return fmt.Errorf("error storing secret %s key %s: "+ - "%v", opts.SecretName, entry.key, err) + "%v", opts.Name, entry.key, err) } } diff --git a/example-init-wallet-k8s.sh b/example-init-wallet-k8s.sh index e513f5a..9e5dda9 100644 --- a/example-init-wallet-k8s.sh +++ b/example-init-wallet-k8s.sh @@ -23,8 +23,8 @@ lndinit gen-password \ --target=k8s \ --k8s.base64 \ --k8s.namespace="${WALLET_SECRET_NAMESPACE}" \ - --k8s.secret-name="${WALLET_SECRET_NAME}" \ - --k8s.secret-key-name=walletpassword + --k8s.name="${WALLET_SECRET_NAME}" \ + --k8s.key-name=walletpassword echo "" echo "[STARTUP] Asserting seed exists in secret ${WALLET_SECRET_NAME}" @@ -33,8 +33,8 @@ lndinit gen-seed \ --target=k8s \ --k8s.base64 \ --k8s.namespace="${WALLET_SECRET_NAMESPACE}" \ - --k8s.secret-name="${WALLET_SECRET_NAME}" \ - --k8s.secret-key-name=walletseed + --k8s.name="${WALLET_SECRET_NAME}" \ + --k8s.key-name=walletseed echo "" echo "[STARTUP] Asserting wallet is created with values from secret ${WALLET_SECRET_NAME}" @@ -42,7 +42,7 @@ lndinit -v init-wallet \ --secret-source=k8s \ --k8s.base64 \ --k8s.namespace="${WALLET_SECRET_NAMESPACE}" \ - --k8s.secret-name="${WALLET_SECRET_NAME}" \ + --k8s.name="${WALLET_SECRET_NAME}" \ --k8s.seed-key-name=walletseed \ --k8s.wallet-password-key-name=walletpassword \ --init-file.output-wallet-dir="${WALLET_DIR}" \ @@ -59,8 +59,8 @@ lndinit -v load-secret \ --source=k8s \ --k8s.base64 \ --k8s.namespace="${WALLET_SECRET_NAMESPACE}" \ - --k8s.secret-name="${WALLET_SECRET_NAME}" \ - --k8s.secret-key-name=walletpassword > "${WALLET_PASSWORD_FILE}" & + --k8s.name="${WALLET_SECRET_NAME}" \ + --k8s.key-name=walletpassword > "${WALLET_PASSWORD_FILE}" & # In case we have a remote signing setup, we also need to provision the RPC # secrets of the remote signer. @@ -70,14 +70,14 @@ if [[ "${REMOTE_SIGNING}" == "1" ]]; then --source=k8s \ --k8s.base64 \ --k8s.namespace="${REMOTE_SIGNER_RPC_SECRETS_NAMESPACE}" \ - --k8s.secret-name="${REMOTE_SIGNER_RPC_SECRETS_NAME}" \ - --k8s.secret-key-name=tls.cert > "${REMOTE_SIGNER_RPC_SECRETS_DIR}/tls.cert" + --k8s.name="${REMOTE_SIGNER_RPC_SECRETS_NAME}" \ + --k8s.key-name=tls.cert > "${REMOTE_SIGNER_RPC_SECRETS_DIR}/tls.cert" lndinit -v load-secret \ --source=k8s \ --k8s.base64 \ --k8s.namespace="${REMOTE_SIGNER_RPC_SECRETS_NAMESPACE}" \ - --k8s.secret-name="${REMOTE_SIGNER_RPC_SECRETS_NAME}" \ - --k8s.secret-key-name=admin.macaroon > "${REMOTE_SIGNER_RPC_SECRETS_DIR}/admin.macaroon" + --k8s.name="${REMOTE_SIGNER_RPC_SECRETS_NAME}" \ + --k8s.key-name=admin.macaroon > "${REMOTE_SIGNER_RPC_SECRETS_DIR}/admin.macaroon" fi # In case we want to upload the TLS certificate and macaroons to k8s secrets as @@ -97,7 +97,7 @@ if [[ "${UPLOAD_RPC_SECRETS}" == "1" ]]; then --target=k8s \ --k8s.base64 \ --k8s.namespace="${RPC_SECRETS_NAMESPACE}" \ - --k8s.secret-name="${RPC_SECRETS_NAME}" \ + --k8s.name="${RPC_SECRETS_NAME}" \ "${CERT_DIR}/tls.cert" \ "${WALLET_DIR}"/*.macaroon \ /tmp/accounts.json & diff --git a/k8s.go b/k8s.go index 45dceb4..9b86170 100644 --- a/k8s.go +++ b/k8s.go @@ -17,18 +17,33 @@ const ( defaultK8sResourcePolicy = "keep" ) -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"` - SecretKeyName string `long:"secret-key-name" description:"The name of the key/entry within the secret"` - Base64 bool `long:"base64" description:"Encode as base64 when storing and decode as base64 when reading"` +type k8sObjectType string + +const ( + ObjectTypeSecret k8sObjectType = "Secret" + ObjectTypeConfigMap k8sObjectType = "ConfigMap" +) + +type k8sObjectOptions struct { + Namespace string `long:"namespace" description:"The Kubernetes namespace the object is located in"` + Name string `long:"name" description:"The name of the Kubernetes object"` + KeyName string `long:"key-name" description:"The name of the key/entry within the object"` + Base64 bool `long:"base64" description:"Encode as base64 when storing and decode as base64 when reading"` + ObjectType k8sObjectType `long:"object-type" description:"Kubernetes resource type"` } -func (s *k8sSecretOptions) AnySet() bool { - return s.Namespace != defaultK8sNamespace || s.SecretName != "" || - s.SecretKeyName != "" +func (s *k8sObjectOptions) AnySet() bool { + return s.Namespace != defaultK8sNamespace || s.Name != "" || + s.KeyName != "" } +// type k8sConfigmapOptions struct { +// Namespace string `long:"namespace" description:"The Kubernetes namespace the configamp is located in"` +// ConfigmapName string `long:"configmap-name" description:"The name of the Kubernetes configmap"` +// ConfigmapKeyName string `long:"configmap-key-name" description:"The name of the key/entry within the configmap"` +// Base64 bool `long:"base64" description:"Encode as base64 when storing and decode as base64 when reading"` +// } + 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,38 +55,66 @@ 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 } if exists { - return updateSecretValueK8s( - client, secret, opts, overwrite, content, - ) + return updateSecretValueK8s(client, secret, opts, overwrite, content) } 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 +122,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 +193,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 +210,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 +231,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 +258,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 +267,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 +295,89 @@ 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) + } + + configMap.Data[opts.KeyName] = content + + log("Attempting to update key %s of configmap %s in namespace %s", opts.KeyName, opts.Name, opts.Namespace) + 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.