diff --git a/core/src/cloud/api.ts b/core/src/cloud/api.ts index a1bab1e96b..846eaa140a 100644 --- a/core/src/cloud/api.ts +++ b/core/src/cloud/api.ts @@ -154,6 +154,7 @@ export interface CloudEnvironment { export interface CloudProject { id: string name: string + organizationId: string repositoryUrl: string environments: CloudEnvironment[] } @@ -164,9 +165,7 @@ export interface GetSecretsParams { environmentName: string } -function toCloudProject( - project: GetProjectResponse["data"] | ListProjectsResponse["data"][0] | CreateProjectsForRepoResponse["data"][0] -): CloudProject { +function toCloudProject(project: GetProjectResponse["data"] | CreateProjectsForRepoResponse["data"][0]): CloudProject { const environments: CloudEnvironment[] = [] for (const environment of project.environments) { @@ -176,6 +175,7 @@ function toCloudProject( return { id: project.id, name: project.name, + organizationId: project.organization.id, repositoryUrl: project.repositoryUrl, environments, } @@ -423,12 +423,7 @@ export class CloudApi { return undefined } - const project = toCloudProject(projectList[0]) - - // Cache the entry by ID - this.projects.set(project.id, project) - - return project + return await this.getProjectById(projectList[0].id) } async createProject(projectName: string): Promise { @@ -988,19 +983,30 @@ export class CloudApi { return { results, errors } } - async registerCloudBuilderBuild(body: { + async registerCloudBuilderBuild({ + organizationId, + ...body + }: { + organizationId: string actionName: string actionUid: string + actionVersion: string coreSessionId: string + platforms: string[] + mtlsClientPublicKeyPEM: string | undefined }): Promise { try { - return await this.post(`/cloudbuilder/builds/`, { - body, - }) + return await this.post( + `/organizations/${organizationId}/cloudbuilder/builds/`, + { + body, + } + ) + // TODO: error handling } catch (err) { return { data: { - version: "v1", + version: "v2", availability: { available: false, reason: `Failed to determine Garden Cloud Builder availability: ${extractErrorMessageBodyFromGotError(err) ?? err}`, @@ -1036,10 +1042,10 @@ export class CloudApi { } // TODO(cloudbuilder): import these from api-types -type V1RegisterCloudBuilderBuildResponse = { +type RegisterCloudBuilderBuildResponseV2 = { data: { - version: "v1" - availability: CloudBuilderAvailability + version: "v2" + availability: CloudBuilderAvailabilityV2 } } type UnsupportedRegisterCloudBuilderBuildResponse = { @@ -1048,16 +1054,25 @@ type UnsupportedRegisterCloudBuilderBuildResponse = { } } type RegisterCloudBuilderBuildResponse = - | V1RegisterCloudBuilderBuildResponse + | RegisterCloudBuilderBuildResponseV2 | UnsupportedRegisterCloudBuilderBuildResponse -export type CloudBuilderAvailable = { +export type CloudBuilderAvailableV2 = { available: true - token: string - region: "eu" // location of the builder. Currently only eu is supported + + buildx: { + endpoints: { + platform: string + mtlsEndpoint: string + serverCaPem: string + }[] + clientCertificatePem: string + // only defined if the request did not include a "mtlsClientPublicKeyPEM" + privateKeyPem: string | undefined + } } -export type CloudBuilderNotAvailable = { +export type CloudBuilderNotAvailableV2 = { available: false reason: string } -export type CloudBuilderAvailability = CloudBuilderAvailable | CloudBuilderNotAvailable +export type CloudBuilderAvailabilityV2 = CloudBuilderAvailableV2 | CloudBuilderNotAvailableV2 diff --git a/core/src/garden.ts b/core/src/garden.ts index 61232a27ee..e43e36ca5f 100644 --- a/core/src/garden.ts +++ b/core/src/garden.ts @@ -1934,11 +1934,6 @@ export const resolveGardenParams = profileAsync(async function _resolveGardenPar commandName: opts.commandInfo.name, }) - // If the user is logged in and a cloud project exists we use that ID - // but fallback to the one set in the config (even if the user isn't logged in). - // Same applies for domains. - const projectId = cloudProject?.id || config.id - config = resolveProjectConfig({ log, defaultEnvironmentName: configDefaultEnvironment, @@ -2006,7 +2001,10 @@ export const resolveGardenParams = profileAsync(async function _resolveGardenPar artifactsPath, vcsInfo, sessionId, - projectId, + // If the user is logged in and a cloud project exists we use that ID + // but fallback to the one set in the config (even if the user isn't logged in). + // Same applies for domains. + projectId: cloudProject?.id || config.id, cloudDomain, projectConfig: config, projectRoot, diff --git a/core/src/plugin-context.ts b/core/src/plugin-context.ts index ada4dbf8d2..7d37a5b08c 100644 --- a/core/src/plugin-context.ts +++ b/core/src/plugin-context.ts @@ -32,6 +32,7 @@ export type WrappedFromGarden = Pick< | "gardenDirPath" | "workingCopyId" | "cloudApi" + | "projectId" // TODO: remove this from the interface | "environmentName" | "namespace" @@ -85,6 +86,7 @@ export const pluginContextSchema = createSchema({ .description("Indicate if the current environment is a production environment.") .example(true), projectName: projectNameSchema(), + projectId: joi.string().optional().description("The unique ID of the current project."), projectRoot: joi.string().description("The absolute path of the project root."), projectSources: projectSourcesSchema(), provider: providerSchema().description("The provider being used for this context.").id("ctxProviderSchema"), @@ -224,5 +226,6 @@ export async function createPluginContext({ tools: await garden.getTools(), workingCopyId: garden.workingCopyId, cloudApi: garden.cloudApi, + projectId: garden.projectId, } } diff --git a/core/src/plugins/container/build.ts b/core/src/plugins/container/build.ts index 535ed282dd..95b0e67a67 100644 --- a/core/src/plugins/container/build.ts +++ b/core/src/plugins/container/build.ts @@ -25,10 +25,10 @@ import { import type { Writable } from "stream" import type { ActionLog } from "../../logger/log-entry.js" import type { PluginContext } from "../../plugin-context.js" -import type { SpawnOutput } from "../../util/util.js" import { cloudBuilder } from "./cloudbuilder.js" import { styles } from "../../logger/styles.js" -import type { CloudBuilderAvailable } from "../../cloud/api.js" +import type { CloudBuilderAvailableV2 } from "../../cloud/api.js" +import type { SpawnOutput } from "../../util/util.js" export const validateContainerBuild: BuildActionHandler<"validate", ContainerBuildAction> = async ({ action }) => { // configure concurrency limit for build status task nodes. @@ -206,7 +206,7 @@ const BUILDKIT_LAYER_CACHED_REGEX = /^#[0-9]+ CACHED/ async function buildContainerInCloudBuilder(params: { action: Resolved - availability: CloudBuilderAvailable + availability: CloudBuilderAvailableV2 outputStream: Writable timeout: number log: ActionLog diff --git a/core/src/plugins/container/cloudbuilder.ts b/core/src/plugins/container/cloudbuilder.ts index c99b963a6a..6c772f2347 100644 --- a/core/src/plugins/container/cloudbuilder.ts +++ b/core/src/plugins/container/cloudbuilder.ts @@ -8,24 +8,38 @@ import type { PluginContext } from "../../plugin-context.js" import type { Resolved } from "../../actions/types.js" import type { ContainerBuildAction } from "./config.js" -import { ChildProcessError, ConfigurationError, InternalError } from "../../exceptions.js" -import type { ContainerProvider } from "./container.js" +import { ConfigurationError, InternalError } from "../../exceptions.js" +import type { ContainerProvider, ContainerProviderConfig } from "./container.js" import dedent from "dedent" import { styles } from "../../logger/styles.js" import type { KubernetesPluginContext } from "../kubernetes/config.js" -import { uuidv4 } from "../../util/random.js" import fsExtra from "fs-extra" - -const { mkdirp, rm } = fsExtra -import { join } from "path" +const { mkdirp, rm, writeFile, stat } = fsExtra +import { basename, dirname, join } from "path" import { tmpdir } from "node:os" -import type { CloudBuilderAvailability, CloudBuilderAvailable } from "../../cloud/api.js" +import type { CloudBuilderAvailabilityV2, CloudBuilderAvailableV2 } from "../../cloud/api.js" import { emitNonRepeatableWarning } from "../../warnings.js" import { LRUCache } from "lru-cache" -import { getPlatform } from "../../util/arch-platform.js" -import { gardenEnv } from "../../constants.js" +import { DEFAULT_GARDEN_CLOUD_DOMAIN, gardenEnv } from "../../constants.js" import type { ActionRuntime, ActionRuntimeKind } from "../../plugin/base.js" -import { getPackageVersion } from "../../util/util.js" +import { getCloudDistributionName } from "../../util/cloud.js" +import crypto from "crypto" +import { promisify } from "util" +import AsyncLock from "async-lock" +import { containerHelpers } from "./helpers.js" +import { hashString } from "../../util/util.js" +import { stableStringify } from "../../util/string.js" +import { homedir } from "os" + +const generateKeyPair = promisify(crypto.generateKeyPair) + +type MtlsKeyPair = { + privateKeyPem: string + publicKeyPem: string +} + +let _mtlsKeyPair: MtlsKeyPair | undefined +const mtlsKeyPairLock = new AsyncLock() type CloudBuilderConfiguration = { isInClusterBuildingConfigured: boolean @@ -34,7 +48,7 @@ type CloudBuilderConfiguration = { // This means that Core will ask Cloud for availability every 5 minutes. // It might well be that we plan to use Cloud Builder for an action, and then we fall back to building locally. -const cloudBuilderAvailability = new LRUCache({ +const cloudBuilderAvailability = new LRUCache({ max: 1000, // 5 minutes ttl: 1000 * 60 * 5, @@ -49,7 +63,10 @@ export const cloudBuilder = { /** * @returns false if Cloud Builder is not configured or not available, otherwise it returns the availability (a required parameter for withBuilder) */ - async getAvailability(ctx: PluginContext, action: Resolved): Promise { + async getAvailability( + ctx: PluginContext, + action: Resolved + ): Promise { const { isInClusterBuildingConfigured, isCloudBuilderEnabled } = getConfiguration(ctx) if (!isCloudBuilderEnabled) { @@ -65,22 +82,6 @@ export const cloudBuilder = { return fromCache } - if (getPlatform() === "windows") { - emitNonRepeatableWarning( - ctx.log, - dedent` - ${styles.bold("Garden Cloud Builder is not available for Windows at the moment.")} - - Please contact our customer support and tell us more if you're interested in Windows support.` - ) - const unsupported: CloudBuilderAvailability = { - available: false, - reason: `Cloud Builder is not available on Windows with Garden version ${getPackageVersion()}`, - } - cloudBuilderAvailability.set(action.uid, unsupported) - return unsupported - } - if (!ctx.cloudApi) { const fallbackDescription = isInClusterBuildingConfigured ? `This forces Garden to use the fall-back option to build images within your Kubernetes cluster, as in-cluster building is configured in the Kubernetes provider settings.` @@ -94,14 +95,28 @@ export const cloudBuilder = { }) } + if (ctx.cloudApi.domain === DEFAULT_GARDEN_CLOUD_DOMAIN && ctx.projectId === undefined) { + throw new InternalError({ message: "Authenticated with community tier, but projectId is undefined" }) + } else if (ctx.projectId === undefined) { + throw new ConfigurationError({ + message: dedent`Please connect your Garden Project with ${getCloudDistributionName(ctx.cloudApi.domain)}. See also ${styles.link("https://cloud.docs.garden.io/getting-started/first-project")}`, + }) + } + + const { publicKeyPem } = await getMtlsKeyPair() + const res = await ctx.cloudApi.registerCloudBuilderBuild({ - // TODO: send requested platforms and action version + organizationId: (await ctx.cloudApi.getProjectById(ctx.projectId)).organizationId, actionUid: action.uid, actionName: action.name, + actionVersion: action.getFullVersion().toString(), coreSessionId: ctx.sessionId, + // if platforms are not set, we default to linux/amd64 + platforms: action.getSpec()["platforms"] || ["linux/amd64"], + mtlsClientPublicKeyPEM: publicKeyPem, }) - if (res.data.version !== "v1") { + if (res.data.version !== "v2") { emitNonRepeatableWarning( ctx.log, dedent` @@ -113,7 +128,7 @@ export const cloudBuilder = { Run ${styles.command("garden self-update")} to update Garden to the latest version.` ) - const unsupported: CloudBuilderAvailability = { available: false, reason: "Unsupported client version" } + const unsupported: CloudBuilderAvailabilityV2 = { available: false, reason: "Unsupported client version" } cloudBuilderAvailability.set(action.uid, unsupported) return unsupported } @@ -137,7 +152,7 @@ export const cloudBuilder = { return availability }, - getActionRuntime(ctx: PluginContext, availability: CloudBuilderAvailability): ActionRuntime { + getActionRuntime(ctx: PluginContext, availability: CloudBuilderAvailabilityV2): ActionRuntime { const { isCloudBuilderEnabled, isInClusterBuildingConfigured } = getConfiguration(ctx) const fallback: ActionRuntimeKind = isInClusterBuildingConfigured @@ -182,86 +197,33 @@ export const cloudBuilder = { async withBuilder( ctx: PluginContext, - availability: CloudBuilderAvailable, + availability: CloudBuilderAvailableV2, performBuild: (builder: string) => Promise ) { - // Docker only accepts builder names that start with a letter - const buildxBuilderName = `cb${uuidv4()}-garden-cloud-builder` - - // Temp dir needs to be as short as possible, otherwise docker fails to connect - // (ERROR: no valid drivers found: unix socket path "..." is too long) - const stateDir = join(tmpdir(), buildxBuilderName.substring(0, 8)) - await mkdirp(stateDir) + const { privateKeyPem } = await getMtlsKeyPair() + + const builder = new BuildxBuilder({ + privateKeyPem, + clientCertificatePem: availability.buildx.clientCertificatePem, + endpoints: availability.buildx.endpoints.map(({ platform, mtlsEndpoint, serverCaPem }) => ({ + platform, + builderUrl: toBuilderUrl(mtlsEndpoint), + serverCaPem, + })), + ctx, + }) try { - ctx.log.debug(`Spawning buildx proxy ${buildxBuilderName}`) - const result = await nscCli({ - // See https://namespace.so/docs/cli/docker-buildx-setup - args: ["docker", "buildx", "setup", "--name", buildxBuilderName, "--state", stateDir, "--background"], - ctx, - nscAuthToken: availability.token, - nscRegion: availability.region, - }) - ctx.log.debug( - `buildx proxy setup process for ${buildxBuilderName} exited with code ${result.exitCode}${result.all?.length ? ` (output: ${result.all})` : ""}` - ) + ctx.log.debug(`Installing Buildkit builder ${builder.name}`) + await builder.install() - return await performBuild(buildxBuilderName) + return await performBuild(builder.name) } finally { - ctx.log.debug(`Cleaning up ${buildxBuilderName}`) - await nscCli({ - args: ["docker", "buildx", "cleanup", "--state", stateDir], - ctx, - nscAuthToken: availability.token, - nscRegion: availability.region, - }) - ctx.log.debug(`Removing ${stateDir}...`) - await rm(stateDir, { recursive: true, force: true }) - } - }, -} + ctx.log.debug(`Cleaning up ${builder.name}`) -// private helpers - -async function nscCli({ - args, - ctx, - nscAuthToken, - nscRegion, -}: { - args: string[] - ctx: PluginContext - nscAuthToken: string - nscRegion: string -}) { - // env variables for the nsc commands - const env = { - // skip update check - NS_DO_NOT_UPDATE: "true", - // this helps avoiding to interfere with user's own nsc authentication, if they happen to use it - NSC_TOKEN_SPEC: Buffer.from( - JSON.stringify({ - version: "v1", - inline_token: nscAuthToken, - }) - ) - // nsc uses https://pkg.go.dev/encoding/base64#RawStdEncoding (standard base64 encoding without padding characters) - .toString("base64") - .replaceAll("=", ""), - } - - const nsc = ctx.tools["container.namespace-cli"] - - try { - return await nsc.exec({ args: ["--region", nscRegion, ...args], log: ctx.log, env }) - } catch (e: unknown) { - if (e instanceof ChildProcessError) { - // if an error happens here, it's likely a bug - throw InternalError.wrapError(e, "Failed to set up Garden Cloud Builder") - } else { - throw e + await builder.clean() } - } + }, } function getConfiguration(ctx: PluginContext): CloudBuilderConfiguration { @@ -294,3 +256,237 @@ function getConfiguration(ctx: PluginContext): CloudBuilderConfiguration { isCloudBuilderEnabled, } } + +async function getMtlsKeyPair(): Promise { + return mtlsKeyPairLock.acquire("generateKeyPair", async () => { + if (_mtlsKeyPair) { + return _mtlsKeyPair + } + + // Docs: https://nodejs.org/api/crypto.html#cryptogeneratekeypairtype-options-callback + const keyPair = await generateKeyPair("ed25519", {}) + + const publicKeyPem = keyPair.publicKey.export({ type: "spki", format: "pem" }).toString() + const privateKeyPem = keyPair.privateKey.export({ type: "pkcs8", format: "pem" }).toString() + + _mtlsKeyPair = { + publicKeyPem, + privateKeyPem, + } + + return _mtlsKeyPair + }) +} + +function toBuilderUrl(mtlsEndpoint: string): string { + try { + const _ = new URL(mtlsEndpoint) + // If it successfully parses as a URL, it's a URL + return mtlsEndpoint + } catch (e) { + if (e instanceof TypeError) { + // mtlsEndpoint is just the hostname. let's add protocol and port as well. + return `tcp://${mtlsEndpoint}:443` + } else { + throw e + } + } +} + +type BuildxEndpoint = { + platform: string + builderUrl: string + serverCaPem: string +} + +class BuildxBuilder { + private static referenceCounter: Record = {} + private static lock = new AsyncLock() + private static tmpdir = tmpdir() + + public readonly name: string + + private readonly privateKeyPem: string + private readonly clientCertificatePem: string + private readonly endpoints: BuildxEndpoint[] + + private readonly ctx: PluginContext + + constructor({ + ctx, + ...identityParams + }: { + ctx: PluginContext + privateKeyPem: string + clientCertificatePem: string + endpoints: BuildxEndpoint[] + }) { + this.name = `garden-cloud-builder-${hashString(stableStringify(identityParams)).substring(0, 8)}` + this.privateKeyPem = identityParams.privateKeyPem + this.clientCertificatePem = identityParams.clientCertificatePem + this.endpoints = identityParams.endpoints + + this.ctx = ctx + } + + public async clean() { + return BuildxBuilder.lock.acquire(this.name, async () => { + const refCount = BuildxBuilder.referenceCounter[this.name] || 0 + + try { + if (refCount === 1) { + await this.remove_tmpdir() + await this.remove_builder() + } + } finally { + // even decrease refcount if removal failed + BuildxBuilder.referenceCounter[this.name] = refCount - 1 + } + }) + } + + public async install() { + return BuildxBuilder.lock.acquire(this.name, async () => { + const refCount = BuildxBuilder.referenceCounter[this.name] || 0 + if (refCount > 0) { + BuildxBuilder.referenceCounter[this.name] = refCount + 1 + return + } + + await this.writeCertificates() + + const success = this.installDirectly() + if (!success) { + await this.installUsingCLI() + } + + // Only increase the refCount by 1 if we successfully completed installation + BuildxBuilder.referenceCounter[this.name] = 1 + }) + } + + // private: clean + + private async remove_tmpdir() { + this.ctx.log.debug(`Removing ${this.certDir}...`) + await rm(this.certDir, { recursive: true, force: true }) + } + + private async remove_builder() { + try { + await rm(this.buildxInstanceJsonPath) + } catch (e) { + // fall back to docker CLI + const result = await containerHelpers.dockerCli({ + cwd: this.certDir, + args: ["buildx", "rm", this.name], + ctx: this.ctx, + log: this.ctx.log, + ignoreError: true, + }) + this.ctx.log.debug( + `buildx rm for ${this.name} exited with code ${result.code}${result.all?.length ? ` (output: ${result.all})` : ""}` + ) + } + } + + // private: installation + + private get dotDockerDirectory(): string { + return join(homedir(), ".docker") + } + + private get buildxInstanceJsonPath(): string { + return join(this.dotDockerDirectory, `buildx/instances/${this.name}`) + } + + private get certDir(): string { + return join(BuildxBuilder.tmpdir, this.name) + } + + private get clientKeyPath(): string { + return join(this.certDir, "client-key.pem") + } + + private get clientCertPath(): string { + return join(this.certDir, "client-cert.pem") + } + + private serverCaPath(platform: string): string { + return join(this.certDir, `server-ca-${platform.replaceAll("/", "-")}.pem`) + } + + private async writeCertificates() { + await mkdirp(this.certDir) + + const writePem = async (pemData: string | undefined, fullPath: string): Promise => { + if (pemData === undefined || pemData.length === 0) { + throw new InternalError({ message: `Empty pemData for ${basename(fullPath)}` }) + } + + await writeFile(fullPath, pemData) + } + + await writePem(this.privateKeyPem, this.clientKeyPath) + await writePem(this.clientCertificatePem, this.clientCertPath) + for (const { serverCaPem, platform } of this.endpoints) { + await writePem(serverCaPem, this.serverCaPath(platform)) + } + } + + private async installDirectly() { + const statResult = await stat(dirname(this.buildxInstanceJsonPath)) + if (statResult.isDirectory()) { + await writeFile(this.buildxInstanceJsonPath, JSON.stringify(this.getBuildxInstanceJson())) + return true + } + return false + } + + private getBuildxInstanceJson() { + return { + Name: this.name, + Driver: "remote", + Nodes: this.endpoints.map(({ platform, builderUrl }) => ({ + Name: platform.replaceAll("/", "-"), + Endpoint: builderUrl, + Platforms: null, + DriverOpts: { + cacert: this.serverCaPath(platform), + cert: this.clientCertPath, + key: this.clientKeyPath, + }, + Flags: null, + Files: null, + })), + Dynamic: false, + } + } + + private async installUsingCLI() { + for (const [i, { builderUrl, platform }] of this.endpoints.entries()) { + const result = await containerHelpers.dockerCli({ + cwd: this.certDir, + args: [ + "buildx", + "create", + "--name", + this.name, + "--node", + platform.replaceAll("/", "-"), + "--driver", + "remote", + "--driver-opt", + `cacert=${this.serverCaPath(platform)},cert=${this.clientCertPath},key=${this.clientKeyPath}`, + ...(i > 0 ? ["--append"] : []), + builderUrl, + ], + ctx: this.ctx, + log: this.ctx.log, + }) + this.ctx.log.debug( + `buildx create for ${this.name}/${platform} exited with code ${result.code}${result.all?.length ? ` (output: ${result.all})` : ""}` + ) + } + } +} diff --git a/core/src/plugins/container/container.ts b/core/src/plugins/container/container.ts index e594400299..615f06ce67 100644 --- a/core/src/plugins/container/container.ts +++ b/core/src/plugins/container/container.ts @@ -161,68 +161,6 @@ export const dockerSpec: PluginToolSpec = { ], } -export const namespaceCliVersion = "0.0.354" -export const namespaceCliSpec: PluginToolSpec = { - name: "namespace-cli", - version: dockerVersion, - description: `Namespace.so CLI v${dockerVersion}`, - type: "binary", - _includeInGardenImage: true, - builds: [ - { - platform: "darwin", - architecture: "amd64", - url: `https://get.namespace.so/packages/nsc/v${namespaceCliVersion}/nsc_${namespaceCliVersion}_darwin_amd64.tar.gz`, - sha256: "a091e5f4afeccfffe30231b3528c318bc3201696e09ac3c07adaf283cea42f91", - extract: { - format: "tar", - targetPath: "nsc", - }, - }, - { - platform: "darwin", - architecture: "arm64", - url: `https://get.namespace.so/packages/nsc/v${namespaceCliVersion}/nsc_${namespaceCliVersion}_darwin_arm64.tar.gz`, - sha256: "7641623358ec141c6ab8d243f5f97eab0417338bb1fd490daaf814947c4ed682", - extract: { - format: "tar", - targetPath: "nsc", - }, - }, - { - platform: "linux", - architecture: "amd64", - url: `https://get.namespace.so/packages/nsc/v${namespaceCliVersion}/nsc_${namespaceCliVersion}_linux_amd64.tar.gz`, - sha256: "8d180cf1c3e2f2861c34e89b722d9a5612888e3889d2d7767b02be955e6fc7ef", - extract: { - format: "tar", - targetPath: "nsc", - }, - }, - { - platform: "linux", - architecture: "arm64", - url: `https://get.namespace.so/packages/nsc/v${namespaceCliVersion}/nsc_${namespaceCliVersion}_linux_arm64.tar.gz`, - sha256: "0646fae1d6ca41888cbcac749b04ad303adcb5b2a7eb5260cddad1d7566ba0d6", - extract: { - format: "tar", - targetPath: "nsc", - }, - }, - // No windows support at the moment, only WSL - // { - // platform: "windows", - // architecture: "amd64", - // url: `https://get.namespace.so/packages/nsc/v${namespaceCliVersion}/nsc_${namespaceCliVersion}_${os}_${architecture}.tar.gz`, - // sha256: "25ff5d9dd8ae176dd30fd97b0b99a896d598fa62fca0b7171b45887ad4d3661b", - // extract: { - // format: "zip", - // targetPath: "docker/docker.exe", - // }, - // }, - ], -} - export const regctlCliVersion = "0.6.1" export const regctlCliSpec: PluginToolSpec = { name: "regctl", @@ -722,7 +660,7 @@ export const gardenPlugin = () => }, ], - tools: [dockerSpec, namespaceCliSpec, regctlCliSpec], + tools: [dockerSpec, regctlCliSpec], }) function validateRuntimeCommon(action: Resolved) { diff --git a/core/src/plugins/kubernetes/container/extensions.ts b/core/src/plugins/kubernetes/container/extensions.ts index 4610088989..ad6d119a76 100644 --- a/core/src/plugins/kubernetes/container/extensions.ts +++ b/core/src/plugins/kubernetes/container/extensions.ts @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import type { CloudBuilderAvailability } from "../../../cloud/api.js" +import type { CloudBuilderAvailabilityV2 } from "../../../cloud/api.js" import type { DeepPrimitiveMap } from "../../../config/common.js" import type { BuildActionExtension, @@ -53,7 +53,7 @@ async function getBuildMode({ availability, }: { ctx: KubernetesPluginContext - availability: CloudBuilderAvailability + availability: CloudBuilderAvailabilityV2 }): Promise { if (availability.available) { // Local build mode knows how to build using Cloud Builder diff --git a/core/test/helpers/api.ts b/core/test/helpers/api.ts index 502c93a0ee..cacea9a11f 100644 --- a/core/test/helpers/api.ts +++ b/core/test/helpers/api.ts @@ -53,6 +53,7 @@ export class FakeCloudApi extends CloudApi { id: apiProjectId, name, repositoryUrl: apiRemoteOriginUrl, + organizationId: uuidv4(), environments: [], } } @@ -62,6 +63,7 @@ export class FakeCloudApi extends CloudApi { id: apiProjectId, name, repositoryUrl: apiRemoteOriginUrl, + organizationId: uuidv4(), environments: [], } } @@ -71,6 +73,7 @@ export class FakeCloudApi extends CloudApi { id: apiProjectId, name: apiProjectName, repositoryUrl: apiRemoteOriginUrl, + organizationId: uuidv4(), environments: [], } } diff --git a/core/test/unit/src/garden.ts b/core/test/unit/src/garden.ts index 92ecad4f4b..f419c7c82d 100644 --- a/core/test/unit/src/garden.ts +++ b/core/test/unit/src/garden.ts @@ -69,7 +69,6 @@ import { TreeCache } from "../../../src/cache.js" import { omitUndefined } from "../../../src/util/objects.js" import { add } from "date-fns" import stripAnsi from "strip-ansi" -import type { CloudProject } from "../../../src/cloud/api.js" import { CloudApi } from "../../../src/cloud/api.js" import { GlobalConfigStore } from "../../../src/config-store/global.js" import { LogLevel, getRootLogger } from "../../../src/logger/logger.js" @@ -79,6 +78,8 @@ import { resolveMsg } from "../../../src/logger/log-entry.js" import { getCloudDistributionName } from "../../../src/util/cloud.js" import { styles } from "../../../src/logger/styles.js" import type { RunActionConfig } from "../../../src/actions/run.js" +import type { ProjectResult } from "@garden-io/platform-api-types" +import { ProjectStatus } from "@garden-io/platform-api-types" const moduleDirName = dirname(fileURLToPath(import.meta.url)) @@ -762,14 +763,26 @@ describe("Garden", () => { const projectId = uuidv4() const projectName = "test" const envName = "default" - const cloudProject: CloudProject = { + + const cloudProject: ProjectResult = { + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + relativePathInRepo: "", + status: ProjectStatus.Connected, id: projectId, name: projectName, repositoryUrl: "", + organization: { + id: uuidv4(), + name: "test", + }, environments: [ { id: uuidv4(), name: envName, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + projectId, }, ], } @@ -926,14 +939,25 @@ describe("Garden", () => { const projectId = uuidv4() const projectName = "test" const envName = "default" - const cloudProject: CloudProject = { + const cloudProject: ProjectResult = { + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + relativePathInRepo: "", + status: ProjectStatus.Connected, id: projectId, name: projectName, repositoryUrl: "", + organization: { + id: uuidv4(), + name: "test", + }, environments: [ { id: uuidv4(), name: envName, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + projectId, }, ], } @@ -945,6 +969,7 @@ describe("Garden", () => { it("should use the community dashboard domain", async () => { scope.get("/api/token/verify").reply(200, {}) scope.get(`/api/projects?name=test&exactMatch=true`).reply(200, { data: [cloudProject] }) + scope.get(`/api/projects/uid/${projectId}`).reply(200, { data: cloudProject }) const cloudApi = await makeCloudApi(DEFAULT_GARDEN_CLOUD_DOMAIN) @@ -1029,6 +1054,7 @@ describe("Garden", () => { it("should not fetch secrets", async () => { scope.get("/api/token/verify").reply(200, {}) scope.get(`/api/projects?name=test&exactMatch=true`).reply(200, { data: [cloudProject] }) + scope.get(`/api/projects/uid/${projectId}`).reply(200, { data: cloudProject }) scope .get(`/api/secrets/projectUid/${projectId}/env/${envName}`) .reply(200, { data: { SECRET_KEY: "secret-val" } }) diff --git a/core/test/unit/src/verify-ext-tool-binary-hashes.ts b/core/test/unit/src/verify-ext-tool-binary-hashes.ts index 5cc535d309..621b42295f 100755 --- a/core/test/unit/src/verify-ext-tool-binary-hashes.ts +++ b/core/test/unit/src/verify-ext-tool-binary-hashes.ts @@ -11,16 +11,12 @@ import { kubectlSpec } from "../../../src/plugins/kubernetes/kubectl.js" import { kustomize4Spec, kustomize5Spec } from "../../../src/plugins/kubernetes/kubernetes-type/kustomize.js" import { helm3Spec } from "../../../src/plugins/kubernetes/helm/helm-cli.js" import { downloadBinariesAndVerifyHashes } from "../../../src/util/testing.js" -import { dockerSpec, namespaceCliSpec, regctlCliSpec } from "../../../src/plugins/container/container.js" +import { dockerSpec, regctlCliSpec } from "../../../src/plugins/container/container.js" describe("Docker binaries", () => { downloadBinariesAndVerifyHashes([dockerSpec]) }) -describe("NamespaceCLI binaries", () => { - downloadBinariesAndVerifyHashes([namespaceCliSpec]) -}) - describe("regctlCLI binaries", () => { downloadBinariesAndVerifyHashes([regctlCliSpec]) })