-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathgenerate-setup-script.py
executable file
·354 lines (291 loc) · 12 KB
/
generate-setup-script.py
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
#!/usr/bin/env python3
#
# This script automates the generation of `setup.ps1` in the `scripts` subdirectory
#
import json, re, subprocess, yaml, sys
from pathlib import Path
class Utility:
@staticmethod
def log(message):
"""
Logs a message to stderr
"""
print('[generate-setup-script.py]: {}'.format(message), flush=True, file=sys.stderr)
@staticmethod
def capture(command, **kwargs):
"""
Executes the specified command and captures its output
"""
# Log the command being executed
Utility.log(command)
# Attempt to execute the specified command
result = subprocess.run(
command,
check = True,
capture_output = True,
universal_newlines = True,
**kwargs
)
# Return the contents of stdout
return result.stdout.strip()
@staticmethod
def writeFile(filename, data):
"""
Writes data to the specified file
"""
return Path(filename).write_bytes(data.encode('utf-8'))
@staticmethod
def commentForStep(name):
"""
Returns a descriptive comment for the build step with the specified name
"""
return {
'ConfigureDirectories': '# Create each of our directories',
'DownloadKubernetes': '# Download the Kubernetes components',
'DownloadEKSArtifacts': '# Download the EKS artifacts archive',
'ExtractEKSArtifacts': '# Extract the EKS artifacts archive',
'MoveEKSArtifacts': '# Move the EKS files into place',
'ExecuteBuildScripts': '# Perform EKS worker node setup',
'RemoveEKSArtifactDownloadDirectory': '# Perform cleanup',
'InstallContainers': '\n'.join([
'# Install the Windows Containers feature',
'# (Note: this is actually a no-op here, since we install the feature beforehand in startup.ps1)'
])
}.get(name, None)
@staticmethod
def parseConstants(constants):
"""
Parses an EC2 ImageBuilder component's constants list
"""
parsed = {}
for entry in constants:
for key, values in entry.items():
parsed[key] = values['value']
return parsed
@staticmethod
def replaceConstants(string, constants):
"""
Converts EC2 ImageBuilder constant references to PowerShell variable references
"""
# If the value of a constant is used as a magic value rather than a reference,
# replace it with a reference to the variable representing the constant instead
transformed = string
for key, value in constants.items():
transformed = transformed.replace(value, '${}'.format(key))
# Convert `{{ variable }}` syntax to PowerShell `$variable` syntax
# (Note that we don't bother to wrap the variable names in curly braces, since we know that none
# of the variable names contain special characters, and they're only ever interpolated as either
# part of a filesystem path surrounded by separators, or as a parameter surrounded by whitespace)
return re.sub('{{ (.+?) }}', '$\\1', transformed)
@staticmethod
def replaceSystemPaths(path):
"""
Replaces hard-coded system paths with the equivalent environment variables
"""
replaced = path
replaced = replaced.replace('C:\\Program Files', '$env:ProgramFiles')
replaced = replaced.replace('C:\\ProgramData', '$env:ProgramData')
return replaced
@staticmethod
def s3UriToHttpsUrl(s3Uri):
"""
Converts an `s3://` URI to an HTTPS URL
"""
url = s3Uri.replace('s3://', '')
components = url.split('/', 1)
return 'https://{}.s3.amazonaws.com/{}'.format(components[0], components[1])
# Retrieve the contents of the "Amazon EKS Optimized Windows AMI" EC2 ImageBuilder component
componentData = json.loads(Utility.capture([
'aws',
'imagebuilder',
'get-component',
'--region=us-east-1',
'--component-build-version-arn',
'arn:aws:imagebuilder:us-east-1:aws:component/eks-optimized-ami-windows/1.24.0'
]))
# Parse the pipeline YAML data and extract the list of constants
pipelineData = yaml.load(componentData['component']['data'], Loader=yaml.Loader)
constants = Utility.parseConstants(pipelineData['constants'])
# Extract the steps for the "build" phase
buildSteps = [p['steps'] for p in pipelineData['phases'] if p['name'] == 'build'][0]
print('CONSTANTS:')
print(json.dumps(constants, indent=4))
print()
print('BUILD STEPS:')
print(json.dumps(buildSteps, indent=4))
# Prepend our header to the generated PowerShell code
generated = '''<#
THIS FILE IS AUTOMATICALLY GENERATED, DO NOT EDIT!
This script is based on the logic from the "Amazon EKS Optimized Windows AMI"
EC2 ImageBuilder component, with modifications to use containerd 1.7.0.
The original ImageBuilder component logic is Copyright Amazon.com, Inc. or
its affiliates, and is licensed under the MIT License.
#>
# Halt execution if we encounter an error
$ErrorActionPreference = 'Stop'
# Applies in-place patches to a file
function PatchFile
{
Param (
$File,
$Patches
)
$patched = Get-Content -Path $File -Raw
$Patches.GetEnumerator() | ForEach-Object {
$patched = $patched.Replace($_.Key, $_.Value)
}
Set-Content -Path $File -Value $patched -NoNewline
}
'''
# Inject an additional constant for the parent of the temp directory, immediately before the child directory
tempPath = {k:v for k,v in constants.items() if k == 'TempPath'}
otherConstants = {k:v for k,v in constants.items() if k != 'TempPath'}
constants = {**otherConstants, 'TempRoot': 'C:\\TempEKSArtifactDir', **tempPath}
# Define variables for each of our constants
generated += '# Constants\n'
existingConstants = {}
for key, value in constants.items():
transformed = Utility.replaceConstants(value, existingConstants)
transformed = Utility.replaceSystemPaths(transformed)
generated += '${} = "{}"\n'.format(key, transformed)
existingConstants[key] = value
# Process each build step in turn
for step in buildSteps:
# Determine whether we have custom preprocessing logic for the step
name = step['name']
if name == 'ConfigureDirectories':
# Add the temp directory to the list of directories to be created
step['loop']['forEach'] += [constants['TempRoot']]
elif name == 'DownloadKubernetes':
# Inject the driver installation step immediately prior to the Kubernetes download step
generated += '\n'.join([
'',
'# Install the NVIDIA GPU drivers',
"$driverBucket = 'ec2-windows-nvidia-drivers'",
"$driver = Get-S3Object -BucketName $driverBucket -KeyPrefix 'latest' -Region 'us-east-1' | Where-Object {$_.Key.Contains('server2022')}",
'Copy-S3Object -BucketName $driverBucket -Key $driver.Key -LocalFile "$TempRoot\driver.exe" -Region \'us-east-1\'',
"Start-Process -FilePath \"$TempRoot\driver.exe\" -ArgumentList @('-s', '-noreboot') -NoNewWindow -Wait",
''
])
elif name == 'ExtractEKSArtifacts':
# Remove the redundant directory creation command
step['inputs']['commands'] = [
c for c in step['inputs']['commands']
if not c.startswith('New-Item')
]
# Use absolute file and directory paths rather than relative paths
step['inputs']['commands'] = [
c.replace('EKS-Artifacts.zip', '"C:\\EKS-Artifacts.zip"').replace('TempEKSArtifactDir', 'C:\\TempEKSArtifactDir')
for c in step['inputs']['commands']
]
elif name == 'InstallContainerRuntimes':
# Inject the containerd 1.7.0 download step, along with our configuration patching steps, immediately prior to the containerd installation step
generated += '\n'.join([
'',
'# -------',
'',
'# TEMPORARY UNTIL EKS ADDS SUPPORT FOR CONTAINERD v1.7.0:',
'# Download and extract the containerd 1.7.0 release build',
'$containerdTarball = "$TempPath\\containerd-1.7.0.tar.gz"',
'$containerdFiles = "$TempPath\\containerd-1.7.0"',
'$webClient.DownloadFile(\'https://github.com/containerd/containerd/releases/download/v1.7.0/containerd-1.7.0-windows-amd64.tar.gz\', $containerdTarball)',
'New-Item -Path "$containerdFiles" -ItemType Directory -Force | Out-Null',
'tar.exe -xvzf "$containerdTarball" -C "$containerdFiles"',
'',
'# Move the containerd files into place',
'Move-Item -Path "$containerdFiles\\bin\\containerd.exe" -Destination "$ContainerdPath\\containerd.exe" -Force',
'Move-Item -Path "$containerdFiles\\bin\\containerd-shim-runhcs-v1.exe" -Destination "$ContainerdPath\\containerd-shim-runhcs-v1.exe" -Force',
'Move-Item -Path "$containerdFiles\\bin\\ctr.exe" -Destination "$ContainerdPath\\ctr.exe" -Force',
'',
'# Clean up the containerd intermediate files',
'Remove-Item -Path "$containerdFiles" -Recurse -Force',
'Remove-Item -Path "$containerdTarball" -Force',
'',
'# -------',
'',
'# Patch the containerd setup script to configure a log file (rather than just discarding log output) and to use the upstream pause',
'# container image rather than the EKS version, since the latter appears to cause errors when attempting to create Windows Pods',
'PatchFile -File "$TempPath\Add-ContainerdRuntime.ps1" -Patches @{',
' "containerd --register-service" = "containerd --register-service --log-file \'C:\\ProgramData\\containerd\\root\\output.log\'";',
' "amazonaws.com/eks/pause-windows:latest" = "registry.k8s.io/pause:3.9"',
'}',
'',
'# Add the full Windows Server 2022 base image and the pause image to the list of images to pre-pull',
'$baseLayersFile = "$TempPath\eks.baselayers.config"',
'$baseLayers = Get-Content -Path $baseLayersFile -Raw | ConvertFrom-Json',
'$baseLayers.2022 += "mcr.microsoft.com/windows/server:ltsc2022"',
'$baseLayers.2022 += "registry.k8s.io/pause:3.9"',
'$patchedJson = ConvertTo-Json -Depth 100 -InputObject $baseLayers',
'Set-Content -Path $baseLayersFile -Value $patchedJson -NoNewline',
'',
])
# Simplify the containerd installation command
step['inputs']['commands'] = [
'',
'# Register containerd as the EKS container runtime',
'Push-Location $TempPath',
'& .\Add-ContainerdRuntime.ps1 -Path "$ContainerdPath"',
'Pop-Location'
]
elif name == 'ExecuteBuildScripts':
# Prefix each script invocation with the call operator
step['loop']['forEach'] = [
'& {}'.format(command)
for command in step['loop']['forEach']
]
# Strip away the boilerplate code surrounding each script invocation
step['inputs']['commands'] = ['Push-Location $TempPath'] + step['loop']['forEach'] + ['Pop-Location']
# -------
# If we have a descriptive comment for the step then include it above its generated code
comment = Utility.commentForStep(name)
if comment != None:
generated += '\n{}\n'.format(comment)
# -------
# Generate code for the step based on its action type
action = step['action']
if action == 'CreateFolder':
directories = [Utility.replaceConstants(d, constants) for d in step['loop']['forEach']]
generated += '\n'.join([
'foreach ($dir in @({})) {{'.format(', '.join(directories)),
'\tNew-Item -Path $dir -ItemType Directory -Force | Out-Null',
'}'
])
elif action == 'DeleteFolder':
generated += '\n'.join([
'Remove-Item -Path "{}" -Recurse -Force'.format(Utility.replaceConstants(input['path'], constants))
for input in step['inputs']
])
elif action == 'MoveFile':
generated += '\n'.join([
'Move-Item -Path "{}" -Destination "{}" -Force'.format(
Utility.replaceConstants(input['source'], constants),
Utility.replaceConstants(input['destination'], constants)
)
for input in step['inputs']
])
elif action == 'S3Download':
generated += '\n'.join([
'$webClient.DownloadFile("{}", "{}")'.format(
Utility.s3UriToHttpsUrl(input['source']),
Utility.replaceConstants(input['destination'], constants)
)
for input in step['inputs']
])
elif action == 'ExecutePowerShell':
generated += '\n'.join([
Utility.replaceConstants(c, constants).replace("'", '"')
for c in step['inputs']['commands']
if not c.startswith('$ErrorActionPreference')
])
elif action == 'Reboot':
Utility.log('Ignoring reboot step.')
continue
else:
raise RuntimeError('Unknown build step action: {}'.format(action))
# -------
# Add a trailing newline after each non-ignored step
generated += '\n'
# Write the generated code to the output script file
outfile = Path(__file__).parent / 'scripts' / 'setup.ps1'
Utility.writeFile(outfile, generated)
Utility.log('Wrote generated code to {}'.format(outfile))