UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

172 lines 7.41 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RemoteBuildService = void 0; const axios_retry_1 = require("axios-retry"); const client_1 = require("../../session/client"); const axiosRetry_1 = require("../../util/axiosRetry"); const config_1 = require("./config"); const errors_1 = require("./errors"); // ANCHOR - Constants // A local build service may be plain http; anything else must not carry the token in the clear. const LOOPBACK_HOSTS = ['localhost', '127.0.0.1', '[::1]']; // The base every call spreads from, so the environment header rides all of them. const TIMED = { timeout: config_1.REQUEST_TIMEOUT_MS, headers: (0, config_1.resolveRemoteBuildHeaders)() }; // Poll calls manage their own retry semantics in RemoteBuildWatch and RemoteGitConnect. const NO_RETRY = { ...TIMED, 'axios-retry': { retries: 0 } }; // Plan and submit carry a requestId, so network errors retry too. const IDEMPOTENT_POST = { ...TIMED, 'axios-retry': { retries: config_1.IDEMPOTENT_POST_RETRIES, retryCondition: (error) => axios_retry_1.default.isNetworkError(error) || (0, axiosRetry_1.shouldRetryRequest)(error), }, }; // ANCHOR - RemoteBuildService /** * The typed gateway to the build service: plans context uploads, submits builds, and * reads build and provider-connection state. Owns retry semantics per call; never * prints, and never interprets build state — that is RemoteBuildWatch's job. */ class RemoteBuildService { constructor(client) { this.client = client; } // Public Static Methods // /** * Builds the gateway authenticated as the given session. Constructed before the * session's first API request so makeSessionClient attaches its per-request token * refresh instead of baking a static Authorization header. * * @param {Session} session - The CLI session. * @returns {RemoteBuildService} The authenticated gateway. */ static forSession(session) { const endpoint = (0, config_1.resolveRemoteBuildEndpoint)(session.request.endpoint); assertSecureEndpoint(endpoint); return new RemoteBuildService((0, client_1.makeSessionClient)(session, endpoint)); } // Public Methods // /** * Sends the folder manifest and returns the upload plan (presigned PUT URLs for the * changed files, or one seed-archive URL). * * @param {ContextPlanRequest} request - The org, image, and folder manifest. * @returns {Promise<ContextPlan>} The upload plan. */ async planUpload(request) { try { return await this.client.post('/context/plan', request, IDEMPOTENT_POST); } catch (e) { throw (0, errors_1.requestFailure)(e, 'prepare the upload', request.org); } } /** * Submits a build and returns the started record. The submission's requestId makes * retries idempotent: the service returns the build it already created. Throws * NotConnectedError when the git provider is not connected to the org. * * @param {BuildSubmission} submission - The target image and source. * @returns {Promise<BuildRecord>} The started build record. */ async submitBuild(submission) { try { return await this.client.post('/build', submission, IDEMPOTENT_POST); } catch (e) { const pending = (0, errors_1.notConnectedPayload)(e); if (pending) { throw new errors_1.NotConnectedError(pending); } throw (0, errors_1.requestFailure)(e, 'start the build', submission.org); } } /** * Reads the current build record. Errors propagate unwrapped: RemoteBuildWatch * counts transient failures and classifies terminal ones. * * @param {string} id - The build id. * @returns {Promise<BuildRecord>} The current build record. */ async getBuild(id) { return this.client.get(`/build/${encodeURIComponent(id)}`, NO_RETRY); } /** * Reads the next slice of the BuildKit log from an opaque cursor. Errors * propagate unwrapped: RemoteBuildWatch tolerates transient failures and * degrades gracefully when the service predates the endpoint. * * @param {string} id - The build id. * @param {number} offset - The cursor from the previous slice, 0 for the start. * @returns {Promise<BuildLogChunk>} The next log slice. */ async getBuildLog(id, offset) { return this.client.get(`/build/${encodeURIComponent(id)}/log`, { params: { offset }, ...NO_RETRY }); } /** * Reads the provider-connection status for a pending repo build, normalized so the * connect payload always lands under `pending`. Errors propagate unwrapped: * RemoteGitConnect decides what keeps polling and what fails fast. * * @param {ConnectionQuery} query - The connection lookup: host, nonce, org, and repoUrl. * @returns {Promise<ConnectionStatus>} The current connection status. */ async getConnection(query) { const reply = await this.client.get('/connections/status', { params: query, ...NO_RETRY, }); return normalizeConnectionStatus(reply); } } exports.RemoteBuildService = RemoteBuildService; // SECTION - Functions /** * Rejects a build service endpoint that would carry the org's token in the clear. * Loopback stays open for local service development. * * @param {string} endpoint - The resolved build service base URL. * @returns {void} */ function assertSecureEndpoint(endpoint) { let url; try { url = new URL(endpoint); } catch (_a) { throw new errors_1.RemoteBuildError('request-failed', `CPLN_IMAGE_BUILD_ENDPOINT is not a valid URL ("${endpoint}")`, 'Unset it to use the default build service.'); } if (url.protocol === 'https:' || LOOPBACK_HOSTS.includes(url.hostname)) { return; } throw new errors_1.RemoteBuildError('request-failed', `CPLN_IMAGE_BUILD_ENDPOINT must be an https:// URL ("${endpoint}")`, 'Unset it to use the default build service.'); } /** * Normalizes a connection-status reply to the shape callers read: `connected` becomes * a definite boolean, and the connect payload, wherever the service put it, lands * under `pending`. * * @param {ConnectionStatusReply} reply - The reply as the service sent it. * @returns {ConnectionStatus} The normalized status. */ function normalizeConnectionStatus(reply) { return { connected: reply.connected === true, pending: connectPayload(reply) }; } /** * Extracts the connect payload from a status reply, whether it sits under `pending` * or flat at the top level. A payload missing any field cannot drive the connect * flow and is discarded. * * @param {ConnectionStatusReply} reply - The reply as the service sent it. * @returns {NotConnectedPayload | undefined} The complete payload, or undefined. */ function connectPayload(reply) { var _a; const source = (_a = reply.pending) !== null && _a !== void 0 ? _a : reply; if (typeof source.provider !== 'string' || typeof source.connectUrl !== 'string' || typeof source.nonce !== 'string') { return undefined; } return { provider: source.provider, connectUrl: source.connectUrl, nonce: source.nonce }; } // !SECTION //# sourceMappingURL=service.js.map