@iexec/iapp-maker 0.0.1-alpha-nightly-6be476717b877e4857383c375d5f209dbd288f99 → 0.0.1-alpha-nightly-c88f1c62428cda426ca60c8b374fb74970fb6fba
Sign up to get free protection for your applications and to get access to all the features.
- package/package.json +1 -2
- package/src/cmd/deploy.js +8 -7
- package/src/cmd/init.js +12 -0
- package/src/cmd/run.js +1 -2
- package/src/utils/dockerhub.js +13 -2
- package/src/utils/iAppConfigFile.js +26 -11
- package/src/utils/prepareInputFile.js +1 -4
- package/src/utils/sconify.js +53 -23
- package/src/utils/sleep.js +1 -0
- package/templates/js/package-lock.json +5 -5
- package/templates/js/package.json +1 -1
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@iexec/iapp-maker",
|
3
|
-
"version": "0.0.1-alpha-nightly-
|
3
|
+
"version": "0.0.1-alpha-nightly-c88f1c62428cda426ca60c8b374fb74970fb6fba",
|
4
4
|
"description": "A CLI to guide you through the process of building an iExec iApp",
|
5
5
|
"main": "src/index.js",
|
6
6
|
"type": "module",
|
@@ -44,7 +44,6 @@
|
|
44
44
|
"magic-bytes.js": "^1.10.0",
|
45
45
|
"ora": "^8.1.1",
|
46
46
|
"prompts": "^2.4.2",
|
47
|
-
"undici": "^6.21.0",
|
48
47
|
"uuid": "^11.0.3",
|
49
48
|
"yargs": "^17.7.2",
|
50
49
|
"zod": "^3.23.8",
|
package/src/cmd/deploy.js
CHANGED
@@ -6,7 +6,10 @@ import {
|
|
6
6
|
import { sconify } from '../utils/sconify.js';
|
7
7
|
import { askForDockerhubUsername } from '../cli-helpers/askForDockerhubUsername.js';
|
8
8
|
import { askForWalletAddress } from '../cli-helpers/askForWalletAddress.js';
|
9
|
-
import {
|
9
|
+
import {
|
10
|
+
projectNameToImageName,
|
11
|
+
readIAppConfig,
|
12
|
+
} from '../utils/iAppConfigFile.js';
|
10
13
|
import { askForDockerhubAccessToken } from '../cli-helpers/askForDockerhubAccessToken.js';
|
11
14
|
import { handleCliError } from '../cli-helpers/handleCliError.js';
|
12
15
|
import { getSpinner } from '../cli-helpers/spinner.js';
|
@@ -22,6 +25,8 @@ export async function deploy() {
|
|
22
25
|
const spinner = getSpinner();
|
23
26
|
try {
|
24
27
|
await goToProjectRoot({ spinner });
|
28
|
+
const { projectName } = await readIAppConfig();
|
29
|
+
|
25
30
|
const dockerhubUsername = await askForDockerhubUsername({ spinner });
|
26
31
|
const dockerhubAccessToken = await askForDockerhubAccessToken({ spinner });
|
27
32
|
|
@@ -38,6 +43,8 @@ export async function deploy() {
|
|
38
43
|
throw Error('Invalid version');
|
39
44
|
}
|
40
45
|
|
46
|
+
const imageTag = `${dockerhubUsername}/${projectNameToImageName(projectName)}:${iAppVersion}`;
|
47
|
+
|
41
48
|
const appSecret = await askForAppSecret({ spinner });
|
42
49
|
|
43
50
|
const walletAddress = await askForWalletAddress({ spinner });
|
@@ -54,11 +61,6 @@ export async function deploy() {
|
|
54
61
|
iexec = getIExecDebug(privateKey);
|
55
62
|
}
|
56
63
|
|
57
|
-
const config = await readPackageJonConfig();
|
58
|
-
const iAppName = config.name.toLowerCase();
|
59
|
-
|
60
|
-
const imageTag = `${dockerhubUsername}/${iAppName}:${iAppVersion}`;
|
61
|
-
|
62
64
|
// just start the spinner, no need to persist success in terminal
|
63
65
|
spinner.start('Checking docker daemon is running...');
|
64
66
|
await checkDockerDaemon();
|
@@ -89,7 +91,6 @@ export async function deploy() {
|
|
89
91
|
'Transforming your image into a TEE image and deploying on iExec, this may take a few minutes...'
|
90
92
|
);
|
91
93
|
const { sconifiedImage, appContractAddress } = await sconify({
|
92
|
-
sconifyForProd: false,
|
93
94
|
iAppNameToSconify: imageTag,
|
94
95
|
walletAddress,
|
95
96
|
dockerhubAccessToken,
|
package/src/cmd/init.js
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import chalk from 'chalk';
|
2
2
|
import figlet from 'figlet';
|
3
3
|
import { mkdir } from 'node:fs/promises';
|
4
|
+
import { fromError } from 'zod-validation-error';
|
4
5
|
import { folderExists } from '../utils/fs.utils.js';
|
5
6
|
import { initIAppWorkspace } from '../utils/initIAppWorkspace.js';
|
6
7
|
import { getSpinner } from '../cli-helpers/spinner.js';
|
@@ -8,6 +9,7 @@ import { handleCliError } from '../cli-helpers/handleCliError.js';
|
|
8
9
|
import { generateWallet } from '../utils/generateWallet.js';
|
9
10
|
import * as color from '../cli-helpers/color.js';
|
10
11
|
import { hintBox } from '../cli-helpers/box.js';
|
12
|
+
import { projectNameSchema } from '../utils/iAppConfigFile.js';
|
11
13
|
|
12
14
|
const targetDir = 'hello-world';
|
13
15
|
|
@@ -30,6 +32,16 @@ export async function init() {
|
|
30
32
|
name: 'projectName',
|
31
33
|
message: `What's your project name? ${color.promptHelper('(A folder with this name will be created)')}`,
|
32
34
|
initial: targetDir,
|
35
|
+
validate: (value) => {
|
36
|
+
try {
|
37
|
+
projectNameSchema.parse(value);
|
38
|
+
return true;
|
39
|
+
} catch (e) {
|
40
|
+
return fromError(e)
|
41
|
+
.details.map((issue) => issue.message)
|
42
|
+
.join('; ');
|
43
|
+
}
|
44
|
+
},
|
33
45
|
});
|
34
46
|
|
35
47
|
if (await folderExists(projectName)) {
|
package/src/cmd/run.js
CHANGED
@@ -133,8 +133,7 @@ export async function runInDebug({
|
|
133
133
|
tag: SCONE_TAG,
|
134
134
|
});
|
135
135
|
const apporder = await iexec.order.signApporder(apporderTemplate);
|
136
|
-
|
137
|
-
spinner.succeed('AppOrder created and published');
|
136
|
+
spinner.succeed('AppOrder created');
|
138
137
|
|
139
138
|
// Dataset Order
|
140
139
|
let datasetorder;
|
package/src/utils/dockerhub.js
CHANGED
@@ -29,6 +29,17 @@ export async function getAuthToken({
|
|
29
29
|
`Fail to get authorization token for scope=${repository}:${action}`
|
30
30
|
);
|
31
31
|
}
|
32
|
-
|
33
|
-
|
32
|
+
return response
|
33
|
+
.json()
|
34
|
+
.catch(() => {
|
35
|
+
throw Error(`Unexpected response from dockerhub auth server`);
|
36
|
+
})
|
37
|
+
.then(({ token }) => {
|
38
|
+
if (!token) {
|
39
|
+
throw Error(
|
40
|
+
`Unexpected response from dockerhub auth server: Missing token`
|
41
|
+
);
|
42
|
+
}
|
43
|
+
return token;
|
44
|
+
});
|
34
45
|
}
|
@@ -3,8 +3,27 @@ import { z } from 'zod';
|
|
3
3
|
import { fromError } from 'zod-validation-error';
|
4
4
|
import { CONFIG_FILE } from '../config/config.js';
|
5
5
|
|
6
|
+
export const projectNameSchema = z
|
7
|
+
.string()
|
8
|
+
.min(2, 'Must contain at least 2 characters') // docker image name constraint
|
9
|
+
.refine(
|
10
|
+
(value) => !/(^\s)|(\s$)/.test(value),
|
11
|
+
'Should not start or end with space'
|
12
|
+
)
|
13
|
+
.refine(
|
14
|
+
(value) => /^[a-zA-Z0-9- ]+$/.test(value ?? ''),
|
15
|
+
'Only letters, numbers, spaces, and hyphens are allowed'
|
16
|
+
);
|
17
|
+
|
18
|
+
const dockerImageNameSchema = z
|
19
|
+
.string()
|
20
|
+
.refine(
|
21
|
+
(value) => /^[a-z0-9-]+$/.test(value ?? ''),
|
22
|
+
'Invalid docker image name'
|
23
|
+
);
|
24
|
+
|
6
25
|
const jsonConfigFileSchema = z.object({
|
7
|
-
projectName:
|
26
|
+
projectName: projectNameSchema,
|
8
27
|
dockerhubUsername: z.string().optional(),
|
9
28
|
dockerhubAccessToken: z.string().optional(),
|
10
29
|
walletAddress: z.string().optional(),
|
@@ -12,6 +31,12 @@ const jsonConfigFileSchema = z.object({
|
|
12
31
|
appSecret: z.string().optional().nullable(), // can be null or string (null means do no use secret)
|
13
32
|
});
|
14
33
|
|
34
|
+
// transform the projectName into a suitable docker image name (no space, lowercase only)
|
35
|
+
export function projectNameToImageName(projectName = '') {
|
36
|
+
const imageName = projectName.toLowerCase().replaceAll(' ', '-');
|
37
|
+
return dockerImageNameSchema.parse(imageName);
|
38
|
+
}
|
39
|
+
|
15
40
|
// Read JSON configuration file
|
16
41
|
export async function readIAppConfig() {
|
17
42
|
const configContent = await readFile(CONFIG_FILE, 'utf8').catch(() => {
|
@@ -37,16 +62,6 @@ export async function readIAppConfig() {
|
|
37
62
|
}
|
38
63
|
}
|
39
64
|
|
40
|
-
// Read package.json file
|
41
|
-
export async function readPackageJonConfig() {
|
42
|
-
try {
|
43
|
-
const packageContent = await readFile('./package.json', 'utf8');
|
44
|
-
return JSON.parse(packageContent);
|
45
|
-
} catch {
|
46
|
-
throw Error('Failed to read `package.json` file.');
|
47
|
-
}
|
48
|
-
}
|
49
|
-
|
50
65
|
// Utility function to write the iApp JSON configuration file
|
51
66
|
export async function writeIAppConfig(config) {
|
52
67
|
await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
|
@@ -1,6 +1,5 @@
|
|
1
1
|
import { mkdir, writeFile } from 'node:fs/promises';
|
2
2
|
import { join } from 'node:path';
|
3
|
-
import { request } from 'undici';
|
4
3
|
import { TEST_INPUT_DIR } from '../config/config.js';
|
5
4
|
|
6
5
|
// TODO we may want to cache to avoid downloading large input files over and over
|
@@ -19,9 +18,7 @@ export async function prepareInputFile(url) {
|
|
19
18
|
if (name === '.' || name === '..') {
|
20
19
|
throw Error('Invalid computed file name');
|
21
20
|
}
|
22
|
-
await
|
23
|
-
throwOnError: true,
|
24
|
-
}).then(async (response) => {
|
21
|
+
await fetch(url).then(async (response) => {
|
25
22
|
await mkdir(TEST_INPUT_DIR, { recursive: true }); // ensure input dir
|
26
23
|
await writeFile(join(TEST_INPUT_DIR, name), response.body);
|
27
24
|
});
|
package/src/utils/sconify.js
CHANGED
@@ -1,19 +1,19 @@
|
|
1
|
-
import { request } from 'undici';
|
2
1
|
import { addDeploymentData } from './cacheExecutions.js';
|
3
2
|
import { SCONIFY_API_URL } from '../config/config.js';
|
4
3
|
import { getAuthToken } from '../utils/dockerhub.js';
|
4
|
+
import { sleep } from './sleep.js';
|
5
|
+
|
6
|
+
const INITIAL_RETRY_PERIOD = 20 * 1000; // 20s
|
7
|
+
|
8
|
+
class TooManyRequestsError extends Error {}
|
5
9
|
|
6
10
|
export async function sconify({
|
7
|
-
sconifyForProd,
|
8
11
|
iAppNameToSconify,
|
9
12
|
walletAddress,
|
10
13
|
dockerhubAccessToken,
|
11
14
|
dockerhubUsername,
|
15
|
+
tryCount = 0,
|
12
16
|
}) {
|
13
|
-
if (sconifyForProd) {
|
14
|
-
throw Error('This feature is not yet implemented. Coming soon ...');
|
15
|
-
}
|
16
|
-
|
17
17
|
let appContractAddress;
|
18
18
|
let sconifiedImage;
|
19
19
|
try {
|
@@ -26,7 +26,7 @@ export async function sconify({
|
|
26
26
|
dockerhubUsername,
|
27
27
|
});
|
28
28
|
|
29
|
-
const
|
29
|
+
const jsonResponse = await fetch(`${SCONIFY_API_URL}/sconify`, {
|
30
30
|
method: 'POST',
|
31
31
|
headers: {
|
32
32
|
'Content-Type': 'application/json',
|
@@ -37,26 +37,56 @@ export async function sconify({
|
|
37
37
|
dockerhubPushToken: pushToken, // used for pushing sconified image on user repo
|
38
38
|
yourWalletPublicAddress: walletAddress,
|
39
39
|
}),
|
40
|
-
|
41
|
-
|
40
|
+
})
|
41
|
+
.catch(() => {
|
42
|
+
throw Error("Can't reach TEE transformation server!");
|
43
|
+
})
|
44
|
+
.then((res) => {
|
45
|
+
if (res.ok) {
|
46
|
+
return res.json().catch(() => {
|
47
|
+
// failed to parse body
|
48
|
+
throw Error('Unexpected server response');
|
49
|
+
});
|
50
|
+
}
|
51
|
+
if (res.status === 429) {
|
52
|
+
throw new TooManyRequestsError(
|
53
|
+
'TEE transformation server is busy, retry later'
|
54
|
+
);
|
55
|
+
}
|
56
|
+
// try getting error message from json body
|
57
|
+
return res
|
58
|
+
.json()
|
59
|
+
.catch(() => {
|
60
|
+
// failed to parse body
|
61
|
+
throw Error('Unknown server error');
|
62
|
+
})
|
63
|
+
.then(({ error }) => {
|
64
|
+
throw Error(error || 'Unknown server error');
|
65
|
+
});
|
66
|
+
});
|
42
67
|
|
43
68
|
// Extract necessary information
|
44
|
-
|
45
|
-
|
46
|
-
|
69
|
+
if (!jsonResponse.appContractAddress) {
|
70
|
+
throw Error('Unexpected server response: missing appContractAddress');
|
71
|
+
}
|
72
|
+
if (!jsonResponse.sconifiedImage) {
|
73
|
+
throw Error('Unexpected server response: missing sconifiedImage');
|
74
|
+
}
|
75
|
+
appContractAddress = jsonResponse.appContractAddress;
|
76
|
+
sconifiedImage = jsonResponse.sconifiedImage;
|
47
77
|
} catch (err) {
|
48
|
-
|
49
|
-
if (err
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
78
|
+
// retry with exponential backoff
|
79
|
+
if (err instanceof TooManyRequestsError && tryCount < 3) {
|
80
|
+
await sleep(INITIAL_RETRY_PERIOD * Math.pow(2, tryCount));
|
81
|
+
return sconify({
|
82
|
+
iAppNameToSconify,
|
83
|
+
walletAddress,
|
84
|
+
dockerhubAccessToken,
|
85
|
+
dockerhubUsername,
|
86
|
+
tryCount: tryCount + 1,
|
87
|
+
});
|
58
88
|
}
|
59
|
-
throw Error(`Failed to transform your app into a TEE app: ${
|
89
|
+
throw Error(`Failed to transform your app into a TEE app: ${err.message}`);
|
60
90
|
}
|
61
91
|
|
62
92
|
// Add deployment data to deployments.json
|
@@ -0,0 +1 @@
|
|
1
|
+
export const sleep = async (ms) => new Promise((res) => setTimeout(res, ms));
|
@@ -1,13 +1,13 @@
|
|
1
1
|
{
|
2
|
-
"name": "hello-world",
|
3
|
-
"version": "
|
2
|
+
"name": "iexec-hello-world-iapp",
|
3
|
+
"version": "0.0.1",
|
4
4
|
"lockfileVersion": 1,
|
5
5
|
"requires": true,
|
6
6
|
"dependencies": {
|
7
7
|
"@iexec/dataprotector-deserializer": {
|
8
|
-
"version": "0.1.
|
9
|
-
"resolved": "https://registry.npmjs.org/@iexec/dataprotector-deserializer/-/dataprotector-deserializer-0.1.
|
10
|
-
"integrity": "sha512-
|
8
|
+
"version": "0.1.1",
|
9
|
+
"resolved": "https://registry.npmjs.org/@iexec/dataprotector-deserializer/-/dataprotector-deserializer-0.1.1.tgz",
|
10
|
+
"integrity": "sha512-CSz1JWnslm2X3gjL1cx/qqovnmvJSFWDyJMw0ZGvqnYnNatgIqHn+Aky2iO4K0HsArfqmgV3ySIpdPfu/N2M0w==",
|
11
11
|
"requires": {
|
12
12
|
"borsh": "^2.0.0",
|
13
13
|
"jszip": "^3.10.1"
|