-
Notifications
You must be signed in to change notification settings - Fork 387
Expand file tree
/
Copy pathaws.js
More file actions
199 lines (174 loc) · 6.94 KB
/
Copy pathaws.js
File metadata and controls
199 lines (174 loc) · 6.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
const { EC2Client, RunInstancesCommand, TerminateInstancesCommand, waitUntilInstanceRunning } = require('@aws-sdk/client-ec2');
const core = require('@actions/core');
const config = require('./config');
// User data scripts are run as the root user
function buildUserDataScript(githubRegistrationToken, label) {
let userData;
if (config.input.runnerHomeDir) {
// If runner home directory is specified, we expect the actions-runner software (and dependencies)
// to be pre-installed in the AMI, so we simply cd into that directory and then start the runner
userData = [
'#!/bin/bash',
'exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1',
`cd "${config.input.runnerHomeDir}"`,
`cat <<'EOF' > pre-runner-script.sh`,
config.input.preRunnerScript,
`EOF`,
'source pre-runner-script.sh',
'export RUNNER_ALLOW_RUNASROOT=1',
`./config.sh --url https://github.com/${config.githubContext.owner}/${config.githubContext.repo} --token ${githubRegistrationToken} --labels ${label}`,
];
} else {
userData = [
'#!/bin/bash',
'exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1',
'mkdir actions-runner && cd actions-runner',
`cat <<'EOF' > pre-runner-script.sh`,
config.input.preRunnerScript,
`EOF`,
'source pre-runner-script.sh',
'case $(uname -m) in aarch64) ARCH="arm64" ;; amd64|x86_64) ARCH="x64" ;; esac && export RUNNER_ARCH=${ARCH}',
'curl -O -L https://github.com/actions/runner/releases/download/v2.313.0/actions-runner-linux-${RUNNER_ARCH}-2.313.0.tar.gz',
'tar xzf ./actions-runner-linux-${RUNNER_ARCH}-2.313.0.tar.gz',
'export RUNNER_ALLOW_RUNASROOT=1',
`./config.sh --url https://github.com/${config.githubContext.owner}/${config.githubContext.repo} --token ${githubRegistrationToken} --labels ${label}`,
];
}
if (config.input.runAsUser) {
userData.push(`chown -R ${config.input.runAsUser} .`);
}
if (config.input.runAsService) {
userData.push(`./svc.sh install ${config.input.runAsUser || ''}`);
userData.push('./svc.sh start');
} else {
userData.push(`${config.input.runAsUser ? `su ${config.input.runAsUser} -c` : ''} ./run.sh`);
}
return userData;
}
function buildMarketOptions() {
if (config.input.marketType !== 'spot') {
return undefined;
}
return {
MarketType: config.input.marketType,
SpotOptions: {
SpotInstanceType: 'one-time',
},
};
}
async function createEc2InstanceWithParams(imageId, subnetId, securityGroupId, label, githubRegistrationToken, region) {
// Region is always specified now, so we can directly use it
const ec2ClientOptions = { region };
const ec2 = new EC2Client(ec2ClientOptions);
const userData = buildUserDataScript(githubRegistrationToken, label);
const params = {
ImageId: imageId,
InstanceType: config.input.ec2InstanceType,
MaxCount: 1,
MinCount: 1,
SecurityGroupIds: [securityGroupId],
SubnetId: subnetId,
UserData: Buffer.from(userData.join('\n')).toString('base64'),
IamInstanceProfile: config.input.iamRoleName ? { Name: config.input.iamRoleName } : undefined,
TagSpecifications: config.tagSpecifications,
InstanceMarketOptions: buildMarketOptions(),
KeyName: config.input.keyName,
};
if (config.input.ec2VolumeSize !== '' || config.input.ec2VolumeType !== '') {
params.BlockDeviceMappings = [
{
DeviceName: config.input.ec2DeviceName,
Ebs: {
...(config.input.ec2VolumeSize !== '' && { VolumeSize: config.input.ec2VolumeSize }),
...(config.input.ec2VolumeType !== '' && { VolumeType: config.input.ec2VolumeType }),
},
},
];
}
if (config.input.blockDeviceMappings.length > 0) {
params.BlockDeviceMappings = config.input.blockDeviceMappings;
}
const result = await ec2.send(new RunInstancesCommand(params));
const ec2InstanceId = result.Instances[0].InstanceId;
return ec2InstanceId;
}
async function startEc2Instance(label, githubRegistrationToken) {
core.info(`Attempting to start EC2 instance using ${config.availabilityZones.length} availability zone configuration(s)`);
const errors = [];
// Try each availability zone configuration in sequence
for (let i = 0; i < config.availabilityZones.length; i++) {
const azConfig = config.availabilityZones[i];
// Region is now always specified in the availability zone config
const region = azConfig.region;
core.info(`Trying availability zone configuration ${i + 1}/${config.availabilityZones.length}`);
core.info(`Using imageId: ${azConfig.imageId}, subnetId: ${azConfig.subnetId}, securityGroupId: ${azConfig.securityGroupId}, region: ${region}`);
try {
const ec2InstanceId = await createEc2InstanceWithParams(
azConfig.imageId,
azConfig.subnetId,
azConfig.securityGroupId,
label,
githubRegistrationToken,
region
);
core.info(`Successfully started AWS EC2 instance ${ec2InstanceId} using availability zone configuration ${i + 1} in region ${region}`);
return { ec2InstanceId, region };
} catch (error) {
const errorMessage = `Failed to start EC2 instance with configuration ${i + 1} in region ${region}: ${error.message}`;
core.warning(errorMessage);
errors.push(errorMessage);
// Continue to the next availability zone configuration
continue;
}
}
// If we've tried all configurations and none worked, throw an error
core.error('All availability zone configurations failed');
throw new Error(`Failed to start EC2 instance in any availability zone. Errors: ${errors.join('; ')}`);
}
async function terminateEc2Instance() {
const ec2 = new EC2Client();
const params = {
InstanceIds: [config.input.ec2InstanceId],
};
try {
await ec2.send(new TerminateInstancesCommand(params));
core.info(`AWS EC2 instance ${config.input.ec2InstanceId} is terminated`);
return;
} catch (error) {
core.error(`AWS EC2 instance ${config.input.ec2InstanceId} termination error`);
throw error;
}
}
async function waitForInstanceRunning(ec2InstanceId, region) {
// Region is always provided now
const ec2ClientOptions = { region };
const ec2 = new EC2Client(ec2ClientOptions);
core.info(`Using region ${region} for checking instance ${ec2InstanceId} status`);
try {
core.info(`Checking for instance ${ec2InstanceId} to be up and running`);
await waitUntilInstanceRunning(
{
client: ec2,
maxWaitTime: 300,
},
{
Filters: [
{
Name: 'instance-id',
Values: [ec2InstanceId],
},
],
},
);
core.info(`AWS EC2 instance ${ec2InstanceId} is up and running`);
return;
} catch (error) {
core.error(`AWS EC2 instance ${ec2InstanceId} initialization error`);
throw error;
}
}
module.exports = {
startEc2Instance,
terminateEc2Instance,
waitForInstanceRunning,
};