Пример #1
0
def test_update_service_tokens(tmp_path: Path, config: Config,
                               mock_kubernetes: MockKubernetesApi) -> None:
    asyncio.run(initialize_database(config, reset=True))
    asyncio.run(
        mock_kubernetes.create_namespaced_custom_object(
            "gafaelfawr.lsst.io",
            "v1alpha1",
            "mobu",
            "gafaelfawrservicetokens",
            {
                "apiVersion": "gafaelfawr.lsst.io/v1alpha1",
                "kind": "GafaelfawrServiceToken",
                "metadata": {
                    "name": "gafaelfawr-secret",
                    "namespace": "mobu",
                    "generation": 1,
                },
                "spec": {
                    "service": "mobu",
                    "scopes": ["admin:token"],
                },
            },
        ))

    runner = CliRunner()
    result = runner.invoke(main, ["update-service-tokens"])

    assert result.exit_code == 0
    assert mock_kubernetes.get_all_objects_for_test("Secret")
Пример #2
0
async def test_route_retire(
    client: AsyncClient,
    dossier: Dossier,
    mock_kubernetes: MockKubernetesApi,
    mock_kubernetes_watch: MockKubernetesWatch,
) -> None:
    """Retire is configured to not have any containers."""
    r = await client.post("/moneypenny/users", json=dossier.dict())
    assert r.status_code == 303
    await wait_for_completion(client, dossier.username, mock_kubernetes,
                              mock_kubernetes_watch)

    r = await client.get(f"/moneypenny/users/{dossier.username}")
    assert r.status_code == 200
    data = r.json()
    assert data["status"] == "active"

    r = await client.post(f"/moneypenny/users/{dossier.username}/retire",
                          json=dossier.dict())
    assert r.status_code == 204

    r = await client.get(f"/moneypenny/users/{dossier.username}")
    assert r.status_code == 404

    assert mock_kubernetes.get_all_objects_for_test("ConfigMap") == []
    assert mock_kubernetes.get_all_objects_for_test("Pod") == []
Пример #3
0
async def assert_kubernetes_secrets_are_correct(
    factory: ComponentFactory, mock: MockKubernetesApi, is_fresh: bool = True
) -> None:
    token_service = factory.create_token_service()

    # Get all of the GafaelfawrServiceToken custom objects.
    service_tokens = mock.get_all_objects_for_test("GafaelfawrServiceToken")

    # Calculate the expected secrets.
    expected = [
        V1Secret(
            api_version="v1",
            kind="Secret",
            data={"token": ANY},
            metadata=V1ObjectMeta(
                name=t["metadata"]["name"],
                namespace=t["metadata"]["namespace"],
                annotations=t["metadata"].get("annotations", {}),
                labels=t["metadata"].get("labels", {}),
                owner_references=[
                    V1OwnerReference(
                        api_version="gafaelfawr.lsst.io/v1alpha1",
                        block_owner_deletion=True,
                        controller=True,
                        kind="GafaelfawrServiceToken",
                        name=t["metadata"]["name"],
                        uid=t["metadata"]["uid"],
                    ),
                ],
            ),
            type="Opaque",
        )
        for t in service_tokens
    ]
    expected = sorted(
        expected, key=lambda o: (o.metadata.namespace, o.metadata.name)
    )
    assert mock.get_all_objects_for_test("Secret") == expected

    # Now check that every token in those secrets is correct.
    for service_token in service_tokens:
        name = service_token["metadata"]["name"]
        namespace = service_token["metadata"]["namespace"]
        secret = await mock.read_namespaced_secret(name, namespace)
        data = await token_data_from_secret(token_service, secret)
        assert data == TokenData(
            token=data.token,
            username=service_token["spec"]["service"],
            token_type=TokenType.service,
            scopes=service_token["spec"]["scopes"],
            created=data.created,
            expires=None,
            name=None,
            uid=None,
            groups=None,
        )
        if is_fresh:
            now = current_datetime()
            assert now - timedelta(seconds=5) <= data.created <= now
Пример #4
0
async def test_mock(mock_kubernetes: MockKubernetesApi) -> None:
    custom: Dict[str, Any] = {
        "apiVersion": "gafaelfawr.lsst.io/v1alpha1",
        "kind": "GafaelfawrServiceToken",
        "metadata": {
            "name": "gafaelfawr-secret",
            "namespace": "mobu",
            "generation": 1,
        },
        "spec": {
            "service": "mobu",
            "scopes": ["admin:token"],
        },
    }
    secret = V1Secret(
        api_version="v1",
        kind="Secret",
        data={"token": "bogus"},
        metadata=V1ObjectMeta(name="gafaelfawr-secret", namespace="mobu"),
        type="Opaque",
    )
    await mock_kubernetes.create_namespaced_custom_object(
        "gafaelfawr.lsst.io",
        "v1alpha1",
        custom["metadata"]["namespace"],
        "gafaelfawrservicetokens",
        custom,
    )
    await mock_kubernetes.create_namespaced_secret(secret.metadata.namespace,
                                                   secret)

    assert await mock_kubernetes.list_cluster_custom_object(
        "gafaelfawr.lsst.io", "v1alpha1", "gafaelfawrservicetokens") == {
            "items": [{
                **custom, "metadata": {
                    **custom["metadata"], "uid": ANY
                }
            }]
        }
    assert mock_kubernetes.get_all_objects_for_test("Secret") == [secret]

    def error(method: str, *args: Any) -> None:
        assert method == "replace_namespaced_custom_object"
        raise ValueError("some exception")

    mock_kubernetes.error_callback = error
    with pytest.raises(ValueError) as excinfo:
        await mock_kubernetes.replace_namespaced_custom_object(
            "gafaelfawr.lsst.io",
            "v1alpha1",
            custom["metadata"]["namespace"],
            "gafaelfawrservicetokens",
            "gafaelfawr-secret",
            custom,
        )
    assert str(excinfo.value) == "some exception"
Пример #5
0
def test_update_service_tokens_error(
    tmp_path: Path,
    config: Config,
    mock_kubernetes: MockKubernetesApi,
    caplog: LogCaptureFixture,
) -> None:
    asyncio.run(initialize_database(config, reset=True))
    caplog.clear()

    def error_callback(method: str, *args: Any) -> None:
        if method == "list_cluster_custom_object":
            raise ApiException(status=500, reason="Some error")

    mock_kubernetes.error_callback = error_callback
    runner = CliRunner()
    result = runner.invoke(main, ["update-service-tokens"])

    assert result.exit_code == 1
    assert parse_log(caplog) == [
        {
            "event": "Unable to list GafaelfawrServiceToken objects",
            "error": "Kubernetes API error: (500)\nReason: Some error\n",
            "severity": "error",
        },
        {
            "error": "Kubernetes API error: (500)\nReason: Some error\n",
            "event": "Failed to update service token secrets",
            "severity": "error",
        },
    ]
Пример #6
0
async def test_errors(
    client: AsyncClient,
    dossier: Dossier,
    mock_kubernetes: MockKubernetesApi,
    mock_kubernetes_watch: MockKubernetesWatch,
) -> None:
    """Test resource creation when Kubernetes calls fail."""
    count = 0

    def error_callback(method: str, *args: Any, **kwargs: Any) -> None:
        nonlocal count
        count += 1
        if method == "create_namespaced_config_map" and count <= 1:
            raise ApiException(status=409, reason="Resoure conflict")
        elif method == "create_namespaced_config_map" and count <= 6:
            raise ApiException(status=500, reason="Other error")
        elif method == "create_namespaced_pod" and count <= 3:
            raise ApiException(status=409, reason="Resource conflict")
        elif method == "create_namespaced_pod" and count <= 4:
            raise ApiException(status=500, reason="Other error")

    mock_kubernetes.error_callback = error_callback

    r = await client.post("/moneypenny/users", json=dossier.dict())
    assert r.status_code == 303

    r = await client.get(f"/moneypenny/users/{dossier.username}")
    assert r.status_code == 200
    data = r.json()
    assert data["status"] == "commissioning"

    await wait_for_completion(client, dossier.username, mock_kubernetes,
                              mock_kubernetes_watch)
Пример #7
0
async def test_errors_replace_read(
    factory: ComponentFactory, mock_kubernetes: MockKubernetesApi
) -> None:
    await create_test_service_tokens(mock_kubernetes)
    kubernetes_service = factory.create_kubernetes_service(MagicMock())
    token_service = factory.create_token_service()

    # Create a secret that should exist but has an invalid token.
    secret = V1Secret(
        api_version="v1",
        data={"token": token_as_base64(Token())},
        metadata=V1ObjectMeta(name="gafaelfawr-secret", namespace="mobu"),
        type="Opaque",
    )
    await mock_kubernetes.create_namespaced_secret("mobu", secret)

    # Simulate some errors.  The callback function takes the operation and the
    # secret name.
    def error_callback_replace(method: str, *args: Any) -> None:
        if method in ("replace_namespaced_secret"):
            raise ApiException(status=500, reason="Some error")

    mock_kubernetes.error_callback = error_callback_replace

    # Now run the synchronization.  The secret should be left unchanged, but
    # we should still create the missing nublado2 secret.
    await kubernetes_service.update_service_tokens()
    objects = mock_kubernetes.get_all_objects_for_test("Secret")
    assert secret in objects
    good_secret = await mock_kubernetes.read_namespaced_secret(
        "gafaelfawr", "nublado2"
    )
    assert await token_data_from_secret(token_service, good_secret)

    # We should have also updated the status of the parent custom object.
    service_token = await mock_kubernetes.get_namespaced_custom_object(
        "gafaelfawr.lsst.io",
        "v1alpha1",
        "mobu",
        "gafaelfawrservicetokens",
        "gafaelfawr-secret",
    )
    assert service_token["status"]["conditions"] == [
        {
            "lastTransitionTime": ANY,
            "message": "Kubernetes API error: (500)\nReason: Some error\n",
            "observedGeneration": 1,
            "reason": StatusReason.Failed.value,
            "status": "False",
            "type": "SecretCreated",
        }
    ]

    # Try again, but simulating an error in retrieving a secret.
    def error_callback_read(method: str, *args: Any) -> None:
        if method == "read_namespaced_secret":
            raise ApiException(status=500, reason="Some error")

    mock_kubernetes.error_callback = error_callback_read

    # Now run the synchronization.  As before, the secret should be left
    # unchanged, and the good secret should also be left unchanged.
    await kubernetes_service.update_service_tokens()
    objects = mock_kubernetes.get_all_objects_for_test("Secret")
    assert secret in objects
Пример #8
0
async def test_create(
    factory: ComponentFactory,
    mock_kubernetes: MockKubernetesApi,
    caplog: LogCaptureFixture,
) -> None:
    await create_test_service_tokens(mock_kubernetes)
    kubernetes_service = factory.create_kubernetes_service(MagicMock())
    await kubernetes_service.update_service_tokens()
    await assert_kubernetes_secrets_are_correct(factory, mock_kubernetes)

    service_token = await mock_kubernetes.get_namespaced_custom_object(
        "gafaelfawr.lsst.io",
        "v1alpha1",
        "mobu",
        "gafaelfawrservicetokens",
        "gafaelfawr-secret",
    )
    assert service_token["status"]["conditions"] == [
        {
            "lastTransitionTime": ANY,
            "message": "Secret was created",
            "observedGeneration": 1,
            "reason": StatusReason.Created.value,
            "status": "True",
            "type": "SecretCreated",
        }
    ]
    service_token = await mock_kubernetes.get_namespaced_custom_object(
        "gafaelfawr.lsst.io",
        "v1alpha1",
        "nublado2",
        "gafaelfawrservicetokens",
        "gafaelfawr",
    )
    assert service_token["status"]["conditions"] == [
        {
            "lastTransitionTime": ANY,
            "message": "Secret was created",
            "observedGeneration": 45,
            "reason": StatusReason.Created.value,
            "status": "True",
            "type": "SecretCreated",
        }
    ]

    assert parse_log(caplog) == [
        {
            "event": "Created new service token",
            "key": ANY,
            "severity": "info",
            "token_scope": "admin:token",
            "token_username": "******",
        },
        {
            "event": "Created mobu/gafaelfawr-secret secret",
            "scopes": ["admin:token"],
            "severity": "info",
            "service": "mobu",
        },
        {
            "event": "Created new service token",
            "key": ANY,
            "severity": "info",
            "token_scope": "",
            "token_username": "******",
        },
        {
            "event": "Created nublado2/gafaelfawr secret",
            "scopes": [],
            "severity": "info",
            "service": "nublado-hub",
        },
    ]

    # Running creation again should not change anything.
    caplog.clear()
    objects = mock_kubernetes.get_all_objects_for_test("Secret")
    await kubernetes_service.update_service_tokens()
    assert mock_kubernetes.get_all_objects_for_test("Secret") == objects
    assert caplog.record_tuples == []
Пример #9
0
async def test_route_commission(
    client: AsyncClient,
    dossier: Dossier,
    mock_kubernetes: MockKubernetesApi,
    mock_kubernetes_watch: MockKubernetesWatch,
) -> None:
    r = await client.post("/moneypenny/users", json=dossier.dict())
    assert r.status_code == 303
    assert r.headers["Location"] == url_for(f"users/{dossier.username}")

    r = await client.get(f"/moneypenny/users/{dossier.username}")
    assert r.status_code == 200
    data = r.json()
    assert data == {
        "username": dossier.username,
        "status": "commissioning",
        "last_changed": ANY,
        "uid": dossier.uid,
        "groups": [g.dict() for g in dossier.groups],
    }

    # Requesting the exact same thing again even though it's not complete is
    # fine and produces the same redirect.
    r = await client.post("/moneypenny/users", json=dossier.dict())
    assert r.status_code == 303
    assert r.headers["Location"] == url_for(f"users/{dossier.username}")

    assert mock_kubernetes.get_all_objects_for_test("ConfigMap") == [
        V1ConfigMap(
            metadata=V1ObjectMeta(
                name=f"{dossier.username}-cm",
                namespace="default",
                owner_references=[
                    V1OwnerReference(
                        api_version="v1",
                        kind="Pod",
                        name="moneypenny-78547dcf97-9xqq8",
                        uid="00386592-214f-40c5-88e1-b9657d53a7c6",
                    )
                ],
            ),
            data={
                "dossier.json":
                json.dumps(dossier.dict(), sort_keys=True, indent=4)
            },
        )
    ]
    assert mock_kubernetes.get_all_objects_for_test("Pod") == [
        V1Pod(
            metadata=V1ObjectMeta(
                name=f"{dossier.username}-pod",
                namespace="default",
                owner_references=[
                    V1OwnerReference(
                        api_version="v1",
                        kind="Pod",
                        name="moneypenny-78547dcf97-9xqq8",
                        uid="00386592-214f-40c5-88e1-b9657d53a7c6",
                    )
                ],
            ),
            spec=V1PodSpec(
                automount_service_account_token=False,
                containers=[{
                    "name":
                    "farthing",
                    "image":
                    "lsstsqre/farthing",
                    "securityContext": {
                        "runAsUser": 1000,
                        "runAsNonRootUser": True,
                        "allowPrivilegeEscalation": False,
                    },
                    "volumeMounts": [
                        {
                            "mountPath": "/homedirs",
                            "name": "homedirs",
                        },
                        {
                            "mountPath": "/opt/dossier",
                            "name": f"dossier-{dossier.username}-vol",
                            "readOnly": True,
                        },
                    ],
                }],
                image_pull_secrets=[],
                init_containers=[],
                restart_policy="OnFailure",
                security_context=V1PodSecurityContext(run_as_group=1000,
                                                      run_as_user=1000),
                volumes=[
                    {
                        "name": "homedirs",
                        "nfs": {
                            "server": "10.10.10.10",
                            "path": "/homedirs",
                        },
                    },
                    V1Volume(
                        name=f"dossier-{dossier.username}-vol",
                        config_map=V1ConfigMapVolumeSource(
                            default_mode=0o644,
                            name=f"{dossier.username}-cm",
                        ),
                    ),
                ],
            ),
            status=V1PodStatus(phase="Running"),
        )
    ]

    await wait_for_completion(client, dossier.username, mock_kubernetes,
                              mock_kubernetes_watch)