From 0edb3f9925162eb07566c4a64276af65d22bfd97 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Tue, 2 Jun 2026 23:13:25 -0700 Subject: [PATCH] feat: expand kubernetes plugin with defaults, named args, and docs - Add defaultSecret/defaultConfigMap to @initKubernetes() for the common one-Secret-per-app deployment pattern - Support mixed positional + named args (id=, name=, key=) on all four resolver functions, with explicit conflict errors - Rename KubernetesAuthConfig -> KubernetesInstanceConfig (now holds more than auth) - Switch icon to mdi:kubernetes - Drop dead 'unreachable' error guards in the bulk resolvers - Expand website docs and README to match other plugins (scope statement, Discord pointer for deeper k8s integration, auth priority, RBAC setup, formal Reference, troubleshooting) - Add tests for defaults, named args, and conflict errors --- packages/plugins/kubernetes/README.md | 350 ++++++++++-- packages/plugins/kubernetes/src/plugin.ts | 220 ++++--- .../kubernetes/test/kubernetes.test.ts | 86 +++ .../src/content/docs/plugins/kubernetes.mdx | 539 ++++++++++++++++-- 4 files changed, 1036 insertions(+), 159 deletions(-) diff --git a/packages/plugins/kubernetes/README.md b/packages/plugins/kubernetes/README.md index 623ef798b..c921c76cc 100644 --- a/packages/plugins/kubernetes/README.md +++ b/packages/plugins/kubernetes/README.md @@ -2,14 +2,27 @@ Load values from Kubernetes [Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) and [ConfigMaps](https://kubernetes.io/docs/concepts/configuration/configmap/) into your Varlock configuration. +## Scope + +This plugin is **read-only**. It performs `get` requests on Secrets and ConfigMaps in a configured namespace and surfaces the values to your `.env` schema — nothing more. It does **not** create, update, or delete cluster resources, generate or template manifests, watch for changes, or manage deployments. + +**Typical use cases:** +- **Local development** — pull dev/staging Secrets and ConfigMaps from a cluster into your local app without copying values by hand +- **In-cluster runtime** — read additional Secrets/ConfigMaps at runtime that aren't already mounted into the pod via `envFrom`/`valueFrom` +- **CI/CD** — read Secrets/ConfigMaps from a cluster using an explicit service account token + +Deeper Kubernetes integration is on our radar. If you have ideas, a use case this plugin doesn't cover, or feedback from running it in production, come chat on [Discord](https://chat.dmno.dev) — we'd love to hear from you. + ## Features -- Fetch individual keys from Kubernetes Secrets and ConfigMaps +- Zero-config local development using your default kubeconfig +- In-cluster authentication using the pod's mounted service account +- Explicit auth via cluster API URL + bearer token (CI/CD, deployed apps) +- Fetch individual keys from Secrets and ConfigMaps - Bulk-load whole Secrets or ConfigMaps with `@setValuesBulk` -- Local kubeconfig, in-cluster service account, or explicit API server/token auth +- Configurable default Secret / ConfigMap so common cases don't repeat themselves - Multiple plugin instances for different namespaces or clusters -- Auto-infer Secret/ConfigMap keys from Varlock item names -- Read-only behavior; the plugin does not write to the cluster +- Auto-decode base64 Secret values and ConfigMap `binaryData` ## Installation @@ -23,66 +36,145 @@ Then load it from your `.env.schema`: # @plugin(@varlock/kubernetes-plugin) ``` -## Setup +## Setup + Auth -For local development, the plugin uses your default kubeconfig unless configured otherwise: +### Automatic auth (Recommended for local dev) + +For local development, just initialize the plugin and it will use your default kubeconfig (`$KUBECONFIG` or `~/.kube/config`): ```env-spec # @plugin(@varlock/kubernetes-plugin) # @initKubernetes(namespace=default) -# --- ``` -Inside a pod, omit kubeconfig settings and the plugin will use the in-cluster service account. +Inside a pod, omit kubeconfig-related settings and the plugin will use the mounted service account credentials at `/var/run/secrets/kubernetes.io/serviceaccount/`. -For explicit API server authentication: +### Explicit cluster server + token (For CI/CD) + +For deployments without a kubeconfig, provide the API server URL and a bearer token directly: ```env-spec # @plugin(@varlock/kubernetes-plugin) # @initKubernetes( # namespace=default, -# clusterServer="https://kubernetes.default.svc", +# clusterServer="https://kubernetes.example.com:6443", # token=$KUBERNETES_TOKEN # ) # --- + # @type=kubernetesBearerToken @sensitive KUBERNETES_TOKEN= ``` -## Usage +See [Kubernetes Setup](#kubernetes-setup) below for how to mint a long-lived service account token. + +### Raw kubeconfig + +You can also pass a full kubeconfig as a string (YAML or JSON) — useful when injecting credentials from a secret manager: + +```env-spec +# @initKubernetes(kubeconfig=$KUBECONFIG_DATA) +# --- +# @sensitive +KUBECONFIG_DATA= +``` + +### Authentication priority + +The plugin tries authentication methods in this order: +1. **Explicit cluster server + token** — if `clusterServer` is provided +2. **Explicit kubeconfig** — if `kubeconfig` is provided (file path or raw YAML/JSON) +3. **In-cluster service account** — auto-detected via `KUBERNETES_SERVICE_HOST` / `KUBERNETES_SERVICE_PORT` +4. **Default kubeconfig** — `$KUBECONFIG` or `~/.kube/config` + +You can override the active kubeconfig context with `context=...`. + +### Multiple instances + +To read from multiple namespaces or clusters, register named instances: + +```env-spec +# @initKubernetes(id=dev, namespace=dev) +# @initKubernetes(id=prod, namespace=prod, context=prod-cluster) +# --- + +DEV_DATABASE_URL=k8sSecret(dev, app-secrets, DATABASE_URL) +PROD_DATABASE_URL=k8sSecret(prod, app-secrets, DATABASE_URL) +``` + +## Reading values + +### How Secrets and ConfigMaps are structured + +A Kubernetes Secret/ConfigMap is a **named resource that holds a map of key/value pairs**: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: app-secrets # ← the resource name +data: + DATABASE_URL: cG9zdGdyZXM6... # ← keys inside the resource + API_KEY: c2VjcmV0LWtleQ== +``` + +So fetching a value is always a two-level lookup: which resource (`name`), and which key inside it (`key`). ### Secret keys +Secret `data` values are base64-decoded automatically. + ```env-spec -# The key defaults to the Varlock config item name +# Auto-infer key from item name (fetches app-secrets.data.DATABASE_URL) DATABASE_URL=k8sSecret(app-secrets) -# Or provide the key explicitly +# Explicit key name DB_URL=k8sSecret(app-secrets, DATABASE_URL) -``` -Kubernetes Secret values are base64-decoded before being returned. +# Named args also work +DB_URL=k8sSecret(name=app-secrets, key=DATABASE_URL) +``` ### ConfigMap keys +Both `data` (strings) and `binaryData` (base64-decoded) fields are supported. + ```env-spec PUBLIC_API_HOST=k8sConfigMap(app-config) API_HOST=k8sConfigMap(app-config, PUBLIC_API_HOST) ``` -### Multiple instances +### Default Secret / ConfigMap + +The idiomatic k8s pattern is one Secret + one ConfigMap per app. Set defaults on the init decorator and skip the name argument: ```env-spec -# @initKubernetes(id=dev, namespace=dev) -# @initKubernetes(id=prod, namespace=prod, context=prod) +# @plugin(@varlock/kubernetes-plugin) +# @initKubernetes( +# namespace=default, +# defaultSecret=app-secrets, +# defaultConfigMap=app-config, +# ) # --- -DEV_DATABASE_URL=k8sSecret(dev, app-secrets, DATABASE_URL) -PROD_DATABASE_URL=k8sSecret(prod, app-secrets, DATABASE_URL) +# Both default to app-secrets / app-config and infer the key from the item name +DATABASE_URL=k8sSecret() +API_KEY=k8sSecret() +PUBLIC_API_HOST=k8sConfigMap() + +# Override just the key while still using the default Secret +STRIPE_KEY=k8sSecret(key=stripe_api_key) + +# Override the resource name to read from a different Secret +SHARED_TOKEN=k8sSecret(shared-secrets, AUTH_TOKEN) ``` +You can mix positional and named arguments, but providing the same field both ways (e.g. `k8sSecret(app-secrets, name=other)`) is a schema error. + ### Bulk loading +Use bulk loading when one Secret or ConfigMap contains several env vars. The bulk resolvers return a JSON object, which pairs with `@setValuesBulk`: + ```env-spec # @plugin(@varlock/kubernetes-plugin) # @initKubernetes(namespace=default) @@ -91,29 +183,201 @@ PROD_DATABASE_URL=k8sSecret(prod, app-secrets, DATABASE_URL) # --- DATABASE_URL= +API_KEY= PUBLIC_API_HOST= ``` -## `@initKubernetes()` options - -- `id` optional static instance id, defaults to `_default` -- `namespace` optional namespace, defaults to kubeconfig context namespace or `default` -- `context` optional kubeconfig context name -- `kubeconfig` optional path to kubeconfig file, or raw kubeconfig YAML/JSON -- `clusterServer` optional Kubernetes API server URL for explicit auth -- `token` optional bearer token for explicit auth -- `skipTlsVerify` optional boolean for explicit auth only -- `allowMissing` optional boolean; missing resources or keys return `undefined` instead of throwing - -## Resolver functions - -- `k8sSecret(secretName)` -- `k8sSecret(secretName, key)` -- `k8sSecret(instanceId, secretName, key)` -- `k8sConfigMap(configMapName)` -- `k8sConfigMap(configMapName, key)` -- `k8sConfigMap(instanceId, configMapName, key)` -- `k8sSecretBulk(secretName)` -- `k8sSecretBulk(instanceId, secretName)` -- `k8sConfigMapBulk(configMapName)` -- `k8sConfigMapBulk(instanceId, configMapName)` +Only items declared in your schema will be populated — extra keys in the resource are ignored. Bulk resolvers also pick up `defaultSecret` / `defaultConfigMap`, so the names can be omitted. + +### Optional values + +By default, fetching from a missing Secret/ConfigMap throws. To resolve missing resources or keys to `undefined`, set `allowMissing=true`: + +```env-spec +# @initKubernetes(namespace=default, allowMissing=true) +# --- + +# @required=false +OPTIONAL_FLAG=k8sConfigMap(feature-flags, NEW_UI) +``` + +Mark optional items with `@required=false` (or wrap with `fallback()`) so validation doesn't fail. + +## Reference + +### Root decorators + +#### `@initKubernetes()` + +Initialize a Kubernetes plugin instance for the resolvers below. + +**Parameters:** + +- `id?: string` (static) - Instance identifier for multiple instances (defaults to `_default`) +- `namespace?: string` - Kubernetes namespace. Defaults to the kubeconfig context namespace, the in-cluster service account namespace, or `default` +- `context?: string` - Kubeconfig context name (overrides the current context) +- `kubeconfig?: string` - Path to a kubeconfig file, or raw kubeconfig YAML/JSON content +- `clusterServer?: string` - Kubernetes API server URL for explicit auth (e.g., `https://kubernetes.example.com:6443`) +- `token?: string` - Bearer token for explicit auth +- `skipTlsVerify?: boolean` - Skip TLS verification (only applies to explicit `clusterServer` + `token` auth) +- `allowMissing?: boolean` - Missing resources or keys resolve to `undefined` instead of throwing +- `defaultSecret?: string` - Default Secret name for `k8sSecret()` / `k8sSecretBulk()` +- `defaultConfigMap?: string` - Default ConfigMap name for `k8sConfigMap()` / `k8sConfigMapBulk()` + +### Functions + +All resolvers accept positional and named arguments. The same field cannot be provided both ways. + +#### `k8sSecret()` + +Fetch a single key from a Secret. Values are base64-decoded automatically. + +**Signatures:** + +- `k8sSecret()` - Uses `defaultSecret`, infers key from item name +- `k8sSecret(name)` - Uses given Secret, infers key from item name +- `k8sSecret(name, key)` - Explicit name and key +- `k8sSecret(instanceId, name, key)` - With explicit instance +- `k8sSecret(name=..., key=..., id=...)` - Same with named args + +#### `k8sConfigMap()` + +Fetch a single key from a ConfigMap. Both `data` and `binaryData` are supported. + +**Signatures:** same shape as `k8sSecret()` — substitute `k8sConfigMap` and `defaultConfigMap`. + +#### `k8sSecretBulk()` + +Fetch all keys from a Secret as a JSON object. Designed for `@setValuesBulk(..., format=json)`. + +**Signatures:** + +- `k8sSecretBulk()` - Uses `defaultSecret` +- `k8sSecretBulk(name)` - Explicit Secret name +- `k8sSecretBulk(instanceId, name)` - With explicit instance +- `k8sSecretBulk(name=..., id=...)` - Same with named args + +#### `k8sConfigMapBulk()` + +Same shape as `k8sSecretBulk()` — substitute `k8sConfigMapBulk` and `defaultConfigMap`. + +### Data Types + +- `kubernetesBearerToken` - Kubernetes service account / API server bearer token (sensitive) + +--- + +## Kubernetes Setup + +### Required RBAC permissions + +The identity used by the plugin (your kubeconfig user, an in-cluster service account, or an explicit token) needs read access to Secrets and/or ConfigMaps in the target namespace. + +The minimum permissions are `get` on `secrets` and `configmaps`: + +```yaml +# varlock-rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: varlock-reader + namespace: default +rules: + - apiGroups: [""] + resources: ["secrets", "configmaps"] + verbs: ["get"] +``` + +```bash +kubectl apply -f varlock-rbac.yaml +``` + +For least privilege, omit `"secrets"` if you only need ConfigMaps. Prefer namespaced `Role`s over `ClusterRole`s whenever possible. + +### Service account for in-cluster use + +```bash +kubectl create serviceaccount varlock-reader -n default +``` + +Bind the role to the service account: + +```yaml +# varlock-rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: varlock-reader-binding + namespace: default +subjects: + - kind: ServiceAccount + name: varlock-reader + namespace: default +roleRef: + kind: Role + name: varlock-reader + apiGroup: rbac.authorization.k8s.io +``` + +Then use it in your pod spec: + +```yaml +spec: + serviceAccountName: varlock-reader + containers: + - name: app + image: my-app:latest +``` + +### Generate a bearer token for explicit auth + +For CI/CD or other external use cases, create a long-lived service account token: + +```bash +kubectl apply -f - < -n ` +- Double-check the namespace — the plugin only reads from the configured namespace +- Resource names are case-sensitive and namespace-scoped +- If the resource is genuinely optional, set `allowMissing=true` on `@initKubernetes()` and `@required=false` on the item + +### Permission denied (403) +- Check that the active identity has the required RBAC: `kubectl auth can-i get secrets -n ` +- For in-cluster use, verify the pod's `serviceAccountName` is set and bound to a `Role`/`ClusterRole` that grants `get` on `secrets`/`configmaps` +- The error message includes the exact `Role` snippet you need to grant + +### Authentication failed (401) +- **Local dev:** Run `kubectl config current-context` and `kubectl get secrets` to confirm your kubeconfig works +- **Explicit token:** Verify the token isn't expired or revoked +- **In-cluster:** Check the pod's mounted service account token at `/var/run/secrets/kubernetes.io/serviceaccount/token` + +### Connection refused or TLS errors +- Verify the cluster API URL is reachable from your machine/pod +- For clusters with self-signed certificates and explicit auth, set `skipTlsVerify=true` (development only) +- If `kubectl` works but the plugin doesn't, your kubeconfig may rely on an exec credential plugin (`aws eks get-token`, `gke-gcloud-auth-plugin`, `kubelogin`) — ensure the helper binary is on your `$PATH` + +### Wrong namespace +- The plugin uses (in order): explicit `namespace` argument, kubeconfig context namespace, pod's mounted SA namespace, or `default` +- Force a specific namespace with `@initKubernetes(namespace=my-ns)` diff --git a/packages/plugins/kubernetes/src/plugin.ts b/packages/plugins/kubernetes/src/plugin.ts index 3c7f6ffdb..3ede11c15 100644 --- a/packages/plugins/kubernetes/src/plugin.ts +++ b/packages/plugins/kubernetes/src/plugin.ts @@ -4,7 +4,7 @@ import * as k8s from '@kubernetes/client-node'; const { SchemaError, ResolutionError, ValidationError } = plugin.ERRORS; -const KUBERNETES_ICON = 'logos:kubernetes'; +const KUBERNETES_ICON = 'mdi:kubernetes'; const SERVICE_ACCOUNT_NAMESPACE_PATH = '/var/run/secrets/kubernetes.io/serviceaccount/namespace'; plugin.name = 'kubernetes'; @@ -20,7 +20,7 @@ plugin.standardVars = { }, }; -type KubernetesAuthConfig = { +type KubernetesInstanceConfig = { namespace?: string; context?: string; kubeconfig?: string; @@ -28,6 +28,8 @@ type KubernetesAuthConfig = { token?: string; skipTlsVerify?: boolean; allowMissing?: boolean; + defaultSecret?: string; + defaultConfigMap?: string; }; type KubernetesObjectKind = 'Secret' | 'ConfigMap'; @@ -111,7 +113,7 @@ function decodeConfigMapData(configMap: { } class KubernetesPluginInstance { - private authConfig: KubernetesAuthConfig = {}; + private config: KubernetesInstanceConfig = {}; private namespace = 'default'; private clientPromise?: Promise; @@ -119,12 +121,12 @@ class KubernetesPluginInstance { readonly id: string, ) {} - setAuth(config: KubernetesAuthConfig) { - this.authConfig = config; + setConfig(config: KubernetesInstanceConfig) { + this.config = config; debug( 'kubernetes instance', this.id, - 'set auth - namespace:', + 'set config - namespace:', config.namespace, 'context:', config.context, @@ -136,9 +138,17 @@ class KubernetesPluginInstance { !!config.token, 'allowMissing:', !!config.allowMissing, + 'defaultSecret:', + config.defaultSecret, + 'defaultConfigMap:', + config.defaultConfigMap, ); } + getDefaultName(kind: KubernetesObjectKind): string | undefined { + return kind === 'Secret' ? this.config.defaultSecret : this.config.defaultConfigMap; + } + private async initKubeConfig(): Promise { const { namespace, @@ -147,7 +157,7 @@ class KubernetesPluginInstance { clusterServer, token, skipTlsVerify, - } = this.authConfig; + } = this.config; const kc = new k8s.KubeConfig(); @@ -229,17 +239,12 @@ class KubernetesPluginInstance { return this.clientPromise; } - private missingValue(): undefined { - if (this.authConfig.allowMissing) return undefined; - throw new Error('unreachable'); - } - - private handleReadError(err: unknown, kind: KubernetesObjectKind, resourceName: string): never | undefined { + private handleReadError(err: unknown, kind: KubernetesObjectKind, resourceName: string): undefined { const status = getApiExceptionStatus(err); const location = `${kind} "${resourceName}" in namespace "${this.namespace}"`; if (status === 404) { - if (this.authConfig.allowMissing) return this.missingValue(); + if (this.config.allowMissing) return undefined; throw new ResolutionError(`${location} not found`, { tip: `Check the ${kind} name and namespace, or set allowMissing=true on @initKubernetes()`, }); @@ -271,7 +276,7 @@ class KubernetesPluginInstance { key: string, availableKeys: Array, ): string | undefined { - if (this.authConfig.allowMissing) return undefined; + if (this.config.allowMissing) return undefined; throw new ResolutionError(`Key "${key}" not found in Kubernetes ${kind} "${resourceName}"`, { tip: availableKeys.length ? `Available keys: ${availableKeys.join(', ')}` @@ -324,9 +329,8 @@ class KubernetesPluginInstance { return JSON.stringify(decodeSecretData(secretName, secret.data)); } catch (err) { if (err instanceof ResolutionError) throw err; - const maybeMissing = this.handleReadError(err, 'Secret', secretName); - if (maybeMissing === undefined) return '{}'; - throw new Error('unreachable'); + this.handleReadError(err, 'Secret', secretName); + return '{}'; } } @@ -341,9 +345,8 @@ class KubernetesPluginInstance { return JSON.stringify(decodeConfigMapData(configMap)); } catch (err) { if (err instanceof ResolutionError) throw err; - const maybeMissing = this.handleReadError(err, 'ConfigMap', configMapName); - if (maybeMissing === undefined) return '{}'; - throw new Error('unreachable'); + this.handleReadError(err, 'ConfigMap', configMapName); + return '{}'; } } } @@ -394,65 +397,122 @@ function inferItemKey(resolverCtx: any, resolverName: string): string { }); } -function parseKeyResolverArgs(resolverCtx: any, resolverName: string) { +function parseStaticInstanceId(resolver: Resolver, paramLabel: string): string { + if (!resolver.isStatic) { + throw new SchemaError(`Expected ${paramLabel} to be a static value`); + } + return String(resolver.staticValue); +} + +function parseKeyResolverArgs(resolverCtx: any, resolverName: string, kind: KubernetesObjectKind) { + const arrArgs: Array = resolverCtx.arrArgs || []; + const objArgs: Record = resolverCtx.objArgs || {}; + let instanceId = '_default'; - let resourceNameResolver: Resolver; + let resourceNameResolver: Resolver | undefined; let keyResolver: Resolver | undefined; - let inferredKey: string | undefined; - - const argCount = resolverCtx.arrArgs?.length ?? 0; - if (argCount === 1) { - resourceNameResolver = resolverCtx.arrArgs[0]; - inferredKey = inferItemKey(resolverCtx, resolverName); - } else if (argCount === 2) { - resourceNameResolver = resolverCtx.arrArgs[0]; - keyResolver = resolverCtx.arrArgs[1]; - } else if (argCount === 3) { - if (!resolverCtx.arrArgs[0].isStatic) { - throw new SchemaError('Expected instance id to be a static value'); + + if (arrArgs.length === 3) { + instanceId = parseStaticInstanceId(arrArgs[0], 'instance id'); + resourceNameResolver = arrArgs[1]; + keyResolver = arrArgs[2]; + } else if (arrArgs.length === 2) { + resourceNameResolver = arrArgs[0]; + keyResolver = arrArgs[1]; + } else if (arrArgs.length === 1) { + resourceNameResolver = arrArgs[0]; + } else if (arrArgs.length > 3) { + throw new SchemaError(`Expected ${resolverName}() to receive 0-3 positional arguments`); + } + + if (objArgs.id) { + if (arrArgs.length === 3) { + throw new SchemaError('Cannot use both positional and named id'); + } + instanceId = parseStaticInstanceId(objArgs.id, 'id'); + } + if (objArgs.name) { + if (resourceNameResolver) { + throw new SchemaError(`Cannot use both positional and named name for ${resolverName}()`); } - instanceId = String(resolverCtx.arrArgs[0].staticValue); - resourceNameResolver = resolverCtx.arrArgs[1]; - keyResolver = resolverCtx.arrArgs[2]; - } else { - throw new SchemaError(`Expected ${resolverName}() to receive 1-3 arguments`); + resourceNameResolver = objArgs.name; + } + if (objArgs.key) { + if (keyResolver) { + throw new SchemaError(`Cannot use both positional and named key for ${resolverName}()`); + } + keyResolver = objArgs.key; } + const inferredKey = keyResolver ? undefined : inferItemKey(resolverCtx, resolverName); + getPluginInstance(instanceId, resolverName); return { instanceId, - resourceNameResolver: resourceNameResolver!, + resourceNameResolver, keyResolver, inferredKey, + kind, }; } -function parseBulkResolverArgs(resolverCtx: any, resolverName: string) { +function parseBulkResolverArgs(resolverCtx: any, resolverName: string, kind: KubernetesObjectKind) { + const arrArgs: Array = resolverCtx.arrArgs || []; + const objArgs: Record = resolverCtx.objArgs || {}; + let instanceId = '_default'; - let resourceNameResolver: Resolver; - - const argCount = resolverCtx.arrArgs?.length ?? 0; - if (argCount === 1) { - resourceNameResolver = resolverCtx.arrArgs[0]; - } else if (argCount === 2) { - if (!resolverCtx.arrArgs[0].isStatic) { - throw new SchemaError('Expected instance id to be a static value'); + let resourceNameResolver: Resolver | undefined; + + if (arrArgs.length === 2) { + instanceId = parseStaticInstanceId(arrArgs[0], 'instance id'); + resourceNameResolver = arrArgs[1]; + } else if (arrArgs.length === 1) { + resourceNameResolver = arrArgs[0]; + } else if (arrArgs.length > 2) { + throw new SchemaError(`Expected ${resolverName}() to receive 0-2 positional arguments`); + } + + if (objArgs.id) { + if (arrArgs.length === 2) { + throw new SchemaError('Cannot use both positional and named id'); } - instanceId = String(resolverCtx.arrArgs[0].staticValue); - resourceNameResolver = resolverCtx.arrArgs[1]; - } else { - throw new SchemaError(`Expected ${resolverName}() to receive 1-2 arguments`); + instanceId = parseStaticInstanceId(objArgs.id, 'id'); + } + if (objArgs.name) { + if (resourceNameResolver) { + throw new SchemaError(`Cannot use both positional and named name for ${resolverName}()`); + } + resourceNameResolver = objArgs.name; } getPluginInstance(instanceId, resolverName); return { instanceId, - resourceNameResolver: resourceNameResolver!, + resourceNameResolver, + kind, }; } +async function resolveResourceName( + instanceId: string, + resourceNameResolver: Resolver | undefined, + kind: KubernetesObjectKind, +): Promise { + if (resourceNameResolver) { + return resolveString(resourceNameResolver, `${kind} name`); + } + const defaultName = pluginInstances[instanceId].getDefaultName(kind); + if (!defaultName) { + const defaultParam = kind === 'Secret' ? 'defaultSecret' : 'defaultConfigMap'; + throw new SchemaError(`No ${kind} name provided`, { + tip: `Pass a name argument, or set ${defaultParam} on @initKubernetes()`, + }); + } + return defaultName; +} + plugin.registerRootDecorator({ name: 'initKubernetes', description: 'Initialize a Kubernetes plugin instance for k8sSecret() and k8sConfigMap() resolvers', @@ -480,6 +540,8 @@ plugin.registerRootDecorator({ tokenResolver: objArgs.token, skipTlsVerifyResolver: objArgs.skipTlsVerify, allowMissingResolver: objArgs.allowMissing, + defaultSecretResolver: objArgs.defaultSecret, + defaultConfigMapResolver: objArgs.defaultConfigMap, }; }, async execute({ @@ -491,6 +553,8 @@ plugin.registerRootDecorator({ tokenResolver, skipTlsVerifyResolver, allowMissingResolver, + defaultSecretResolver, + defaultConfigMapResolver, }) { const namespace = asOptionalString(await namespaceResolver?.resolve()); const context = asOptionalString(await contextResolver?.resolve()); @@ -499,8 +563,10 @@ plugin.registerRootDecorator({ const token = asOptionalString(await tokenResolver?.resolve()); const skipTlsVerify = asOptionalBoolean(await skipTlsVerifyResolver?.resolve(), 'skipTlsVerify'); const allowMissing = asOptionalBoolean(await allowMissingResolver?.resolve(), 'allowMissing'); + const defaultSecret = asOptionalString(await defaultSecretResolver?.resolve()); + const defaultConfigMap = asOptionalString(await defaultConfigMapResolver?.resolve()); - pluginInstances[id].setAuth({ + pluginInstances[id].setConfig({ namespace, context, kubeconfig, @@ -508,6 +574,8 @@ plugin.registerRootDecorator({ token, skipTlsVerify, allowMissing, + defaultSecret, + defaultConfigMap, }); }, }); @@ -536,17 +604,17 @@ plugin.registerResolverFunction({ label: 'Fetch key from Kubernetes Secret', icon: KUBERNETES_ICON, argsSchema: { - type: 'array', - arrayMinLength: 1, + type: 'mixed', + arrayMinLength: 0, arrayMaxLength: 3, }, process() { - return parseKeyResolverArgs(this, 'k8sSecret'); + return parseKeyResolverArgs(this, 'k8sSecret', 'Secret'); }, async resolve({ - instanceId, resourceNameResolver, keyResolver, inferredKey, + instanceId, resourceNameResolver, keyResolver, inferredKey, kind, }) { - const resourceName = await resolveString(resourceNameResolver, 'Secret name'); + const resourceName = await resolveResourceName(instanceId, resourceNameResolver, kind); const key = keyResolver ? await resolveString(keyResolver, 'Secret key') : inferredKey; if (!key) throw new SchemaError('No Secret key provided'); return pluginInstances[instanceId].getSecretKey(resourceName, key); @@ -558,17 +626,17 @@ plugin.registerResolverFunction({ label: 'Fetch key from Kubernetes ConfigMap', icon: KUBERNETES_ICON, argsSchema: { - type: 'array', - arrayMinLength: 1, + type: 'mixed', + arrayMinLength: 0, arrayMaxLength: 3, }, process() { - return parseKeyResolverArgs(this, 'k8sConfigMap'); + return parseKeyResolverArgs(this, 'k8sConfigMap', 'ConfigMap'); }, async resolve({ - instanceId, resourceNameResolver, keyResolver, inferredKey, + instanceId, resourceNameResolver, keyResolver, inferredKey, kind, }) { - const resourceName = await resolveString(resourceNameResolver, 'ConfigMap name'); + const resourceName = await resolveResourceName(instanceId, resourceNameResolver, kind); const key = keyResolver ? await resolveString(keyResolver, 'ConfigMap key') : inferredKey; if (!key) throw new SchemaError('No ConfigMap key provided'); return pluginInstances[instanceId].getConfigMapKey(resourceName, key); @@ -580,15 +648,15 @@ plugin.registerResolverFunction({ label: 'Load all keys from Kubernetes Secret', icon: KUBERNETES_ICON, argsSchema: { - type: 'array', - arrayMinLength: 1, + type: 'mixed', + arrayMinLength: 0, arrayMaxLength: 2, }, process() { - return parseBulkResolverArgs(this, 'k8sSecretBulk'); + return parseBulkResolverArgs(this, 'k8sSecretBulk', 'Secret'); }, - async resolve({ instanceId, resourceNameResolver }) { - const resourceName = await resolveString(resourceNameResolver, 'Secret name'); + async resolve({ instanceId, resourceNameResolver, kind }) { + const resourceName = await resolveResourceName(instanceId, resourceNameResolver, kind); return pluginInstances[instanceId].getSecretBulk(resourceName); }, }); @@ -598,15 +666,15 @@ plugin.registerResolverFunction({ label: 'Load all keys from Kubernetes ConfigMap', icon: KUBERNETES_ICON, argsSchema: { - type: 'array', - arrayMinLength: 1, + type: 'mixed', + arrayMinLength: 0, arrayMaxLength: 2, }, process() { - return parseBulkResolverArgs(this, 'k8sConfigMapBulk'); + return parseBulkResolverArgs(this, 'k8sConfigMapBulk', 'ConfigMap'); }, - async resolve({ instanceId, resourceNameResolver }) { - const resourceName = await resolveString(resourceNameResolver, 'ConfigMap name'); + async resolve({ instanceId, resourceNameResolver, kind }) { + const resourceName = await resolveResourceName(instanceId, resourceNameResolver, kind); return pluginInstances[instanceId].getConfigMapBulk(resourceName); }, }); diff --git a/packages/plugins/kubernetes/test/kubernetes.test.ts b/packages/plugins/kubernetes/test/kubernetes.test.ts index ecd12d2bd..556371d37 100644 --- a/packages/plugins/kubernetes/test/kubernetes.test.ts +++ b/packages/plugins/kubernetes/test/kubernetes.test.ts @@ -259,6 +259,92 @@ describe('kubernetes plugin', () => { } }); + test('uses defaultSecret when no name argument is provided', async () => { + const api = await startFakeKubeApi(); + try { + await runKubernetesTest(api, { + initParams: 'defaultSecret=app-secrets', + schema: outdent` + DATABASE_URL=k8sSecret() + SECRET_KEY=k8sSecret(key=API_KEY) + `, + expectValues: { + DATABASE_URL: 'postgres://example', + SECRET_KEY: 'secret-api-key', + }, + }); + } finally { + await api.close(); + } + }); + + test('uses defaultConfigMap when no name argument is provided', async () => { + const api = await startFakeKubeApi(); + try { + await runKubernetesTest(api, { + initParams: 'defaultConfigMap=app-config', + header: outdent` + # @setValuesBulk(k8sConfigMapBulk(), format=json) + `, + schema: outdent` + PUBLIC_API_HOST= + CERT= + API_HOST=k8sConfigMap(name=app-config, key=PUBLIC_API_HOST) + `, + expectValues: { + PUBLIC_API_HOST: 'api.example.com', + CERT: 'cert-data', + API_HOST: 'api.example.com', + }, + }); + } finally { + await api.close(); + } + }); + + test('positional name overrides defaultSecret', async () => { + const api = await startFakeKubeApi(); + try { + await runKubernetesTest(api, { + initParams: 'defaultSecret=app-secrets', + schema: outdent` + DATABASE_URL=k8sSecret() + STRIPE_KEY=k8sSecret(other-secrets, API_KEY) + `, + expectValues: { + DATABASE_URL: 'postgres://example', + STRIPE_KEY: Error, + }, + }); + } finally { + await api.close(); + } + }); + + test('errors when no name is provided and no default is configured', async () => { + const api = await startFakeKubeApi(); + try { + await runKubernetesTest(api, { + schema: 'DATABASE_URL=k8sSecret()', + expectValues: { DATABASE_URL: Error }, + }); + } finally { + await api.close(); + } + }); + + test('rejects mixing positional and named args for the same field', async () => { + const api = await startFakeKubeApi(); + try { + await runKubernetesTest(api, { + schema: 'MIXED=k8sSecret(app-secrets, name=other-secrets)', + expectValues: { MIXED: Error }, + }); + } finally { + await api.close(); + } + }); + test('validates init and token schema', async () => { await pluginTest({ schema: outdent` diff --git a/packages/varlock-website/src/content/docs/plugins/kubernetes.mdx b/packages/varlock-website/src/content/docs/plugins/kubernetes.mdx index 43952f0a0..8a1059cf9 100644 --- a/packages/varlock-website/src/content/docs/plugins/kubernetes.mdx +++ b/packages/varlock-website/src/content/docs/plugins/kubernetes.mdx @@ -3,67 +3,224 @@ title: Kubernetes Plugin description: Using Kubernetes Secrets and ConfigMaps with Varlock --- +import { Steps, Icon } from '@astrojs/starlight/components'; import Badge from '@/components/Badge.astro';
-The Kubernetes plugin reads values from Kubernetes Secrets and ConfigMaps using declarative instructions in your `.env` files. +Our Kubernetes plugin enables loading values from Kubernetes [Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) and [ConfigMaps](https://kubernetes.io/docs/concepts/configuration/configmap/) using declarative instructions within your `.env` files. -It is read-only: it does not create, update, or delete Kubernetes resources. +## Scope + +This plugin is **read-only**. It performs `get` requests on Secrets and ConfigMaps in a configured namespace and surfaces the values to your `.env` schema — nothing more. It does **not** create, update, or delete cluster resources, generate or template manifests, watch for changes, or manage deployments. + +**Typical use cases:** +- **Local development** — pull dev/staging Secrets and ConfigMaps from a cluster into your local app without copying values by hand +- **In-cluster runtime** — read additional Secrets/ConfigMaps at runtime that aren't already mounted into the pod via `envFrom` / `valueFrom` +- **CI/CD** — read Secrets/ConfigMaps from a cluster using an explicit service account token + +It supports local kubeconfig, in-cluster service account credentials, and explicit API server + token authentication. + +:::note[Want deeper Kubernetes integration?] +We're considering broader Kubernetes support (manifest generation, schema-to-deployment validation, change watching, etc.). If you have a use case this plugin doesn't cover, or feedback from running it in production, come chat on [Discord](https://chat.dmno.dev) — we'd love to hear from you. +::: ## Features -- **Fetch Secret keys** with `k8sSecret()` -- **Fetch ConfigMap keys** with `k8sConfigMap()` -- **Bulk-load resources** with `k8sSecretBulk()` and `k8sConfigMapBulk()` -- **Use local kubeconfig** for development -- **Use in-cluster service account auth** when running inside Kubernetes -- **Multiple plugin instances** for different namespaces or clusters +- **Zero-config local development** - Automatically uses your default kubeconfig (`~/.kube/config`) +- **In-cluster authentication** - Auto-detects the mounted service account when running inside a pod +- **Explicit auth** - Provide a cluster API URL and bearer token directly +- **Fetch Secret keys** with `k8sSecret()` (values are automatically base64-decoded) +- **Fetch ConfigMap keys** with `k8sConfigMap()` (including `binaryData`) +- **Bulk-load** whole Secrets or ConfigMaps with `k8sSecretBulk()` / `k8sConfigMapBulk()` +- **Auto-infer keys** from environment variable names +- **Multiple instances** for different namespaces or clusters +- **Read-only** — the plugin never mutates cluster state ## Installation and setup +In a JS/TS project, you may install the `@varlock/kubernetes-plugin` package as a normal dependency. +Otherwise you can just load it directly from your `.env.schema` file, as long as you add a version specifier. +See the [plugins guide](/guides/plugins/#installation) for more instructions on installing plugins. + ```env-spec title=".env.schema" +# 1. Load the plugin # @plugin(@varlock/kubernetes-plugin) +# +# 2. Initialize the plugin - see below for more details on options # @initKubernetes(namespace=default) -# --- ``` -For local development, the plugin uses your default kubeconfig. Inside a pod, it uses the mounted service account credentials. +### Authentication options -You can also provide explicit API server auth: +The plugin tries authentication methods in this priority order: +1. **Explicit cluster server + token** - If `clusterServer` is provided in `@initKubernetes()` +2. **Explicit kubeconfig** - If `kubeconfig` is provided (file path or raw YAML/JSON string) +3. **In-cluster service account** - Auto-detected via `KUBERNETES_SERVICE_HOST`/`KUBERNETES_SERVICE_PORT` env vars (set automatically inside pods) +4. **Default kubeconfig** - Loads from `$KUBECONFIG` or `~/.kube/config` + +### Automatic authentication (Recommended for local dev) + +For local development, just initialize the plugin and the plugin will pick up your default kubeconfig automatically: ```env-spec title=".env.schema" # @plugin(@varlock/kubernetes-plugin) -# @initKubernetes( -# namespace=default, -# clusterServer="https://kubernetes.default.svc", -# token=$KUBERNETES_TOKEN -# ) +# @initKubernetes(namespace=default) +``` + +**How this works:** + +- **Local development:** Uses your active `kubectl` context from `~/.kube/config` (or `$KUBECONFIG`) +- **Inside a pod:** Uses the pod's mounted service account credentials at `/var/run/secrets/kubernetes.io/serviceaccount/` + +If your kubeconfig has multiple contexts, you can select one explicitly: + +```env-spec title=".env.schema" +# @initKubernetes(namespace=default, context=my-dev-cluster) +``` + +### Explicit cluster server and token + +If you don't want to rely on a kubeconfig file — for example, in CI/CD or other deployed environments — provide the API server URL and a bearer token directly: + + +1. **Create a service account and RBAC** in your cluster (see Kubernetes Setup section below) + +2. **Wire up the credentials in your config**. Add a config item for the token and reference it when initializing the plugin. + + ```env-spec title=".env.schema" + # @plugin(@varlock/kubernetes-plugin) + # @initKubernetes( + # namespace=default, + # clusterServer="https://kubernetes.example.com:6443", + # token=$KUBERNETES_TOKEN + # ) + # --- + + # @type=kubernetesBearerToken @sensitive + KUBERNETES_TOKEN= + ``` + +3. **Set your credentials in deployed environments**. Use your platform's env var management UI to securely inject the bearer token. + + +For clusters that present self-signed or custom-CA certificates, you can disable TLS verification by adding `skipTlsVerify=true` to `@initKubernetes()`. This should generally be avoided outside of development. + +### Raw kubeconfig + +You can also pass an entire kubeconfig as a string (YAML or JSON) — useful when injecting credentials from a secret manager or platform env var: + +```env-spec title=".env.schema" +# @initKubernetes(kubeconfig=$KUBECONFIG_DATA) # --- -# @type=kubernetesBearerToken @sensitive -KUBERNETES_TOKEN= +# @sensitive +KUBECONFIG_DATA= +``` + +The plugin auto-detects whether `kubeconfig` is a file path or raw content by looking for YAML/JSON markers. + +### Multiple instances + +If you need to read from multiple namespaces or clusters, register multiple named instances: + +```env-spec title=".env.schema" +# @initKubernetes(id=dev, namespace=dev) +# @initKubernetes(id=prod, namespace=prod, context=prod-cluster) +# --- + +DEV_DATABASE_URL=k8sSecret(dev, app-secrets, DATABASE_URL) +PROD_DATABASE_URL=k8sSecret(prod, app-secrets, DATABASE_URL) ``` ## Loading values +Once the plugin is installed and initialized, you can start adding config items that load values using the `k8sSecret()` and `k8sConfigMap()` resolver functions. + +### How Secrets and ConfigMaps are structured + +A Kubernetes Secret or ConfigMap is a **named resource that holds a map of key/value pairs**: + +```yaml title="Example Secret" +apiVersion: v1 +kind: Secret +metadata: + name: app-secrets # ← the resource name +data: + DATABASE_URL: cG9zdGdyZXM6... # ← keys inside the resource + API_KEY: c2VjcmV0LWtleQ== +``` + +So fetching a value is always a two-level lookup: which Secret/ConfigMap (`name`), and which key inside it (`key`). The resolvers reflect this directly: + +- `k8sSecret(name, key)` — read `name.data.key` +- `k8sConfigMap(name, key)` — read `name.data.key` (or `name.binaryData.key`) + +When `key` is omitted, the plugin uses the config item's name as the key — this works well when your Secret keys already match your env var names. + +### Secret keys + +The `k8sSecret()` function fetches a key from a Kubernetes Secret. Values stored in Secret `data` are base64-encoded; the plugin decodes them automatically before returning. + ```env-spec title=".env.schema" -# The key defaults to the config item key +# Auto-infer key from item name (fetches "DATABASE_URL" from "app-secrets") DATABASE_URL=k8sSecret(app-secrets) -# Or provide the key explicitly +# Explicit key name DB_URL=k8sSecret(app-secrets, DATABASE_URL) -PUBLIC_API_HOST=k8sConfigMap(app-config, PUBLIC_API_HOST) +# Named args also work +DB_URL=k8sSecret(name=app-secrets, key=DATABASE_URL) + +# With named instance +PROD_DB_URL=k8sSecret(prod, app-secrets, DATABASE_URL) ``` -Kubernetes Secret values are base64-decoded before being returned. +### ConfigMap keys -## Bulk loading +The `k8sConfigMap()` function fetches a key from a Kubernetes ConfigMap. Both `data` (string) and `binaryData` (base64-encoded) fields are supported. -Use bulk loading when a Secret or ConfigMap contains several environment variables: +```env-spec title=".env.schema" +# Auto-infer key from item name +PUBLIC_API_HOST=k8sConfigMap(app-config) + +# Explicit key name +API_HOST=k8sConfigMap(app-config, PUBLIC_API_HOST) +``` + +### Default Secret/ConfigMap (Recommended for the common case) + +The idiomatic Kubernetes deployment pattern is one Secret + one ConfigMap per app, mounted into the pod via `envFrom`. If your app follows this pattern, set `defaultSecret` and `defaultConfigMap` once on the init decorator and skip the name argument on every call: + +```env-spec title=".env.schema" +# @plugin(@varlock/kubernetes-plugin) +# @initKubernetes( +# namespace=default, +# defaultSecret=app-secrets, +# defaultConfigMap=app-config, +# ) +# --- + +# Both default to app-secrets / app-config and infer the key from the item name +DATABASE_URL=k8sSecret() +API_KEY=k8sSecret() +JWT_SECRET=k8sSecret() +PUBLIC_API_HOST=k8sConfigMap() + +# Override just the key while still using the default Secret +STRIPE_KEY=k8sSecret(key=stripe_api_key) + +# Override the resource name to read from a different Secret +SHARED_TOKEN=k8sSecret(shared-secrets, AUTH_TOKEN) +``` + +You can mix positional and named arguments, but you can't provide the same field twice — e.g., `k8sSecret(app-secrets, name=other-secrets)` is a schema error. + +### Bulk loading + +Use bulk loading when a single Secret or ConfigMap contains several environment variables you want to map into your config. The bulk resolvers return all keys as a JSON object, which pairs naturally with `@setValuesBulk`: ```env-spec title=".env.schema" # @plugin(@varlock/kubernetes-plugin) @@ -77,28 +234,330 @@ API_KEY= PUBLIC_API_HOST= ``` -## Multiple instances +Only items declared in your schema will be populated — any extra keys in the Secret or ConfigMap are ignored. + +Bulk resolvers also pick up `defaultSecret`/`defaultConfigMap`, so the names can be omitted entirely: ```env-spec title=".env.schema" -# @initKubernetes(id=dev, namespace=dev) -# @initKubernetes(id=prod, namespace=prod, context=prod) +# @initKubernetes(defaultSecret=app-secrets, defaultConfigMap=app-config) +# @setValuesBulk(k8sSecretBulk(), format=json) +# @setValuesBulk(k8sConfigMapBulk(), format=json) +``` + +### Optional values + +By default, fetching a key from a missing Secret/ConfigMap throws an error. If you want missing resources or keys to resolve to `undefined` instead, set `allowMissing=true`: + +```env-spec title=".env.schema" +# @initKubernetes(namespace=default, allowMissing=true) # --- -DEV_DATABASE_URL=k8sSecret(dev, app-secrets, DATABASE_URL) -PROD_DATABASE_URL=k8sSecret(prod, app-secrets, DATABASE_URL) +# @required=false +OPTIONAL_FLAG=k8sConfigMap(feature-flags, NEW_UI) +``` + +When `allowMissing=true`, also mark the corresponding items with `@required=false` (or wrap the resolver with `fallback()`) so validation does not fail. + +--- + +## Kubernetes Setup + +### Required RBAC permissions + +The identity used by the plugin (your kubeconfig user, an in-cluster service account, or an explicit token) needs read access to Secrets and/or ConfigMaps in the target namespace. + +The minimum permissions are `get` on `secrets` and `configmaps`: + +```yaml title="varlock-rbac.yaml" +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: varlock-reader + namespace: default +rules: + - apiGroups: [""] + resources: ["secrets", "configmaps"] + verbs: ["get"] +``` + +Apply it with: + +```bash +kubectl apply -f varlock-rbac.yaml +``` + +:::tip[Least privilege principle] +Scope down `resources` to only what you need. If you're only reading ConfigMaps, omit `"secrets"` entirely. For multi-namespace access, use a `ClusterRole` + `ClusterRoleBinding` instead, but prefer namespaced `Role`s whenever possible. +::: + +### Service account for in-cluster use + +When running inside a pod, the plugin uses the pod's mounted service account. Create a dedicated service account and bind it to the role above: + + +1. **Create a service account** + ```bash + kubectl create serviceaccount varlock-reader -n default + ``` + +2. **Bind the role to the service account** + ```yaml title="varlock-rolebinding.yaml" + apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: varlock-reader-binding + namespace: default + subjects: + - kind: ServiceAccount + name: varlock-reader + namespace: default + roleRef: + kind: Role + name: varlock-reader + apiGroup: rbac.authorization.k8s.io + ``` + + ```bash + kubectl apply -f varlock-rolebinding.yaml + ``` + +3. **Use the service account in your pod spec** + ```yaml title="deployment.yaml" + spec: + serviceAccountName: varlock-reader + containers: + - name: app + image: my-app:latest + ``` + + +### Generate a bearer token for explicit auth + +For CI/CD or other external use cases that need an explicit token, create a long-lived service account token: + +```bash +# Create a token secret bound to the service account +kubectl apply -f - < +
+#### `@initKubernetes()` + +Initialize a Kubernetes plugin instance for `k8sSecret()`, `k8sConfigMap()`, `k8sSecretBulk()`, and `k8sConfigMapBulk()` resolvers. + +**Key/value args:** +- `id` (optional, static): Instance identifier for multiple instances, defaults to `_default` +- `namespace` (optional): Kubernetes namespace to read from. Defaults to the kubeconfig context namespace, the pod's mounted service account namespace (when in-cluster), or `default` +- `context` (optional): Kubeconfig context name to use (overrides the current context) +- `kubeconfig` (optional): Path to a kubeconfig file, or raw kubeconfig YAML/JSON content +- `clusterServer` (optional): Kubernetes API server URL for explicit auth (e.g., `https://kubernetes.example.com:6443`) +- `token` (optional): Bearer token for explicit auth +- `skipTlsVerify` (optional): Set to `true` to skip TLS verification on the cluster certificate. Only applies when using `clusterServer` + `token` +- `allowMissing` (optional): If `true`, missing resources or keys return `undefined` instead of throwing +- `defaultSecret` (optional): Default Secret name used by `k8sSecret()` / `k8sSecretBulk()` when no name argument is provided +- `defaultConfigMap` (optional): Default ConfigMap name used by `k8sConfigMap()` / `k8sConfigMapBulk()` when no name argument is provided + +```env-spec "@initKubernetes" +# @initKubernetes(namespace=default, defaultSecret=app-secrets, defaultConfigMap=app-config) +``` +
+ + +### Data types +
+
+#### `kubernetesBearerToken` + +Represents a Kubernetes bearer token used for API server authentication (typically a service account token). This type is marked as `@sensitive`. + +```env-spec "kubernetesBearerToken" +# @type=kubernetesBearerToken +KUBERNETES_TOKEN= +``` +
+
+ +### Resolver functions +
+
+#### `k8sSecret()` + +Fetch a single key from a Kubernetes Secret. Secret `data` values are base64-decoded automatically. + +Arguments can be provided positionally or as named arguments, but the same field cannot be provided both ways. + +**Array args (positional):** +- `instanceId` (optional, static): Instance identifier to use when multiple plugin instances are initialized +- `name` (optional): Name of the Secret resource. Required unless `defaultSecret` is set on `@initKubernetes()` +- `key` (optional): Key inside the Secret's `data` to fetch. If omitted, uses the item key (variable name) + +**Named args:** +- `id` (optional, static): same as positional `instanceId` +- `name` (optional): same as positional `name` +- `key` (optional): same as positional `key` + +```env-spec /k8sSecret\(.*\)/ +# Auto-infer key from item name +DATABASE_URL=k8sSecret(app-secrets) + +# Explicit key name (positional) +DB_URL=k8sSecret(app-secrets, DATABASE_URL) + +# Override just the key (uses defaultSecret from @initKubernetes) +STRIPE_KEY=k8sSecret(key=stripe_api_key) + +# Both positional and named +DB_URL=k8sSecret(name=app-secrets, key=DATABASE_URL) + +# With instance ID +PROD_DB=k8sSecret(prod, app-secrets, DATABASE_URL) ``` +
+ +
+#### `k8sConfigMap()` + +Fetch a single key from a Kubernetes ConfigMap. Both `data` and `binaryData` fields are supported. + +Arguments can be provided positionally or as named arguments, but the same field cannot be provided both ways. + +**Array args (positional):** +- `instanceId` (optional, static): Instance identifier to use when multiple plugin instances are initialized +- `name` (optional): Name of the ConfigMap resource. Required unless `defaultConfigMap` is set on `@initKubernetes()` +- `key` (optional): Key inside the ConfigMap to fetch. If omitted, uses the item key (variable name) + +**Named args:** +- `id` (optional, static): same as positional `instanceId` +- `name` (optional): same as positional `name` +- `key` (optional): same as positional `key` + +```env-spec /k8sConfigMap\(.*\)/ +# Auto-infer key from item name +PUBLIC_API_HOST=k8sConfigMap(app-config) + +# Explicit key name +API_HOST=k8sConfigMap(app-config, PUBLIC_API_HOST) + +# Named args +API_HOST=k8sConfigMap(name=app-config, key=PUBLIC_API_HOST) + +# With instance ID +DEV_HOST=k8sConfigMap(dev, app-config, PUBLIC_API_HOST) +``` +
+ +
+#### `k8sSecretBulk()` + +Fetch all keys from a Kubernetes Secret as a JSON object string. Designed to be used with `@setValuesBulk(..., format=json)`. + +**Array args (positional):** +- `instanceId` (optional, static): Instance identifier to use when multiple plugin instances are initialized +- `name` (optional): Name of the Secret resource. Required unless `defaultSecret` is set on `@initKubernetes()` + +**Named args:** +- `id` (optional, static): same as positional `instanceId` +- `name` (optional): same as positional `name` + +```env-spec /k8sSecretBulk\(.*\)/ +# Uses defaultSecret from @initKubernetes +# @setValuesBulk(k8sSecretBulk(), format=json) + +# Explicit name +# @setValuesBulk(k8sSecretBulk(app-secrets), format=json) + +# With instance ID +# @setValuesBulk(k8sSecretBulk(prod, app-secrets), format=json) +``` +
+ +
+#### `k8sConfigMapBulk()` + +Fetch all keys from a Kubernetes ConfigMap as a JSON object string. Designed to be used with `@setValuesBulk(..., format=json)`. + +**Array args (positional):** +- `instanceId` (optional, static): Instance identifier to use when multiple plugin instances are initialized +- `name` (optional): Name of the ConfigMap resource. Required unless `defaultConfigMap` is set on `@initKubernetes()` + +**Named args:** +- `id` (optional, static): same as positional `instanceId` +- `name` (optional): same as positional `name` + +```env-spec /k8sConfigMapBulk\(.*\)/ +# @setValuesBulk(k8sConfigMapBulk(app-config), format=json) + +# @setValuesBulk(k8sConfigMapBulk(prod, app-config), format=json) +``` +
+
+ +--- + +## Troubleshooting + +### Secret or ConfigMap not found (404) +- Verify the resource exists: `kubectl get secret -n ` or `kubectl get configmap -n ` +- Double-check the namespace — the plugin reads only from the configured namespace +- Resource names are case-sensitive and namespace-scoped +- If the resource is genuinely optional, set `allowMissing=true` on `@initKubernetes()` and `@required=false` on the item + +### Permission denied (403) +- Check that the active identity has the required RBAC: `kubectl auth can-i get secrets -n ` +- For in-cluster use, verify the pod's `serviceAccountName` is set and bound to a `Role`/`ClusterRole` that grants `get` on `secrets`/`configmaps` +- The error message includes the exact `Role` snippet you need to grant + +### Authentication failed (401) +- **Local dev:** Run `kubectl config current-context` and verify it points to the right cluster; run `kubectl get secrets` to confirm your kubeconfig works +- **Explicit token:** Verify the token isn't expired or revoked. Service account tokens created from `kubernetes.io/service-account-token` Secrets are long-lived, but TokenRequest-issued tokens have shorter TTLs +- **In-cluster:** Check the pod's mounted service account token at `/var/run/secrets/kubernetes.io/serviceaccount/token` -## Options +### Connection refused or TLS errors +- Verify the cluster API URL is reachable from your machine/pod +- For clusters with self-signed certificates and explicit auth, set `skipTlsVerify=true` (development only) +- If using `kubectl` works but the plugin doesn't, your kubeconfig may rely on an [exec credential plugin](https://kubernetes.io/docs/reference/config-api/client-authentication.v1beta1/) (e.g., `aws eks get-token`, `gke-gcloud-auth-plugin`, `kubelogin`) — ensure the helper binary is on your `$PATH` -`@initKubernetes()` supports: +### Wrong namespace +- The plugin uses (in order): the explicit `namespace` argument, the current kubeconfig context's namespace, the pod's mounted SA namespace, or `default` +- To force a specific namespace, pass it explicitly: `@initKubernetes(namespace=my-ns)` -- `id` optional static instance id, defaults to `_default` -- `namespace` optional namespace, defaults to kubeconfig context namespace or `default` -- `context` optional kubeconfig context name -- `kubeconfig` optional path to a kubeconfig file, or raw kubeconfig YAML/JSON -- `clusterServer` optional Kubernetes API server URL for explicit auth -- `token` optional bearer token for explicit auth -- `skipTlsVerify` optional boolean for explicit auth -- `allowMissing` optional boolean; missing resources or keys return `undefined` +## Resources -If `allowMissing=true` is used, mark optional config items with `@required=false` or wrap the resolver with `fallback()`. +- [Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) +- [Kubernetes ConfigMaps](https://kubernetes.io/docs/concepts/configuration/configmap/) +- [Kubernetes RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) +- [Service Account Tokens](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) +- [`@kubernetes/client-node`](https://github.com/kubernetes-client/javascript)