@@ -11,6 +11,7 @@ import subprocess
11
11
import sys
12
12
import time
13
13
import unittest
14
+ import base64
14
15
15
16
REMOTE_BASE_IMAGE_NAME = "public.ecr.aws/w0m4q9l7/github-awslabs-smithy-rs-ci"
16
17
LOCAL_BASE_IMAGE_NAME = "smithy-rs-base-image"
@@ -41,32 +42,41 @@ class Platform(Enum):
41
42
42
43
# Script context
43
44
class Context :
44
- def __init__ (self , start_path , script_path , tools_path , user_id , image_tag , allow_local_build , github_actions ):
45
+ def __init__ (self , start_path , script_path , tools_path , user_id , image_tag , allow_local_build , github_actions ,
46
+ encrypted_docker_password , docker_passphrase ):
45
47
self .start_path = start_path
46
48
self .script_path = script_path
47
49
self .tools_path = tools_path
50
+ self .docker_image_path = tools_path + "/ci-build"
48
51
self .user_id = user_id
49
52
self .image_tag = image_tag
50
53
self .allow_local_build = allow_local_build
51
54
self .github_actions = github_actions
55
+ self .encrypted_docker_password = encrypted_docker_password
56
+ self .docker_passphrase = docker_passphrase
52
57
53
58
@staticmethod
54
59
def default ():
55
60
start_path = os .path .realpath (os .curdir )
56
61
script_path = os .path .dirname (os .path .realpath (__file__ ))
57
62
tools_path = get_cmd_output ("git rev-parse --show-toplevel" , cwd = script_path )[1 ] + "/tools"
58
63
user_id = get_cmd_output ("id -u" )[1 ]
59
- image_tag = get_cmd_output ("./ci-build/tools -hash" , cwd = tools_path )[1 ]
64
+ image_tag = get_cmd_output ("./docker-image -hash" , cwd = script_path )[1 ]
60
65
allow_local_build = os .getenv ("ALLOW_LOCAL_BUILD" ) != "false"
61
66
github_actions = os .getenv ("GITHUB_ACTIONS" ) == "true"
67
+ encrypted_docker_password = os .getenv ("ENCRYPTED_DOCKER_PASSWORD" ) or None
68
+ docker_passphrase = os .getenv ("DOCKER_LOGIN_TOKEN_PASSPHRASE" ) or None
69
+
62
70
print (f"Start path: { start_path } " )
63
71
print (f"Script path: { script_path } " )
64
72
print (f"Tools path: { tools_path } " )
65
73
print (f"User ID: { user_id } " )
66
74
print (f"Required base image tag: { image_tag } " )
67
75
print (f"Allow local build: { allow_local_build } " )
68
76
print (f"Running in GitHub Actions: { github_actions } " )
69
- return Context (start_path , script_path , tools_path , user_id , image_tag , allow_local_build , github_actions )
77
+ return Context (start_path = start_path , script_path = script_path , tools_path = tools_path , user_id = user_id ,
78
+ image_tag = image_tag , allow_local_build = allow_local_build , github_actions = github_actions ,
79
+ encrypted_docker_password = encrypted_docker_password , docker_passphrase = docker_passphrase )
70
80
71
81
72
82
def output_contains_any (stdout , stderr , messages ):
@@ -75,7 +85,6 @@ def output_contains_any(stdout, stderr, messages):
75
85
return True
76
86
return False
77
87
78
-
79
88
# Mockable shell commands
80
89
class Shell :
81
90
# Returns the platform that this script is running on
@@ -90,6 +99,9 @@ class Shell:
90
99
(status , _ , _ ) = get_cmd_output (f"docker inspect \" { image_name } :{ image_tag } \" " , check = False )
91
100
return status == 0
92
101
102
+ def docker_login (self , password ):
103
+ get_cmd_output ("docker login --username AWS --password-stdin public.ecr.aws" , input = password .encode ('utf-8' ))
104
+
93
105
# Pulls the requested `image_name` with `image_tag`. Returns `DockerPullResult`.
94
106
def docker_pull (self , image_name , image_tag ):
95
107
(status , stdout , stderr ) = get_cmd_output (f"docker pull \" { image_name } :{ image_tag } \" " , check = False )
@@ -101,7 +113,7 @@ class Shell:
101
113
print ("-------------------" )
102
114
103
115
not_found_messages = ["not found: manifest unknown" ]
104
- throttle_messages = ["toomanyrequests: Rate exceeded" , "toomanyrequests: Data limit exceeded " ]
116
+ throttle_messages = ["toomanyrequests:" ]
105
117
retryable_messages = ["net/http: TLS handshake timeout" ]
106
118
if status == 0 :
107
119
return DockerPullResult .SUCCESS
@@ -118,10 +130,10 @@ class Shell:
118
130
run (f"docker build -t \" smithy-rs-base-image:{ image_tag } \" ." , cwd = path )
119
131
120
132
# Builds the local build image
121
- def docker_build_build_image (self , user_id , script_path ):
133
+ def docker_build_build_image (self , user_id , docker_image_path ):
122
134
run (
123
135
f"docker build -t smithy-rs-build-image --file add-local-user.dockerfile --build-arg=USER_ID={ user_id } ." ,
124
- cwd = script_path
136
+ cwd = docker_image_path
125
137
)
126
138
127
139
# Saves the Docker image named `image_name` with `image_tag` to `output_path`
@@ -134,10 +146,10 @@ class Shell:
134
146
135
147
136
148
# Pulls a Docker image and retries if it gets throttled
137
- def docker_pull_with_retry (shell , image_name , image_tag , throttle_sleep_time = 45 , retryable_error_sleep_time = 1 ):
149
+ def docker_pull_with_retry (shell , image_name , image_tag , throttle_sleep_time = 120 , retryable_error_sleep_time = 1 ):
138
150
if shell .platform () == Platform .ARM_64 :
139
151
return DockerPullResult .REMOTE_ARCHITECTURE_MISMATCH
140
- for attempt in range (1 , 5 ):
152
+ for attempt in range (1 , 6 ):
141
153
announce (f"Attempting to pull remote image { image_name } :{ image_tag } (attempt { attempt } )..." )
142
154
result = shell .docker_pull (image_name , image_tag )
143
155
if result == DockerPullResult .ERROR_THROTTLED :
@@ -159,17 +171,39 @@ def run(command, cwd=None):
159
171
160
172
161
173
# Returns (status, output) from a shell command
162
- def get_cmd_output (command , cwd = None , check = True ):
174
+ def get_cmd_output (command , cwd = None , check = True , ** kwargs ):
175
+ if isinstance (command , str ):
176
+ command = shlex .split (command )
177
+
163
178
result = subprocess .run (
164
- shlex . split ( command ) ,
179
+ command ,
165
180
capture_output = True ,
166
- check = check ,
167
- cwd = cwd
181
+ check = False ,
182
+ cwd = cwd ,
183
+ ** kwargs
168
184
)
169
- return (result .returncode , result .stdout .decode ("utf-8" ).strip (), result .stderr .decode ("utf-8" ).strip ())
185
+ stdout = result .stdout .decode ("utf-8" ).strip ()
186
+ stderr = result .stderr .decode ("utf-8" ).strip ()
187
+ if check and result .returncode != 0 :
188
+ raise Exception (f"failed to run '{ command } .\n { stdout } \n { stderr } " )
189
+
190
+ return result .returncode , stdout , stderr
191
+
192
+
193
+ def decrypt_and_login (shell , secret , passphrase ):
194
+ decoded = base64 .b64decode (secret , validate = True )
195
+ if not passphrase :
196
+ raise Exception ("a secret was set but no passphrase was set (or it was empty)" )
197
+ (code , password , err ) = get_cmd_output (
198
+ ["gpg" , "--decrypt" , "--batch" , "--quiet" , "--passphrase" , passphrase , "--output" , "-" ],
199
+ input = decoded )
200
+ shell .docker_login (password )
201
+ print ("Docker login success!" )
170
202
171
203
172
204
def acquire_build_image (context = Context .default (), shell = Shell ()):
205
+ if context .encrypted_docker_password is not None :
206
+ decrypt_and_login (shell , context .encrypted_docker_password , context .docker_passphrase )
173
207
# If the image doesn't already exist locally, then look remotely
174
208
if not shell .docker_image_exists_locally (LOCAL_BASE_IMAGE_NAME , context .image_tag ):
175
209
announce ("Base image not found locally." )
@@ -188,7 +222,7 @@ def acquire_build_image(context=Context.default(), shell=Shell()):
188
222
return 1
189
223
190
224
announce ("Building a new image locally." )
191
- shell .docker_build_base_image (context .image_tag , context .tools_path )
225
+ shell .docker_build_base_image (context .image_tag , context .docker_image_path )
192
226
193
227
if context .github_actions :
194
228
announce ("Saving base image for use in later jobs..." )
@@ -205,20 +239,23 @@ def acquire_build_image(context=Context.default(), shell=Shell()):
205
239
206
240
announce ("Creating local build image..." )
207
241
shell .docker_tag (LOCAL_BASE_IMAGE_NAME , context .image_tag , LOCAL_BASE_IMAGE_NAME , LOCAL_TAG )
208
- shell .docker_build_build_image (context .user_id , context .script_path )
242
+ shell .docker_build_build_image (context .user_id , context .docker_image_path )
209
243
return 0
210
244
211
245
212
246
class SelfTest (unittest .TestCase ):
213
- def test_context (self , allow_local_build = False , github_actions = False ):
247
+ def test_context (self , github_actions = False , allow_local_build = False , encrypted_docker_password = None ,
248
+ docker_passphrase = None ):
214
249
return Context (
215
250
start_path = "/tmp/test/start-path" ,
216
251
script_path = "/tmp/test/script-path" ,
217
252
tools_path = "/tmp/test/tools-path" ,
218
253
user_id = "123" ,
219
254
image_tag = "someimagetag" ,
255
+ encrypted_docker_password = encrypted_docker_password ,
256
+ docker_passphrase = docker_passphrase ,
257
+ github_actions = github_actions ,
220
258
allow_local_build = allow_local_build ,
221
- github_actions = github_actions
222
259
)
223
260
224
261
def mock_shell (self ):
@@ -230,6 +267,7 @@ class SelfTest(unittest.TestCase):
230
267
shell .docker_pull = MagicMock ()
231
268
shell .docker_save = MagicMock ()
232
269
shell .docker_tag = MagicMock ()
270
+ shell .docker_login = MagicMock ()
233
271
return shell
234
272
235
273
def test_retry_architecture_mismatch (self ):
@@ -246,6 +284,13 @@ class SelfTest(unittest.TestCase):
246
284
)
247
285
)
248
286
287
+ def test_docker_login (self ):
288
+ shell = self .mock_shell ()
289
+ acquire_build_image (self .test_context (
290
+ encrypted_docker_password = "jA0ECQMCvYU/JxsX3g/70j0BxbLLW8QaFWWb/DqY9gPhTuEN/xdYVxaoDnV6Fha+lAWdT7xN0qZr5DHPBalLfVvvM1SEXRBI8qnfXyGI" ,
291
+ docker_passphrase = "secret" ), shell )
292
+ shell .docker_login .assert_called_with ("payload" )
293
+
249
294
def test_retry_immediate_success (self ):
250
295
shell = self .mock_shell ()
251
296
shell .docker_pull .side_effect = [DockerPullResult .SUCCESS ]
@@ -373,7 +418,7 @@ class SelfTest(unittest.TestCase):
373
418
374
419
shell .docker_image_exists_locally .assert_called_once ()
375
420
shell .docker_tag .assert_called_with (LOCAL_BASE_IMAGE_NAME , "someimagetag" , LOCAL_BASE_IMAGE_NAME , LOCAL_TAG )
376
- shell .docker_build_build_image .assert_called_with ("123" , "/tmp/test/script -path" )
421
+ shell .docker_build_build_image .assert_called_with ("123" , "/tmp/test/tools -path/ci-build " )
377
422
378
423
# When:
379
424
# - the base image doesn't exist locally
@@ -390,10 +435,10 @@ class SelfTest(unittest.TestCase):
390
435
391
436
self .assertEqual (0 , acquire_build_image (context , shell ))
392
437
shell .docker_image_exists_locally .assert_called_once ()
393
- shell .docker_build_base_image .assert_called_with ("someimagetag" , "/tmp/test/tools-path" )
438
+ shell .docker_build_base_image .assert_called_with ("someimagetag" , "/tmp/test/tools-path/ci-build " )
394
439
shell .docker_save .assert_not_called ()
395
440
shell .docker_tag .assert_called_with (LOCAL_BASE_IMAGE_NAME , "someimagetag" , LOCAL_BASE_IMAGE_NAME , LOCAL_TAG )
396
- shell .docker_build_build_image .assert_called_with ("123" , "/tmp/test/script -path" )
441
+ shell .docker_build_build_image .assert_called_with ("123" , "/tmp/test/tools -path/ci-build " )
397
442
398
443
# When:
399
444
# - the base image doesn't exist locally
@@ -410,10 +455,10 @@ class SelfTest(unittest.TestCase):
410
455
411
456
self .assertEqual (0 , acquire_build_image (context , shell ))
412
457
shell .docker_image_exists_locally .assert_called_once ()
413
- shell .docker_build_base_image .assert_called_with ("someimagetag" , "/tmp/test/tools-path" )
458
+ shell .docker_build_base_image .assert_called_with ("someimagetag" , "/tmp/test/tools-path/ci-build " )
414
459
shell .docker_save .assert_not_called ()
415
460
shell .docker_tag .assert_called_with (LOCAL_BASE_IMAGE_NAME , "someimagetag" , LOCAL_BASE_IMAGE_NAME , LOCAL_TAG )
416
- shell .docker_build_build_image .assert_called_with ("123" , "/tmp/test/script -path" )
461
+ shell .docker_build_build_image .assert_called_with ("123" , "/tmp/test/tools -path/ci-build " )
417
462
418
463
# When:
419
464
# - the base image doesn't exist locally
@@ -430,14 +475,14 @@ class SelfTest(unittest.TestCase):
430
475
431
476
self .assertEqual (0 , acquire_build_image (context , shell ))
432
477
shell .docker_image_exists_locally .assert_called_once ()
433
- shell .docker_build_base_image .assert_called_with ("someimagetag" , "/tmp/test/tools-path" )
478
+ shell .docker_build_base_image .assert_called_with ("someimagetag" , "/tmp/test/tools-path/ci-build " )
434
479
shell .docker_save .assert_called_with (
435
480
LOCAL_BASE_IMAGE_NAME ,
436
481
"someimagetag" ,
437
482
"/tmp/test/start-path/smithy-rs-base-image"
438
483
)
439
484
shell .docker_tag .assert_called_with (LOCAL_BASE_IMAGE_NAME , "someimagetag" , LOCAL_BASE_IMAGE_NAME , LOCAL_TAG )
440
- shell .docker_build_build_image .assert_called_with ("123" , "/tmp/test/script -path" )
485
+ shell .docker_build_build_image .assert_called_with ("123" , "/tmp/test/tools -path/ci-build " )
441
486
442
487
# When:
443
488
# - the base image doesn't exist locally
@@ -477,7 +522,7 @@ class SelfTest(unittest.TestCase):
477
522
call (REMOTE_BASE_IMAGE_NAME , "someimagetag" , LOCAL_BASE_IMAGE_NAME , "someimagetag" ),
478
523
call (LOCAL_BASE_IMAGE_NAME , "someimagetag" , LOCAL_BASE_IMAGE_NAME , LOCAL_TAG )
479
524
])
480
- shell .docker_build_build_image .assert_called_with ("123" , "/tmp/test/script -path" )
525
+ shell .docker_build_build_image .assert_called_with ("123" , "/tmp/test/tools -path/ci-build " )
481
526
482
527
483
528
def main ():
0 commit comments