diff --git a/core/src/cloud/api.ts b/core/src/cloud/api.ts index e7ab96a5f1..acd0ad1711 100644 --- a/core/src/cloud/api.ts +++ b/core/src/cloud/api.ts @@ -10,11 +10,11 @@ import type { IncomingHttpHeaders } from "http" import type { GotHeaders, GotJsonOptions, GotResponse } from "../util/http.js" import { got, GotHttpError } from "../util/http.js" -import { CloudApiError, InternalError } from "../exceptions.js" +import { CloudApiError, GardenError, InternalError } from "../exceptions.js" import type { Log } from "../logger/log-entry.js" import { DEFAULT_GARDEN_CLOUD_DOMAIN, gardenEnv } from "../constants.js" import { Cookie } from "tough-cookie" -import { cloneDeep, isObject } from "lodash-es" +import { cloneDeep, isObject, omit } from "lodash-es" import { dedent, deline } from "../util/string.js" import type { BaseResponse, @@ -27,6 +27,9 @@ import type { GetProfileResponse, GetProjectResponse, ListProjectsResponse, + ListSecretsResponse, + SecretResult as CloudApiSecretResult, + SecretResult, UpdateSecretRequest, UpdateSecretResponse, } from "@garden-io/platform-api-types" @@ -41,6 +44,9 @@ import type { StringMap } from "../config/common.js" import { styles } from "../logger/styles.js" import { RequestError } from "got" import type { Garden } from "../garden.js" +import type { ApiCommandError } from "../commands/cloud/helpers.js" +import { enumerate } from "../util/enumerate.js" +import queryString from "query-string" const gardenClientName = "garden-core" const gardenClientVersion = getPackageVersion() @@ -68,6 +74,8 @@ function isGotResponseOk(response: GotResponse) { const refreshThreshold = 10 // Threshold (in seconds) subtracted to jwt validity when checking if a refresh is needed +const secretsPageLimit = 100 + export interface ApiFetchParams { headers: GotHeaders method: "GET" | "POST" | "PUT" | "PATCH" | "HEAD" | "DELETE" @@ -77,6 +85,28 @@ export interface ApiFetchParams { body?: any } +interface BulkOperationResult { + results: SecretResult[] + errors: ApiCommandError[] +} + +export interface Secret { + name: string + value: string +} + +export interface BulkCreateSecretRequest extends Omit { + secrets: Secret[] +} + +export interface SingleUpdateSecretRequest extends UpdateSecretRequest { + id: string +} + +export interface BulkUpdateSecretRequest { + secrets: SingleUpdateSecretRequest[] +} + export interface ApiFetchOptions { headers?: GotHeaders /** @@ -874,14 +904,84 @@ export class CloudApi { return secrets } + async fetchAllSecrets(projectId: string, log: Log): Promise { + let page = 0 + const secrets: CloudApiSecretResult[] = [] + let hasMore = true + while (hasMore) { + log.debug(`Fetching page ${page}`) + const q = queryString.stringify({ projectId, offset: page * secretsPageLimit, limit: secretsPageLimit }) + const res = await this.get(`/secrets?${q}`) + if (res.data.length === 0) { + hasMore = false + } else { + secrets.push(...res.data) + page++ + } + } + return secrets + } + async createSecret(request: CreateSecretRequest): Promise { return await this.post(`/secrets`, { body: request }) } + async createSecrets({ request, log }: { request: BulkCreateSecretRequest; log: Log }): Promise { + 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 this.createSecret(body) + results.push(res.data) + } catch (err) { + if (!(err instanceof GardenError)) { + throw err + } + errors.push({ + identifier: name, + message: err.message, + }) + } + } + + return { results, errors } + } + async updateSecret(secretId: string, request: UpdateSecretRequest): Promise { return await this.put(`/secrets/${secretId}`, { body: request }) } + async updateSecrets({ request, log }: { request: BulkUpdateSecretRequest; log: Log }): Promise { + 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 this.updateSecret(secret.id, body) + results.push(res.data) + } catch (err) { + if (!(err instanceof GardenError)) { + throw err + } + errors.push({ + identifier: secret.name, + message: err.message, + }) + } + } + + return { results, errors } + } + async registerCloudBuilderBuild(body: { actionName: string actionUid: string diff --git a/core/src/commands/cloud/secrets/secret-helpers.ts b/core/src/commands/cloud/secrets/secret-helpers.ts index 3a8c58d9c1..43f935d3cd 100644 --- a/core/src/commands/cloud/secrets/secret-helpers.ts +++ b/core/src/commands/cloud/secrets/secret-helpers.ts @@ -6,20 +6,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -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, GardenError } from "../../../exceptions.js" +import type { SecretResult as CloudApiSecretResult } from "@garden-io/platform-api-types" +import type { CloudEnvironment, CloudProject } from "../../../cloud/api.js" +import { CloudApiError } 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 @@ -62,98 +52,6 @@ export function getEnvironmentByNameOrThrow({ }) } -// 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 { - secrets: Secret[] -} - -export async function createSecrets({ - request, - api, - log, -}: { - request: BulkCreateSecretRequest - api: CloudApi - log: Log -}): Promise { - 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 { - 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, @@ -176,23 +74,3 @@ export function makeSecretFromResponse(res: CloudApiSecretResult): SecretResult } return secret } - -const secretsPageLimit = 100 - -export async function fetchAllSecrets(api: CloudApi, projectId: string, log: Log): Promise { - let page = 0 - const secrets: CloudApiSecretResult[] = [] - let hasMore = true - while (hasMore) { - log.debug(`Fetching page ${page}`) - const q = queryString.stringify({ projectId, offset: page * secretsPageLimit, limit: secretsPageLimit }) - const res = await api.get(`/secrets?${q}`) - if (res.data.length === 0) { - hasMore = false - } else { - secrets.push(...res.data) - page++ - } - } - return secrets -} diff --git a/core/src/commands/cloud/secrets/secrets-create.ts b/core/src/commands/cloud/secrets/secrets-create.ts index 57a81ff4be..dc0c478f24 100644 --- a/core/src/commands/cloud/secrets/secrets-create.ts +++ b/core/src/commands/cloud/secrets/secrets-create.ts @@ -14,7 +14,7 @@ import { handleBulkOperationResult, noApiMsg, readInputKeyValueResources } from import { dedent, deline } from "../../../util/string.js" import { PathParameter, StringParameter, StringsParameter } from "../../../cli/params.js" import type { SecretResult } from "./secret-helpers.js" -import { createSecrets } from "./secret-helpers.js" +import { makeSecretFromResponse } from "./secret-helpers.js" import { getEnvironmentByNameOrThrow } from "./secret-helpers.js" export const secretsCreateArgs = { @@ -117,9 +117,8 @@ export class SecretsCreateCommand extends Command { } } - const { errors, results } = await createSecrets({ + const { errors, results } = await api.createSecrets({ request: { secrets, environmentId, userId, projectId: project.id }, - api, log, }) @@ -129,7 +128,7 @@ export class SecretsCreateCommand extends Command { action: "create", resource: "secret", errors, - results, + results: results.map(makeSecretFromResponse), }) } } diff --git a/core/src/commands/cloud/secrets/secrets-list.ts b/core/src/commands/cloud/secrets/secrets-list.ts index 93ef20fce8..e74b0db6c2 100644 --- a/core/src/commands/cloud/secrets/secrets-list.ts +++ b/core/src/commands/cloud/secrets/secrets-list.ts @@ -17,7 +17,6 @@ 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 = { "filter-envs": new StringsParameter({ @@ -67,7 +66,7 @@ export class SecretsListCommand extends Command<{}, Opts> { projectName: garden.projectName, }) - const secrets = await fetchAllSecrets(api, project.id, log) + const secrets = await api.fetchAllSecrets(project.id, log) log.info("") if (secrets.length === 0) { diff --git a/core/src/commands/cloud/secrets/secrets-update.ts b/core/src/commands/cloud/secrets/secrets-update.ts index 23a51634c8..936b4b29b4 100644 --- a/core/src/commands/cloud/secrets/secrets-update.ts +++ b/core/src/commands/cloud/secrets/secrets-update.ts @@ -16,11 +16,10 @@ import type { CommandParams, CommandResult } from "../../base.js" import { Command } from "../../base.js" import { handleBulkOperationResult, noApiMsg, readInputKeyValueResources } from "../helpers.js" import type { Log } from "../../../logger/log-entry.js" -import type { BulkCreateSecretRequest, BulkUpdateSecretRequest, Secret, SecretResult } from "./secret-helpers.js" -import { updateSecrets } from "./secret-helpers.js" -import { createSecrets } from "./secret-helpers.js" -import { fetchAllSecrets, getEnvironmentByNameOrThrow } from "./secret-helpers.js" -import type { CloudApi } from "../../../cloud/api.js" +import type { SecretResult } from "./secret-helpers.js" +import { makeSecretFromResponse } from "./secret-helpers.js" +import { getEnvironmentByNameOrThrow } from "./secret-helpers.js" +import type { BulkCreateSecretRequest, BulkUpdateSecretRequest, CloudApi, Secret } from "../../../cloud/api.js" export const secretsUpdateArgs = { secretNamesOrIds: new StringsParameter({ @@ -146,15 +145,13 @@ export class SecretsUpdateCommand extends Command { cmdLog.info(`${secretsCreateRequest.secrets.length} new secret(s) to be created.`) } - const { errors: updateErrors, results: updateResults } = await updateSecrets({ + const { errors: updateErrors, results: updateResults } = await api.updateSecrets({ request: secretsUpdateRequest, - api, log, }) - const { errors: creationErrors, results: creationResults } = await createSecrets({ + const { errors: creationErrors, results: creationResults } = await api.createSecrets({ request: secretsCreateRequest, - api, log, }) @@ -164,7 +161,7 @@ export class SecretsUpdateCommand extends Command { action: "update", resource: "secret", errors: [...updateErrors, ...creationErrors], - results: [...updateResults, ...creationResults], + results: [...updateResults, ...creationResults].map(makeSecretFromResponse), }) } } @@ -182,7 +179,7 @@ async function prepareSecretsRequests(params: { }): Promise<{ secretsCreateRequest: BulkCreateSecretRequest; secretsUpdateRequest: BulkUpdateSecretRequest }> { const { api, environmentId, environmentName, inputSecrets, log, projectId, updateById, upsert, userId } = params - const allSecrets = await fetchAllSecrets(api, projectId, log) + const allSecrets = await api.fetchAllSecrets(projectId, log) let secretsToCreate: Secret[] let secretsToUpdate: Array diff --git a/core/test/unit/src/commands/cloud/secrets/secrets-update.ts b/core/test/unit/src/commands/cloud/secrets/secrets-update.ts index 47c6af25f0..fc4cf434b1 100644 --- a/core/test/unit/src/commands/cloud/secrets/secrets-update.ts +++ b/core/test/unit/src/commands/cloud/secrets/secrets-update.ts @@ -15,7 +15,7 @@ import { } from "../../../../../../src/commands/cloud/secrets/secrets-update.js" import { deline } from "../../../../../../src/util/string.js" import { expectError, getDataDir, makeTestGarden } from "../../../../../helpers.js" -import type { Secret } from "../../../../../../src/commands/cloud/secrets/secret-helpers.js" +import type { Secret } from "../../../../../../src/cloud/api.js" describe("SecretsUpdateCommand", () => { const projectRoot = getDataDir("test-project-b")