@iexec/iapp
Version:
A CLI to guide you through the process of building an iExec iApp
240 lines (225 loc) • 7.15 kB
text/typescript
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,
};
}