Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use docker login when possible #2265

Merged
merged 31 commits into from
Feb 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a3f06d2
Use docker login (wip)
rcoh Jan 30, 2023
e6a9a01
setup job hierarchy
rcoh Jan 30, 2023
01cab22
Move save code to always run, fix throttling backoff
rcoh Jan 30, 2023
b232210
use ECR login
rcoh Jan 30, 2023
2d34997
hax
rcoh Jan 30, 2023
09ac023
Merge branch 'main' into docker-login-if-possible
rcoh Jan 30, 2023
f40cfa5
fix loop
rcoh Jan 30, 2023
fc66f41
Merge branch 'docker-login-if-possible' of github.com:awslabs/smithy-…
rcoh Jan 30, 2023
6e85210
Merge branch 'main' into docker-login-if-possible
rcoh Jan 31, 2023
0d64314
fix chmod on script
rcoh Jan 31, 2023
396a43f
Merge branch 'docker-login-if-possible' of github.com:awslabs/smithy-…
rcoh Jan 31, 2023
badeb7c
backout changes to acquire-build-image
rcoh Jan 31, 2023
2124e98
wip save a token
rcoh Jan 31, 2023
288f477
Try actually logging in...
rcoh Jan 31, 2023
fedb329
bail
rcoh Jan 31, 2023
85f7753
use a real token
rcoh Jan 31, 2023
0a130cd
use a real token for all jobs
rcoh Jan 31, 2023
ed66404
use a real token for all jobs
rcoh Jan 31, 2023
2906394
use a real token for all jobs
rcoh Jan 31, 2023
0ff3997
fix docs, add more logs
rcoh Feb 1, 2023
62c82ee
rename secret name
rcoh Feb 1, 2023
936b560
Merge branch 'main' of github.com:awslabs/smithy-rs into docker-login…
rcoh Feb 1, 2023
d9d710b
docs, naming, nicer errors
rcoh Feb 1, 2023
e26dbda
Merge branch 'main' of github.com:awslabs/smithy-rs into docker-login…
rcoh Feb 1, 2023
33d7cb1
wip
rcoh Feb 2, 2023
7514885
fix CI
rcoh Feb 2, 2023
ffa2d71
refactor and add tests
rcoh Feb 2, 2023
a1607cf
remove hack
rcoh Feb 2, 2023
cc97fe8
Merge branch 'main' of github.com:awslabs/smithy-rs into docker-login…
rcoh Feb 2, 2023
dbc0025
fix bad merge
rcoh Feb 2, 2023
1ee44b4
revert another bad commit
rcoh Feb 2, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 63 additions & 19 deletions .github/scripts/acquire-build-image
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import subprocess
import sys
import time
import unittest
import base64

REMOTE_BASE_IMAGE_NAME = "public.ecr.aws/w0m4q9l7/github-awslabs-smithy-rs-ci"
LOCAL_BASE_IMAGE_NAME = "smithy-rs-base-image"
Expand Down Expand Up @@ -41,7 +42,8 @@ class Platform(Enum):

# Script context
class Context:
def __init__(self, start_path, script_path, tools_path, user_id, image_tag, allow_local_build, github_actions):
def __init__(self, start_path, script_path, tools_path, user_id, image_tag, allow_local_build, github_actions,
encrypted_docker_password, docker_passphrase):
self.start_path = start_path
self.script_path = script_path
self.tools_path = tools_path
Expand All @@ -50,6 +52,8 @@ class Context:
self.image_tag = image_tag
self.allow_local_build = allow_local_build
self.github_actions = github_actions
self.encrypted_docker_password = encrypted_docker_password
self.docker_passphrase = docker_passphrase

@staticmethod
def default():
Expand All @@ -60,14 +64,19 @@ class Context:
image_tag = get_cmd_output("./docker-image-hash", cwd=script_path)[1]
allow_local_build = os.getenv("ALLOW_LOCAL_BUILD") != "false"
github_actions = os.getenv("GITHUB_ACTIONS") == "true"
encrypted_docker_password = os.getenv("ENCRYPTED_DOCKER_PASSWORD")
docker_passphrase = os.getenv("DOCKER_LOGIN_TOKEN_PASSPHRASE")

print(f"Start path: {start_path}")
print(f"Script path: {script_path}")
print(f"Tools path: {tools_path}")
print(f"User ID: {user_id}")
print(f"Required base image tag: {image_tag}")
print(f"Allow local build: {allow_local_build}")
print(f"Running in GitHub Actions: {github_actions}")
return Context(start_path, script_path, tools_path, user_id, image_tag, allow_local_build, github_actions)
return Context(start_path=start_path, script_path=script_path, tools_path=tools_path, user_id=user_id,
image_tag=image_tag, allow_local_build=allow_local_build, github_actions=github_actions,
encrypted_docker_password=encrypted_docker_password, docker_passphrase=docker_passphrase)


def output_contains_any(stdout, stderr, messages):
Expand All @@ -76,7 +85,6 @@ def output_contains_any(stdout, stderr, messages):
return True
return False


# Mockable shell commands
class Shell:
# Returns the platform that this script is running on
Expand All @@ -91,6 +99,9 @@ class Shell:
(status, _, _) = get_cmd_output(f"docker inspect \"{image_name}:{image_tag}\"", check=False)
return status == 0

def docker_login(self, password):
get_cmd_output("docker login --username AWS --password-stdin public.ecr.aws", input=password.encode('utf-8'))

# Pulls the requested `image_name` with `image_tag`. Returns `DockerPullResult`.
def docker_pull(self, image_name, image_tag):
(status, stdout, stderr) = get_cmd_output(f"docker pull \"{image_name}:{image_tag}\"", check=False)
Expand All @@ -102,7 +113,7 @@ class Shell:
print("-------------------")

not_found_messages = ["not found: manifest unknown"]
throttle_messages = ["toomanyrequests: Rate exceeded", "toomanyrequests: Data limit exceeded"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this was unintentionally reverted. We definitely want this "Data limit exceeded" retry for forked PRs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh thanks I saw that and TODO but forgot

throttle_messages = ["toomanyrequests:"]
retryable_messages = ["net/http: TLS handshake timeout"]
if status == 0:
return DockerPullResult.SUCCESS
Expand Down Expand Up @@ -160,17 +171,39 @@ def run(command, cwd=None):


# Returns (status, output) from a shell command
def get_cmd_output(command, cwd=None, check=True):
def get_cmd_output(command, cwd=None, check=True, **kwargs):
if isinstance(command, str):
command = shlex.split(command)

result = subprocess.run(
shlex.split(command),
command,
capture_output=True,
check=check,
cwd=cwd
check=False,
cwd=cwd,
**kwargs
)
return (result.returncode, result.stdout.decode("utf-8").strip(), result.stderr.decode("utf-8").strip())
stdout = result.stdout.decode("utf-8").strip()
stderr = result.stderr.decode("utf-8").strip()
if check and result.returncode != 0:
raise Exception(f"failed to run '{command}.\n{stdout}\n{stderr}")

return result.returncode, stdout, stderr


def decrypt_and_login(shell, secret, passphrase):
decoded = base64.b64decode(secret, validate=True)
if not passphrase:
raise Exception("a secret was set but no passphrase was set (or it was empty)")
(code, password, err) = get_cmd_output(
["gpg", "--decrypt", "--batch", "--quiet", "--passphrase", passphrase, "--output", "-"],
input=decoded)
shell.docker_login(password)
print("Docker login success!")


def acquire_build_image(context=Context.default(), shell=Shell()):
if context.encrypted_docker_password is not None:
decrypt_and_login(shell, context.encrypted_docker_password, context.docker_passphrase)
# If the image doesn't already exist locally, then look remotely
if not shell.docker_image_exists_locally(LOCAL_BASE_IMAGE_NAME, context.image_tag):
announce("Base image not found locally.")
Expand Down Expand Up @@ -211,15 +244,18 @@ def acquire_build_image(context=Context.default(), shell=Shell()):


class SelfTest(unittest.TestCase):
def test_context(self, allow_local_build=False, github_actions=False):
def test_context(self, github_actions=False, allow_local_build=False, encrypted_docker_password=None,
docker_passphrase=None):
return Context(
start_path="/tmp/test/start-path",
script_path="/tmp/test/script-path",
tools_path="/tmp/test/tools-path",
user_id="123",
image_tag="someimagetag",
encrypted_docker_password=encrypted_docker_password,
docker_passphrase=docker_passphrase,
github_actions=github_actions,
allow_local_build=allow_local_build,
github_actions=github_actions
)

def mock_shell(self):
Expand All @@ -231,6 +267,7 @@ class SelfTest(unittest.TestCase):
shell.docker_pull = MagicMock()
shell.docker_save = MagicMock()
shell.docker_tag = MagicMock()
shell.docker_login = MagicMock()
return shell

def test_retry_architecture_mismatch(self):
Expand All @@ -247,6 +284,13 @@ class SelfTest(unittest.TestCase):
)
)

def test_docker_login(self):
shell = self.mock_shell()
acquire_build_image(self.test_context(
encrypted_docker_password="jA0ECQMCvYU/JxsX3g/70j0BxbLLW8QaFWWb/DqY9gPhTuEN/xdYVxaoDnV6Fha+lAWdT7xN0qZr5DHPBalLfVvvM1SEXRBI8qnfXyGI",
docker_passphrase="secret"), shell)
shell.docker_login.assert_called_with("payload")

def test_retry_immediate_success(self):
shell = self.mock_shell()
shell.docker_pull.side_effect = [DockerPullResult.SUCCESS]
Expand Down Expand Up @@ -374,7 +418,7 @@ class SelfTest(unittest.TestCase):

shell.docker_image_exists_locally.assert_called_once()
shell.docker_tag.assert_called_with(LOCAL_BASE_IMAGE_NAME, "someimagetag", LOCAL_BASE_IMAGE_NAME, LOCAL_TAG)
shell.docker_build_build_image.assert_called_with("123", "/tmp/test/tools-path")
shell.docker_build_build_image.assert_called_with("123", "/tmp/test/tools-path/ci-build")

# When:
# - the base image doesn't exist locally
Expand All @@ -391,10 +435,10 @@ class SelfTest(unittest.TestCase):

self.assertEqual(0, acquire_build_image(context, shell))
shell.docker_image_exists_locally.assert_called_once()
shell.docker_build_base_image.assert_called_with("someimagetag", "/tmp/test/tools-path")
shell.docker_build_base_image.assert_called_with("someimagetag", "/tmp/test/tools-path/ci-build")
shell.docker_save.assert_not_called()
shell.docker_tag.assert_called_with(LOCAL_BASE_IMAGE_NAME, "someimagetag", LOCAL_BASE_IMAGE_NAME, LOCAL_TAG)
shell.docker_build_build_image.assert_called_with("123", "/tmp/test/tools-path")
shell.docker_build_build_image.assert_called_with("123", "/tmp/test/tools-path/ci-build")

# When:
# - the base image doesn't exist locally
Expand All @@ -411,10 +455,10 @@ class SelfTest(unittest.TestCase):

self.assertEqual(0, acquire_build_image(context, shell))
shell.docker_image_exists_locally.assert_called_once()
shell.docker_build_base_image.assert_called_with("someimagetag", "/tmp/test/tools-path")
shell.docker_build_base_image.assert_called_with("someimagetag", "/tmp/test/tools-path/ci-build")
shell.docker_save.assert_not_called()
shell.docker_tag.assert_called_with(LOCAL_BASE_IMAGE_NAME, "someimagetag", LOCAL_BASE_IMAGE_NAME, LOCAL_TAG)
shell.docker_build_build_image.assert_called_with("123", "/tmp/test/tools-path")
shell.docker_build_build_image.assert_called_with("123", "/tmp/test/tools-path/ci-build")

# When:
# - the base image doesn't exist locally
Expand All @@ -431,14 +475,14 @@ class SelfTest(unittest.TestCase):

self.assertEqual(0, acquire_build_image(context, shell))
shell.docker_image_exists_locally.assert_called_once()
shell.docker_build_base_image.assert_called_with("someimagetag", "/tmp/test/tools-path")
shell.docker_build_base_image.assert_called_with("someimagetag", "/tmp/test/tools-path/ci-build")
shell.docker_save.assert_called_with(
LOCAL_BASE_IMAGE_NAME,
"someimagetag",
"/tmp/test/start-path/smithy-rs-base-image"
)
shell.docker_tag.assert_called_with(LOCAL_BASE_IMAGE_NAME, "someimagetag", LOCAL_BASE_IMAGE_NAME, LOCAL_TAG)
shell.docker_build_build_image.assert_called_with("123", "/tmp/test/tools-path")
shell.docker_build_build_image.assert_called_with("123", "/tmp/test/tools-path/ci-build")

# When:
# - the base image doesn't exist locally
Expand Down Expand Up @@ -478,7 +522,7 @@ class SelfTest(unittest.TestCase):
call(REMOTE_BASE_IMAGE_NAME, "someimagetag", LOCAL_BASE_IMAGE_NAME, "someimagetag"),
call(LOCAL_BASE_IMAGE_NAME, "someimagetag", LOCAL_BASE_IMAGE_NAME, LOCAL_TAG)
])
shell.docker_build_build_image.assert_called_with("123", "/tmp/test/tools-path")
shell.docker_build_build_image.assert_called_with("123", "/tmp/test/tools-path/ci-build")


def main():
Expand Down
39 changes: 38 additions & 1 deletion .github/workflows/ci-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,45 @@ env:
ecr_repository: public.ecr.aws/w0m4q9l7/github-awslabs-smithy-rs-ci

jobs:
# This job will, if possible, save a docker login password to the job outputs. The token will
# be encrypted with the passphrase stored as a GitHub secret. The login password expires after 12h.
# The login password is encrypted with the repo secret DOCKER_LOGIN_TOKEN_PASSPHRASE
save-docker-login-token:
outputs:
docker-login-password: ${{ steps.set-token.outputs.docker-login-password }}
permissions:
id-token: write
contents: read
continue-on-error: true
name: Save a docker login token
runs-on: ubuntu-latest
steps:
- name: Attempt to load a docker login password
uses: aws-actions/configure-aws-credentials@v1-node16
with:
role-to-assume: ${{ secrets.SMITHY_RS_PUBLIC_ECR_PUSH_ROLE_ARN }}
role-session-name: GitHubActions
aws-region: us-west-2
- name: Save the docker login password to the output
id: set-token
run: |
ENCRYPTED_PAYLOAD=$(
gpg --symmetric --batch --passphrase "${{ secrets.DOCKER_LOGIN_TOKEN_PASSPHRASE }}" --output - <(aws ecr-public get-login-password --region us-east-1) | base64 -w0
)
echo "docker-login-password=$ENCRYPTED_PAYLOAD" >> $GITHUB_OUTPUT


# This job detects if the PR made changes to build tools. If it did, then it builds a new
# build Docker image. Otherwise, it downloads a build image from Public ECR. In both cases,
# it uploads the image as a build artifact for other jobs to download and use.
acquire-base-image:
name: Acquire Base Image
needs: save-docker-login-token
if: ${{ github.event.pull_request.head.repo.full_name == 'awslabs/smithy-rs' }}
runs-on: ubuntu-latest
env:
ENCRYPTED_DOCKER_PASSWORD: ${{ needs.save-docker-login-token.outputs.docker-login-password }}
DOCKER_LOGIN_TOKEN_PASSPHRASE: ${{ secrets.DOCKER_LOGIN_TOKEN_PASSPHRASE }}
permissions:
id-token: write
contents: read
Expand Down Expand Up @@ -50,11 +82,16 @@ jobs:

# Run shared CI after the Docker build image has either been rebuilt or found in ECR
ci:
needs: acquire-base-image
needs:
- save-docker-login-token
- acquire-base-image
if: ${{ github.event.pull_request.head.repo.full_name == 'awslabs/smithy-rs' }}
uses: ./.github/workflows/ci.yml
with:
run_sdk_examples: true
secrets:
ENCRYPTED_DOCKER_PASSWORD: ${{ needs.save-docker-login-token.outputs.docker-login-password }}
DOCKER_LOGIN_TOKEN_PASSPHRASE: ${{ secrets.DOCKER_LOGIN_TOKEN_PASSPHRASE }}

# The PR bot requires a Docker build image, so make it depend on the `acquire-base-image` job.
pr_bot:
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,18 @@ on:
required: false
type: string
default: ''
secrets:
# the docker login password for ECR. This is
ENCRYPTED_DOCKER_PASSWORD:
required: false
DOCKER_LOGIN_TOKEN_PASSPHRASE:
required: false

env:
rust_version: 1.62.1
rust_toolchain_components: clippy,rustfmt
ENCRYPTED_DOCKER_PASSWORD: ${{ secrets.ENCRYPTED_DOCKER_PASSWORD }}
DOCKER_LOGIN_TOKEN_PASSPHRASE: ${{ secrets.DOCKER_LOGIN_TOKEN_PASSPHRASE }}

jobs:
# The `generate` job runs scripts that produce artifacts that are required by the `test` job,
Expand Down