Skip to content

Commit 1b86d56

Browse files
authored
feat(api): make cloud util commands visible (#7742)
...plus add commands for deleting remote variables, listing remote variables and getting the current user.
1 parent d463e38 commit 1b86d56

File tree

7 files changed

+505
-16
lines changed

7 files changed

+505
-16
lines changed

core/src/commands/create/create-cloud-variables.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ export class CreateCloudVariablesCommand extends Command<Args, Opts> {
105105

106106
override arguments = createCloudVariablesArgs
107107
override options = createCloudVariablesOpts
108-
override hidden = true
109108

110109
override printHeader({ log }) {
111110
printHeader(log, "Create remote variables", "☁️")

core/src/commands/delete.ts

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import dedent from "dedent"
1212
import { printHeader } from "../logger/util.js"
1313
import type { EnvironmentStatusMap } from "../plugin/handlers/Provider/getEnvironmentStatus.js"
1414
import { DeleteDeployTask, deletedDeployStatuses } from "../tasks/delete-deploy.js"
15-
import { joi, joiIdentifierMap } from "../config/common.js"
15+
import { joi, joiIdentifierMap, joiArray } from "../config/common.js"
1616
import { environmentStatusSchema } from "../config/status.js"
1717
import { BooleanParameter, StringsParameter } from "../cli/params.js"
1818
import { deline } from "../util/string.js"
@@ -21,14 +21,20 @@ import { isDeployAction } from "../actions/deploy.js"
2121
import { omit, mapValues } from "lodash-es"
2222
import type { DeployStatus, DeployStatusMap } from "../plugin/handlers/Deploy/get-status.js"
2323
import { getDeployStatusSchema } from "../plugin/handlers/Deploy/get-status.js"
24+
import { CommandError, ConfigurationError, GardenError } from "../exceptions.js"
25+
import { enumerate } from "../util/enumerate.js"
26+
import { handleBulkOperationResult, noApiMsg, throwIfLegacyCloud } from "./helpers.js"
27+
import { confirmDelete } from "./cloud/helpers.js"
28+
import type { DeleteResult } from "./cloud/helpers.js"
29+
import type { EmptyObject } from "type-fest"
2430

2531
// TODO: rename this to CleanupCommand, and do the same for all related classes, constants, variables and functions
2632
export class DeleteCommand extends CommandGroup {
2733
name = "cleanup"
2834
override aliases = ["del", "delete"]
2935
help = "Clean up resources."
3036

31-
subCommands = [DeleteEnvironmentCommand, DeleteDeployCommand]
37+
subCommands = [DeleteEnvironmentCommand, DeleteDeployCommand, DeleteRemoteVariablesCommand]
3238
}
3339

3440
const dependantsFirstOpt = {
@@ -87,7 +93,7 @@ export class DeleteEnvironmentCommand extends Command<{}, DeleteEnvironmentOpts>
8793
garden,
8894
log,
8995
opts,
90-
}: CommandParams<{}, DeleteEnvironmentOpts>): Promise<CommandResult<DeleteEnvironmentResult>> {
96+
}: CommandParams<EmptyObject, DeleteEnvironmentOpts>): Promise<CommandResult<DeleteEnvironmentResult>> {
9197
const actions = await garden.getActionRouter()
9298
const graph = await garden.getConfigGraph({ log, emit: true })
9399
const deployStatuses = await actions.deleteDeploys({
@@ -211,3 +217,96 @@ export class DeleteDeployCommand extends Command<DeleteDeployArgs, DeleteDeployO
211217
return { result }
212218
}
213219
}
220+
221+
export const deleteRemoteVariablesArgs = {
222+
ids: new StringsParameter({
223+
help: deline`The ID(s) of the cloud variables to delete.`,
224+
spread: true,
225+
}),
226+
}
227+
228+
type DeleteRemoteVariablesArgs = typeof deleteRemoteVariablesArgs
229+
230+
export class DeleteRemoteVariablesCommand extends Command<DeleteRemoteVariablesArgs> {
231+
name = "remote-variables"
232+
help = "Delete remote variables from Garden Cloud."
233+
emoji = "☁️"
234+
235+
override aliases = ["cloud-variables"]
236+
237+
override description = dedent`
238+
Delete remote variables in Garden Cloud. You will need the IDs of the variables you want to delete,
239+
which you can get from the \`garden get remote-variables\` command.
240+
241+
Examples:
242+
garden delete remote-variables <ID 1> <ID 2> <ID 3> # delete the remote variables with the given IDs.
243+
`
244+
245+
override arguments = deleteRemoteVariablesArgs
246+
247+
override printHeader({ log }) {
248+
printHeader(log, "Delete remote variables", "☁️")
249+
}
250+
251+
override outputsSchema = () =>
252+
joi.object().keys({
253+
variables: joiArray(
254+
joi.object().keys({
255+
id: joi.string(),
256+
success: joi.boolean(),
257+
})
258+
).description("A list of deleted variables"),
259+
})
260+
261+
async action({ garden, args, log, opts }: CommandParams<DeleteRemoteVariablesArgs>): Promise<CommandResult> {
262+
throwIfLegacyCloud(garden, "garden cloud variables delete")
263+
264+
const variablesToDelete = args.ids || []
265+
if (variablesToDelete.length === 0) {
266+
throw new CommandError({
267+
message: `No variable IDs provided.`,
268+
})
269+
}
270+
271+
if (!opts.yes && !(await confirmDelete("remote variable", variablesToDelete.length))) {
272+
return {}
273+
}
274+
275+
if (!garden.cloudApi) {
276+
throw new ConfigurationError({ message: noApiMsg("delete", "remote variables") })
277+
}
278+
279+
const cmdLog = log.createLog({ name: "variables-command" })
280+
cmdLog.info("Deleting remote variables...")
281+
282+
const errors: { identifier: string; message?: string }[] = []
283+
const results: DeleteResult[] = []
284+
for (const [counter, id] of enumerate(variablesToDelete, 1)) {
285+
cmdLog.info({ msg: `Deleting remote variables... → ${counter}/${variablesToDelete.length}` })
286+
try {
287+
const res = await garden.cloudApi.trpc.variable.delete.mutate({
288+
organizationId: garden.cloudApi.organizationId,
289+
variableId: id,
290+
})
291+
results.push({ id, status: res.success ? "success" : "error" })
292+
} catch (err) {
293+
if (!(err instanceof GardenError)) {
294+
throw err
295+
}
296+
errors.push({
297+
identifier: id,
298+
message: err.message,
299+
})
300+
}
301+
}
302+
303+
return handleBulkOperationResult({
304+
log,
305+
cmdLog,
306+
errors,
307+
action: "delete",
308+
resource: "variable",
309+
results,
310+
})
311+
}
312+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*
2+
* Copyright (C) 2018-2025 Garden Technologies, Inc. <info@garden.io>
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
*/
8+
9+
import type { CommandParams, CommandResult } from "../base.js"
10+
import { Command } from "../base.js"
11+
import { printEmoji, printHeader } from "../../logger/util.js"
12+
import { dedent, renderTable } from "../../util/string.js"
13+
import { styles } from "../../logger/styles.js"
14+
import { joi, joiArray } from "../../config/common.js"
15+
import { ConfigurationError } from "../../exceptions.js"
16+
import { getCloudListCommandBaseDescription, noApiMsg, throwIfLegacyCloud } from "../helpers.js"
17+
import { makeDocsLinkPlain } from "../../docs/common.js"
18+
import { getVarlistIdsFromRemoteVarsConfig } from "../../config/project.js"
19+
import type { RouterOutput } from "../../cloud/api/trpc.js"
20+
import type { EmptyObject } from "type-fest"
21+
22+
const getRemoteVariablesOpts = {}
23+
24+
type Opts = typeof getRemoteVariablesOpts
25+
26+
interface RemoteVariable {
27+
name: string
28+
id: string
29+
value: string
30+
isSecret: boolean
31+
variableListName: string
32+
scopedToEnvironment: string
33+
scopedToUser: string
34+
expiresAt: string
35+
description: string
36+
scopedAccountId: string | null
37+
scopedEnvironmentId: string | null
38+
}
39+
40+
export class GetRemoteVariablesCommand extends Command<EmptyObject, Opts> {
41+
name = "remote-variables"
42+
help = "Get remote variables from Garden Cloud"
43+
emoji = "☁️"
44+
45+
override aliases = ["cloud-variables"]
46+
47+
override description = dedent`
48+
${getCloudListCommandBaseDescription("remote variables")}
49+
50+
List all remote variables for the variable lists configured in this project. This is useful for
51+
seeing the IDs of remote variables (e.g. for use with the \`garden delete remote-variables\` command)
52+
and for viewing cloud-specific information such as scoping and expiration.
53+
54+
Examples:
55+
garden get remote-variables # list remote variables and pretty print results
56+
garden get remote-variables --output json # returns remote variables as a JSON object, useful for scripting
57+
58+
See the [Variables and Templating guide](${makeDocsLinkPlain`features/variables-and-templating`}) for more information.
59+
60+
`
61+
62+
override options = getRemoteVariablesOpts
63+
64+
override printHeader({ log }) {
65+
printHeader(log, "Get remote variables", "☁️")
66+
}
67+
68+
override outputsSchema = () =>
69+
joi.object().keys({
70+
variables: joiArray(
71+
joi.object().keys({
72+
name: joi.string(),
73+
id: joi.string(),
74+
value: joi.string(),
75+
isSecret: joi.boolean(),
76+
variableListName: joi.string(),
77+
environmentScope: joi.string(),
78+
userScope: joi.string(),
79+
expiresAt: joi.string().allow(""),
80+
description: joi.string(),
81+
})
82+
).description("A list of remote variables"),
83+
})
84+
85+
async action({
86+
garden,
87+
log,
88+
}: CommandParams<EmptyObject, Opts>): Promise<CommandResult<{ variables: RemoteVariable[] }>> {
89+
throwIfLegacyCloud(garden, "garden cloud variables list")
90+
91+
if (!garden.cloudApi) {
92+
throw new ConfigurationError({ message: noApiMsg("get", "cloud variables") })
93+
}
94+
95+
const config = await garden.dumpConfigWithInteralFields({
96+
log,
97+
includeDisabled: false,
98+
resolveGraph: false,
99+
resolveProviders: false,
100+
resolveWorkflows: false,
101+
})
102+
103+
const variableListIds = getVarlistIdsFromRemoteVarsConfig(config.importVariables)
104+
105+
if (variableListIds.length === 0) {
106+
log.info("No variable lists configured in this project.")
107+
return { result: { variables: [] } }
108+
}
109+
110+
const allVariables: RouterOutput["variableList"]["listVariables"]["items"] = []
111+
112+
for (const variableListId of variableListIds) {
113+
let cursor: number | undefined = undefined
114+
115+
do {
116+
log.debug(`Fetching variables for variable list ${variableListId}`)
117+
const response = await garden.cloudApi.trpc.variableList.listVariables.query({
118+
organizationId: garden.cloudApi.organizationId,
119+
variableListId,
120+
...(cursor && { cursor }),
121+
})
122+
123+
allVariables.push(...response.items)
124+
cursor = response.nextCursor
125+
} while (cursor)
126+
}
127+
128+
const variables: RemoteVariable[] = allVariables.map((v) => ({
129+
name: v.name,
130+
id: v.id,
131+
value: v.isSecret ? "<secret>" : v.value,
132+
isSecret: v.isSecret,
133+
variableListName: v.variableListName || "N/A",
134+
scopedToEnvironment: v.scopedGardenEnvironmentName || "None",
135+
scopedToUser: v.scopedAccountName || "None",
136+
expiresAt: v.expiresAt ? new Date(v.expiresAt).toISOString() : "Never",
137+
description: v.description || "",
138+
scopedAccountId: v.scopedAccountId,
139+
scopedEnvironmentId: v.scopedGardenEnvironmentId,
140+
}))
141+
142+
const heading = [
143+
"Name",
144+
"ID",
145+
"Value",
146+
"Variable List",
147+
"Environment Scope",
148+
"User Scope",
149+
"Expires At",
150+
"Secret",
151+
].map((s) => styles.bold(s))
152+
153+
const rows: string[][] = variables.map((v) => {
154+
return [
155+
styles.highlight.bold(v.name),
156+
v.id,
157+
v.value,
158+
v.variableListName,
159+
v.scopedToEnvironment,
160+
v.scopedToUser,
161+
v.expiresAt,
162+
v.isSecret ? "Yes" : "No",
163+
]
164+
})
165+
166+
log.info("")
167+
log.info(renderTable([heading].concat(rows)))
168+
log.info(styles.success("OK") + " " + printEmoji("✔️", log))
169+
170+
return { result: { variables } }
171+
}
172+
}

0 commit comments

Comments
 (0)