UNPKG

@iexec/iapp

Version:

A CLI to guide you through the process of building an iExec iApp

240 lines (225 loc) 7.15 kB
import { DEFAULT_SCONE_VERSION, SCONIFY_API_HTTP_URL, SCONIFY_API_WS_URL, } from '../config/config.js'; import { getAuthToken } from './dockerhub.js'; import { sleep } from './sleep.js'; import { createReconnectingWs, deserializeData, serializeData, } from './websocket.js'; import { debug } from './debug.js'; import { useWsApi } from './featureFlags.js'; const INITIAL_RETRY_PERIOD = 20 * 1000; // 20s class TooManyRequestsError extends Error {} export async function sconify({ iAppNameToSconify, template, walletAddress, dockerhubAccessToken, dockerhubUsername, tryCount = 0, }: { iAppNameToSconify: string; template?: string; walletAddress: string; dockerhubAccessToken: string; dockerhubUsername: string; tryCount?: number; }): Promise<{ dockerImage: string; dockerImageDigest: string; sconeVersion: string; fingerprint: string; entrypoint: string; }> { let dockerImage; let dockerImageDigest; let sconeVersion; let entrypoint; let fingerprint; try { const [dockerRepository] = iAppNameToSconify.split(':'); const getPushToken = async () => getAuthToken({ repository: dockerRepository, action: 'pull,push', dockerhubAccessToken, dockerhubUsername, }); const pushToken = await getPushToken(); let sconifyResult: { dockerImage?: string; dockerImageDigest?: string; entrypoint?: string; fingerprint?: string; sconeVersion?: string; }; if (useWsApi) { // experimental ws connection sconifyResult = await new Promise((resolve, reject) => { createReconnectingWs(SCONIFY_API_WS_URL, { headers: { 'x-wallet': walletAddress, }, connectCallback: (ws) => { const handleError = (e: unknown) => { ws.close(1000); // normal ws close reject(e); }; ws.on('message', (data) => { let message; // handle communication errors try { message = deserializeData(data); debug(`ws message: ${JSON.stringify(message, undefined, 2)}`); } catch (e) { handleError(e); } // handle server responses if (message?.type === 'RESPONSE') { if (message?.target === 'SCONIFY_BUILD') { ws.close(1000); // normal ws close if (message?.success === true && message.result) { resolve(message.result); } else { reject(Error(message.error || 'Server unknown error')); } } } // handle server requests if (message?.type === 'REQUEST') { if (message?.target === 'RENEW_PUSH_TOKEN') { getPushToken() .then((renewedPushToken) => { ws.send( serializeData({ type: 'RESPONSE', target: 'RENEW_PUSH_TOKEN', result: { dockerhubPushToken: renewedPushToken, }, }) ); }) .catch(handleError); } } // handle server info if (message?.type === 'INFO') { // TODO server feedback } }); }, initCallback: (ws) => { ws.send( serializeData({ type: 'REQUEST', target: 'SCONIFY_BUILD', // call sconify handler template, dockerhubImageToSconify: iAppNameToSconify, dockerhubPushToken: pushToken, yourWalletPublicAddress: walletAddress, sconeVersion: DEFAULT_SCONE_VERSION, }) ); }, errorCallback: reject, }); }); } else { // standard http call sconifyResult = await fetch(`${SCONIFY_API_HTTP_URL}/sconify/build`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-wallet': walletAddress, }, body: JSON.stringify({ template, dockerhubImageToSconify: iAppNameToSconify, dockerhubPushToken: pushToken, // used for pushing sconified image on user repo yourWalletPublicAddress: walletAddress, sconeVersion: DEFAULT_SCONE_VERSION, }), }) .catch(() => { throw Error("Can't reach TEE transformation server!"); }) .then((res) => { if (res.ok) { return res.json().catch(() => { // failed to parse body throw Error('Unexpected server response'); }); } if (res.status === 429) { throw new TooManyRequestsError( 'TEE transformation server is busy, retry later' ); } // try getting error message from json body return res .json() .catch(() => { // failed to parse body throw Error('Unknown server error'); }) .then(({ error }) => { throw Error(error || 'Unknown server error'); }); }); } // Extract necessary information if (!sconifyResult.dockerImage) { throw Error('Unexpected server response: missing dockerImage'); } if (!sconifyResult.dockerImageDigest) { throw Error('Unexpected server response: missing dockerImageDigest'); } if (!sconifyResult.sconeVersion) { throw Error('Unexpected server response: missing sconeVersion'); } if (!sconifyResult.entrypoint) { throw Error('Unexpected server response: missing entrypoint'); } if (!sconifyResult.fingerprint) { throw Error('Unexpected server response: missing fingerprint'); } dockerImage = sconifyResult.dockerImage; dockerImageDigest = sconifyResult.dockerImageDigest; sconeVersion = sconifyResult.sconeVersion; entrypoint = sconifyResult.entrypoint; fingerprint = sconifyResult.fingerprint; } catch (err) { debug(`sconify error: ${err}`); // retry with exponential backoff if (err instanceof TooManyRequestsError && tryCount < 3) { const retryAfter = INITIAL_RETRY_PERIOD * Math.pow(2, tryCount); debug( `server is busy retrying in after ${retryAfter} ms (count: ${tryCount})` ); await sleep(retryAfter); return sconify({ iAppNameToSconify, template, walletAddress, dockerhubAccessToken, dockerhubUsername, tryCount: tryCount + 1, }); } throw Error( `Failed to transform your app into a TEE app: ${(err as Error)?.message}` ); } return { dockerImage, dockerImageDigest, sconeVersion, entrypoint, fingerprint, }; }