| #!/usr/bin/env python3 |
| # -*- coding: utf-8 -*- |
| |
| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| |
| """ |
| Docker login tests |
| """ |
| import os |
| import subprocess |
| import unittest |
| from unittest.mock import create_autospec, patch, call, MagicMock |
| |
| import boto3 |
| from boto3 import client |
| from botocore.stub import Stubber |
| |
| from docker_login import login_dockerhub, logout_dockerhub, main, DOCKERHUB_RETRY_SECONDS, DOCKERHUB_LOGIN_NUM_RETRIES |
| |
| |
| SECRET_NAME = "secret_name" |
| SECRET_ENDPOINT_URL = "https://endpoint.url" |
| SECRET_ENDPOINT_REGION = "us-east-2" |
| |
| |
| def mock_boto(num_calls: int = 1): |
| mock_client = client("secretsmanager", region_name="us-east-1") |
| mock_session = create_autospec(boto3.Session) |
| mock_session.client.return_value = mock_client |
| |
| # Stub get_secret_value response |
| stub = Stubber(mock_client) |
| for i in range(num_calls): |
| stub.add_response( |
| method="get_secret_value", |
| expected_params={ |
| "SecretId": "secret_name" # Matches os.environ['SECRET_NAME'] |
| }, service_response={ |
| "SecretString": """{"username": "myuser", "password": "mypass"}""" |
| }) |
| return mock_session, stub |
| |
| |
| class TestDockerLogin(unittest.TestCase): |
| |
| @patch("subprocess.run", name="mock_subprocess_run") |
| def test_docker_login_success(self, mock_run): |
| """ |
| Tests successful docker login returns True and calls docker appropriately |
| """ |
| mock_session, stub = mock_boto() |
| stub.activate() |
| with patch("boto3.Session", return_value=mock_session): |
| mock_process = MagicMock(auto_spec=subprocess.Popen, name="mock_process") |
| |
| # Simulate successful login |
| mock_process.returncode = 0 |
| mock_run.return_value = mock_process |
| |
| login_dockerhub(SECRET_NAME, SECRET_ENDPOINT_URL, SECRET_ENDPOINT_REGION) |
| |
| # Check boto client is properly created |
| print(mock_session.client.call_args_list) |
| assert mock_session.client.call_args_list == [ |
| call(service_name="secretsmanager", region_name="us-east-2", endpoint_url="https://endpoint.url") |
| ] |
| |
| # Check that login call passes in the password in the correct way |
| assert mock_run.call_args_list == [ |
| call( |
| ["docker", "login", "--username", "myuser", "--password-stdin"], |
| stdout=subprocess.PIPE, |
| input=str.encode("mypass") |
| ) |
| ] |
| stub.deactivate() |
| |
| @patch("subprocess.run", name="mock_subprocess_run") |
| @patch("time.sleep") |
| def test_docker_login_retry(self, mock_sleep, mock_run): |
| """ |
| Tests retry mechanism |
| """ |
| num_tries = 3 |
| mock_session, stub = mock_boto(num_calls=num_tries) |
| stub.activate() |
| with patch("boto3.Session", return_value=mock_session): |
| mock_process = MagicMock(auto_spec=subprocess.Popen, name="mock_process") |
| |
| # Simulate successful login |
| mock_process.returncode = 0 |
| |
| # Simulate (num_tries - 1) errors + 1 success |
| mock_run.side_effect = \ |
| [subprocess.CalledProcessError(1, "cmd", "some error")] * (num_tries - 1) + [mock_process] |
| |
| login_dockerhub(SECRET_NAME, SECRET_ENDPOINT_URL, SECRET_ENDPOINT_REGION) |
| |
| # Check boto client is properly created |
| print(mock_session.client.call_args_list) |
| assert mock_session.client.call_args_list == [ |
| call(service_name="secretsmanager", region_name="us-east-2", endpoint_url="https://endpoint.url") |
| ] * num_tries |
| |
| # Check that login call passes in the password in the correct way |
| cmd = ["docker", "login", "--username", "myuser", "--password-stdin"] |
| assert mock_run.call_args_list == [ |
| call(cmd, stdout=subprocess.PIPE, input=str.encode("mypass")) |
| ] * num_tries |
| |
| # Assert sleep was called appropriately |
| assert mock_sleep.call_args_list == [ |
| call(2 ** retry_num * DOCKERHUB_RETRY_SECONDS) for retry_num in range(0, num_tries - 1) |
| ] |
| stub.deactivate() |
| |
| @patch("subprocess.run", name="mock_subprocess_run") |
| @patch("time.sleep") |
| def test_docker_login_retry_exhausted(self, mock_sleep, mock_run): |
| """ |
| Tests retry mechanism |
| """ |
| num_tries = DOCKERHUB_LOGIN_NUM_RETRIES |
| mock_session, stub = mock_boto(num_calls=num_tries) |
| stub.activate() |
| with patch("boto3.Session", return_value=mock_session): |
| # Simulate num_tries errors |
| mock_run.side_effect = [subprocess.CalledProcessError(1, "cmd", "some error")] * num_tries |
| |
| with self.assertRaises(subprocess.CalledProcessError): |
| login_dockerhub(SECRET_NAME, SECRET_ENDPOINT_URL, SECRET_ENDPOINT_REGION) |
| |
| # Check boto client is properly created |
| assert mock_session.client.call_args_list == [ |
| call(service_name="secretsmanager", region_name="us-east-2", endpoint_url="https://endpoint.url") |
| ] * num_tries |
| |
| # Check that login call passes in the password in the correct way |
| cmd = ["docker", "login", "--username", "myuser", "--password-stdin"] |
| assert mock_run.call_args_list == [ |
| call(cmd, stdout=subprocess.PIPE, input=str.encode("mypass")) |
| ] * num_tries |
| |
| # Assert sleep was called appropriately |
| assert mock_sleep.call_args_list == [ |
| call(2 ** retry_num * DOCKERHUB_RETRY_SECONDS) for retry_num in range(0, num_tries-1) |
| ] |
| stub.deactivate() |
| |
| @patch("subprocess.run", name="mock_subprocess_run") |
| def test_docker_login_failed(self, mock_run): |
| """ |
| Tests failed docker login return false |
| """ |
| mock_session, stub = mock_boto() |
| stub.activate() |
| with patch("boto3.Session", return_value=mock_session): |
| |
| mock_process = MagicMock(auto_spec=subprocess.Popen, name="mock_process") |
| |
| # Simulate failed login |
| mock_process.returncode = 1 |
| mock_run.return_value = mock_process |
| |
| with self.assertRaises(RuntimeError): |
| login_dockerhub(SECRET_NAME, SECRET_ENDPOINT_URL, SECRET_ENDPOINT_REGION) |
| stub.deactivate() |
| |
| @patch("subprocess.call", name="mock_subprocess_call") |
| def test_logout(self, mock_call): |
| """ |
| Tests logout calls docker command appropriately |
| """ |
| logout_dockerhub() |
| assert mock_call.call_args_list == [ |
| call(["docker", "logout"]) |
| ] |
| |
| @patch("docker_login.login_dockerhub") |
| def test_main_exit(self, mock_login): |
| """ |
| Tests main exits with error on failed docker login |
| """ |
| mock_login.side_effect = RuntimeError("Didn't work") |
| with self.assertRaises(SystemExit): |
| main(["--secret-name", "name", "--secret-endpoint-url", "url", "--secret-endpoint-region", "r"]) |
| |
| @patch("docker_login.login_dockerhub") |
| def test_main_default_argument_values(self, mock_login): |
| """ |
| Tests default arguments |
| """ |
| |
| # Good env |
| env = { |
| "DOCKERHUB_SECRET_ENDPOINT_URL": "url", |
| "DOCKERHUB_SECRET_ENDPOINT_REGION": "region" |
| } |
| with patch.dict(os.environ, env): |
| main(["--secret-name", "name"]) |
| assert mock_login.call_args_list == [ |
| call("name", "url", "region") |
| ] |
| |
| # Bad envs - none or not all required vars defined |
| tests = [ |
| {}, |
| {"DOCKERHUB_SECRET_ENDPOINT_URL": "url"}, |
| {"DOCKERHUB_SECRET_ENDPOINT_REGION": "region"} |
| ] |
| for bad_env in tests: |
| with patch.dict(os.environ, bad_env): |
| with self.assertRaises(RuntimeError): |
| main(["--secret-name", "name"]) |
| |