diff --git a/docs/modules/superset/partials/supported-versions.adoc b/docs/modules/superset/partials/supported-versions.adoc index 98da91db..29c31be7 100644 --- a/docs/modules/superset/partials/supported-versions.adoc +++ b/docs/modules/superset/partials/supported-versions.adoc @@ -2,5 +2,6 @@ // This is a separate file, since it is used by both the direct Superset documentation, and the overarching // Stackable Platform documentation. -- 6.0.0 +- 6.1.0 +- 6.0.0 (deprecated) - 4.1.4 (LTS) diff --git a/tests/templates/kuttl/upgrade/00-limit-range.yaml b/tests/templates/kuttl/upgrade/00-limit-range.yaml new file mode 100644 index 00000000..8fd02210 --- /dev/null +++ b/tests/templates/kuttl/upgrade/00-limit-range.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: v1 +kind: LimitRange +metadata: + name: limit-request-ratio +spec: + limits: + - type: "Container" + maxLimitRequestRatio: + cpu: 5 + memory: 1 diff --git a/tests/templates/kuttl/upgrade/00-patch-ns.yaml.j2 b/tests/templates/kuttl/upgrade/00-patch-ns.yaml.j2 new file mode 100644 index 00000000..67185acf --- /dev/null +++ b/tests/templates/kuttl/upgrade/00-patch-ns.yaml.j2 @@ -0,0 +1,9 @@ +{% if test_scenario['values']['openshift'] == 'true' %} +# see https://github.com/stackabletech/issues/issues/566 +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: kubectl patch namespace $NAMESPACE -p '{"metadata":{"labels":{"pod-security.kubernetes.io/enforce":"privileged"}}}' + timeout: 120 +{% endif %} diff --git a/tests/templates/kuttl/upgrade/10-assert.yaml b/tests/templates/kuttl/upgrade/10-assert.yaml new file mode 100644 index 00000000..e9c60b15 --- /dev/null +++ b/tests/templates/kuttl/upgrade/10-assert.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +metadata: + name: test-superset-postgresql +timeout: 480 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: superset-postgresql +status: + readyReplicas: 1 + replicas: 1 diff --git a/tests/templates/kuttl/upgrade/10-install-postgresql.yaml b/tests/templates/kuttl/upgrade/10-install-postgresql.yaml new file mode 100644 index 00000000..a9fc0d36 --- /dev/null +++ b/tests/templates/kuttl/upgrade/10-install-postgresql.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: >- + helm install superset-postgresql + --namespace $NAMESPACE + --version 12.5.6 + -f 10_helm-bitnami-postgresql-values.yaml + --repo https://charts.bitnami.com/bitnami postgresql + --wait + timeout: 600 diff --git a/tests/templates/kuttl/upgrade/10_helm-bitnami-postgresql-values.yaml.j2 b/tests/templates/kuttl/upgrade/10_helm-bitnami-postgresql-values.yaml.j2 new file mode 100644 index 00000000..2e851682 --- /dev/null +++ b/tests/templates/kuttl/upgrade/10_helm-bitnami-postgresql-values.yaml.j2 @@ -0,0 +1,44 @@ +--- +global: + security: + allowInsecureImages: true # needed starting with Chart version 16.3.0 if modifying images + +image: + repository: bitnamilegacy/postgresql + +volumePermissions: + enabled: false + image: + repository: bitnamilegacy/os-shell + securityContext: + runAsUser: auto + +metrics: + image: + repository: bitnamilegacy/postgres-exporter + +primary: + podSecurityContext: +{% if test_scenario['values']['openshift'] == 'true' %} + enabled: false +{% else %} + enabled: true +{% endif %} + containerSecurityContext: + enabled: false + resources: + requests: + memory: "128Mi" + cpu: "512m" + limits: + memory: "128Mi" + cpu: "1" + +shmVolume: + chmod: + enabled: false + +auth: + username: superset + password: superset + database: superset diff --git a/tests/templates/kuttl/upgrade/20-install-vector-aggregator-discovery-configmap.yaml.j2 b/tests/templates/kuttl/upgrade/20-install-vector-aggregator-discovery-configmap.yaml.j2 new file mode 100644 index 00000000..2d6a0df5 --- /dev/null +++ b/tests/templates/kuttl/upgrade/20-install-vector-aggregator-discovery-configmap.yaml.j2 @@ -0,0 +1,9 @@ +{% if lookup('env', 'VECTOR_AGGREGATOR') %} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: vector-aggregator-discovery +data: + ADDRESS: {{ lookup('env', 'VECTOR_AGGREGATOR') }} +{% endif %} diff --git a/tests/templates/kuttl/upgrade/30-assert.yaml.j2 b/tests/templates/kuttl/upgrade/30-assert.yaml.j2 new file mode 100644 index 00000000..f253a856 --- /dev/null +++ b/tests/templates/kuttl/upgrade/30-assert.yaml.j2 @@ -0,0 +1,16 @@ +{% if test_scenario['values']['authentication'] == 'oidc' %} +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +metadata: + name: test-keycloak +timeout: 480 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: keycloak1 +status: + readyReplicas: 1 + replicas: 1 +{% endif %} diff --git a/tests/templates/kuttl/upgrade/30-keycloak.yaml.j2 b/tests/templates/kuttl/upgrade/30-keycloak.yaml.j2 new file mode 100644 index 00000000..029c6b63 --- /dev/null +++ b/tests/templates/kuttl/upgrade/30-keycloak.yaml.j2 @@ -0,0 +1,17 @@ +{% if test_scenario['values']['authentication'] == 'oidc' %} +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: | + INSTANCE_NAME=keycloak1 \ + REALM=test1 \ + USERNAME=jane.doe \ + FIRST_NAME=Jane \ + LAST_NAME=Doe \ + EMAIL=jane.doe@stackable.tech \ + PASSWORD=T8mn72D9 \ + CLIENT_ID=superset1 \ + CLIENT_SECRET=R1bxHUD569vHeQdw \ + envsubst < 30_install-keycloak.yaml | kubectl apply -n $NAMESPACE -f - +{% endif %} diff --git a/tests/templates/kuttl/upgrade/30_install-keycloak.yaml.j2 b/tests/templates/kuttl/upgrade/30_install-keycloak.yaml.j2 new file mode 100644 index 00000000..37680200 --- /dev/null +++ b/tests/templates/kuttl/upgrade/30_install-keycloak.yaml.j2 @@ -0,0 +1,188 @@ +# The environment variables must be replaced. +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: $INSTANCE_NAME-realms +data: + test-realm.json: | + { + "realm": "$REALM", + "enabled": true, + "users": [ + { + "enabled": true, + "username": "$USERNAME", + "firstName" : "$FIRST_NAME", + "lastName" : "$LAST_NAME", + "email" : "$EMAIL", + "credentials": [ + { + "type": "password", + "value": "$PASSWORD" + } + ], + "realmRoles": [ + "user" + ] + } + ], + "roles": { + "realm": [ + { + "name": "user", + "description": "User privileges" + } + ] + }, + "clients": [ + { + "clientId": "$CLIENT_ID", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "$CLIENT_SECRET", + "redirectUris": [ + "*" + ], + "webOrigins": [ + "*" + ], + "standardFlowEnabled": true, + "protocol": "openid-connect" + } + ] + } +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: $INSTANCE_NAME + labels: + app: $INSTANCE_NAME +spec: + replicas: 1 + selector: + matchLabels: + app: $INSTANCE_NAME + template: + metadata: + labels: + app: $INSTANCE_NAME + spec: + serviceAccountName: keycloak + containers: + - name: keycloak + image: quay.io/keycloak/keycloak:23.0.4 + args: + - start-dev + - --import-realm + - --https-certificate-file=/tls/tls.crt + - --https-certificate-key-file=/tls/tls.key + env: + - name: KEYCLOAK_ADMIN + value: admin + - name: KEYCLOAK_ADMIN_PASSWORD + value: admin + # Both requests and limits must be set and satisfy the ratios in + # 00-limit-range.yaml (memory ratio 1 requires request == limit), + # otherwise the LimitRanger admission controller rejects the pod. + resources: + requests: + memory: "1Gi" + cpu: "500m" + limits: + memory: "1Gi" + cpu: "2" + ports: + - name: https + containerPort: 8443 + volumeMounts: + - name: realms + mountPath: /opt/keycloak/data/import + - name: tls + mountPath: /tls + readinessProbe: + httpGet: + scheme: HTTPS + path: /realms/$REALM + port: 8443 + volumes: + - name: realms + configMap: + name: $INSTANCE_NAME-realms + - name: tls + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls + secrets.stackable.tech/scope: service=$INSTANCE_NAME + spec: + storageClassName: secrets.stackable.tech + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" +--- +apiVersion: v1 +kind: Service +metadata: + name: $INSTANCE_NAME +spec: + selector: + app: $INSTANCE_NAME + ports: + - protocol: TCP + port: 8443 +--- +apiVersion: authentication.stackable.tech/v1alpha1 +kind: AuthenticationClass +metadata: + name: $INSTANCE_NAME-$NAMESPACE +spec: + provider: + oidc: + hostname: $INSTANCE_NAME.$NAMESPACE.svc.cluster.local + port: 8443 + rootPath: /realms/$REALM/ + scopes: + - email + - openid + - profile + principalClaim: preferred_username + providerHint: Keycloak + tls: + verification: + server: + caCert: + secretClass: tls +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: keycloak +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: keycloak +{% if test_scenario['values']['openshift'] == 'true' %} +rules: +- apiGroups: ["security.openshift.io"] + resources: ["securitycontextconstraints"] + resourceNames: ["privileged"] + verbs: ["use"] +{% endif %} +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: keycloak +subjects: + - kind: ServiceAccount + name: keycloak +roleRef: + kind: Role + name: keycloak + apiGroup: rbac.authorization.k8s.io diff --git a/tests/templates/kuttl/upgrade/40-assert.yaml b/tests/templates/kuttl/upgrade/40-assert.yaml new file mode 100644 index 00000000..daae9510 --- /dev/null +++ b/tests/templates/kuttl/upgrade/40-assert.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +metadata: + name: install-superset +timeout: 600 +commands: + - script: kubectl -n $NAMESPACE wait --for=condition=available=true supersetclusters.superset.stackable.tech/superset --timeout 601s +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: superset-node-default +status: + readyReplicas: 1 + replicas: 1 diff --git a/tests/templates/kuttl/upgrade/40-install-superset.yaml b/tests/templates/kuttl/upgrade/40-install-superset.yaml new file mode 100644 index 00000000..c97f66e8 --- /dev/null +++ b/tests/templates/kuttl/upgrade/40-install-superset.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +timeout: 300 +commands: + - script: > + envsubst '$NAMESPACE' < 40_install-superset.yaml | + kubectl apply -n $NAMESPACE -f - diff --git a/tests/templates/kuttl/upgrade/40_install-superset.yaml.j2 b/tests/templates/kuttl/upgrade/40_install-superset.yaml.j2 new file mode 100644 index 00000000..2785c497 --- /dev/null +++ b/tests/templates/kuttl/upgrade/40_install-superset.yaml.j2 @@ -0,0 +1,79 @@ +# $NAMESPACE will be replaced with the namespace of the test case. +--- +apiVersion: v1 +kind: Secret +metadata: + name: superset-admin-credentials +type: Opaque +stringData: + adminUser.username: admin + adminUser.firstname: Superset + adminUser.lastname: Admin + adminUser.email: admin@superset.com + adminUser.password: admin +--- +apiVersion: v1 +kind: Secret +metadata: + name: superset-postgresql-credentials +stringData: + username: superset + password: superset +{% if test_scenario['values']['authentication'] == 'oidc' %} +--- +apiVersion: v1 +kind: Secret +metadata: + name: superset-keycloak1-client +stringData: + clientId: superset1 + clientSecret: R1bxHUD569vHeQdw +{% endif %} +--- +apiVersion: superset.stackable.tech/v1alpha1 +kind: SupersetCluster +metadata: + name: superset +spec: + image: +{% if test_scenario['values']['superset-old'].find(",") > 0 %} + custom: "{{ test_scenario['values']['superset-old'].split(',')[1] }}" + productVersion: "{{ test_scenario['values']['superset-old'].split(',')[0] }}" +{% else %} + productVersion: "{{ test_scenario['values']['superset-old'] }}" +{% endif %} + pullPolicy: IfNotPresent + clusterConfig: +{% if test_scenario['values']['authentication'] == 'oidc' %} + authentication: + - authenticationClass: keycloak1-$NAMESPACE + oidc: + clientCredentialsSecret: superset-keycloak1-client +{% endif %} + credentialsSecretName: superset-admin-credentials + metadataDatabase: + postgresql: + host: superset-postgresql + database: superset + credentialsSecretName: superset-postgresql-credentials +{% if lookup('env', 'VECTOR_AGGREGATOR') %} + vectorAggregatorConfigMapName: vector-aggregator-discovery +{% endif %} + nodes: +{% if test_scenario['values']['authentication'] != 'oidc' %} +{# upgrade-test.py requires database authentication which is replaced by #} +{# OAuth in the oidc scenario, so the override is not needed there. #} + configOverrides: + superset_config.py: + FILE_FOOTER: | + # The FAB security API is required by upgrade-test.py to seed a role + # and a user. Superset versions before 6.0.0 do not enable it by + # default. + FAB_ADD_SECURITY_API = True +{% endif %} + config: + logging: + enableVectorAgent: {{ lookup('env', 'VECTOR_AGGREGATOR') | length > 0 }} + roleGroups: + default: + replicas: 1 diff --git a/tests/templates/kuttl/upgrade/50-assert.yaml b/tests/templates/kuttl/upgrade/50-assert.yaml new file mode 100644 index 00000000..58987778 --- /dev/null +++ b/tests/templates/kuttl/upgrade/50-assert.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +metadata: + name: install-test-container +timeout: 300 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: python +status: + readyReplicas: 1 + replicas: 1 diff --git a/tests/templates/kuttl/upgrade/50-install-test-container.yaml.j2 b/tests/templates/kuttl/upgrade/50-install-test-container.yaml.j2 new file mode 100644 index 00000000..ff787c33 --- /dev/null +++ b/tests/templates/kuttl/upgrade/50-install-test-container.yaml.j2 @@ -0,0 +1,88 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: python +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: python +{% if test_scenario['values']['openshift'] == 'true' %} +rules: +- apiGroups: ["security.openshift.io"] + resources: ["securitycontextconstraints"] + resourceNames: ["privileged"] + verbs: ["use"] +{% endif %} +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: python +subjects: + - kind: ServiceAccount + name: python +roleRef: + kind: Role + name: python + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +metadata: + name: install-test-container +timeout: 300 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: python + labels: + app: python +spec: + replicas: 1 + selector: + matchLabels: + app: python + template: + metadata: + labels: + app: python + spec: + serviceAccountName: python + securityContext: + fsGroup: 1000 + containers: + - name: python + image: oci.stackable.tech/sdp/testing-tools:0.3.0-stackable0.0.0-dev + stdin: true + tty: true + resources: + requests: + memory: "128Mi" + cpu: "512m" + limits: + memory: "128Mi" + cpu: "1" + volumeMounts: + - name: tls + mountPath: /stackable/tls + env: + - name: REQUESTS_CA_BUNDLE + value: /stackable/tls/ca.crt + volumes: + - name: tls + ephemeral: + volumeClaimTemplate: + metadata: + annotations: + secrets.stackable.tech/class: tls + secrets.stackable.tech/scope: pod + spec: + storageClassName: secrets.stackable.tech + accessModes: + - ReadWriteOnce + resources: + requests: + storage: "1" diff --git a/tests/templates/kuttl/upgrade/60-assert.yaml.j2 b/tests/templates/kuttl/upgrade/60-assert.yaml.j2 new file mode 100644 index 00000000..02f06fdc --- /dev/null +++ b/tests/templates/kuttl/upgrade/60-assert.yaml.j2 @@ -0,0 +1,19 @@ +{% if test_scenario['values']['authentication'] == 'oidc' %} +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +metadata: + name: login +timeout: 300 +commands: + - script: kubectl exec -n $NAMESPACE python-0 -- python /stackable/60_login.py +{% else %} +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +metadata: + name: seed +timeout: 300 +commands: + - script: kubectl exec -n $NAMESPACE python-0 -- python /tmp/upgrade-test.py seed +{% endif %} diff --git a/tests/templates/kuttl/upgrade/60-copy-test-script.yaml.j2 b/tests/templates/kuttl/upgrade/60-copy-test-script.yaml.j2 new file mode 100644 index 00000000..f7b72e21 --- /dev/null +++ b/tests/templates/kuttl/upgrade/60-copy-test-script.yaml.j2 @@ -0,0 +1,19 @@ +{% if test_scenario['values']['authentication'] == 'oidc' %} +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +metadata: + name: login +commands: + - script: > + envsubst '$NAMESPACE' < 60_login.py | + kubectl exec -n $NAMESPACE -i python-0 -- tee /stackable/60_login.py > /dev/null +{% else %} +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +metadata: + name: seed +commands: + - script: kubectl cp -n $NAMESPACE ./upgrade-test.py python-0:/tmp +{% endif %} diff --git a/tests/templates/kuttl/upgrade/60_login.py b/tests/templates/kuttl/upgrade/60_login.py new file mode 100644 index 00000000..da5e9b82 --- /dev/null +++ b/tests/templates/kuttl/upgrade/60_login.py @@ -0,0 +1,57 @@ +# $NAMESPACE will be replaced with the namespace of the test case. + +import json +import logging +import sys +import requests +from bs4 import BeautifulSoup + +logging.basicConfig( + level="DEBUG", format="%(asctime)s %(levelname)s: %(message)s", stream=sys.stdout +) + +session = requests.Session() + +# Click on "Sign In with keycloak" in Superset +login_page = session.get("http://superset-node:8088/login/keycloak?next=") + +assert login_page.ok, "Redirection from Superset to Keycloak failed" +assert login_page.url.startswith( + "https://keycloak1.$NAMESPACE.svc.cluster.local:8443/realms/test1/protocol/openid-connect/auth?response_type=code&client_id=superset1" +), "Redirection to the Keycloak login page expected" + +# Enter username and password into the Keycloak login page and click on "Sign In" +login_page_html = BeautifulSoup(login_page.text, "html.parser") +authenticate_url = login_page_html.form["action"] +welcome_page = session.post( + authenticate_url, data={"username": "jane.doe", "password": "T8mn72D9"} +) + +assert welcome_page.ok, "Login failed" +assert welcome_page.url == "http://superset-node:8088/superset/welcome/", ( + "Redirection to the Superset welcome page expected" +) + +# Open the user information page in Superset +userinfo_page = session.get("http://superset-node:8088/users/userinfo/") + +assert userinfo_page.ok, "Retrieving user information failed" +assert userinfo_page.url == "http://superset-node:8088/superset/welcome/", ( + "Redirection to the Superset welcome page expected" +) + +# Expect the user data provided by Keycloak in Superset +userinfo_page_html = BeautifulSoup(userinfo_page.text, "html.parser") +raw_data = userinfo_page_html.find(id="app")["data-bootstrap"] +data = json.loads(raw_data) +user_data = data["user"] + +assert user_data["firstName"] == "Jane", ( + "The first name of the user in Superset should match the one provided by Keycloak" +) +assert user_data["lastName"] == "Doe", ( + "The last name of the user in Superset should match the one provided by Keycloak" +) +assert user_data["email"] == "jane.doe@stackable.tech", ( + "The email of the user in Superset should match the one provided by Keycloak" +) diff --git a/tests/templates/kuttl/upgrade/70-assert.yaml.j2 b/tests/templates/kuttl/upgrade/70-assert.yaml.j2 new file mode 100644 index 00000000..6083cebd --- /dev/null +++ b/tests/templates/kuttl/upgrade/70-assert.yaml.j2 @@ -0,0 +1,41 @@ +{# Compute the app.kubernetes.io/version label value which operator-rs derives #} +{# from the product image: "-" for custom images and #} +{# "-stackable" otherwise; tests always run #} +{# against a 0.0.0-dev operator. #} +{% if test_scenario['values']['superset-new'].find(",") > 0 %} +{% set product_version = test_scenario['values']['superset-new'].split(',')[0] %} +{% set image_tag = test_scenario['values']['superset-new'].split(',')[1].split(':')[-1] %} +{% set app_version = product_version ~ '-' ~ image_tag %} +{% else %} +{% set app_version = test_scenario['values']['superset-new'] ~ '-stackable0.0.0-dev' %} +{% endif %} +--- +# A ready pod implies that `superset db upgrade`, `superset init` and the +# gunicorn startup all succeeded on the new version, because they run before +# the webserver can serve the /health readiness probe. Asserting the version +# label on the pod (not only on the StatefulSet) ensures that the assertion +# cannot pass before the old pod was actually replaced. +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +metadata: + name: upgrade-superset +timeout: 600 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: superset-node-default + labels: + app.kubernetes.io/version: "{{ app_version }}" +status: + readyReplicas: 1 + replicas: 1 +--- +apiVersion: v1 +kind: Pod +metadata: + name: superset-node-default-0 + labels: + app.kubernetes.io/version: "{{ app_version }}" +status: + phase: Running diff --git a/tests/templates/kuttl/upgrade/70-upgrade-superset.yaml.j2 b/tests/templates/kuttl/upgrade/70-upgrade-superset.yaml.j2 new file mode 100644 index 00000000..4017ca79 --- /dev/null +++ b/tests/templates/kuttl/upgrade/70-upgrade-superset.yaml.j2 @@ -0,0 +1,14 @@ +--- +apiVersion: superset.stackable.tech/v1alpha1 +kind: SupersetCluster +metadata: + name: superset +spec: + image: +{% if test_scenario['values']['superset-new'].find(",") > 0 %} + custom: "{{ test_scenario['values']['superset-new'].split(',')[1] }}" + productVersion: "{{ test_scenario['values']['superset-new'].split(',')[0] }}" +{% else %} + custom: null + productVersion: "{{ test_scenario['values']['superset-new'] }}" +{% endif %} diff --git a/tests/templates/kuttl/upgrade/80-assert.yaml.j2 b/tests/templates/kuttl/upgrade/80-assert.yaml.j2 new file mode 100644 index 00000000..f3f6851e --- /dev/null +++ b/tests/templates/kuttl/upgrade/80-assert.yaml.j2 @@ -0,0 +1,25 @@ +{% if test_scenario['values']['authentication'] == 'oidc' %} +--- +# Log in via Keycloak again after the upgrade. The user record was created by +# Flask-AppBuilder during the pre-upgrade login (step 60), so this exercises +# the existing-user OAuth login path on the new Superset/FAB version. The +# script was already copied to the test pod in step 60. +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +metadata: + name: login-after-upgrade +timeout: 300 +commands: + - script: kubectl exec -n $NAMESPACE python-0 -- python /stackable/60_login.py +{% else %} +--- +# Verify that the content seeded in step 60 survived the metadata database +# migrations and that a real SQL query still works. +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +metadata: + name: verify +timeout: 300 +commands: + - script: kubectl exec -n $NAMESPACE python-0 -- python /tmp/upgrade-test.py verify +{% endif %} diff --git a/tests/templates/kuttl/upgrade/upgrade-test.py b/tests/templates/kuttl/upgrade/upgrade-test.py new file mode 100644 index 00000000..b3ec6b9d --- /dev/null +++ b/tests/templates/kuttl/upgrade/upgrade-test.py @@ -0,0 +1,233 @@ +"""Seed Superset with content before a product upgrade and verify it afterwards. + +Usage: upgrade-test.py seed|verify + +"seed" creates (idempotently, so kuttl assert retries are safe): + * a database connection pointing at the metadata PostgreSQL + * a physical dataset on the ab_user table + * a table chart on that dataset, attached to a dashboard + * a saved query + * a custom role and a user holding it + +"verify" runs after the upgrade and asserts that all of the above survived +the metadata database migrations and that a real SQL query still works. +""" + +import logging +import sys + +import requests + +BASE_URL = "http://superset-node:8088" + +DATABASE_NAME = "upgrade-test-database" +DATASET_TABLE = "ab_user" +CHART_NAME = "upgrade-test-chart" +DASHBOARD_TITLE = "upgrade-test-dashboard" +SAVED_QUERY_LABEL = "upgrade-test-saved-query" +SAVED_QUERY_SQL = "SELECT 1" +ROLE_NAME = "upgrade-test-role" +USER_NAME = "upgrade-test-user" +USER_PASSWORD = "upgrade-test-password" + +logging.basicConfig( + level="INFO", format="%(asctime)s %(levelname)s: %(message)s", stream=sys.stdout +) + + +def login(username, password): + session = requests.Session() + response = session.post( + f"{BASE_URL}/api/v1/security/login", + json={ + "username": username, + "password": password, + "provider": "db", + "refresh": True, + }, + ) + assert response.status_code == 200, f"Login of [{username}] failed: {response.text}" + session.headers["Authorization"] = f"Bearer {response.json()['access_token']}" + + response = session.get(f"{BASE_URL}/api/v1/security/csrf_token/") + assert response.status_code == 200, f"Fetching CSRF token failed: {response.text}" + session.headers["X-CSRFToken"] = response.json()["result"] + session.headers["Referer"] = BASE_URL + + return session + + +def find_by(session, endpoint, attribute, value): + """Return the first item of a list endpoint whose attribute matches, else None.""" + response = session.get(f"{BASE_URL}/api/v1/{endpoint}/?q=(page_size:100)") + assert response.status_code == 200, f"Listing [{endpoint}] failed: {response.text}" + return next( + (item for item in response.json()["result"] if item.get(attribute) == value), + None, + ) + + +def create(session, endpoint, payload): + response = session.post(f"{BASE_URL}/api/v1/{endpoint}/", json=payload) + assert response.status_code == 201, f"Creating [{endpoint}] failed: {response.text}" + return response.json()["id"] + + +def ensure(session, endpoint, attribute, value, payload): + """Create an object unless an object with the same attribute value exists.""" + existing = find_by(session, endpoint, attribute, value) + if existing: + logging.info("[%s] with %s=%s already exists", endpoint, attribute, value) + return existing["id"] + object_id = create(session, endpoint, payload) + logging.info("Created [%s] with %s=%s", endpoint, attribute, value) + return object_id + + +def seed(): + session = login("admin", "admin") + + database_id = ensure( + session, + "database", + "database_name", + DATABASE_NAME, + { + "database_name": DATABASE_NAME, + "sqlalchemy_uri": "postgresql://superset:superset@superset-postgresql:5432/superset", + "expose_in_sqllab": True, + }, + ) + + dataset_id = ensure( + session, + "dataset", + "table_name", + DATASET_TABLE, + {"database": database_id, "schema": "public", "table_name": DATASET_TABLE}, + ) + + dashboard_id = ensure( + session, + "dashboard", + "dashboard_title", + DASHBOARD_TITLE, + {"dashboard_title": DASHBOARD_TITLE, "published": True}, + ) + + ensure( + session, + "chart", + "slice_name", + CHART_NAME, + { + "slice_name": CHART_NAME, + "datasource_id": dataset_id, + "datasource_type": "table", + "viz_type": "table", + "params": "{}", + "dashboards": [dashboard_id], + }, + ) + + ensure( + session, + "saved_query", + "label", + SAVED_QUERY_LABEL, + {"label": SAVED_QUERY_LABEL, "sql": SAVED_QUERY_SQL, "db_id": database_id}, + ) + + role_id = ensure(session, "security/roles", "name", ROLE_NAME, {"name": ROLE_NAME}) + gamma_role = find_by(session, "security/roles", "name", "Gamma") + assert gamma_role, "Built-in Gamma role not found." + + ensure( + session, + "security/users", + "username", + USER_NAME, + { + "username": USER_NAME, + "password": USER_PASSWORD, + "first_name": "Upgrade", + "last_name": "Test", + "email": "upgrade-test@superset.com", + "active": True, + "roles": [gamma_role["id"], role_id], + }, + ) + + +def verify(): + session = login("admin", "admin") + + database = find_by(session, "database", "database_name", DATABASE_NAME) + assert database, f"Database [{DATABASE_NAME}] not found after upgrade." + + dataset = find_by(session, "dataset", "table_name", DATASET_TABLE) + assert dataset, f"Dataset [{DATASET_TABLE}] not found after upgrade." + + # Run an actual SQL query against the dataset to prove that the upgraded + # installation can still query data, not just serve its metadata. + response = session.post( + f"{BASE_URL}/api/v1/chart/data", + json={ + "datasource": {"id": dataset["id"], "type": "table"}, + "queries": [{"columns": ["username"], "row_limit": 10}], + "result_format": "json", + "result_type": "full", + }, + ) + assert response.status_code == 200, f"Chart data query failed: {response.text}" + data = response.json()["result"][0]["data"] + assert data, "Chart data query returned no rows." + logging.info("Chart data query returned %d rows", len(data)) + + chart = find_by(session, "chart", "slice_name", CHART_NAME) + assert chart, f"Chart [{CHART_NAME}] not found after upgrade." + response = session.get(f"{BASE_URL}/api/v1/chart/{chart['id']}") + assert response.status_code == 200, f"Fetching chart failed: {response.text}" + dashboard_titles = [ + dashboard["dashboard_title"] + for dashboard in response.json()["result"].get("dashboards", []) + ] + assert DASHBOARD_TITLE in dashboard_titles, ( + f"Chart [{CHART_NAME}] is no longer attached " + f"to dashboard [{DASHBOARD_TITLE}]: {dashboard_titles}" + ) + + dashboard = find_by(session, "dashboard", "dashboard_title", DASHBOARD_TITLE) + assert dashboard, f"Dashboard [{DASHBOARD_TITLE}] not found after upgrade." + + saved_query = find_by(session, "saved_query", "label", SAVED_QUERY_LABEL) + assert saved_query, f"Saved query [{SAVED_QUERY_LABEL}] not found after upgrade." + response = session.get(f"{BASE_URL}/api/v1/saved_query/{saved_query['id']}") + assert response.status_code == 200, f"Fetching saved query failed: {response.text}" + assert response.json()["result"]["sql"] == SAVED_QUERY_SQL, ( + "Saved query SQL changed after upgrade." + ) + + user = find_by(session, "security/users", "username", USER_NAME) + assert user, f"User [{USER_NAME}] not found after upgrade." + role_names = [role["name"] for role in user.get("roles", [])] + assert ROLE_NAME in role_names, ( + f"User [{USER_NAME}] lost role [{ROLE_NAME}] after upgrade: {role_names}" + ) + + # The seeded user must still be able to log in with the FAB version + # shipped in the new Superset version. + user_session = login(USER_NAME, USER_PASSWORD) + response = user_session.get(f"{BASE_URL}/api/v1/me/") + assert response.status_code == 200, f"Fetching own user failed: {response.text}" + + logging.info("All seeded objects survived the upgrade.") + + +if __name__ == "__main__": + if sys.argv[1:] == ["seed"]: + seed() + elif sys.argv[1:] == ["verify"]: + verify() + else: + sys.exit(f"Usage: {sys.argv[0]} seed|verify (got {sys.argv[1:]})") diff --git a/tests/test-definition.yaml b/tests/test-definition.yaml index 1dc0e360..f8d59409 100644 --- a/tests/test-definition.yaml +++ b/tests/test-definition.yaml @@ -8,17 +8,32 @@ dimensions: values: - 4.1.4 - 6.0.0 + - 6.1.0 # Or use a custom image: - # - x.x.x,oci.stackable.tech/razvan/superset:x.x.x-stackable0.0.0-dev + # - 6.1.0,oci.stackable.tech/sandbox/superset:6.1.0 - name: superset-latest values: + - 6.1.0 + # - 6.0.0,oci.stackable.tech/sandbox/superset:6.0.0 + - name: superset-old + values: + - 4.1.4 - 6.0.0 - # - x.x.x,oci.stackable.tech/razvan/superset:x.x.x-stackable0.0.0-dev + - name: superset-new + values: + - 6.1.0 - name: ldap-authentication values: - no-tls - insecure-tls - server-verification-tls + # Authentication used in the upgrade test. "none" tests that seeded content + # survives the metadata database migrations, "oidc" tests that the OAuth + # login of an existing user still works after the upgrade. + - name: authentication + values: + - none + - oidc - name: opa-latest values: - 1.16.2 @@ -68,6 +83,12 @@ tests: dimensions: - superset - openshift + - name: upgrade + dimensions: + - superset-old + - superset-new + - authentication + - openshift suites: - name: nightly patch: