diff --git a/jobs/integration/conftest.py b/jobs/integration/conftest.py index 29978e348..ec671aee0 100644 --- a/jobs/integration/conftest.py +++ b/jobs/integration/conftest.py @@ -36,7 +36,12 @@ def pytest_addoption(parser): "--controller", action="store", required=True, help="Juju controller to use" ) parser.addoption("--model", action="store", required=True, help="Juju model to use") - parser.addoption("--series", action="store", default="bionic", help="Base series") + parser.addoption( + "--series", + action="store", + default=os.environ.get("SERIES", "focal"), + help="Base series", + ) parser.addoption( "--cloud", action="store", default="aws/us-east-2", help="Juju cloud to use" ) @@ -152,6 +157,7 @@ async def _load(self): self.k8s_connection = f"{self.controller_name}:{self.k8s_model_name_full}" self.is_series_upgrade = request.config.getoption("--is-series-upgrade") self.charm_channel = request.config.getoption("--charm-channel") + self.snap_channel = request.config.getoption("--snap-channel") self.vault_unseal_command = request.config.getoption("--vault-unseal-command") self.juju_ssh_proxy = request.config.getoption("--juju-ssh-proxy") self.use_existing_ceph_apps = request.config.getoption( @@ -448,7 +454,9 @@ def _charm_name(app): """Resolve charm_name from juju.applications.Application""" cs, charm_url = "cs:", app.data["charm-url"].rpartition("-")[0] if charm_url.startswith(cs): - return charm_url[len(cs) :] # noqa: E203 + charm_url = charm_url[len(cs) :] # noqa: E203 + elif any(charm_url.startswith(prefix) for prefix in ("ch:", "local:")): + charm_url = charm_url.split("/")[-1] # noqa: E203 return charm_url diff --git a/jobs/integration/templates/keystone/keystone-apiserver-webhook.yaml b/jobs/integration/templates/keystone/keystone-apiserver-webhook.yaml new file mode 100644 index 000000000..f9828cc81 --- /dev/null +++ b/jobs/integration/templates/keystone/keystone-apiserver-webhook.yaml @@ -0,0 +1,17 @@ +# https://github.com/kubernetes/cloud-provider-openstack/blob/release-1.30/examples/webhook/keystone-apiserver-webhook.yaml +apiVersion: v1 +kind: Config +preferences: {} +clusters: + - cluster: + insecure-skip-tls-verify: true + server: https://{{ keystone_auth_service_ip }}:8443/webhook + name: webhook +users: + - name: webhook +contexts: + - context: + cluster: webhook + user: webhook + name: webhook +current-context: webhook \ No newline at end of file diff --git a/jobs/integration/templates/keystone/keystone-deployment.yaml b/jobs/integration/templates/keystone/keystone-deployment.yaml new file mode 100644 index 000000000..038622d0a --- /dev/null +++ b/jobs/integration/templates/keystone/keystone-deployment.yaml @@ -0,0 +1,47 @@ +# https://github.com/kubernetes/cloud-provider-openstack/blob/release-1.30/examples/webhook/keystone-deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: k8s-keystone-auth + namespace: kube-system + labels: + app: k8s-keystone-auth +spec: + replicas: 2 + selector: + matchLabels: + app: k8s-keystone-auth + template: + metadata: + labels: + app: k8s-keystone-auth + spec: + serviceAccountName: k8s-keystone + containers: + - name: k8s-keystone-auth + image: rocks.canonical.com:443/cdk/provider-os/k8s-keystone-auth:v1.28.1 + args: + - ./bin/k8s-keystone-auth + env: + - name: OS_AUTH_URL + value: {{ keystone_server_url }} + - name: TLS_CERT_FILE + value: /etc/pki/tls.crt + - name: TLS_PRIVATE_KEY_FILE + value: /etc/pki/tls.key + - name: KEYSTONE_POLICY_CONFIGMAP_NAME + value: k8s-auth-policy + {% if keystone_server_ca %} + - name: KEYSTONE_CA_FILE + value: /etc/pki/ca.crt + {% endif %} + volumeMounts: + - mountPath: /etc/pki + name: certs + readOnly: true + ports: + - containerPort: 8443 + volumes: + - name: certs + secret: + secretName: keystone-auth-certs \ No newline at end of file diff --git a/jobs/integration/templates/keystone/keystone-kubeconfig.yaml b/jobs/integration/templates/keystone/keystone-kubeconfig.yaml new file mode 100644 index 000000000..fe3ffbdd2 --- /dev/null +++ b/jobs/integration/templates/keystone/keystone-kubeconfig.yaml @@ -0,0 +1,94 @@ +apiVersion: v1 +kind: Config +users: +- name: good-user + user: + exec: + command: "/snap/bin/client-keystone-auth" + + # API version to use when encoding and decoding the ExecCredentials + # resource. Required. + # + # The API version returned by the plugin MUST match the version encoded. + apiVersion: "client.authentication.k8s.io/v1beta1" + + # Environment variables to set when executing the plugin. Optional. + env: + - name: "OS_USERNAME" + value: "{{ keystone_user }}" + - name: "OS_PASSWORD" + value: "{{ keystone_password }}" + - name: "OS_PROJECT_NAME" + value: "{{ keystone_project }}" + + # Arguments to pass when executing the plugin. Optional. + args: + - "--domain-name={{ keystone_domain }}" + - "--keystone-url={{ keystone_server_url }}" +- name: bad-user + user: + exec: + command: "/snap/bin/client-keystone-auth" + + # API version to use when encoding and decoding the ExecCredentials + # resource. Required. + # + # The API version returned by the plugin MUST match the version encoded. + apiVersion: "client.authentication.k8s.io/v1beta1" + + # Environment variables to set when executing the plugin. Optional. + env: + - name: "OS_USERNAME" + value: "fake" + - name: "OS_PASSWORD" + value: "bad" + - name: "OS_PROJECT_NAME" + value: "{{ keystone_project }}" + + # Arguments to pass when executing the plugin. Optional. + args: + - "--domain-name={{ keystone_domain }}" + - "--keystone-url={{ keystone_server_url }}" +- name: bad-password + user: + exec: + command: "/snap/bin/client-keystone-auth" + + # API version to use when encoding and decoding the ExecCredentials + # resource. Required. + # + # The API version returned by the plugin MUST match the version encoded. + apiVersion: "client.authentication.k8s.io/v1beta1" + + # Environment variables to set when executing the plugin. Optional. + env: + - name: "OS_USERNAME" + value: "{{ keystone_user }}" + - name: "OS_PASSWORD" + value: "bad" + - name: "OS_PROJECT_NAME" + value: "{{ keystone_project }}" + + # Arguments to pass when executing the plugin. Optional. + args: + - "--domain-name={{ keystone_domain }}" + - "--keystone-url={{ keystone_server_url }}" +clusters: +- name: my-cluster + cluster: + server: {{ kubernetes_api_server }} + insecure-skip-tls-verify: true +contexts: +- name: good-context + context: + cluster: my-cluster + user: good-user +- name: bad-user-context + context: + cluster: my-cluster + user: bad-user +- name: bad-password-context + context: + cluster: my-cluster + user: bad-password +current-context: good-context diff --git a/jobs/integration/templates/keystone/keystone-policy-configmap.yaml b/jobs/integration/templates/keystone/keystone-policy-configmap.yaml new file mode 100644 index 000000000..b7d86e028 --- /dev/null +++ b/jobs/integration/templates/keystone/keystone-policy-configmap.yaml @@ -0,0 +1,19 @@ +# https://github.com/kubernetes/cloud-provider-openstack/blob/release-1.30/examples/webhook/keystone-policy-configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: k8s-auth-policy + namespace: kube-system +data: + policies: | + [ + { + "users": { + "projects": ["admin"], + "user": ["admin"] + }, + "resource_permissions": { + "default/pods": ["get", "list", "watch"] + } + } + ] \ No newline at end of file diff --git a/jobs/integration/templates/keystone/keystone-rbac.yaml b/jobs/integration/templates/keystone/keystone-rbac.yaml new file mode 100644 index 000000000..a7961f600 --- /dev/null +++ b/jobs/integration/templates/keystone/keystone-rbac.yaml @@ -0,0 +1,33 @@ +# https://github.com/kubernetes/cloud-provider-openstack/blob/release-1.30/examples/webhook/keystone-rbac.yaml +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + labels: + k8s-app: k8s-keystone-auth + name: k8s-keystone-auth +rules: + # Allow k8s-keystone-auth to get k8s-auth-policy configmap +- apiGroups: [""] + resources: ["configmaps"] + verbs: ["get", "watch", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: k8s-keystone-auth + labels: + k8s-app: k8s-keystone-auth +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: k8s-keystone-auth +subjects: +- kind: ServiceAccount + name: k8s-keystone + namespace: kube-system +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: k8s-keystone + namespace: kube-system \ No newline at end of file diff --git a/jobs/integration/templates/keystone/keystone-secret.yaml b/jobs/integration/templates/keystone/keystone-secret.yaml new file mode 100644 index 000000000..86ca8269b --- /dev/null +++ b/jobs/integration/templates/keystone/keystone-secret.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Secret +metadata: + name: keystone-auth-certs + namespace: kube-system +type: Opaque +data: + tls.crt: {{ keystone_auth_crt }} + tls.key: {{ keystone_auth_key }} +{% if keystone_server_ca %} + ca.crt: {{ keystone_server_ca }} +{% endif %} \ No newline at end of file diff --git a/jobs/integration/templates/keystone/keystone-service.yaml b/jobs/integration/templates/keystone/keystone-service.yaml new file mode 100644 index 000000000..9c1c2317c --- /dev/null +++ b/jobs/integration/templates/keystone/keystone-service.yaml @@ -0,0 +1,13 @@ +# https://github.com/kubernetes/cloud-provider-openstack/blob/release-1.30/examples/webhook/keystone-service.yaml +kind: Service +apiVersion: v1 +metadata: + name: k8s-keystone-auth-service + namespace: kube-system +spec: + selector: + app: k8s-keystone-auth + ports: + - protocol: TCP + port: 8443 + targetPort: 8443 \ No newline at end of file diff --git a/jobs/integration/validation.py b/jobs/integration/validation.py index c6f5b0846..1ef7671d6 100644 --- a/jobs/integration/validation.py +++ b/jobs/integration/validation.py @@ -5,6 +5,7 @@ import backoff import ipaddress +import jinja2 import json import os import requests @@ -52,6 +53,8 @@ import urllib.request from bs4 import BeautifulSoup as bs from bs4.element import ResultSet as bs_ResultSet +from juju.application import Application +from juju.unit import Unit # Quiet the noise logging.getLogger("websockets.protocol").setLevel(logging.INFO) @@ -1335,94 +1338,40 @@ async def get_webhook_server_entry_count(): @pytest.fixture() async def any_keystone(model, apps_by_charm, tools): - def _find_relation(*specs): - for rel in model.relations: - if rel.matches(*specs): - yield rel - keystone_apps = apps_by_charm("keystone") - masters = model.applications["kubernetes-control-plane"] - keystone_creds = "kubernetes-control-plane:keystone-credentials" if len(keystone_apps) > 1: pytest.fail(f"More than one keystone app available {','.join(keystone_apps)}") elif len(keystone_apps) == 1: - # One keystone found, ensure related to kubernetes-control-plane + # One keystone found keystone, *_ = keystone_apps.values() - credentials_rel = list(_find_relation(keystone_creds)) - if not credentials_rel: - await keystone.add_relation("identity-credentials", keystone_creds) - await tools.juju_wait() keystone_main = random.choice(keystone.units) action = await juju_run(keystone_main, "leader-get admin_passwd") admin_password = action.stdout.strip() - # Work around the following bugs which lead to the CA used by Keystone not being passed along - # and honored from the keystone-credentials relation itself by getting the CA directly from Vault and - # passing it in via explicit config. - # * https://bugs.launchpad.net/charm-keystone/+bug/1954835 - # * https://bugs.launchpad.net/charm-kubernetes-control-plane/+bug/1954838 - keystone_ssl_ca = (await masters.get_config())["keystone-ssl-ca"]["value"] - if not keystone_ssl_ca: - vault_root_ca = None - vault_apps = apps_by_charm("vault") - for name, vault_app in vault_apps.items(): - vault_tls = f"{name}:certificates" - rels = set( - app.name - for rel in _find_relation(vault_tls) - for app in rel.applications - ) - if all( - name in rels - for name in [ - "kubernetes-control-plane", - "kubernetes-worker", - keystone.name, - ] - ): - vault_unit = random.choice(vault_app.units) - action = await juju_run_action(vault_unit, "get-root-ca") - vault_root_ca = action.results.get("output") - if vault_root_ca: - vault_root_ca = base64.b64encode( - vault_root_ca.encode("ascii") - ).decode("ascii") - break - - if vault_root_ca: - await masters.set_config({"keystone-ssl-ca": vault_root_ca}) - yield SimpleNamespace(app=keystone, admin_password=admin_password) - - if not credentials_rel: - await keystone.destroy_relation("identity-credentials", keystone_creds) - await tools.juju_wait() - - await masters.set_config({"keystone-ssl-ca": keystone_ssl_ca}) else: # No keystone available, add/setup one - series = os.environ["SERIES"] admin_password = "testpw" channel, mysql_channel = "yoga/stable", "8.0/stable" keystone = await model.deploy( "keystone", channel=channel, - series=series, + series=tools.series, config={"admin-password": admin_password}, ) db_router = await model.deploy( "mysql-router", application_name="keystone-mysql-router", - series=series, + series=tools.series, channel=mysql_channel, ) db = await model.deploy( "mysql-innodb-cluster", channel=mysql_channel, constraints="cores=2 mem=8G root-disk=64G", - series=series, + series=tools.series, num_units=3, config={ "enable-binlogs": True, @@ -1432,9 +1381,8 @@ def _find_relation(*specs): }, ) - await model.add_relation(keystone_creds, "keystone:identity-credentials") - await model.add_relation("keystone:shared-db", f"{db_router.name}:shared-db") - await model.add_relation(f"{db.name}:db-router", f"{db_router.name}:db-router") + await model.integrate("keystone:shared-db", f"{db_router.name}:shared-db") + await model.integrate(f"{db.name}:db-router", f"{db_router.name}:db-router") await tools.juju_wait() yield SimpleNamespace(app=keystone, admin_password=admin_password) @@ -1459,6 +1407,140 @@ def _find_relation(*specs): pytest.fail(f"Timed out waiting for {db_name} to go away") +async def load_keystone_ca(model, apps_by_charm, keystone) -> str: + def _find_relation(*specs): + for rel in model.relations: + if rel.matches(*specs): + yield rel + + vault_root_ca = None + vault_apps = apps_by_charm("vault") + for name, vault_app in vault_apps.items(): + vault_tls = f"{name}:certificates" + rels = set( + app.name for rel in _find_relation(vault_tls) for app in rel.applications + ) + if keystone.name in rels: + vault_unit = random.choice(vault_app.units) + action = await juju_run_action(vault_unit, "get-root-ca") + vault_root_ca: str = action.results.get("output") + if vault_root_ca: + return base64.b64encode(vault_root_ca.encode()).decode() + return "" + + +def cert_encode(cert: str) -> str: + return base64.b64encode(cert.encode()).decode() + + +async def leader_read(unit: Unit, path: str): + action = await juju_run(unit, f"cat {path}") + return action.stdout + + +@pytest.fixture() +async def keystone_deployment(model, apps_by_charm, any_keystone, tools, tmp_path): + keystone: Application = any_keystone.app + control_plane: Application = model.applications["kubernetes-control-plane"] + control_plane_unit = control_plane.units[0] + kubeapi_loadbalancer: Application = model.applications["kubeapi-load-balancer"] + server_crt = await leader_read(control_plane_unit, "/root/cdk/server.crt") + server_key = await leader_read(control_plane_unit, "/root/cdk/server.key") + context = dict( + # keystone endpoint data + keystone_server_url=f"http://{keystone.units[0].public_address}:5000/v3", + keystone_server_ca=await load_keystone_ca(model, apps_by_charm, keystone), + # keystone auth endpoint data + keystone_auth_crt=cert_encode(server_crt), + keystone_auth_key=cert_encode(server_key), + keystone_auth_service_ip="", + # keystone login data + keystone_user="admin", + keystone_password=any_keystone.admin_password, + keystone_project="admin", + keystone_domain="admin_domain", + # kube api data + kubernetes_api_server=f"https://{kubeapi_loadbalancer.units[0].public_address}", + ) + resources = [ + "templates/keystone/keystone-deployment.yaml", + "templates/keystone/keystone-policy-configmap.yaml", + "templates/keystone/keystone-rbac.yaml", + "templates/keystone/keystone-secret.yaml", + "templates/keystone/keystone-service.yaml", + ] + + def _render(resource): + source = Path(__file__).parent / resource + template = jinja2.Template(source.read_text()) + rendered = tmp_path / source.name + rendered.write_text(template.render(context)) + return rendered + + async def _render_and_apply(resource): + await kubectl_apply(_render(resource), model) + + async def _render_and_delete(resource): + await kubectl_delete(_render(resource), model) + + await asyncio.gather(*(_render_and_apply(resource) for resource in resources)) + + # find the service ip of the keystone auth service + while context["keystone_auth_service_ip"] == "": + auth_svc = await find_entities( + control_plane_unit, "svc", ["k8s-keystone-auth-service"], "-n kube-system" + ) + if auth_svc and (svc_ip := auth_svc[0]["spec"]["clusterIP"]): + context["keystone_auth_service_ip"] = svc_ip + + keystone_webhook_endpoint = f"https://{svc_ip}:8443/webhook" + webhook_config = _render("templates/keystone/keystone-apiserver-webhook.yaml") + + original_config = await control_plane.get_config() + await control_plane.set_config( + { + "authorization-webhook-config-file": webhook_config.read_text(), + "authorization-mode": "Node,Webhook,RBAC", + "authn-webhook-endpoint": keystone_webhook_endpoint, + } + ) + if not (keystone_client := model.applications.get("keystone-client")): + keystone_client = await model.deploy( + "ubuntu", + application_name="keystone-client", + series=tools.series, + config={}, + num_units=1, + channel="stable", + ) + await tools.juju_wait() + any_keystone.client = keystone_client.units[0] + kubeconfig = _render("templates/keystone/keystone-kubeconfig.yaml") + await scp_to( + kubeconfig, + any_keystone.client, + "/home/ubuntu/config", + tools.controller_name, + tools.connection, + proxy=tools.juju_ssh_proxy, + ) + await juju_run( + any_keystone.client, + ( + f'snap install kubectl --channel={original_config["channel"]["value"]} --classic;' + f'snap install client-keystone-auth --channel={original_config["channel"]["value"]};' + "mkdir -p /root/.kube;" + "mv /home/ubuntu/config /root/.kube/config;" + "chown -R root:root /root/.kube;" + ), + ) + + yield any_keystone + await control_plane.set_config(original_config) + await tools.juju_wait() + await asyncio.gather(*(_render_and_delete(resource) for resource in resources)) + + @pytest.mark.usefixtures("ceph_apps") class TestCeph: async def test_plugins_installed(self, model): @@ -1489,38 +1571,9 @@ async def test_storage_class(self, model, log_open, storage_class): @pytest.mark.skip_arch(["aarch64"]) @pytest.mark.clouds(["ec2", "vsphere"]) -@pytest.mark.skip("Feature removed in ops rewrite") -async def test_keystone(model, tools, any_keystone): +async def test_keystone(model, keystone_deployment): control_plane = model.applications["kubernetes-control-plane"] - - # save off config - config = await control_plane.get_config() - - # verify kubectl config file has keystone in it - control_plane_unit = random.choice(control_plane.units) - for i in range(60): - action = await juju_run( - control_plane_unit, "cat /home/ubuntu/config", check=False - ) - if "client-keystone-auth" in action.stdout: - break - click.echo("Unable to find keystone information in kubeconfig, retrying...") - await asyncio.sleep(10) - - assert "client-keystone-auth" in action.stdout - - # verify kube-keystone.sh exists - control_plane_unit = random.choice(control_plane.units) - action = await juju_run(control_plane_unit, "cat /home/ubuntu/kube-keystone.sh") - assert "OS_AUTH_URL" in action.stdout - - # verify webhook enabled on apiserver - await wait_for_process(model, "authentication-token-webhook-config-file") control_plane_unit = random.choice(control_plane.units) - action = await juju_run( - control_plane_unit, "sudo cat /root/cdk/keystone/webhook.yaml" - ) - assert "webhook" in action.stdout # verify keystone pod is running await retry_async_with_timeout( @@ -1529,150 +1582,34 @@ async def test_keystone(model, tools, any_keystone): timeout_msg="Unable to find keystone auth pod before timeout", ) - skip_tests = False - action = await juju_run( - control_plane_unit, "cat /snap/cdk-addons/current/templates/keystone-rbac.yaml" - ) - if "kind: Role" in action.stdout: - # we need to skip tests for the old template that incorrectly had a Role instead - # of a ClusterRole - skip_tests = True - - if skip_tests: - await control_plane.set_config({"enable-keystone-authorization": "true"}) - else: - # verify authorization - await control_plane.set_config( - { - "enable-keystone-authorization": "true", - "authorization-mode": "Node,Webhook,RBAC", - } - ) + # verify kube-apiserver configuration + await wait_for_process(model, "authentication-token-webhook-config-file") await wait_for_process(model, "authorization-webhook-config-file") # verify auth fail - bad user - control_plane_unit = random.choice(control_plane.units) - await juju_run( - control_plane_unit, "/usr/bin/snap install --edge client-keystone-auth" - ) - - cmd = "source /home/ubuntu/kube-keystone.sh && \ - OS_PROJECT_NAME=k8s OS_DOMAIN_NAME=k8s OS_USERNAME=fake \ - OS_PASSWORD=bad /snap/bin/kubectl --kubeconfig /home/ubuntu/config get clusterroles" - output = await juju_run(control_plane_unit, cmd, check=False) + kubectl = "kubectl --kubeconfig=/root/.kube/config" + cmd = f"{kubectl} --context bad-user-context get clusterroles" + output = await juju_run(keystone_deployment.client, cmd, check=False) assert output.status == "completed" - if "invalid user credentials" not in output.stderr.lower(): - click.echo("Failing, auth did not fail as expected") - click.echo(pformat(output)) - assert False + assert "invalid user credentials" in output.stderr.lower(), output.stderr # verify auth fail - bad password - cmd = "source /home/ubuntu/kube-keystone.sh && \ - OS_PROJECT_NAME=admin OS_DOMAIN_NAME=admin_domain OS_USERNAME=admin \ - OS_PASSWORD=badpw /snap/bin/kubectl --kubeconfig /home/ubuntu/config get clusterroles" - output = await juju_run(control_plane_unit, cmd, check=False) + cmd = f"{kubectl} --context bad-password-context get clusterroles" + output = await juju_run(keystone_deployment.client, cmd, check=False) assert output.status == "completed" - if "invalid user credentials" not in output.stderr.lower(): - click.echo("Failing, auth did not fail as expected") - click.echo(pformat(output)) - assert False - - if not skip_tests: - # set up read only access to pods only - await control_plane.set_config( - { - "keystone-policy": """apiVersion: v1 -kind: ConfigMap -metadata: - name: k8s-auth-policy - namespace: kube-system - labels: - k8s-app: k8s-keystone-auth -data: - policies: | - [ - { - "resource": { - "verbs": ["get", "list", "watch"], - "resources": ["pods"], - "version": "*", - "namespace": "default" - }, - "match": [ - { - "type": "user", - "values": ["admin"] - } - ] - } - ]""" - } - ) - await tools.juju_wait() + assert "invalid user credentials" in output.stderr.lower(), output.stderr - # verify auth failure on something not a pod - cmd = f"source /home/ubuntu/kube-keystone.sh && \ - OS_PROJECT_NAME=admin OS_DOMAIN_NAME=admin_domain OS_USERNAME=admin \ - OS_PASSWORD={any_keystone.admin_password} /snap/bin/kubectl \ - --kubeconfig /home/ubuntu/config get clusterroles" - output = await juju_run(control_plane_unit, cmd, check=False) - assert output.status == "completed" - assert "error" in output.stderr.lower() - - # the config set writes out a file and updates a configmap, which is then picked up by the - # keystone pod and updated. This all takes time and I don't know of a great way to tell - # that it is all done. I could compare the configmap to this, but that doesn't mean the - # pod has updated. The pod does write a log line about the configmap updating, but - # I'd need to watch both in succession and it just seems much easier and just as reliable - # to just retry on failure a few times. - - for i in range(18): # 3 minutes - # verify auth success on pods - cmd = f"source /home/ubuntu/kube-keystone.sh && \ - OS_PROJECT_NAME=admin OS_DOMAIN_NAME=admin_domain OS_USERNAME=admin \ - OS_PASSWORD={any_keystone.admin_password} /snap/bin/kubectl \ - --kubeconfig /home/ubuntu/config get po" - output = await juju_run(control_plane_unit, cmd, check=False) - if ( - output.status == "completed" - and "invalid user credentials" not in output.stderr.lower() - and "error" not in output.stderr.lower() - ): - break - click.echo("Unable to verify configmap change, retrying...") - await asyncio.sleep(10) - - assert output.status == "completed" - assert "invalid user credentials" not in output.stderr.lower() - assert "error" not in output.stderr.lower() - - # verify auth failure on pods outside of default namespace - cmd = f"source /home/ubuntu/kube-keystone.sh && \ - OS_PROJECT_NAME=admin OS_DOMAIN_NAME=admin_domain OS_USERNAME=admin \ - OS_PASSWORD={any_keystone.admin_password} /snap/bin/kubectl \ - --kubeconfig /home/ubuntu/config get po -n kube-system" - output = await juju_run(control_plane_unit, cmd, check=False) - assert output.status == "completed" - assert "invalid user credentials" not in output.stderr.lower() - assert "forbidden" in output.stderr.lower() - - # verify auth works now that it is off - original_auth = config["authorization-mode"]["value"] - await control_plane.set_config( - { - "enable-keystone-authorization": "false", - "authorization-mode": original_auth, - } - ) - await wait_for_not_process(model, "authorization-webhook-config-file") - await tools.juju_wait() - cmd = "/snap/bin/kubectl --context=juju-context \ - --kubeconfig /home/ubuntu/config get clusterroles" - output = await juju_run(control_plane_unit, cmd, check=False) + # verify auth failure on pods outside of default namespace + cmd = f"{kubectl} --context good-context get pod -n kube-system" + output = await juju_run(keystone_deployment.client, cmd, check=False) + assert output.status == "completed" + assert 'cannot list resource "pods"' in output.stderr.lower(), output.stderr + + # verify auth success on pods + cmd = f"{kubectl} --context good-context get pod" + output = await juju_run(keystone_deployment.client, cmd, check=False) assert output.status == "completed" - assert "invalid user credentials" not in output.stderr.lower() - assert "error" not in output.stderr.lower() - assert "forbidden" not in output.stderr.lower() + assert output.code == 0, output.stderr @pytest.mark.skip_arch(["aarch64"])