예제 #1
0
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)
예제 #2
0
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
예제 #3
0
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"]
예제 #4
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': []
    }
예제 #5
0
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')
예제 #6
0
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',
            }
        }
    }]
예제 #7
0
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('---')
예제 #8
0
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"]
        }
    }
예제 #9
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)
예제 #11
0
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"])
예제 #12
0
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%"
        }
    }
예제 #13
0
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()
예제 #15
0
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)
예제 #16
0
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}
예제 #17
0
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)
예제 #18
0
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"
        }
    }
예제 #19
0
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}
예제 #20
0
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])
예제 #21
0
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"
예제 #22
0
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',
            }],
        }
    }
예제 #23
0
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
        },
    ]
예제 #27
0
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)