Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Support for multiple registries (#193)
* envvar parsing of multiple container registries

* rename value, support for pushing modules based on module.json

* string comparison code clean up

* modify envvars with better values & refactor dockercls and .env.tmp

* modified variable names, minor fixes, added envvar testing specific to container registry

* add tests for additional cr, comments to explain code, fix merge conflict

* add additional testing for mutliple registries, fix logic around given/expected env vars

* fix env load in tests

* Tell travis to use DOTENV_FILE
  • Loading branch information
kln4dt authored and jongio committed Aug 11, 2018
1 parent ea831dd commit 271e28e
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 43 deletions.
10 changes: 9 additions & 1 deletion .env.tmp
Expand Up @@ -9,7 +9,7 @@ DEVICE_CONNECTION_STRING=""
#
# CONTAINER REGISTRY
#
# Settings for your container registry, set CONTAINER_REGISTRY_SERVER to the following:
# Settings for your container registry, set CONTAINER_REGISTRY_SERVER to the following as the default registry:
# Local Registry: "localhost:5000" - USERNAME/PASSWORD not required.
# Azure Container Registry: "jong.azurecr.io", Also set USERNAME/PASSWORD
# Docker Hub: "jongallant" - Your Docker hub username. Enter your Docker hub username into the CONTAINER_REGISTRY_USERNAME setting. Also set the PASSWORD.
Expand All @@ -18,6 +18,14 @@ CONTAINER_REGISTRY_SERVER="localhost:5000"
CONTAINER_REGISTRY_USERNAME=""
CONTAINER_REGISTRY_PASSWORD=""

# To specify additional container registries ensure the prefix is CONTAINER_REGISTRY_SERVER, CONTAINER_REGISTRY_USERNAME, CONTAINER_REGISTRY_PASSWORD
# And the token following the prefix uniquely associates the SERVER/USERNAME/PASSWORD
# Token can be any string of alphanumeric characters

# CONTAINER_REGISTRY_SERVER2=""
# CONTAINER_REGISTRY_USERNAME2=""
# CONTAINER_REGISTRY_PASSWORD2=""

#
# HOST
#
Expand Down
4 changes: 3 additions & 1 deletion .travis.yml
Expand Up @@ -7,4 +7,6 @@ python:
install:
- pip install -r requirements_travis.txt
script:
- pytest -m unit # & pylint iotedgedev # or py.test for Python versions 3.5 and below
- pytest -m unit # & pylint iotedgedev # or py.test for Python versions 3.5 and below
env:
- DOTENV_FILE=".env.tmp"
7 changes: 7 additions & 0 deletions iotedgedev/containerregistry.py
@@ -0,0 +1,7 @@
class ContainerRegistry:
def __init__(self, server, username, password):
self.server = server
self.username = username
self.password = password


46 changes: 12 additions & 34 deletions iotedgedev/dockercls.py
Expand Up @@ -28,20 +28,22 @@ def get_os_type(self):

def init_registry(self):

self.output.header("INITIALIZING CONTAINER REGISTRY")
self.output.info("REGISTRY: " + self.envvars.CONTAINER_REGISTRY_SERVER)
for registry in self.envvars.CONTAINER_REGISTRY_MAP.values():
self.output.header("INITIALIZING CONTAINER REGISTRY")
self.output.info("REGISTRY: " + registry.server)

if "localhost" in self.envvars.CONTAINER_REGISTRY_SERVER:
self.init_local_registry()
if "localhost" in registry.server:
self.init_local_registry(registry.server)

self.output.line()

def init_local_registry(self):
def init_local_registry(self, local_server):

parts = self.envvars.CONTAINER_REGISTRY_SERVER.split(":")
parts = local_server.split(":")

if len(parts) < 2:
self.output.error("You must specific a port for your local registry server. Expected: 'localhost:5000'. Found: " + self.envvars.CONTAINER_REGISTRY_SERVER)
self.output.error("You must specific a port for your local registry server. Expected: 'localhost:5000'. Found: " +
local_server)
sys.exit()

port = parts[1]
Expand All @@ -66,44 +68,20 @@ def init_local_registry(self):
self.output.info("Running registry container")
self.docker_client.containers.run("registry:2", detach=True, name="registry", ports=ports, restart_policy={"Name": "always"})

def login_registry(self):
try:

if "localhost" in self.envvars.CONTAINER_REGISTRY_SERVER:
client_login_status = self.docker_client.login(self.envvars.CONTAINER_REGISTRY_SERVER)
api_login_status = self.docker_api.login(self.envvars.CONTAINER_REGISTRY_SERVER)
else:

client_login_status = self.docker_client.login(registry=self.envvars.CONTAINER_REGISTRY_SERVER,
username=self.envvars.CONTAINER_REGISTRY_USERNAME,
password=self.envvars.CONTAINER_REGISTRY_PASSWORD)

api_login_status = self.docker_api.login(registry=self.envvars.CONTAINER_REGISTRY_SERVER,
username=self.envvars.CONTAINER_REGISTRY_USERNAME,
password=self.envvars.CONTAINER_REGISTRY_PASSWORD)

self.output.info("Successfully logged into container registry: " + self.envvars.CONTAINER_REGISTRY_SERVER)

except Exception as ex:
self.output.error(
"Could not login to Container Registry. 1. Make sure Docker is running locally. 2. Verify your credentials in CONTAINER_REGISTRY_ environment variables. "
"3. If you are using WSL, then please set DOCKER_HOST Environment Variable. See the Azure IoT Edge Dev readme at https://aka.ms/iotedgedev for full instructions.")
self.output.error(str(ex))
sys.exit(-1)

def setup_registry(self):
self.output.header("SETTING UP CONTAINER REGISTRY")
self.init_registry()
self.output.info("PUSHING EDGE IMAGES TO CONTAINER REGISTRY")
image_names = ["azureiotedge-agent", "azureiotedge-hub", "azureiotedge-simulated-temperature-sensor"]
default_cr = self.envvars.CONTAINER_REGISTRY_MAP['']

for image_name in image_names:

microsoft_image_name = "microsoft/{0}:{1}".format(
image_name, self.envvars.RUNTIME_TAG)

container_registry_image_name = "{0}/{1}:{2}".format(
self.envvars.CONTAINER_REGISTRY_SERVER, image_name, self.envvars.RUNTIME_TAG)
default_cr.server, image_name, self.envvars.RUNTIME_TAG)

# Pull image from Microsoft Docker Hub
try:
Expand Down Expand Up @@ -134,7 +112,7 @@ def setup_registry(self):
container_registry_image_name))

response = self.docker_client.images.push(repository=container_registry_image_name, tag=self.envvars.RUNTIME_TAG, stream=True, auth_config={
"username": self.envvars.CONTAINER_REGISTRY_USERNAME, "password": self.envvars.CONTAINER_REGISTRY_PASSWORD})
"username": default_cr.username, "password": default_cr.password})
self.process_api_response(response)
self.output.info("SUCCESSFULLY PUSHED IMAGE: '{0}'".format(
container_registry_image_name))
Expand Down
35 changes: 32 additions & 3 deletions iotedgedev/envvars.py
Expand Up @@ -10,6 +10,7 @@

from .args import Args
from .connectionstring import DeviceConnectionString, IoTHubConnectionString
from .containerregistry import ContainerRegistry


class EnvVars:
Expand Down Expand Up @@ -120,6 +121,8 @@ def load(self, force=False):
self.output.error("Unable to parse DEVICE_CONNECTION_STRING Environment Variable. Please ensure that you have the right connection string set.")
self.output.error(str(ex))
sys.exit(-1)

self.get_registries()

self.RUNTIME_HOST_NAME = self.get_envvar("RUNTIME_HOST_NAME", default=".")
if self.RUNTIME_HOST_NAME == ".":
Expand All @@ -134,9 +137,6 @@ def load(self, force=False):
self.set_envvar("RUNTIME_CONFIG_DIR", self.get_runtime_config_dir())
self.BYPASS_MODULES = self.get_envvar("BYPASS_MODULES")
self.ACTIVE_DOCKER_PLATFORMS = self.get_envvar("ACTIVE_DOCKER_PLATFORMS", altkeys=["ACTIVE_DOCKER_ARCH"])
self.CONTAINER_REGISTRY_SERVER = self.get_envvar("CONTAINER_REGISTRY_SERVER")
self.CONTAINER_REGISTRY_USERNAME = self.get_envvar("CONTAINER_REGISTRY_USERNAME")
self.CONTAINER_REGISTRY_PASSWORD = self.get_envvar("CONTAINER_REGISTRY_PASSWORD")
self.CONTAINER_TAG = self.get_envvar("CONTAINER_TAG")
self.RUNTIME_TAG = self.get_envvar("RUNTIME_TAG")
self.RUNTIME_VERBOSITY = self.get_envvar("RUNTIME_VERBOSITY")
Expand Down Expand Up @@ -235,6 +235,35 @@ def save_envvar(self, key, value):
self.output.error(f("Could not update the environment variable {key} in file {dotenv_path}"))
sys.exit(-1)

def get_registries(self):
registries = {}
self.CONTAINER_REGISTRY_MAP = {}
length_container_registry_server = len('container_registry_server')
length_container_registry_username_or_password = len('container_registry_username')
length_container_registry = len('container_registry_')
# loops through .env file for key matching container_registry_server, container_registry_username, container_registry_password
for key in os.environ:
key = key.upper()
# get token for container_registry_server key
if key.startswith('CONTAINER_REGISTRY_SERVER'):
token = key[length_container_registry_server:]
# if the token doesn't already exist as an item in the dictionary, add it. if it does, add the server value
if token not in registries:
registries[token] = {'username': '', 'password': ''}
registries[token]['server'] = self.get_envvar(key, required=True)
# get token for container_registry_username or container_registry_password key and get subkey (username or password)
elif key.startswith(('CONTAINER_REGISTRY_USERNAME', 'CONTAINER_REGISTRY_PASSWORD')):
token = key[length_container_registry_username_or_password:]
subkey = key[length_container_registry:length_container_registry_username_or_password]
# if the token doesn't already exist as an item in the dictionary, add it. if it does, add the subkey(username/password) value
if token not in registries:
registries[token] = {'username': '', 'password': ''}
registries[token][subkey] = self.get_envvar(key)

# store parsed values as a dicitonary of containerregistry objects
for key, value in registries.items():
self.CONTAINER_REGISTRY_MAP[key] = ContainerRegistry(value['server'], value['username'], value['password'])

def get_runtime_home_dir(self):
if self.is_posix():
return "/var/lib/azure-iot-edge"
Expand Down
13 changes: 11 additions & 2 deletions iotedgedev/modules.py
Expand Up @@ -138,10 +138,19 @@ def build_push(self, no_build=False, no_push=False):
if not no_push:
# PUSH TO CONTAINER REGISTRY
self.output.info("PUSHING DOCKER IMAGE: " + tag)
registry_key = None
for key, registry in self.envvars.CONTAINER_REGISTRY_MAP.items():
#Split the repository tag in the module.json (ex: Localhost:5000/filtermodule)
if registry.server.lower() == tag.split('/')[0].lower():
registry_key = key
break
if registry_key is None:
self.output.error("Could not find registry server with name {0}. Please make sure your envvar is set.".format(tag.split('/')[0].lower()))
self.output.info("module json reading {0}".format(tag))

response = self.dock.docker_client.images.push(repository=tag, stream=True, auth_config={
"username": self.envvars.CONTAINER_REGISTRY_USERNAME,
"password": self.envvars.CONTAINER_REGISTRY_PASSWORD})
"username": self.envvars.CONTAINER_REGISTRY_MAP[registry_key].username,
"password": self.envvars.CONTAINER_REGISTRY_MAP[registry_key].password})
self.dock.process_api_response(response)
self.output.footer("BUILD COMPLETE", suppress=no_build)
self.output.footer("PUSH COMPLETE", suppress=no_push)
Expand Down
142 changes: 140 additions & 2 deletions tests/test_envvars.py
Expand Up @@ -59,7 +59,6 @@ def test_set_envvar():
assert setlevel == "debug"
envvars.set_envvar("RUNTIME_LOG_LEVEL", loglevel)


def test_envvar_clean():
output = Output()
envvars = EnvVars(output)
Expand All @@ -71,7 +70,6 @@ def test_envvar_clean():
if PY2:
assert isinstance(os.environ[envvar_clean_name], str)


def test_in_command_list_true_1():
output = Output()
envvars = EnvVars(output)
Expand Down Expand Up @@ -148,3 +146,143 @@ def test_is_terse_command_empty():
output = Output()
envvars = EnvVars(output)
assert envvars.is_terse_command("")

def test_default_container_registry_server_key_exists():
output = Output()
envvars = EnvVars(output)
envvars.load()
assert "CONTAINER_REGISTRY_SERVER" in os.environ

def test_default_container_registry_server_value_exists():
output = Output()
envvars = EnvVars(output)
server = envvars.get_envvar("CONTAINER_REGISTRY_SERVER")
assert server is not None

def test_default_container_registry_username_value_exists_or_returns_empty_string():
output = Output()
envvars = EnvVars(output)
username = envvars.get_envvar("CONTAINER_REGISTRY_USERNAME")
assert username is not None

def test_default_container_registry_password_value_exists_or_returns_empty_string():
output = Output()
envvars = EnvVars(output)
password = envvars.get_envvar("CONTAINER_REGISTRY_PASSWORD")
assert password is not None

def test_container_registry_server_key_missing_sys_exit():
with pytest.raises(SystemExit):
output = Output()
envvars = EnvVars(output)
envvars.get_envvar("CONTAINER_REGISTRY_SERVERUNITTEST", required=True)

@pytest.fixture
def setup_test_env(request):
output = Output()
envvars = EnvVars(output)
envvars.set_envvar("CONTAINER_REGISTRY_SERVERUNITTEST", '')

def clean():
os.environ.pop("CONTAINER_REGISTRY_SERVERUNITTEST")
request.addfinalizer(clean)

return

def test_container_registry_server_value_missing_sys_exit(setup_test_env):
with pytest.raises(SystemExit):
output = Output()
envvars = EnvVars(output)
envvars.get_envvar("CONTAINER_REGISTRY_SERVERUNITTEST", required=True)

def test_unique_container_registry_server_tokens():
unique = set()
length_container_registry_server = len('container_registry_server')
is_unique = True
output = Output()
envvars = EnvVars(output)
envvars.load()
for key in os.environ:
key = key.lower()
if key.startswith('container_registry_server'):
token = key[length_container_registry_server:]
if token not in unique:
unique.add(token)
else:
is_unique = False
assert is_unique

def test_unique_container_registry_username_tokens():
unique = set()
length_container_registry_username = len('container_registry_username')
is_unique = True
output = Output()
envvars = EnvVars(output)
envvars.load()
for key in os.environ:
key = key.lower()
if key.startswith('container_registry_username'):
token = key[length_container_registry_username:]
if token not in unique:
unique.add(token)
else:
is_unique = False
assert is_unique

def test_unique_container_registry_password_tokens():
unique = set()
length_container_registry_password = len('container_registry_password')
is_unique = True
output = Output()
envvars = EnvVars(output)
envvars.load()
for key in os.environ:
key = key.lower()
if key.startswith('container_registry_password'):
token = key[length_container_registry_password:]
if token not in unique:
unique.add(token)
else:
is_unique = False
assert is_unique

def test_container_registry_map_has_val():
output = Output()
envvars = EnvVars(output)
envvars.load()
result = envvars.verify_envvar_has_val("CONTAINER_REGISTRY_MAP", envvars.CONTAINER_REGISTRY_MAP)
assert not result

def test_additional_container_registry_server_has_val():
output = Output()
envvars = EnvVars(output)
envvars.load()
if len(envvars.CONTAINER_REGISTRY_MAP) > 1:
keys = envvars.CONTAINER_REGISTRY_MAP.keys()
for key in keys:
if key != '':
token = key
assert envvars.CONTAINER_REGISTRY_MAP[token].server is not None

def test_additional_container_registry_username_has_val():
output = Output()
envvars = EnvVars(output)
envvars.load()
if len(envvars.CONTAINER_REGISTRY_MAP) > 1:
keys = envvars.CONTAINER_REGISTRY_MAP.keys()
for key in keys:
if key != '':
token = key
assert envvars.CONTAINER_REGISTRY_MAP[token].username is not None

def test_additional_container_registry_password_has_val():
output = Output()
envvars = EnvVars(output)
envvars.load()
if len(envvars.CONTAINER_REGISTRY_MAP) > 1:
keys = envvars.CONTAINER_REGISTRY_MAP.keys()
for key in keys:
if key != '':
token = key
assert envvars.CONTAINER_REGISTRY_MAP[token].password is not None

0 comments on commit 271e28e

Please sign in to comment.