@cloudcommerce/cli 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/lib/setup-gcloud.js +120 -20
- package/package.json +2 -2
- package/src/cli.ts +1 -1
- package/src/setup-gcloud.ts +146 -21
package/.turbo/turbo-build.log
CHANGED
package/lib/setup-gcloud.js
CHANGED
|
@@ -1,25 +1,112 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
$, fs, question, echo,
|
|
4
|
+
} from 'zx';
|
|
3
5
|
|
|
6
|
+
let gcpAccessToken;
|
|
4
7
|
const serviceAccountId = 'cloud-commerce-gh-actions';
|
|
5
8
|
const getAccountEmail = (projectId) => {
|
|
6
9
|
return `${serviceAccountId}@${projectId}.iam.gserviceaccount.com`;
|
|
7
10
|
};
|
|
11
|
+
const requestApi = async (projectId, options) => {
|
|
12
|
+
const body = options?.body;
|
|
13
|
+
let url = options?.baseURL
|
|
14
|
+
|| `https://iam.googleapis.com/v1/projects/${projectId}/serviceAccounts`;
|
|
15
|
+
url += options?.url || '';
|
|
16
|
+
const data = await (await fetch(url, {
|
|
17
|
+
method: options?.method || 'GET',
|
|
18
|
+
headers: {
|
|
19
|
+
Authorization: `Bearer ${gcpAccessToken}`,
|
|
20
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
21
|
+
},
|
|
22
|
+
body,
|
|
23
|
+
})).json();
|
|
24
|
+
const { error } = data;
|
|
25
|
+
if (error) {
|
|
26
|
+
let msgErr = 'Unexpected error in request';
|
|
27
|
+
msgErr = error.message ? `Code: ${error.code} - ${error.message}` : msgErr;
|
|
28
|
+
const err = new Error(msgErr);
|
|
29
|
+
throw err;
|
|
30
|
+
}
|
|
31
|
+
return data;
|
|
32
|
+
};
|
|
33
|
+
const getGcpAccessToken = async () => {
|
|
34
|
+
await echo`-- Get the Google Cloud account credentials:
|
|
35
|
+
1. Access https://shell.cloud.google.com/?fromcloudshell=true&show=terminal
|
|
36
|
+
2. Execute \`gcloud auth application-default print-access-token\` in Cloud Shell
|
|
37
|
+
`;
|
|
38
|
+
return question('Google Cloud access token: ');
|
|
39
|
+
};
|
|
8
40
|
const checkServiceAccountExists = async (projectId) => {
|
|
9
41
|
let hasServiceAccount;
|
|
10
42
|
try {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
43
|
+
if (!gcpAccessToken) {
|
|
44
|
+
const { stderr } = await $`gcloud iam service-accounts describe ${getAccountEmail(projectId)}`;
|
|
45
|
+
hasServiceAccount = !/not_?found/i.test(stderr);
|
|
46
|
+
} else {
|
|
47
|
+
// https://cloud.google.com/iam/docs/creating-managing-service-accounts?hl=pt-br#listing
|
|
48
|
+
const { accounts: listAccounts } = await requestApi(projectId);
|
|
49
|
+
const accountFound = listAccounts
|
|
50
|
+
&& listAccounts.find(({ email }) => email === getAccountEmail(projectId));
|
|
51
|
+
hasServiceAccount = Boolean(accountFound);
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
14
54
|
return null;
|
|
15
55
|
}
|
|
16
56
|
return hasServiceAccount;
|
|
17
57
|
};
|
|
18
58
|
const siginGcloudAndSetIAM = async (projectId, pwd) => {
|
|
19
|
-
|
|
20
|
-
|
|
59
|
+
let hasGcloud;
|
|
60
|
+
try {
|
|
61
|
+
hasGcloud = Boolean(await $`command -v gcloud`);
|
|
62
|
+
} catch {
|
|
63
|
+
hasGcloud = false;
|
|
64
|
+
}
|
|
65
|
+
if (hasGcloud) {
|
|
66
|
+
if (/no credential/i.test((await $`gcloud auth list`).stderr)) {
|
|
67
|
+
await $`gcloud auth login`;
|
|
68
|
+
}
|
|
69
|
+
await $`gcloud config set project ${projectId}`;
|
|
70
|
+
} else {
|
|
71
|
+
gcpAccessToken = await getGcpAccessToken();
|
|
72
|
+
}
|
|
73
|
+
const serviceAccount = await checkServiceAccountExists(projectId);
|
|
74
|
+
if (!serviceAccount) {
|
|
75
|
+
const description = 'A service account with permission to deploy Cloud Commerce'
|
|
76
|
+
+ ' from the GitHub repository to Firebase';
|
|
77
|
+
const displayName = 'Cloud Commerce GH Actions';
|
|
78
|
+
if (hasGcloud) {
|
|
79
|
+
await $`gcloud iam service-accounts create ${serviceAccountId} \
|
|
80
|
+
--description=${description} --display-name=${displayName}`;
|
|
81
|
+
} else if (gcpAccessToken) {
|
|
82
|
+
const body = JSON.stringify({
|
|
83
|
+
accountId: serviceAccountId,
|
|
84
|
+
serviceAccount: {
|
|
85
|
+
description,
|
|
86
|
+
displayName,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
await requestApi(projectId, { method: 'POST', body });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
await fs.ensureDir(path.join(pwd, '.cloudcommerce'));
|
|
93
|
+
const pathPolicyIAM = path.join(pwd, '.cloudcommerce', 'policyIAM.json');
|
|
94
|
+
let policyIAM = {};
|
|
95
|
+
const version = 3; // most recent
|
|
96
|
+
const baseURL = `https://cloudresourcemanager.googleapis.com/v1/projects/${projectId}`;
|
|
97
|
+
if (hasGcloud) {
|
|
98
|
+
await $`gcloud projects get-iam-policy ${projectId} --format json > ${pathPolicyIAM}`;
|
|
99
|
+
policyIAM = fs.readJSONSync(pathPolicyIAM);
|
|
100
|
+
} else if (gcpAccessToken) {
|
|
101
|
+
// https://cloud.google.com/iam/docs/granting-changing-revoking-access?hl=pt-br#view-access
|
|
102
|
+
// POST https://cloudresourcemanager.googleapis.com/API_VERSION/RESOURCE_TYPE/RESOURCE_ID:getIamPolicy
|
|
103
|
+
policyIAM = await requestApi(projectId, {
|
|
104
|
+
baseURL,
|
|
105
|
+
url: ':getIamPolicy',
|
|
106
|
+
method: 'POST',
|
|
107
|
+
body: JSON.stringify({ options: { requestedPolicyVersion: version } }),
|
|
108
|
+
});
|
|
21
109
|
}
|
|
22
|
-
await $`gcloud config set project ${projectId}`;
|
|
23
110
|
const roles = [
|
|
24
111
|
'roles/firebase.admin',
|
|
25
112
|
'roles/appengine.appAdmin',
|
|
@@ -32,17 +119,10 @@ const siginGcloudAndSetIAM = async (projectId, pwd) => {
|
|
|
32
119
|
'roles/serviceusage.apiKeysViewer',
|
|
33
120
|
'roles/serviceusage.serviceUsageAdmin',
|
|
34
121
|
];
|
|
35
|
-
|
|
36
|
-
if (!
|
|
37
|
-
|
|
38
|
-
--description="A service account with permission to deploy Cloud Commerce from the GitHub repository to Firebase" \
|
|
39
|
-
--display-name="Cloud Commerce GH Actions"`;
|
|
122
|
+
let { bindings } = policyIAM;
|
|
123
|
+
if (!bindings) {
|
|
124
|
+
bindings = [];
|
|
40
125
|
}
|
|
41
|
-
await fs.ensureDir(path.join(pwd, '.cloudcommerce'));
|
|
42
|
-
const pathPolicyIAM = path.join(pwd, '.cloudcommerce', 'policyIAM.json');
|
|
43
|
-
await $`gcloud projects get-iam-policy ${projectId} --format json > ${pathPolicyIAM}`;
|
|
44
|
-
const policyIAM = fs.readJSONSync(pathPolicyIAM);
|
|
45
|
-
const { bindings } = policyIAM;
|
|
46
126
|
let mustUpdatePolicy = false;
|
|
47
127
|
roles.forEach((role) => {
|
|
48
128
|
const roleFound = bindings.find((binding) => binding.role === role);
|
|
@@ -73,16 +153,36 @@ const siginGcloudAndSetIAM = async (projectId, pwd) => {
|
|
|
73
153
|
}
|
|
74
154
|
});
|
|
75
155
|
if (mustUpdatePolicy) {
|
|
76
|
-
|
|
77
|
-
|
|
156
|
+
if (hasGcloud) {
|
|
157
|
+
fs.writeJSONSync(pathPolicyIAM, policyIAM);
|
|
158
|
+
return $`gcloud projects set-iam-policy ${projectId} ${pathPolicyIAM}`;
|
|
159
|
+
}
|
|
160
|
+
if (gcpAccessToken) {
|
|
161
|
+
Object.assign(policyIAM, { version, bindings });
|
|
162
|
+
// POST https://cloudresourcemanager.googleapis.com/API_VERSION/RESOURCE_TYPE/RESOURCE_ID:setIamPolicy
|
|
163
|
+
return requestApi(projectId, {
|
|
164
|
+
baseURL,
|
|
165
|
+
url: ':setIamPolicy',
|
|
166
|
+
method: 'POST',
|
|
167
|
+
body: JSON.stringify({ policy: policyIAM }),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
78
170
|
}
|
|
79
171
|
return null;
|
|
80
172
|
};
|
|
81
173
|
const createServiceAccountKey = async (projectId, pwd) => {
|
|
82
174
|
try {
|
|
83
175
|
const pathFileKey = path.join(pwd, '.cloudcommerce', 'serviceAccountKey.json');
|
|
84
|
-
|
|
176
|
+
if (!gcpAccessToken) {
|
|
177
|
+
await $`gcloud iam service-accounts keys create ${pathFileKey} \
|
|
85
178
|
--iam-account=${getAccountEmail(projectId)}`;
|
|
179
|
+
} else {
|
|
180
|
+
const { privateKeyData } = await requestApi(projectId, {
|
|
181
|
+
url: `/${getAccountEmail(projectId)}/keys`,
|
|
182
|
+
method: 'POST',
|
|
183
|
+
});
|
|
184
|
+
await $`echo '${privateKeyData}' | base64 --decode > ${pathFileKey}`;
|
|
185
|
+
}
|
|
86
186
|
return JSON.stringify(fs.readJSONSync(pathFileKey));
|
|
87
187
|
} catch (e) {
|
|
88
188
|
return null;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudcommerce/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.1",
|
|
5
5
|
"description": "E-Com Plus Cloud Commerce CLI tools",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cloudcommerce": "./bin/run.mjs"
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"libsodium-wrappers": "^0.7.10",
|
|
27
27
|
"md5": "^2.3.0",
|
|
28
28
|
"zx": "^7.1.1",
|
|
29
|
-
"@cloudcommerce/api": "0.4.
|
|
29
|
+
"@cloudcommerce/api": "0.4.1"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "sh ../../scripts/build-lib.sh"
|
package/src/cli.ts
CHANGED
package/src/setup-gcloud.ts
CHANGED
|
@@ -1,27 +1,135 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
$,
|
|
4
|
+
fs,
|
|
5
|
+
question,
|
|
6
|
+
echo,
|
|
7
|
+
} from 'zx';
|
|
3
8
|
|
|
9
|
+
let gcpAccessToken: string | undefined;
|
|
4
10
|
const serviceAccountId = 'cloud-commerce-gh-actions';
|
|
5
11
|
const getAccountEmail = (projectId: string) => {
|
|
6
12
|
return `${serviceAccountId}@${projectId}.iam.gserviceaccount.com`;
|
|
7
13
|
};
|
|
8
14
|
|
|
15
|
+
const requestApi = async (
|
|
16
|
+
projectId: string,
|
|
17
|
+
options?: {
|
|
18
|
+
baseURL?: string,
|
|
19
|
+
url?: string,
|
|
20
|
+
method: string,
|
|
21
|
+
body?: string,
|
|
22
|
+
},
|
|
23
|
+
) => {
|
|
24
|
+
const body = options?.body;
|
|
25
|
+
let url = options?.baseURL
|
|
26
|
+
|| `https://iam.googleapis.com/v1/projects/${projectId}/serviceAccounts`;
|
|
27
|
+
url += options?.url || '';
|
|
28
|
+
const data = await (await fetch(
|
|
29
|
+
url,
|
|
30
|
+
{
|
|
31
|
+
method: options?.method || 'GET',
|
|
32
|
+
headers: {
|
|
33
|
+
Authorization: `Bearer ${gcpAccessToken}`,
|
|
34
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
35
|
+
},
|
|
36
|
+
body,
|
|
37
|
+
},
|
|
38
|
+
)).json() as any;
|
|
39
|
+
const { error } = data;
|
|
40
|
+
if (error) {
|
|
41
|
+
let msgErr = 'Unexpected error in request';
|
|
42
|
+
msgErr = error.message ? `Code: ${error.code} - ${error.message}` : msgErr;
|
|
43
|
+
const err = new Error(msgErr);
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
return data;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const getGcpAccessToken = async () => {
|
|
50
|
+
await echo`-- Get the Google Cloud account credentials:
|
|
51
|
+
1. Access https://shell.cloud.google.com/?fromcloudshell=true&show=terminal
|
|
52
|
+
2. Execute \`gcloud auth application-default print-access-token\` in Cloud Shell
|
|
53
|
+
`;
|
|
54
|
+
return question('Google Cloud access token: ');
|
|
55
|
+
};
|
|
56
|
+
|
|
9
57
|
const checkServiceAccountExists = async (projectId: string) => {
|
|
10
58
|
let hasServiceAccount: boolean;
|
|
11
59
|
try {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
60
|
+
if (!gcpAccessToken) {
|
|
61
|
+
const { stderr } = await $`gcloud iam service-accounts describe ${getAccountEmail(projectId)}`;
|
|
62
|
+
hasServiceAccount = !/not_?found/i.test(stderr);
|
|
63
|
+
} else {
|
|
64
|
+
// https://cloud.google.com/iam/docs/creating-managing-service-accounts?hl=pt-br#listing
|
|
65
|
+
const { accounts: listAccounts } = await requestApi(projectId);
|
|
66
|
+
const accountFound = listAccounts
|
|
67
|
+
&& listAccounts.find(({ email }) => email === getAccountEmail(projectId));
|
|
68
|
+
hasServiceAccount = Boolean(accountFound);
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
15
71
|
return null;
|
|
16
72
|
}
|
|
17
73
|
return hasServiceAccount;
|
|
18
74
|
};
|
|
19
75
|
|
|
20
76
|
const siginGcloudAndSetIAM = async (projectId: string, pwd: string) => {
|
|
21
|
-
|
|
22
|
-
|
|
77
|
+
let hasGcloud: boolean;
|
|
78
|
+
try {
|
|
79
|
+
hasGcloud = Boolean(await $`command -v gcloud`);
|
|
80
|
+
} catch {
|
|
81
|
+
hasGcloud = false;
|
|
82
|
+
}
|
|
83
|
+
if (hasGcloud) {
|
|
84
|
+
if (/no credential/i.test((await $`gcloud auth list`).stderr)) {
|
|
85
|
+
await $`gcloud auth login`;
|
|
86
|
+
}
|
|
87
|
+
await $`gcloud config set project ${projectId}`;
|
|
88
|
+
} else {
|
|
89
|
+
gcpAccessToken = await getGcpAccessToken();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const serviceAccount = await checkServiceAccountExists(projectId);
|
|
93
|
+
if (!serviceAccount) {
|
|
94
|
+
const description = 'A service account with permission to deploy Cloud Commerce'
|
|
95
|
+
+ ' from the GitHub repository to Firebase';
|
|
96
|
+
const displayName = 'Cloud Commerce GH Actions';
|
|
97
|
+
if (hasGcloud) {
|
|
98
|
+
await $`gcloud iam service-accounts create ${serviceAccountId} \
|
|
99
|
+
--description=${description} --display-name=${displayName}`;
|
|
100
|
+
} else if (gcpAccessToken) {
|
|
101
|
+
const body = JSON.stringify({
|
|
102
|
+
accountId: serviceAccountId,
|
|
103
|
+
serviceAccount: {
|
|
104
|
+
description,
|
|
105
|
+
displayName,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
await requestApi(projectId, { method: 'POST', body });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
await fs.ensureDir(path.join(pwd, '.cloudcommerce'));
|
|
112
|
+
const pathPolicyIAM = path.join(pwd, '.cloudcommerce', 'policyIAM.json');
|
|
113
|
+
let policyIAM: Record<string, any> = {};
|
|
114
|
+
const version = 3; // most recent
|
|
115
|
+
const baseURL = `https://cloudresourcemanager.googleapis.com/v1/projects/${projectId}`;
|
|
116
|
+
if (hasGcloud) {
|
|
117
|
+
await $`gcloud projects get-iam-policy ${projectId} --format json > ${pathPolicyIAM}`;
|
|
118
|
+
policyIAM = fs.readJSONSync(pathPolicyIAM);
|
|
119
|
+
} else if (gcpAccessToken) {
|
|
120
|
+
// https://cloud.google.com/iam/docs/granting-changing-revoking-access?hl=pt-br#view-access
|
|
121
|
+
// POST https://cloudresourcemanager.googleapis.com/API_VERSION/RESOURCE_TYPE/RESOURCE_ID:getIamPolicy
|
|
122
|
+
policyIAM = await requestApi(
|
|
123
|
+
projectId,
|
|
124
|
+
{
|
|
125
|
+
baseURL,
|
|
126
|
+
url: ':getIamPolicy',
|
|
127
|
+
method: 'POST',
|
|
128
|
+
body: JSON.stringify({ options: { requestedPolicyVersion: version } }),
|
|
129
|
+
},
|
|
130
|
+
);
|
|
23
131
|
}
|
|
24
|
-
|
|
132
|
+
|
|
25
133
|
const roles = [
|
|
26
134
|
'roles/firebase.admin',
|
|
27
135
|
'roles/appengine.appAdmin',
|
|
@@ -34,18 +142,10 @@ const siginGcloudAndSetIAM = async (projectId: string, pwd: string) => {
|
|
|
34
142
|
'roles/serviceusage.apiKeysViewer',
|
|
35
143
|
'roles/serviceusage.serviceUsageAdmin',
|
|
36
144
|
];
|
|
37
|
-
|
|
38
|
-
if (!
|
|
39
|
-
|
|
40
|
-
--description="A service account with permission to deploy Cloud Commerce from the GitHub repository to Firebase" \
|
|
41
|
-
--display-name="Cloud Commerce GH Actions"`;
|
|
145
|
+
let { bindings } = policyIAM;
|
|
146
|
+
if (!bindings) {
|
|
147
|
+
bindings = [];
|
|
42
148
|
}
|
|
43
|
-
await fs.ensureDir(path.join(pwd, '.cloudcommerce'));
|
|
44
|
-
const pathPolicyIAM = path.join(pwd, '.cloudcommerce', 'policyIAM.json');
|
|
45
|
-
await $`gcloud projects get-iam-policy ${projectId} --format json > ${pathPolicyIAM}`;
|
|
46
|
-
const policyIAM = fs.readJSONSync(pathPolicyIAM);
|
|
47
|
-
const { bindings } = policyIAM;
|
|
48
|
-
|
|
49
149
|
let mustUpdatePolicy = false;
|
|
50
150
|
roles.forEach((role) => {
|
|
51
151
|
const roleFound = bindings.find((binding) => binding.role === role);
|
|
@@ -78,8 +178,22 @@ const siginGcloudAndSetIAM = async (projectId: string, pwd: string) => {
|
|
|
78
178
|
}
|
|
79
179
|
});
|
|
80
180
|
if (mustUpdatePolicy) {
|
|
81
|
-
|
|
82
|
-
|
|
181
|
+
if (hasGcloud) {
|
|
182
|
+
fs.writeJSONSync(pathPolicyIAM, policyIAM);
|
|
183
|
+
return $`gcloud projects set-iam-policy ${projectId} ${pathPolicyIAM}`;
|
|
184
|
+
} if (gcpAccessToken) {
|
|
185
|
+
Object.assign(policyIAM, { version, bindings });
|
|
186
|
+
// POST https://cloudresourcemanager.googleapis.com/API_VERSION/RESOURCE_TYPE/RESOURCE_ID:setIamPolicy
|
|
187
|
+
return requestApi(
|
|
188
|
+
projectId,
|
|
189
|
+
{
|
|
190
|
+
baseURL,
|
|
191
|
+
url: ':setIamPolicy',
|
|
192
|
+
method: 'POST',
|
|
193
|
+
body: JSON.stringify({ policy: policyIAM }),
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
}
|
|
83
197
|
}
|
|
84
198
|
return null;
|
|
85
199
|
};
|
|
@@ -87,8 +201,19 @@ const siginGcloudAndSetIAM = async (projectId: string, pwd: string) => {
|
|
|
87
201
|
const createServiceAccountKey = async (projectId: string, pwd: string) => {
|
|
88
202
|
try {
|
|
89
203
|
const pathFileKey = path.join(pwd, '.cloudcommerce', 'serviceAccountKey.json');
|
|
90
|
-
|
|
204
|
+
if (!gcpAccessToken) {
|
|
205
|
+
await $`gcloud iam service-accounts keys create ${pathFileKey} \
|
|
91
206
|
--iam-account=${getAccountEmail(projectId)}`;
|
|
207
|
+
} else {
|
|
208
|
+
const { privateKeyData } = await requestApi(
|
|
209
|
+
projectId,
|
|
210
|
+
{
|
|
211
|
+
url: `/${getAccountEmail(projectId)}/keys`,
|
|
212
|
+
method: 'POST',
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
await $`echo '${privateKeyData}' | base64 --decode > ${pathFileKey}`;
|
|
216
|
+
}
|
|
92
217
|
return JSON.stringify(fs.readJSONSync(pathFileKey));
|
|
93
218
|
} catch (e) {
|
|
94
219
|
return null;
|