def test_fetch_fails_without_working_dir(): settings = app_translator.Settings( app_translator.ContainerDefaults( image="busybox", working_dir=None, ), app_secret_mapping=DummyAppSecretMapping(), ) fields = {"id": "app", "fetch": [{"uri": "http://foobar.baz/0xdeadbeef"}]} with pytest.raises(app_translator.AdditionalFlagNeeded, match=r'.*?--container-working-dir.*?'): app_translator.translate_app(fields, settings)
def test_resource_requests_and_limits(app_resource_fields, expected_k8s_resources): settings = new_settings() app = {"id": "app"} app.update(app_resource_fields) translated = app_translator.translate_app(app, settings) resources = translated.deployment['spec']['template']['spec']['containers'][0]['resources'] assert resources == expected_k8s_resources
def test_second_health_check_dropped_warning(): """ Tests that only the first health check is converted into a liveness probe, and a warning is emitted for all the other health checks. """ app = { "id": "/twice-healthy", "healthChecks": [ { "protocol": "COMMAND", "command": { "value": "exit 0" } }, { "protocol": "COMMAND", "command": { "value": "sleep 1" } }, ], } translated = app_translator.translate_app(app, EMPTY_SETTINGS) assert any("dropped health check" in w.lower() for w in translated.warnings) assert translated.deployment['spec']['template']['spec']['containers'][0]['livenessProbe']['exec']['command'] ==\ ["/bin/sh", "-c", "exit 0"]
def test_artifact_output(): k8s_probe_statefulset = app_translator.translate_app( probes_app_definition, new_settings()).deployment artifacts = stateful_copy.stateful_migrate_artifacts( probes_app_definition, k8s_probe_statefulset) config_lines = artifacts["config.sh"].split("\n") assert ('APP_ID="/stateful-healthy"' in config_lines) assert ('MOUNT_NAMES=("test-data")' in config_lines) assert ('K8S_APP_ID="stateful-healthy"' in config_lines) sleeper_patch = yaml.safe_load(artifacts["k8s-sleeper-command-patch.yaml"]) assert sleeper_patch['spec']['template']['spec']['containers'][0][ "livenessProbe"] == None original_patch = yaml.safe_load( artifacts["k8s-original-command-patch.yaml"]) assert original_patch['spec']['template']['spec']['containers'][0][ "livenessProbe"]['exec']['command'] == ['/bin/sh', '-c', 'exit 0'] app_sleeper_patch = json.loads( artifacts["dcos-sleeper-command-patch.json"]) assert app_sleeper_patch == { 'command': 'sleep 604800', 'checks': [], 'healthChecks': [] }
def test_happy_path_sleeper(): settings = new_settings() hello_app = app_translator.load("tests/test_marathon/test_app_transtalor/resources/simple-command-app.json")[0] translated = app_translator.translate_app(hello_app, settings) assert (translated.deployment['kind'] == "Deployment") assert (translated.deployment['metadata']['name'] == "sleep") assert (translated.deployment['metadata']['labels'] == {'app': 'sleep'}) assert (translated.deployment['spec']['replicas'] == 10) container = translated.deployment['spec']['template']['spec']['containers'][0] assert (container['command'] == ['/bin/sh', '-c', 'sleep 3600']) assert (container['image'] == 'busybox') assert (container['resources'] == { 'requests': { 'cpu': 0.01, 'memory': '64Mi' }, 'limits': { 'cpu': 0.01, 'memory': '64Mi' } }) assert (container['name'] == 'main')
def test_env_secret(): app = { "id": "foobarify", "env": { "FOO": { "secret": "bar" } }, "secrets": { "bar": { "source": "/deadbeef/baz" } }, } settings = app_translator.Settings( app_translator.ContainerDefaults(image="lazybox", working_dir=None), app_secret_mapping=TrackingAppSecretMapping(app['id'], app['secrets']), ) translated = app_translator.translate_app(app, settings) env = translated.deployment['spec']['template']['spec']['containers'][0]['env'] assert env == [{ 'name': 'FOO', 'valueFrom': { 'secretKeyRef': { 'name': 'marathonsecret-foobarify', 'key': 'deadbeef.baz', } } }]
def translate(path: str, settings: Settings, selected_app_id: str) -> None: apps = load(path) for app in apps: app_id = app.get('id', "(NO ID)") if selected_app_id and selected_app_id != app_id: continue app_label = marathon_app_id_to_k8s_app_id(app_id) dcos_package_name = app.get('labels', {}).get("DCOS_PACKAGE_NAME") if dcos_package_name is None: translated = translate_app(app, settings) print("# Converted from an app {}".format(app_id)) print("\n\n".join([''] + translated.warnings).replace( '\n', '\n# ')) print(yaml.safe_dump(translated.deployment)) print("---") k8s_app_id: str = translated.deployment['metadata']['labels'][ 'app'] result, warnings = translate_service(k8s_app_id, app) if result: print("# Converted from an app {}".format(app_id)) print("\n\n".join([''] + list(warnings)).replace('\n', '\n# ')) print(yaml.safe_dump(result)) else: print( '# Skipped an app {}: it is installed from a DCOS package "{}"' .format(app_id, dcos_package_name)) print('---')
def test_command_health_check_with_all_fields_set(): app = { "id": "/healthy", "healthChecks": [{ "protocol": "COMMAND", "command": { "value": "exit 0" }, "gracePeriodSeconds": 123, "intervalSeconds": 45, "timeoutSeconds": 99, "maxConsecutiveFailures": 333 }], } translated = app_translator.translate_app(app, EMPTY_SETTINGS) container = translated.deployment['spec']['template']['spec'][ 'containers'][0] assert 'livenessProbe' in container assert container['livenessProbe'] == { "failureThreshold": 333, "timeoutSeconds": 99, "periodSeconds": 45, "initialDelaySeconds": 123, "exec": { "command": ["/bin/sh", "-c", "exit 0"] } }
def test_translate_network_ports_env_vars(): app = { "id": "nginx", "instances": 2, "container": { "type": "DOCKER", "docker": { "image": "nginx:1.14.2" }, "portMappings": [{ "name": "http", "hostPort": 0, "containerPort": 80, "labels": { "VIP_0": "nginx:80" } }] } } settings = new_settings() translated = app_translator.translate_app(app, settings) container = translated.deployment['spec']['template']['spec']['containers'][0] resulting_env = __entries_list_to_dict(container['env']) assert (resulting_env == {'HOST': '0.0.0.0', 'PORTS': '80', 'PORT0': '80', 'PORT_HTTP': '80'})
def configure(conf: ConfigureArgs, translate_settings: Settings) -> None: # pull the marathon app original_marathon_app = get_dcos_app(conf.app_id) k8s_translate_result = app_translator.translate_app( original_marathon_app, translate_settings) configure_stateful_migrate(original_marathon_app, k8s_translate_result.deployment)
def test_translates_args(): settings = new_settings() hello_app = app_translator.load("tests/test_marathon/test_app_transtalor/resources/container-args-app.json")[0] translated = app_translator.translate_app(hello_app, settings) assert (translated.deployment['kind'] == "Deployment") assert (translated.deployment['metadata']['name'] == "args") container = translated.deployment['spec']['template']['spec']['containers'][0] assert (not "command" in container) assert (container['args'] == ["args", "passed", "to", "entrypoint"])
def test_upgrade_strategy(): settings = new_settings() app = {"id": "app", "upgradeStrategy": {"minimumHealthCapacity": 0.6250, "maximumOverCapacity": 0.455}} translated = app_translator.translate_app(app, settings) assert translated.deployment['spec']['strategy'] == { "type": "RollingUpdate", "rollingUpdate": { "maxUnavailable": "37%", "maxSurge": "45%" } }
def test_network_health_check(health_check, expected_action_key, expected_action): app = {"id": "/server", "healthChecks": [health_check]} translated = app_translator.translate_app(app, EMPTY_SETTINGS) probe = translated.deployment['spec']['template']['spec']['containers'][0][ 'livenessProbe'] assert probe[expected_action_key] == expected_action if not health_check['protocol'].startswith('MESOS'): assert any(["check that the K8s probe is using the correct port" in w\ for w in translated.warnings])
def test_happy_path_stateful(): settings = new_settings() hello_app = app_translator.load("tests/test_marathon/test_app_transtalor/resources/stateful-app.json")[0] translated = app_translator.translate_app(hello_app, settings) assert (translated.deployment['kind'] == 'StatefulSet') [mount] = translated.deployment['spec']['template']['spec']['containers'][0]['volumeMounts'] [vc_template] = translated.deployment['spec']['volumeClaimTemplates'] assert mount['name'] == "data" assert vc_template['spec']['accessModes'] == ['ReadWriteOnce'] assert vc_template['spec']['resources']['requests']['storage'] == '512Mi' assert not 'strategy' in translated.deployment['spec'].keys()
def test_unreachable_strategy(): settings = new_settings() app = {"id": "app", "unreachableStrategy": {"inactiveAfterSeconds": 123, "expungeAfterSeconds": 456}} translated = app_translator.translate_app(app, settings) tolerations = translated.deployment['spec']['template']['spec']['tolerations'] # This test implicitly relies on the fact that "unreachableStartegy" # is the only thing that can result in tolerations being set. assert tolerations == [{ 'effect': 'NoExecute', 'key': 'node.kubernetes.io/unreachable', 'operator': 'Exists', 'tolerationSeconds': 456 }] assert any("inactiveAfterSeconds" in w and "123" in w for w in translated.warnings)
def test_port_from_port_definitions(): app = { "id": "/server", "healthChecks": [{ "protocol": "MESOS_TCP", "portIndex": 2 }], "portDefinitions": [{ "port": 443 }, { "port": 123 }, { "port": 456 }] } translated = app_translator.translate_app(app, EMPTY_SETTINGS) probe = translated.deployment['spec']['template']['spec']['containers'][0][ 'livenessProbe'] assert probe['tcpSocket'] == {'port': 456}
def test_generated_fetch_layout(): """ Tests generation of volume and init container for `fetch`. NOTE: This neither tests the generation of the fetch script that runs in the init container, nor ensures that this script itself actually works! """ settings = app_translator.Settings( app_translator.ContainerDefaults( image="busybox", working_dir="/fetched_artifacts", ), app_secret_mapping=DummyAppSecretMapping(), ) fields = {"id": "app", "fetch": [{"uri": "http://foobar.baz/0xdeadbeef"}]} translated = app_translator.translate_app(fields, settings) template_spec = translated.deployment['spec']['template']['spec'] # The volume for fetching artifacts should be an empty dir. fetch_volume_name = template_spec['volumes'][0]['name'] assert template_spec['volumes'] == [{ 'emptyDir': {}, 'name': fetch_volume_name }] # Ensure that the fetch volume will be mounted into the main container as a working dir. assert template_spec['containers'][0]['volumeMounts'] ==\ [{'name': fetch_volume_name, 'mountPath': settings.container_defaults.working_dir}] # The fetcher itself should be implemented as a SINGLE init container. assert len(template_spec['initContainers']) == 1 fetch_container = template_spec['initContainers'][0] assert fetch_container['volumeMounts'] == \ [{'name': fetch_volume_name, 'mountPath': fetch_container['workingDir']}] # TODO (asekretenko): Write an integration test for the fetch command! assert isinstance(fetch_container['command'], list)
def test_command_ready_with_all_fields_set(): app = { "id": "/ready", "readinessChecks": [{ "name": "readinessCheck", "protocol": "HTTPS", "path": "/ready", "portName": "main", "intervalSeconds": 30, "timeoutSeconds": 20, "httpStatusCodesForReady": [200], "preserveLastResponse": False }], "container": { "portMappings": [{ "containerPort": 3000, "hostPort": 0, "labels": {}, "name": "main", "protocol": "tcp", "servicePort": 10000 }] } } translated = app_translator.translate_app(app, EMPTY_SETTINGS) container = translated.deployment['spec']['template']['spec']['containers'][0] assert 'readinessProbe' in container assert container['readinessProbe'] == { "timeoutSeconds": 20, "periodSeconds": 30, "httpGet": { "path": "/ready", "port": "main", "scheme": "HTTPS" } }
def test_port_from_port_mappings(): app = { "id": "/server", "healthChecks": [{ "protocol": "MESOS_TCP", "portIndex": 1 }], "container": { "portMappings": [{ "hostPort": 119, "containerPort": 80 }, { "hostPort": 332, "containerPort": 443 }] } } translated = app_translator.translate_app(app, EMPTY_SETTINGS) probe = translated.deployment['spec']['template']['spec']['containers'][0][ 'livenessProbe'] assert probe['tcpSocket'] == {'port': 443}
def test_port_zero_in_health_check(): # TODO (asekretenko): This test should be adjusted when we introduce # a uniform support for port 0 throughout the app translation. app = { "id": "/server", "healthChecks": [{ "protocol": "MESOS_TCP", "portIndex": 1 }], "container": { "portMappings": [{ "hostPort": 119, "containerPort": 80 }, { "hostPort": 332, "containerPort": 0 }] } } translated = app_translator.translate_app(app, EMPTY_SETTINGS) assert any(['using port 0' in w for w in translated.warnings])
def test_image_in_app_makes_image_default_unnecessary(): settings = new_settings() app = {"id": "app", "container": {"docker": {"image": "busybox"}}} translated = app_translator.translate_app(app, settings) assert translated.deployment['spec']['template']['spec']['containers'][0]['image'] == "busybox"
def test_constraints(): settings = new_settings() app = { "id": "/foo/barify", "constraints": [ ["backpack", "UNIQUE"], ["hostname", "MAX_PER", 2], ["@hostname", "IS", "private-123.dcos-1.example.com"], ["@region", "LIKE", "antarctic"], ["@zone", "LIKE", "antarctic1024"], ["badluck", "GROUP_BY", 1], ["hostname", "UNIQUE"], ] } translated = app_translator.translate_app(app, settings) node_selector = translated.deployment['spec']['template']['spec']['nodeSelector'] topology_spread = translated.deployment['spec']['template']['spec']['topologySpreadConstraints'] affinity = translated.deployment['spec']['template']['spec']['affinity'] assert node_selector == { "dcos.io/former-dcos-hostname": "private-123-dcos-1-example-com", "topology.kubernetes.io/region": "antarctic", "topology.kubernetes.io/zone": "antarctic1024", } labels = translated.deployment['spec']['template']['metadata']['labels'] assert topology_spread == [ { 'labelSelector': { 'matchLabels': labels }, 'maxSkew': 1, 'topologyKey': 'backpack', 'whenUnsatisfiable': 'DoNotSchedule' }, { 'labelSelector': { 'matchLabels': labels }, 'maxSkew': 1, 'topologyKey': 'kubernetes.io/hostname', 'whenUnsatisfiable': 'DoNotSchedule' }, ] assert any('GROUP_BY' in w for w in translated.warnings) assert translated.required_node_labels == { 'backpack', 'kubernetes.io/hostname', 'dcos.io/former-dcos-hostname', 'topology.kubernetes.io/region', 'topology.kubernetes.io/zone' } assert affinity == { 'podAntiAffinity': { 'requiredDuringSchedulingIgnoredDuringExecution': [{ 'labelSelector': { 'matchLabels': labels }, 'topologyKey': 'kubernetes.io/hostname', }], } }
def test_task_kill_grace_period_seconds(): settings = new_settings() app = {"id": "app", "taskKillGracePeriodSeconds": 123} translated = app_translator.translate_app(app, settings) assert translated.deployment['spec']['template']['spec']['terminationGracePeriodSeconds'] == 123
def test_host_path_volumes(): settings = app_translator.Settings( app_translator.ContainerDefaults( image=None, working_dir=None, ), app_secret_mapping=DummyAppSecretMapping(), ) app = { "id": "app", "container": { "docker": { "image": "python" }, "volumes": [ { "containerPath": "/rw", "hostPath": "/volumes/rw", "mode": "RW" }, { "containerPath": "/ro", "hostPath": "/volumes/ro", "mode": "RO" }, { "containerPath": "/foo", "hostPath": "relative_to_sandbox", "mode": "RO" }, { "containerPath": "foo", # we cannot fully translate this persistent volume because there is no mapping into the container "persistent": { "size": 1024, "type": "root" }, "mode": "RO" }, ], }, } translated = app_translator.translate_app(app, settings) template_spec = translated.deployment['spec']['template']['spec'] volumes = sorted(template_spec['volumes'], key=lambda v: v['name']) assert volumes == [{ 'name': 'volume-0', 'hostPath': { 'path': '/volumes/rw' } }, { 'name': 'volume-1', 'hostPath': { 'path': '/volumes/ro' } }] mounts = sorted(template_spec['containers'][0]['volumeMounts'], key=lambda v: v['name']) assert mounts == [ { "name": "volume-0", "mountPath": "/rw", "readOnly": False }, { "name": "volume-1", "mountPath": "/ro", "readOnly": True }, ] # For now, we do not translate volumes with a "hostPath" relative to # Mesos sandbox (which typically are a part of a persistent volume setup). # Persistent volumes themselves aren't translated either. volume_warnings = [ w for w in translated.warnings if "Cannot translate a volume" in w ] assert len(volume_warnings) == 2 assert any("relative_to_sandbox" in w for w in volume_warnings) assert any("persistent" in w for w in volume_warnings)
def test_host_path_volume_with_fetch(): """ Tests that emitting a volume for fetch artifacts does not interfere with a hostPath volume translation. """ settings = app_translator.Settings( app_translator.ContainerDefaults( image=None, working_dir="/sandbox", ), app_secret_mapping=DummyAppSecretMapping(), ) app = { "id": "app", "container": { "docker": { "image": "python" }, "volumes": [ { "containerPath": "/ro", "hostPath": "/volumes/ro", "mode": "RO" }, ], }, "fetch": [{ "uri": "http://foobar.baz/0xdeadbeef" }], } translated = app_translator.translate_app(app, settings) template_spec = translated.deployment['spec']['template']['spec'] volumes = sorted(template_spec['volumes'], key=lambda v: v['name']) assert volumes == [ { "name": "fetch-artifacts", 'emptyDir': {} }, { "name": "volume-0", 'hostPath': { "path": "/volumes/ro" } }, ] mounts = sorted(template_spec['containers'][0]['volumeMounts'], key=lambda v: v['name']) assert mounts == [ { "name": "fetch-artifacts", "mountPath": "/sandbox" }, { "name": "volume-0", "mountPath": "/ro", "readOnly": True }, ]
def test_multiple_secret_volumes(): """ Tests a secret volume. One of the main things covered is non-interference between generating secret and host path volumes. """ app = { "id": "foobarify", "container": { "docker": { "image": "python" }, "volumes": [ { "containerPath": "/etc/foo", "secret": "foo-secret" }, { "containerPath": "/run/bar", "secret": "bar-secret" }, { "containerPath": "/var/baz", "secret": "baz-secret" }, ], }, "secrets": { "foo-secret": { "source": "foo" }, "bar-secret": { "source": "bar" }, "baz-secret": { "source": "baz" }, }, } settings = app_translator.Settings( app_translator.ContainerDefaults(image=None, working_dir=None), app_secret_mapping=TrackingAppSecretMapping(app['id'], app['secrets']), ) translated = app_translator.translate_app(app, settings) template_spec = translated.deployment['spec']['template']['spec'] volumes = sorted(template_spec['volumes'], key=lambda v: v['name']) assert volumes == [{ "name": "secrets-marathonsecret-foobarify", "secret": { "secretName": "marathonsecret-foobarify", "items": [ { "key": "foo", "path": "foo", "mode": 0o777 }, { "key": "bar", "path": "bar", "mode": 0o777 }, { "key": "baz", "path": "baz", "mode": 0o777 }, ], } }] name = volumes[0]['name'] mounts = sorted(template_spec['containers'][0]['volumeMounts'], key=lambda v: v['name']) assert mounts == [ { "name": name, "mountPath": "/etc/foo", "subPath": "foo", "readOnly": True }, { "name": name, "mountPath": "/run/bar", "subPath": "bar", "readOnly": True }, { "name": name, "mountPath": "/var/baz", "subPath": "baz", "readOnly": True }, ]
def test_image_should_be_present_somewhere(): settings = new_settings(image=None) app = {"id": "app", "command": "sleep 300"} with pytest.raises(app_translator.AdditionalFlagNeeded, match=".*image.*"): app_translator.translate_app(app, settings)