Skip to content

Commit da9944f

Browse files
authored
Merge pull request #59 from InsForge/feat/secret-service-binding
Secret ⇄ service binding (CLI)
2 parents 975e080 + eafb706 commit da9944f

3 files changed

Lines changed: 62 additions & 10 deletions

File tree

src/commands/secrets.ts

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,43 @@ export async function secrets(opts: { branch?: string; output?: string; print?:
2525
info(' tip: `insta run -- <cmd>` injects these per-run with nothing written to disk')
2626
}
2727

28-
export async function secretsList(opts: { branch?: string }): Promise<void> {
28+
// Shape of GET /secrets/tree: the whole binding picture — project-wide secrets, then per-branch
29+
// service groupings plus any branch-level (unbound) secrets.
30+
type Tree = {
31+
projectWide: string[]
32+
branches: { name: string; isDefault: boolean; services: { type: string; name: string; secrets: string[] }[]; unbound: string[] }[]
33+
}
34+
35+
// Render one branch's service-grouped secrets, then its unbound (branch-level) secrets.
36+
function renderBranch(b: Tree['branches'][number]): void {
37+
for (const s of b.services) if (s.secrets.length) { info(` ${s.type}/${s.name}`); for (const n of s.secrets) info(` ${n}`) }
38+
if (b.unbound.length) { info(' (branch-level)'); for (const n of b.unbound) info(` ${n}`) }
39+
}
40+
41+
// Show the full binding tree: project-wide, then every branch grouped by service.
42+
export async function secretsTree(opts: { json?: boolean }): Promise<void> {
43+
const api = await ApiClient.load()
44+
const p = await requireProject()
45+
const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets/tree`)
46+
if (handleApproval(res)) return
47+
const tree: Tree = res.body
48+
if (opts.json) return printJson(tree)
49+
if (tree.projectWide.length) { info('(project-wide)'); for (const n of tree.projectWide) info(` ${n}`) }
50+
for (const b of tree.branches) { info(`${b.name}${b.isDefault ? ' *' : ''}`); renderBranch(b) }
51+
}
52+
53+
// List secret names for the current (or given) branch, grouped by service.
54+
export async function secretsList(opts: { branch?: string; json?: boolean }): Promise<void> {
2955
const api = await ApiClient.load()
3056
const p = await requireProject()
3157
const branch = opts.branch ?? p.branch
32-
const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets${q(branch)}`)
58+
const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets/tree`)
3359
if (handleApproval(res)) return
34-
for (const name of Object.keys(res.body.secrets)) info(name)
60+
const tree: Tree = res.body
61+
const b = tree.branches.find((x) => x.name === branch)
62+
if (opts.json) return printJson({ projectWide: tree.projectWide, branch: b })
63+
if (tree.projectWide.length) { info('(project-wide)'); for (const n of tree.projectWide) info(` ${n}`) }
64+
if (b) { info(`${b.name}`); renderBranch(b) }
3565
}
3666

3767
async function readStdin(): Promise<string> {
@@ -40,17 +70,19 @@ async function readStdin(): Promise<string> {
4070
return data.trim()
4171
}
4272

43-
// Set a user secret. Project-wide by default; --branch scopes it to one branch. Value comes from
44-
// the argument, or stdin when omitted (keeps secret values out of shell history).
45-
export async function secretsSet(name: string, value: string | undefined, opts: { branch?: string }): Promise<void> {
73+
// Set a user secret. Project-wide by default; --branch scopes it to one branch. --service binds
74+
// it to a branch service instead, which implies the current branch (binding requires one). Value
75+
// comes from the argument, or stdin when omitted (keeps secret values out of shell history).
76+
export async function secretsSet(name: string, value: string | undefined, opts: { branch?: string; service?: string }): Promise<void> {
4677
const api = await ApiClient.load()
4778
const p = await requireProject()
4879
const v = value ?? (await readStdin())
4980
if (!v) die('value is required (pass as an argument or on stdin)')
50-
const payload: Record<string, string> = opts.branch ? { value: v, branch: opts.branch } : { value: v }
81+
const branch = opts.service ? (opts.branch ?? p.branch) : opts.branch
82+
const payload: Record<string, string> = { value: v, ...(branch ? { branch } : {}), ...(opts.service ? { service: opts.service } : {}) }
5183
const res = await api.rawRequest('PUT', `/projects/${p.projectId}/secrets/${encodeURIComponent(name)}`, payload)
5284
if (handleApproval(res)) return
53-
info(`set ${name} (${opts.branch ? `branch ${opts.branch}` : 'project-wide'})`)
85+
info(`set ${name}${opts.service ? ` → ${opts.service}` : ''} (${branch ? `branch ${branch}` : 'project-wide'})`)
5486
}
5587

5688
export async function secretsUnset(name: string, opts: { branch?: string }): Promise<void> {

src/commands/services.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,18 @@ export async function servicesUpgrade(type: string, name: string, spec: string,
131131
if (_opts.json) return printJson(res.body.service)
132132
info(`upgraded ${type} ${name} to ${spec}`)
133133
}
134+
135+
// insta services secrets <type> <name> — the secret names bound to a service.
136+
export async function servicesSecrets(type: string, name: string, opts: { branch?: string; json?: boolean } = {}): Promise<void> {
137+
assertType(type)
138+
const api = await ApiClient.load()
139+
const p = await requireProject()
140+
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(opts.branch ?? p.branch)}`)
141+
const id = resolveServiceId(services, type, name)
142+
const res = await api.rawRequest('GET', `/projects/${p.projectId}/services/${id}/secrets`)
143+
if (handleApproval(res)) return
144+
const { secrets } = res.body
145+
if (opts.json) return printJson(secrets)
146+
if (!secrets.length) return info(`(no secrets bound to ${type}/${name})`)
147+
for (const n of secrets) info(n)
148+
}

src/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,16 +117,21 @@ svc.command('scale <type> <name> <number> [region]').description('Set a compute
117117
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((type, name, number, region, o) => services.servicesScale(type, name, number, region, o)))
118118
svc.command('upgrade <type> <name> <spec>').description('Change a compute/postgres service spec (paid plans only)')
119119
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((type, name, spec, o) => services.servicesUpgrade(type, name, spec, o)))
120+
svc.command('secrets <type> <name>').description("List a service's secret names")
121+
.option('--branch <b>').option('--json').action(guard((type, name, o) => services.servicesSecrets(type, name, o)))
120122

121123
// ---- secrets (seam) ----
122124
const sec = program.command('secrets').description('Fetch the credential bundle (secret seam) into .env')
123125
.option('--branch <branch>').option('-o, --output <file>', 'output file (default .env)').option('--print', 'print instead of writing').option('--json')
124126
.action(guard((o) => secretsCmd.secrets(o)))
125-
sec.command('list').description('List secret names only').option('--branch <branch>').action(guard((o) => secretsCmd.secretsList(o)))
127+
sec.command('list').description('List secret names, grouped by service').option('--branch <branch>').option('--json').action(guard((o) => secretsCmd.secretsList(o)))
126128
sec.command('set <name> [value]').description('Set a user secret (project-wide; value from stdin if omitted)')
127-
.option('--branch <branch>', 'scope to one branch').action(guard((n, v, o) => secretsCmd.secretsSet(n, v, o)))
129+
.option('--branch <branch>', 'scope to one branch').option('--service <type/name>', 'bind to a branch service (implies current branch)')
130+
.action(guard((n, v, o) => secretsCmd.secretsSet(n, v, o)))
128131
sec.command('unset <name>').description('Remove a user secret')
129132
.option('--branch <branch>', 'scope to one branch').action(guard((n, o) => secretsCmd.secretsUnset(n, o)))
133+
sec.command('tree').description('Show secrets as project → branch → service → secrets').option('--json')
134+
.action(guard((o) => secretsCmd.secretsTree(o)))
130135

131136
// ---- deploy ----
132137
program.command('deploy [dir]').description('Deploy a source directory (built remotely on Fly) or a prebuilt --image to a branch compute group')

0 commit comments

Comments
 (0)