@controlplane/cli
Version:
Control Plane Corporation CLI
172 lines • 7.41 kB
JavaScript
;
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