blob: 488295d9fa7116fd6bd2a11e64730ed1dd52014f [file] [log] [blame]
#!/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"])