Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@

This file is a history of the changes made to @idearium/cli.

## Unreleased
## v6.1.0 - 2026-09-16

### Added

- Support for 1Password secret references (`op://`) within Kubernetes manifest templates. Templates are rendered first, and then references (which must be bare, not `{{ }}` wrapped) are resolved with `op inject` (only when present), so secret values never flow through the template engine. `type: secret` services can use `stringData`, which is base64 encoded into `data` within the compiled manifest. When references are present, the 1Password cli is authenticated up front (`op whoami`), halting with a friendly signin message otherwise.
- Compiled secret manifests are now written with `0600` permissions, and removed after they've been applied to Kubernetes (`c kc start`, `c kc apply`) or when `c skaffold dev` exits (new `c kc secrets-clean` command, quiet via `-q`). Failures to remove a compiled secret (i.e. permission errors) are surfaced rather than silently ignored. This only applies to the local environment; other environments are unaffected. `c kc manifests` intentionally leaves the compiled files in place, as its output is its purpose.

### Changed

- `c skaffold dev` now halts when manifest compilation fails, rather than continuing into skaffold with stale compiled manifests.

## v6.0.0 - 2026-04-21

Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,29 @@ You'll then need to supply all of the values that the template file requires. Yo

The `c kc apply` will automatically provide the values for `namespace`, `prefix` and `tag`. If you'd like to provide something else, simply write a function that returns an object with `label` and `value`. Then use the value of `label` within a template placeholder (i.e. `{{a}}`) and it will be updated with the `value` (i.e. `{{b}}`).

##### Secret templates

Any template (`.yaml.tmpl`) can contain [1Password secret references](https://www.1password.dev/connect/knowledge-base/secrets-references/) (i.e. `op://vault/item/section/field`). The template is rendered (placeholders) first, and then any references are resolved with the 1Password cli (`op inject`) - only when present. Rendering first means secret values never flow through the template engine (a value containing curly braces could otherwise be mangled). As a consequence, references must be **bare** (i.e. `op://vault/item/section/field`, not wrapped in `{{ }}`): placeholders are gone by the time references are resolved, and a wrapped reference would be blanked as an unknown placeholder. When references are present, the cli first ensures the 1Password cli is installed and authenticated (via `op whoami`), halting with a friendly message if not (`eval $(op signin)`). Templates without secret references never invoke `op`.

This makes it possible to commit a template containing secret references, and have the compiled manifest (within `.compiled`, which should be gitignored) contain the actual secrets, ready to be deployed to Kubernetes.

For a service of `type: secret`, author the template with `stringData` instead of `data`:

```
apiVersion: v1
kind: Secret
metadata:
name: site
namespace: ras-rsc-local
type: Opaque
stringData:
ALGOLIA_SEARCH_API_KEY: op://ras-rsc/site/LOCAL/ALGOLIA_SEARCH_API_KEY
```

When the template is compiled, each `stringData` value will be base64 encoded and written to the compiled manifest as `data`, just as Kubernetes expects. `stringData` values must be single-line, and shouldn't be quoted or contain inline comments.

Compiled secret manifests contain plaintext secrets, so they are treated as sensitive: they're written with `0600` permissions, and removed once they've been applied to Kubernetes. `c kc start` and `c kc apply` remove them after applying, and `c skaffold dev` removes them (via `c kc secrets-clean`) when it exits. `c kc manifests` intentionally leaves them in place, as its compiled output is its purpose. This only applies to the local environment; other environments are unaffected.

### MongoDB configuration

The Idearium cli supports a MongoDB configuration. The MongoDB configuration can be used to access local and remote databases.
Expand Down
19 changes: 12 additions & 7 deletions bin/c-kc-apply.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const {
const { formatProjectPrefix } = require('./lib/c-project');
const {
ensureServiceFilesExist,
removeCompiledSecrets,
renderServicesTemplates,
setLocalsForServices,
} = require('./lib/c-kc');
Expand Down Expand Up @@ -100,35 +101,35 @@ return Promise.all([loadConfig(), loadState()])
return reject(e);
}

return resolve([services, path]);
return resolve([services, path, state]);
})
)
.then(
([services, path]) =>
([services, path, state]) =>
new Promise(async (resolve, reject) => {
try {
await ensureServiceFilesExist(path, services);
} catch (e) {
reject(e);
}

return resolve([services, path]);
return resolve([services, path, state]);
})
)
.then(
([services, path]) =>
([services, path, state]) =>
new Promise(async (resolve, reject) => {
try {
await renderServicesTemplates(path, services);
} catch (e) {
reject(e);
}

return resolve([services, path]);
return resolve([services, path, state]);
})
)
.then(
([services, path]) =>
([services, path, state]) =>
new Promise((resolve) => {
services.forEach((service) => {
exec(
Expand All @@ -141,7 +142,11 @@ return Promise.all([loadConfig(), loadState()])
);
});

return resolve();
return removeCompiledSecrets({
env: state.env,
path,
services,
}).then(resolve);
})
)
.catch((err) => {
Expand Down
69 changes: 69 additions & 0 deletions bin/c-kc-secrets-clean.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
'use strict';

const program = require('commander');
const getPropertyPath = require('get-value');
const {
kubernetesLocationsToObjects,
loadConfig,
loadState,
reportError,
} = require('./lib/c');
const { removeCompiledSecrets } = require('./lib/c-kc');

program
.description(
'This command will remove any compiled secret manifests, so that plaintext secrets do not linger on disk. It only applies to the local environment.'
)
.option('-q', 'Do not print the removed files.')
.parse(process.argv);

return Promise.all([loadState(), loadConfig()])
.then(([state, config]) => {
const { locations, path } = getPropertyPath(
config,
`kubernetes.environments.${state.env}`
);

return [
removeCompiledSecrets({
env: state.env,
path,
services: kubernetesLocationsToObjects(locations),
}),
state.env,
];
})
.then(async ([removal, env]) => {
const removed = await removal;

if (program.Q) {
return;
}

if (removed.length === 0) {
// eslint-disable-next-line no-console
return console.log(
env === 'local'
? 'No compiled secret manifests to remove.'
: `Nothing to do: secrets are only removed for the local environment (currently ${env}).`
);
}

removed.forEach((file) => {
// eslint-disable-next-line no-console
console.log(`Removed ${file}`);
});
})
.catch((err) => {
if (err.code === 'ENOENT') {
return reportError(
new Error(
'Please create a c.js file with your project configuration. See https://github.com/idearium/cli#configuration'
),
false,
true
);
}

return reportError(err, false, true);
});
19 changes: 12 additions & 7 deletions bin/c-kc-start.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const {
const { formatProjectPrefix } = require('./lib/c-project');
const {
ensureServiceFilesExist,
removeCompiledSecrets,
renderServicesTemplates,
setLocalsForServices,
} = require('./lib/c-kc');
Expand Down Expand Up @@ -57,35 +58,35 @@ return Promise.all([loadState(), loadConfig()])
return reject(e);
}

return resolve([services, path]);
return resolve([services, path, state]);
})
)
.then(
([services, path]) =>
([services, path, state]) =>
new Promise(async (resolve, reject) => {
try {
await ensureServiceFilesExist(path, services);
} catch (e) {
reject(e);
}

return resolve([services, path]);
return resolve([services, path, state]);
})
)
.then(
([services, path]) =>
([services, path, state]) =>
new Promise(async (resolve, reject) => {
try {
await renderServicesTemplates(path, services);
} catch (e) {
reject(e);
}

return resolve([services, path]);
return resolve([services, path, state]);
})
)
.then(
([services, path]) =>
([services, path, state]) =>
new Promise((resolve, reject) => {
const [namespace] = services
.filter((service) => service.type === 'namespace')
Expand Down Expand Up @@ -116,7 +117,11 @@ return Promise.all([loadState(), loadConfig()])
)}`
);

return resolve();
return removeCompiledSecrets({
env: state.env,
path,
services,
}).then(resolve);
})
)
.catch((err) => {
Expand Down
4 changes: 4 additions & 0 deletions bin/c-kc.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ program
'Get the name of a pod for a Kubernetes location.'
)
.command('secret', 'Base64 encode a string, ready for a Kubernetes secret.')
.command(
'secrets-clean',
'Remove compiled secret manifests, so plaintext secrets do not linger on disk.'
)
.command('start', 'Deploy all Kubernetes locations.')
.command(
'stop',
Expand Down
2 changes: 2 additions & 0 deletions bin/c-skaffold-dev
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#!/usr/bin/env bash
set -e
trap 'npx c kc secrets-clean -q' EXIT

npx c kc manifests
DOCKER_SCAN_SUGGEST=false skaffold dev
Loading