Skip to content

Commit

Permalink
refactor(cloud): helpers to create and update secrets (#6068)
Browse files Browse the repository at this point in the history
* refactor: add api method for secret creation

* refactor: add helper method for bulk secret creation

* refactor: introduce request object for bulk secrets creation

* refactor: introduce interface for secret object

* refactor: re-use helper for bulk secret creation

* refactor: rename some arguments

* chore: use sets instead of arrays for faster lookup

* fix: fix bug introduced in 9336aaa

* test: fix test assertion

* fix: fix another bug introduced in 9336aaa

* chore: remove unnecessary log line

Detailed messages are printed in the loop below.

* refactor: rename type alias

* refactor: avoid unnecessary data conversion

* refactor: add api method for secret update

* refactor: introduce request object for bulk secrets update

* chore: update todo-comment

* refactor: introduce named type

* refactor: build secrets requests in a dedicated function

* refactor: extract named interface

* chore: remove outdated comment

It was already addressed in #6065
  • Loading branch information
vvagaytsev authored May 24, 2024
1 parent 7c60c6b commit 11059b7
Show file tree
Hide file tree
Showing 8 changed files with 250 additions and 168 deletions.
12 changes: 12 additions & 0 deletions core/src/cloud/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@ import type {
BaseResponse,
CreateEphemeralClusterResponse,
CreateProjectsForRepoResponse,
CreateSecretRequest,
CreateSecretResponse,
EphemeralClusterWithRegistry,
GetKubeconfigResponse,
GetProfileResponse,
GetProjectResponse,
ListProjectsResponse,
UpdateSecretRequest,
UpdateSecretResponse,
} from "@garden-io/platform-api-types"
import { getCloudDistributionName, getCloudLogSectionName } from "../util/cloud.js"
import { getPackageVersion } from "../util/util.js"
Expand Down Expand Up @@ -870,6 +874,14 @@ export class CloudApi {
return secrets
}

async createSecret(request: CreateSecretRequest): Promise<CreateSecretResponse> {
return await this.post<CreateSecretResponse>(`/secrets`, { body: request })
}

async updateSecret(secretId: string, request: UpdateSecretRequest): Promise<UpdateSecretResponse> {
return await this.put<UpdateSecretResponse>(`/secrets/${secretId}`, { body: request })
}

async registerCloudBuilderBuild(body: {
actionName: string
actionUid: string
Expand Down
9 changes: 5 additions & 4 deletions core/src/commands/cloud/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { CommandError, toGardenError } from "../../exceptions.js"
import type { CommandResult } from "../base.js"
import { userPrompt } from "../../util/util.js"
import { styles } from "../../logger/styles.js"
import type { StringMap } from "../../config/common.js"
import dotenv from "dotenv"
import fsExtra from "fs-extra"

Expand Down Expand Up @@ -144,7 +143,7 @@ export async function readInputKeyValueResources({
resourcesFromArgs: string[] | undefined
resourceName: string
log: Log
}): Promise<StringMap> {
}): Promise<[key: string, value: string][]> {
// File source (by naming convention for args/opts, it's defined via --from-file option)
// always takes precedence over the positional arguments.
if (resourceFilePath) {
Expand All @@ -156,7 +155,8 @@ export async function readInputKeyValueResources({
}

const dotEnvFileContent = await readFile(resourceFilePath)
return dotenv.parse(dotEnvFileContent)
const resourceDictionary = dotenv.parse(dotEnvFileContent)
return Object.entries(resourceDictionary)
} catch (err) {
throw new CommandError({
message: `Unable to read ${resourceName}(s) from file at path ${resourceFilePath}: ${err}`,
Expand All @@ -166,7 +166,7 @@ export async function readInputKeyValueResources({

// Get input resources from positional arguments in no input file defined.
if (resourcesFromArgs) {
return resourcesFromArgs.reduce((acc, keyValPair) => {
const resourceDictionary = resourcesFromArgs.reduce((acc, keyValPair) => {
try {
const resourceEntry = dotenv.parse(keyValPair)
Object.assign(acc, resourceEntry)
Expand All @@ -177,6 +177,7 @@ export async function readInputKeyValueResources({
})
}
}, {})
return Object.entries(resourceDictionary)
}

throw new CommandError({
Expand Down
112 changes: 106 additions & 6 deletions core/src/commands/cloud/secrets/secret-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,20 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import type { ListSecretsResponse, SecretResult as SecretResultApi } from "@garden-io/platform-api-types"
import type {
CreateSecretRequest,
ListSecretsResponse,
SecretResult as CloudApiSecretResult,
UpdateSecretRequest,
} from "@garden-io/platform-api-types"
import type { CloudApi, CloudEnvironment, CloudProject } from "../../../cloud/api.js"
import type { Log } from "../../../logger/log-entry.js"
import queryString from "query-string"
import { CloudApiError } from "../../../exceptions.js"
import { CloudApiError, GardenError } from "../../../exceptions.js"
import { dedent } from "../../../util/string.js"
import type { ApiCommandError } from "../helpers.js"
import { enumerate } from "../../../util/enumerate.js"
import { omit } from "lodash-es"

export interface SecretResult {
id: string
Expand Down Expand Up @@ -54,7 +62,99 @@ export function getEnvironmentByNameOrThrow({
})
}

export function makeSecretFromResponse(res: SecretResultApi): SecretResult {
// TODO: consider moving bulk ops to CloudApi

interface BulkOperationResult {
results: SecretResult[]
errors: ApiCommandError[]
}

export interface Secret {
name: string
value: string
}

export interface BulkCreateSecretRequest extends Omit<CreateSecretRequest, "name" | "value"> {
secrets: Secret[]
}

export async function createSecrets({
request,
api,
log,
}: {
request: BulkCreateSecretRequest
api: CloudApi
log: Log
}): Promise<BulkOperationResult> {
const { secrets, environmentId, userId, projectId } = request

const errors: ApiCommandError[] = []
const results: SecretResult[] = []

for (const [counter, { name, value }] of enumerate(secrets, 1)) {
log.info({ msg: `Creating secrets... → ${counter}/${secrets.length}` })
try {
const body = { environmentId, userId, projectId, name, value }
const res = await api.createSecret(body)
results.push(makeSecretFromResponse(res.data))
} catch (err) {
if (!(err instanceof GardenError)) {
throw err
}
errors.push({
identifier: name,
message: err.message,
})
}
}

return { results, errors }
}

export interface SingleUpdateSecretRequest extends UpdateSecretRequest {
id: string
}

export interface BulkUpdateSecretRequest {
secrets: SingleUpdateSecretRequest[]
}

export async function updateSecrets({
request,
api,
log,
}: {
request: BulkUpdateSecretRequest
api: CloudApi
log: Log
}): Promise<BulkOperationResult> {
const { secrets } = request

const errors: ApiCommandError[] = []
const results: SecretResult[] = []

for (const [counter, secret] of enumerate(secrets, 1)) {
log.info({ msg: `Updating secrets... → ${counter}/${secrets.length}` })
try {
const body = omit(secret, "id")
const res = await api.updateSecret(secret.id, body)
results.push(makeSecretFromResponse(res.data))
} catch (err) {
if (!(err instanceof GardenError)) {
throw err
}
errors.push({
identifier: secret.name,
message: err.message,
})
}
}

return { results, errors }
}

export function makeSecretFromResponse(res: CloudApiSecretResult): SecretResult {
const secret = {
name: res.name,
id: res.id,
Expand All @@ -79,9 +179,9 @@ export function makeSecretFromResponse(res: SecretResultApi): SecretResult {

const secretsPageLimit = 100

export async function fetchAllSecrets(api: CloudApi, projectId: string, log: Log): Promise<SecretResult[]> {
export async function fetchAllSecrets(api: CloudApi, projectId: string, log: Log): Promise<CloudApiSecretResult[]> {
let page = 0
const secrets: SecretResult[] = []
const secrets: CloudApiSecretResult[] = []
let hasMore = true
while (hasMore) {
log.debug(`Fetching page ${page}`)
Expand All @@ -90,7 +190,7 @@ export async function fetchAllSecrets(api: CloudApi, projectId: string, log: Log
if (res.data.length === 0) {
hasMore = false
} else {
secrets.push(...res.data.map((secret) => makeSecretFromResponse(secret)))
secrets.push(...res.data)
page++
}
}
Expand Down
48 changes: 16 additions & 32 deletions core/src/commands/cloud/secrets/secrets-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,16 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { CloudApiError, CommandError, ConfigurationError, GardenError } from "../../../exceptions.js"
import type { CreateSecretResponse } from "@garden-io/platform-api-types"
import { CloudApiError, CommandError, ConfigurationError } from "../../../exceptions.js"
import { printHeader } from "../../../logger/util.js"
import type { CommandParams, CommandResult } from "../../base.js"
import { Command } from "../../base.js"
import type { ApiCommandError } from "../helpers.js"
import { handleBulkOperationResult, noApiMsg, readInputKeyValueResources } from "../helpers.js"
import { dedent, deline } from "../../../util/string.js"
import { PathParameter, StringParameter, StringsParameter } from "../../../cli/params.js"
import type { SecretResult } from "./secret-helpers.js"
import { getEnvironmentByNameOrThrow, makeSecretFromResponse } from "./secret-helpers.js"
import { enumerate } from "../../../util/enumerate.js"
import { createSecrets } from "./secret-helpers.js"
import { getEnvironmentByNameOrThrow } from "./secret-helpers.js"

export const secretsCreateArgs = {
secrets: new StringsParameter({
Expand Down Expand Up @@ -88,12 +86,14 @@ export class SecretsCreateCommand extends Command<Args, Opts> {

const cmdLog = log.createLog({ name: "secrets-command" })

const secrets = await readInputKeyValueResources({
resourceFilePath: secretsFilePath,
resourcesFromArgs: args.secrets,
resourceName: "secret",
log: cmdLog,
})
const secrets = (
await readInputKeyValueResources({
resourceFilePath: secretsFilePath,
resourcesFromArgs: args.secrets,
resourceName: "secret",
log: cmdLog,
})
).map(([key, value]) => ({ name: key, value }))

const api = garden.cloudApi
if (!api) {
Expand All @@ -117,27 +117,11 @@ export class SecretsCreateCommand extends Command<Args, Opts> {
}
}

const secretsToCreate = Object.entries(secrets)
cmdLog.info("Creating secrets...")

const errors: ApiCommandError[] = []
const results: SecretResult[] = []
for (const [counter, [name, value]] of enumerate(secretsToCreate, 1)) {
cmdLog.info({ msg: `Creating secrets... → ${counter}/${secretsToCreate.length}` })
try {
const body = { environmentId, userId, projectId: project.id, name, value }
const res = await api.post<CreateSecretResponse>(`/secrets`, { body })
results.push(makeSecretFromResponse(res.data))
} catch (err) {
if (!(err instanceof GardenError)) {
throw err
}
errors.push({
identifier: name,
message: err.message,
})
}
}
const { errors, results } = await createSecrets({
request: { secrets, environmentId, userId, projectId: project.id },
api,
log,
})

return handleBulkOperationResult({
log,
Expand Down
5 changes: 3 additions & 2 deletions core/src/commands/cloud/secrets/secrets-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { sortBy } from "lodash-es"
import { StringsParameter } from "../../../cli/params.js"
import { styles } from "../../../logger/styles.js"
import type { SecretResult } from "./secret-helpers.js"
import { makeSecretFromResponse } from "./secret-helpers.js"
import { fetchAllSecrets } from "./secret-helpers.js"

export const secretsListOpts = {
Expand Down Expand Up @@ -66,7 +67,7 @@ export class SecretsListCommand extends Command<{}, Opts> {
projectName: garden.projectName,
})

const secrets: SecretResult[] = await fetchAllSecrets(api, project.id, log)
const secrets = await fetchAllSecrets(api, project.id, log)
log.info("")

if (secrets.length === 0) {
Expand Down Expand Up @@ -99,6 +100,6 @@ export class SecretsListCommand extends Command<{}, Opts> {

log.info(renderTable([heading].concat(rows)))

return { result: filtered }
return { result: filtered.map(makeSecretFromResponse) }
}
}
Loading

0 comments on commit 11059b7

Please sign in to comment.