diff --git a/application-templates/django-app/api/test_st.py b/application-templates/django-app/api/test_st.py new file mode 100644 index 00000000..fb852433 --- /dev/null +++ b/application-templates/django-app/api/test_st.py @@ -0,0 +1,27 @@ +import os +from pprint import pprint +import schemathesis as st +from schemathesis.checks import response_schema_conformance, not_a_server_error + +from cloudharness_test import apitest_init # include to perform default authorization + +app_url = os.environ.get("APP_URL", "http://samples.ch.local/api") + +try: + schema = st.from_uri(app_url + "/openapi.json") +except: + # support alternative schema location + schema = st.from_uri(app_url.replace("/api", "") + "/openapi.json") + + +@schema.parametrize(endpoint="/ping") +def test_ping(case): + response = case.call() + pprint(response.__dict__) + assert response.status_code == 200, "this api errors on purpose" + +def test_state_machine(): + schema.as_state_machine().run() +# APIWorkflow = schema.as_state_machine() +# APIWorkflow.run() +# TestAPI = APIWorkflow.TestCase \ No newline at end of file diff --git a/cloudharness.png b/cloudharness.png index 1966e6dd..9e151f12 100644 Binary files a/cloudharness.png and b/cloudharness.png differ diff --git a/deployment-configuration/helm/templates/auto-database.yaml b/deployment-configuration/helm/templates/auto-database.yaml index 2deba7fe..6ec31380 100644 --- a/deployment-configuration/helm/templates/auto-database.yaml +++ b/deployment-configuration/helm/templates/auto-database.yaml @@ -74,11 +74,6 @@ spec: volumeMounts: - name: {{ .app.harness.database.name | quote }} mountPath: /data/db - {{- if .root.Values.backup.active }} - - name: "db-backups" - mountPath: {{ (printf "%s/%s/%s" .root.Values.backup.dir .app.harness.database.type .app.harness.database.name) | quote }} - readOnly: true - {{- end }} {{- if eq .app.harness.database.type "postgres" }} - mountPath: /dev/shm name: dshm @@ -92,11 +87,6 @@ spec: medium: Memory name: dshm {{- end }} - {{- if .root.Values.backup.active }} - - name: "db-backups" - persistentVolumeClaim: - claimName: "db-backups" - {{- end }} --- {{- if .root.Values.backup.active }} {{- include (print "deploy_utils.database." .app.harness.database.type ".backup") . }} diff --git a/deployment-configuration/helm/templates/auto-gatekeepers.yaml b/deployment-configuration/helm/templates/auto-gatekeepers.yaml index 47815797..898995cd 100644 --- a/deployment-configuration/helm/templates/auto-gatekeepers.yaml +++ b/deployment-configuration/helm/templates/auto-gatekeepers.yaml @@ -18,8 +18,12 @@ data: enable-default-deny: {{ eq (.app.harness.secured | toString) "true" }} listen: 0.0.0.0:8080 enable-refresh-tokens: true - server-write-timeout: 180s - upstream-response-header-timeout: 180s + server-write-timeout: {{ .app.harness.proxy.timeout.send | default .root.Values.proxy.timeout.send | default 180 }}s + upstream-timeout: {{ .app.harness.proxy.timeout.read | default .root.Values.proxy.timeout.read | default 180 }}s + upstream-response-header-timeout: {{ .app.harness.proxy.timeout.read | default .root.Values.proxy.timeout.read | default 180 }}s + upstream-expect-continue-timeout: {{ .app.harness.proxy.timeout.read | default .root.Values.proxy.timeout.read | default 180 }}s + server-read-timeout: {{ .app.harness.proxy.timeout.read | default .root.Values.proxy.timeout.read | default 180 }}s + upstream-keepalive-timeout: {{ .app.harness.proxy.timeout.keepalive | default .root.Values.proxy.timeout.keepalive | default 180 }}s http-only-cookie: false tls-cert: tls-private-key: @@ -65,7 +69,7 @@ data:

403 Permission Denied

Sorry, you do not have access to this page, please contact your administrator. - If you have been assigned new authorizations try to login again. + If you have been assigned new authorizations, try to refresh the page or to login again.
diff --git a/deployment-configuration/helm/templates/ingress.yaml b/deployment-configuration/helm/templates/ingress.yaml index af59d0a4..38b568a2 100644 --- a/deployment-configuration/helm/templates/ingress.yaml +++ b/deployment-configuration/helm/templates/ingress.yaml @@ -38,10 +38,13 @@ metadata: cert-manager.io/issuer: {{ printf "%s-%s" "letsencrypt" .Values.namespace }} {{- end }} nginx.ingress.kubernetes.io/ssl-redirect: {{ (and $tls .Values.ingress.ssl_redirect) | quote }} - nginx.ingress.kubernetes.io/proxy-body-size: '250m' + nginx.ingress.kubernetes.io/proxy-body-size: '{{ .Values.proxy.payload.max }}m' nginx.ingress.kubernetes.io/proxy-buffer-size: '128k' nginx.ingress.kubernetes.io/from-to-www-redirect: 'true' nginx.ingress.kubernetes.io/rewrite-target: /$1 + nginx.ingress.kubernetes.io/auth-keepalive-timeout: {{ .Values.proxy.timeout.keepalive | quote }} + nginx.ingress.kubernetes.io/proxy-read-timeout: {{ .Values.proxy.timeout.read | quote }} + nginx.ingress.kubernetes.io/proxy-send-timeout: {{ .Values.proxy.timeout.send | quote }} spec: rules: {{- range $app := .Values.apps }} diff --git a/deployment-configuration/helm/values.yaml b/deployment-configuration/helm/values.yaml index 6cf16d1e..434dcac7 100644 --- a/deployment-configuration/helm/values.yaml +++ b/deployment-configuration/helm/values.yaml @@ -67,3 +67,13 @@ backup: memory: "64Mi" # -- K8s cpu resource definition. cpu: "50m" +proxy: + timeout: + # -- Timeout for proxy connections in seconds. + send: 60 + # -- Timeout for proxy responses in seconds. + read: 60 + keepalive: 60 + payload: + # -- Maximum size of payload in MB + max: 250 diff --git a/deployment-configuration/value-template.yaml b/deployment-configuration/value-template.yaml index 16c00b3a..8702e6f6 100644 --- a/deployment-configuration/value-template.yaml +++ b/deployment-configuration/value-template.yaml @@ -22,6 +22,8 @@ harness: - administrator - uri: /api/openapi.json white-listed: true + - uri: /openapi.json + white-listed: true # -- Defines reference deployment parameters. Values maps to k8s spec deployment: # -- When true, enables automatic deployment @@ -125,3 +127,13 @@ harness: smoketest: true ignoreConsoleErrors: false ignoreRequestErrors: false + proxy: + timeout: + # -- Timeout for proxy connections in seconds. + send: + # -- Timeout for proxy responses in seconds. + read: + keepalive: + payload: + # -- Maximum size of payload in MB + max: \ No newline at end of file diff --git a/docs/accounts.md b/docs/accounts.md index 8b52ed2f..8d07ca4a 100644 --- a/docs/accounts.md +++ b/docs/accounts.md @@ -62,7 +62,22 @@ harness: secured: open ``` +#### Proxy specific configurations +Proxy configurations can be personalized in the application in the case that we want to have more restrictive values than the global ones (see [here](./ingress-domains-proxies.md#proxy-configurations) for more ) +```yaml +harness: + proxy: + timeout: + # -- Timeout for proxy connections in seconds. + send: + # -- Timeout for proxy responses in seconds. + read: + keepalive: + payload: + # -- Maximum size of payload in MB + max: +``` ### Secure an enpoint with OpenAPI In every api endpoint that you want to secure, add the bearerAuth security as in the example: diff --git a/docs/applications/databases.md b/docs/applications/databases.md index 6aa6af13..89bd2868 100644 --- a/docs/applications/databases.md +++ b/docs/applications/databases.md @@ -111,14 +111,49 @@ Per default, database backups are disabled. However, you can overwrite backups b ```yaml backup: - active: true + active: true ``` +See all the default values [here](../../deployment-configuration/helm/values.yaml). You can find additional configuration fields for backups to overwrite in the generated `deployment/helm/values.yaml` once you deploy your applications. Backups are defined for `mongo` and `postgres` database in form of a [K8s CronJob](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/) that creates a dump of the database and stores it in a different persistent volume on the same cluster. -This is done periodically according to a configurable schedule, per default once a day. +This is done periodically according to a configurable schedule, per default every 5 minutes. + +A smart retention strategy is used for backups, by default: +- all current days backups +- one per day, last 7 days +- one per week, last 4 weeks +- one per month, last 6 months + +Implementation of backups and retention is based on https://github.com/prodrigestivill/docker-postgres-backup-local. + +#### How to monitor and restore backups + +Backups are stored in a Kubernetes volume named `db-backups`. + +Can mount the volume to your database pod by adding the following to your db deployment: + +```yaml +... +spec: + template: + spec: + containers: + - ... + volumeMounts: + - name: "db-backups" + mountPath: /backups + readOnly: true + ... + volumes: + ... + - name: "db-backups" + persistentVolumeClaim: + claimName: "db-backups" +``` + ### MongoDB diff --git a/docs/ingress-domains-proxies.md b/docs/ingress-domains-proxies.md new file mode 100644 index 00000000..49903818 --- /dev/null +++ b/docs/ingress-domains-proxies.md @@ -0,0 +1,69 @@ +# Ingress, domains and proxies + +## Default configurations for domain and subdomains +Cloud Harness makes it very easy to configure domains and proxies, by making +an underlying assumption: + +- Applications share a main base domain (say ch.org) +- Applications can define a subdomain (say myapp) + +The main domain is configured in the [root values file](../deployment-configuration/values-template.yaml) and +it is usually overridden by the `harness-deployment` command, e.g. + +``` +harness-deployment ... -d ch.org +``` + +The subdomain is defined in the application's values.yaml file in +harness.subdomain (see for instance the [samples application configuration](../applications/samples/deploy/values.yaml)) + +For instance on applications/myapp/deploy/values.yaml: + +```yaml +harness: + subdomain: myapp +``` + +The above configurations put together create an ingress configuration for https://myapp.ch.org and automatically configure letsencrypt to create and renew certificates. + +Note: +that the tls and letsencrypt configurations are enabled by default but should usually be disabled locally with + +``` +harness-deployment ... -dtls -l +``` + +## Main application + +The "main" application is deployed on the base domain. +In order to specify a main application, override the value in your `/deployment-configuration/values-template.yaml` file. + +Example +```yaml +mainapp: myapp +``` +This creates a reverse proxy to https://ch.org pointing to myapp + +## Proxy configurations + +Ingress is a reverse proxy and as such has some configurations to take into account. +The most common configurations are connection timeouts and payload size. + +To configure it, override the following values in your `deployment-configuration/values-template.yaml` file. + +```yaml +proxy: + timeout: + # -- Timeout for proxy connections in seconds. + send: 60 + # -- Timeout for proxy responses in seconds. + read: 60 + keepalive: 60 + payload: + # -- Maximum size of payload in MB + max: 250 +``` + +Note that in the case that gatekeepers are enabled, the same configurations are applied +to the gatekeepers, unless the application override them on `harness.proxy.*`. +See also the [gatekeepers documentation](./accounts.md#secure-and-enpoint-with-the-gatekeeper). \ No newline at end of file diff --git a/docs/model/ApplicationHarnessConfig.md b/docs/model/ApplicationHarnessConfig.md index a83f853a..8e8a251d 100644 --- a/docs/model/ApplicationHarnessConfig.md +++ b/docs/model/ApplicationHarnessConfig.md @@ -30,6 +30,7 @@ Key | Input Type | Accessed Type | Description | Notes **jupyterhub** | [**JupyterHubConfig**](JupyterHubConfig.md) | [**JupyterHubConfig**](JupyterHubConfig.md) | | [optional] **accounts** | [**ApplicationAccountsConfig**](ApplicationAccountsConfig.md) | [**ApplicationAccountsConfig**](ApplicationAccountsConfig.md) | | [optional] **test** | [**ApplicationTestConfig**](ApplicationTestConfig.md) | [**ApplicationTestConfig**](ApplicationTestConfig.md) | | [optional] +**quotas** | [**Quota**](Quota.md) | [**Quota**](Quota.md) | | [optional] **any_string_name** | dict, frozendict.frozendict, str, date, datetime, uuid.UUID, int, float, decimal.Decimal, bool, None, list, tuple, bytes, io.FileIO, io.BufferedReader, | frozendict.frozendict, str, decimal.Decimal, BoolClass, NoneClass, tuple, bytes, FileIO | any string name can be used but the value must be the correct type | [optional] # aliases diff --git a/docs/model/HarnessMainConfig.md b/docs/model/HarnessMainConfig.md index d7d72df1..9f00bc2b 100644 --- a/docs/model/HarnessMainConfig.md +++ b/docs/model/HarnessMainConfig.md @@ -21,7 +21,7 @@ Key | Input Type | Accessed Type | Description | Notes **backup** | [**BackupConfig**](BackupConfig.md) | [**BackupConfig**](BackupConfig.md) | | [optional] **name** | str, | str, | Base name | [optional] **task-images** | [**SimpleMap**](SimpleMap.md) | [**SimpleMap**](SimpleMap.md) | | [optional] -**any_string_name** | dict, frozendict.frozendict, str, date, datetime, int, float, bool, decimal.Decimal, None, list, tuple, bytes, io.FileIO, io.BufferedReader | frozendict.frozendict, str, BoolClass, decimal.Decimal, NoneClass, tuple, bytes, FileIO | any string name can be used but the value must be the correct type | [optional] +**any_string_name** | dict, frozendict.frozendict, str, date, datetime, uuid.UUID, int, float, decimal.Decimal, bool, None, list, tuple, bytes, io.FileIO, io.BufferedReader, | frozendict.frozendict, str, decimal.Decimal, BoolClass, NoneClass, tuple, bytes, FileIO | any string name can be used but the value must be the correct type | [optional] # env diff --git a/docs/model/Quota.md b/docs/model/Quota.md index ddf78dea..8124fe92 100644 --- a/docs/model/Quota.md +++ b/docs/model/Quota.md @@ -8,7 +8,7 @@ dict, frozendict.frozendict, | frozendict.frozendict, | | ### Dictionary Keys Key | Input Type | Accessed Type | Description | Notes ------------ | ------------- | ------------- | ------------- | ------------- -**any_string_name** | str, | str, | any string name can be used but the value must be the correct type | [optional] +**any_string_name** | dict, frozendict.frozendict, str, date, datetime, uuid.UUID, int, float, decimal.Decimal, bool, None, list, tuple, bytes, io.FileIO, io.BufferedReader, | frozendict.frozendict, str, decimal.Decimal, BoolClass, NoneClass, tuple, bytes, FileIO | any string name can be used but the value must be the correct type | [optional] [[Back to Model list]](../../README.md#documentation-for-models) [[Back to API list]](../../README.md#documentation-for-api-endpoints) [[Back to README]](../../README.md) diff --git a/docs/model/SimpleMap.md b/docs/model/SimpleMap.md index 4106066b..37074b56 100644 --- a/docs/model/SimpleMap.md +++ b/docs/model/SimpleMap.md @@ -8,7 +8,7 @@ dict, frozendict.frozendict, | frozendict.frozendict, | | ### Dictionary Keys Key | Input Type | Accessed Type | Description | Notes ------------ | ------------- | ------------- | ------------- | ------------- -**any_string_name** | str, | str, | any string name can be used but the value must be the correct type | [optional] +**any_string_name** | dict, frozendict.frozendict, str, date, datetime, uuid.UUID, int, float, decimal.Decimal, bool, None, list, tuple, bytes, io.FileIO, io.BufferedReader, | frozendict.frozendict, str, decimal.Decimal, BoolClass, NoneClass, tuple, bytes, FileIO | any string name can be used but the value must be the correct type | [optional] [[Back to Model list]](../../README.md#documentation-for-models) [[Back to API list]](../../README.md#documentation-for-api-endpoints) [[Back to README]](../../README.md) diff --git a/docs/testing.md b/docs/testing.md index 11fd8349..27356fea 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -108,16 +108,7 @@ The test can use environmental variables: Examples: - [Sample api test](../applications/samples/test/api/test_st.py) -### Common smoke tests - -Once a test is created for your application, generic smoke tests are also -executed, checking for: -- Main page is reachable -- No errors in the console -- No error responses from network resources and fetch requests (code < 400) - -The smoke tests is defined [in this file](../test/test-e2e/__tests__/common.spec.ts). @@ -125,6 +116,22 @@ The smoke tests is defined [in this file](../test/test-e2e/__tests__/common.spec End to end tests run in a headless browser ([Puppeteer](https://github.com/puppeteer/puppeteer)) against the full deployment on Kubernetes. +Custom configuration: + +```yaml +harness: + ... + test: + e2e: + # -- enable/disable e2e tests + enabled: true + # -- ignore errors on console by default + ignoreConsoleErrors: false + # -- ignore fetched resources errors by default + ignoreRequestErrors: false + # -- enable common smoke tests + smoketest: true +``` ### Write tests with Jest and Puppeteer @@ -159,7 +166,7 @@ executed, checking for: - No errors in the console - No error responses from network resources and fetch requests (code < 400) -The smoke tests is defined [in this file](../test/jest-puppeteer/__tests__/common.spec.ts). +The smoke tests are defined [in this file](../test/jest-puppeteer/__tests__/common.spec.ts). ## Run API and E2E tests in the CI/CD pipeline @@ -182,7 +189,7 @@ deployment. In order to use `harness-test` install the library with ``` -pip install -r requirements-test.txt +pip install -e tools/cloudharness-test ``` In order to run tests against an existing deployment based on a domain (say, my.domain), run: @@ -198,6 +205,25 @@ If you want to run the deployment locally and then run the tests, can use skaffo 1. Wait the deployment to settle 1. Run `harness-test PATHS` +### Tests development +The `harness-test` client is useful while developing and tweaking the tests. +In that case it's better to target the application under development and +the kind of tests we are working on. + + +To target a specific application for end-to-end tests, use: +``` +harness-test . -i [APPNAME] -e +``` + +To target a specific application for api tests, use: +``` +harness-test . -i [APPNAME] -a +``` + +Note that the local version of the openapi.yaml file located at applications/[APPNAME]/api/openapi.yaml is used if available. That's useful to tweak examples and responses +used by schemathesis to generate the test hypotheses. + ## Create test users for your application To create test users: diff --git a/docs/tutorials/cloud-harness-wsl2-setup.md b/docs/tutorials/cloud-harness-wsl2-setup.md new file mode 100644 index 00000000..41f839d1 --- /dev/null +++ b/docs/tutorials/cloud-harness-wsl2-setup.md @@ -0,0 +1,70 @@ + +### The first thing to check + +> Make sure you have JDK installed in your WSL system. Without this, the code is not generated! (with harness-application command). A bunch of errors that I had was solved by this step. In addition, test if you have all the basic requirements needed for the Cloud-Harness + other requirements needed to set up. + +Please check if you are running **Kubernetes with Docker Desktop.** + +If you have Codecloud VS extension, make sure to check that you have docker-desktop ACTIVE. You can also check the namespace which by default is set to `default` + +### Setting namespaces and with `docker-desktop` context + +> The commands related to minikube from the tutorial are not needed in the case of docker-desktop. + +We create namespaces inside docker-desktop… for our case of following the tutorial, it is azathoth + +``` +kubectl create ns azathoth +``` + +If want to change the namespace while keeping the context same (i.e. docker-desktop) then the following is the command. This is because the default namespace is `default` + +``` +kubectl config set-context --current --namespace=azathoth +``` + +### Deployment command distinction + +Use the following command when you want your application installed inside the cloud-harness directory (very less likely; that one would use this) + +``` +harness-deployment . -u -dtls -l -d azathoth.local -e local -n azathoth +``` + +Otherwise (mostly how we use it) + +``` +harness-deployment cloud-harness . -u -dtls -l -d azathoth.local -e local -n azathoth +``` + +### About host file issue + +To be able to visit links like - [http://clockdate.azathoth.local/](http://clockdate.azathoth.local/), or [http://clockdate.azathoth.local/api/ping](http://clockdate.azathoth.local/api/ping), we need to update the host file. Make note that the host file is of Windows (and not for the subsystem), which can be found here: + +``` +C:\Windows\System32\drivers\etc\hosts +``` + +(NOTE: We might be able to work with Linux hosts if we enable - *_Add the .docker.internal names to the host's etc/hosts file (Requires password)_ in the General settings of docker desktop. But I haven’t tested that. ) + +### If Running CLOUD HARNESS for the first time! + +There are certain scripts that need to be run when running the CloudHarness and its dependent applications for the first time in a system. These are `sc.yaml` and `cluster-init.sh` which can be found in the following path w.r.t. cloud-harness. + +``` +kubectl apply -f cloudharness/deployment/sc.yaml +``` + +``` +cd cloud-harness/infrastructure/cluster-configuration && bash cluster-init.sh +``` + +These are one-time setup scripts needed to be run. + +### About Frontend issues + +If the [clockdate.azathoth.local](http://clockdate.azathoth.local/) doesn’t return the frontend part! + +When the frontend is generated through the “harness-application”, the package-lock.json is required which is not mentioned in the docs. Therefore one must have to do the `npm i —legacy-peer-deps` before proceeding with the “scaffold run” command. + + diff --git a/docs/tutorials/simple-date-clock-application.adoc b/docs/tutorials/simple-date-clock-application.adoc index 66208042..8ed52a0e 100644 --- a/docs/tutorials/simple-date-clock-application.adoc +++ b/docs/tutorials/simple-date-clock-application.adoc @@ -71,9 +71,15 @@ If everything is good, you're in the right path to use {ch}! == How to setup your local cluster using minikube (if not done yet) + {ch} is designed to help you generate all the required artifacts for a deployment on a Kubernetes cluster. For a local deployment, you can use minikube, which is basically https://minikube.sigs.k8s.io/docs/start/[a small {kub} cluster on your machine]. + +[IMPORTANT - WSL2] +If you're following this tutorial in a Windows WSL2 - then check => link:./cloud-harness-wsl2-setup.md[cloud-harness-wsl2-setup] for help! + + [IMPORTANT] The setup of a {kub} cluster is not mandatory to use {ch}. This step is proposed in this tutorial to show you how to deploy an application generated with {ch} on a local cluster and to appreciate how much {ch} reduces the pain of writing deployment configuration artifacts. @@ -147,7 +153,7 @@ If you don't create the namespace, the deployment will fail! [source,bash] ---- # ran from the cloud-harness repository root -harness-deployment . -u -dtls -l -d azathoth.local -e local -n azathoth.local +harness-deployment . -u -dtls -l -d azathoth.local -e local -n azathoth ---- In the state of the repository I have on my machine, the apps and services that will be deployed and that `harness-deployment` generated the configuration for are: @@ -307,6 +313,8 @@ The file tree should now be the following. Now you can build/deploy/run it using `skaffold`. +Before running `skaffold run` go inside the newly created application using harness-application; and make sure the frontend for the application contains package-lock.json. If not then install the dependencies by running `npm i --legacy-peer-deps`. + .Building/deploying/running the webapp with skaffold [source,bash] ---- diff --git a/libraries/cloudharness-common/cloudharness/auth/keycloak.py b/libraries/cloudharness-common/cloudharness/auth/keycloak.py index eb51fb15..c86aed3a 100644 --- a/libraries/cloudharness-common/cloudharness/auth/keycloak.py +++ b/libraries/cloudharness-common/cloudharness/auth/keycloak.py @@ -442,13 +442,19 @@ def get_user(self, user_id, with_details=False) -> User: raise UserNotFound(user_id) except InvalidToken as e: raise UserNotFound(user_id) + else: - found_users = admin_client.get_users({"username": user_id}) + found_users = admin_client.get_users({"username": user_id, "exact": True}) if len(found_users) == 0: raise UserNotFound(user_id) - user = admin_client.get_user(found_users[0]['id']) # Load full data - + try: + user = admin_client.get_user(found_users[0]['id']) # Load full data + except KeycloakGetError as e: + raise UserNotFound(user_id) + except InvalidToken as e: + raise UserNotFound(user_id) + user.update({ "userGroups": admin_client.get_user_groups(user_id=user['id'], brief_representation=not with_details), 'realmRoles': admin_client.get_realm_roles_of_user(user['id']) diff --git a/libraries/cloudharness-common/cloudharness/auth/quota.py b/libraries/cloudharness-common/cloudharness/auth/quota.py index a4106ee8..00137ef4 100644 --- a/libraries/cloudharness-common/cloudharness/auth/quota.py +++ b/libraries/cloudharness-common/cloudharness/auth/quota.py @@ -1,3 +1,4 @@ +import re from keycloak import KeycloakError from .keycloak import AuthClient from cloudharness.applications import get_current_configuration @@ -93,10 +94,12 @@ def _compute_quotas_from_tree(node: QuotaNode): child_attrs = _compute_quotas_from_tree(child) for key in child_attrs: try: - child_val = float(child_attrs[key]) + # we expect all quota values to be numbers: the unit is implicit and + # defined at usage time + child_val = attribute_to_quota(child_attrs[key]) except: - # value not a float, skip (use 0) - child_val = 0 + # value not a float, skip + continue if not key in new_attrs or new_attrs[key] < child_val: new_attrs.update({key: child_val}) for key in new_attrs: @@ -104,6 +107,10 @@ def _compute_quotas_from_tree(node: QuotaNode): return node.attrs +def attribute_to_quota(attr_value: str): + return float(re.sub("[^0-9.]", "", attr_value) if type(attr_value) is str else attr_value) + + def get_user_quotas(application_config: ApplicationConfig = None, user_id: str = None) -> dict: """Get the user quota from Keycloak and application @@ -142,5 +149,5 @@ def get_user_quotas(application_config: ApplicationConfig = None, user_id: str = user_quotas.update({key: group_quotas[key]}) for key in base_quotas: if key not in user_quotas: - user_quotas.update({key: base_quotas[key]}) + user_quotas.update({key: attribute_to_quota(base_quotas[key])}) return user_quotas diff --git a/libraries/models/api/openapi.yaml b/libraries/models/api/openapi.yaml index a2d8400e..577bff53 100644 --- a/libraries/models/api/openapi.yaml +++ b/libraries/models/api/openapi.yaml @@ -1,839 +1,848 @@ ---- openapi: 3.0.2 info: - title: cloudharness - version: 1.0.0 + title: cloudharness + version: 1.0.0 components: - schemas: - AutoArtifactSpec: - description: "" - required: - - auto - type: object - properties: - auto: - description: "When true, enables automatic template" - type: boolean - name: - description: "" - type: string - UriRoleMappingConfig: - description: - "Defines the application Gatekeeper configuration, if enabled (i.e.\ - \ `secured: true`." - required: - - roles - - uri - type: object - properties: - uri: - $ref: "#/components/schemas/PathSpecifier" - description: Path to secure - roles: - description: Roles allowed to access the present uri - type: array - items: - type: string - ServiceAutoArtifactConfig: - description: "" - type: object - allOf: - - type: object - properties: - port: - description: Service port - type: integer - - $ref: "#/components/schemas/AutoArtifactSpec" - ApplicationDependenciesConfig: - description: "" - type: object - properties: - hard: - description: - Hard dependencies indicate that the application may not start - without these other applications. - type: array - items: - type: string - soft: - description: - Soft dependencies indicate that the application will work partially - without these other applications. - type: array - items: - type: string - build: - description: - Hard dependencies indicate that the application Docker image - build requires these base/common images - type: array - items: + schemas: + AutoArtifactSpec: + description: '' + required: + - auto + type: object + properties: + auto: + description: 'When true, enables automatic template' + type: boolean + name: + description: '' + type: string + UriRoleMappingConfig: + description: 'Defines the application Gatekeeper configuration, if enabled (i.e. `secured: true`.' + required: + - roles + - uri + type: object + properties: + uri: + $ref: '#/components/schemas/PathSpecifier' + description: Path to secure + roles: + description: Roles allowed to access the present uri + type: array + items: + type: string + ServiceAutoArtifactConfig: + description: '' + type: object + allOf: + - + type: object + properties: + port: + description: Service port + type: integer + - + $ref: '#/components/schemas/AutoArtifactSpec' + ApplicationDependenciesConfig: + description: '' + type: object + properties: + hard: + description: >- + Hard dependencies indicate that the application may not start without these other + applications. + type: array + items: + type: string + soft: + description: >- + Soft dependencies indicate that the application will work partially without these + other applications. + type: array + items: + type: string + build: + description: >- + Hard dependencies indicate that the application Docker image build requires these + base/common images + type: array + items: + type: string + DeploymentResourcesConf: + description: '' + type: object + properties: + requests: + $ref: '#/components/schemas/CpuMemoryConfig' + description: '' + limits: + $ref: '#/components/schemas/CpuMemoryConfig' + description: '' + CpuMemoryConfig: + description: '' + type: object + properties: + cpu: + description: '' + type: string + memory: + description: '' + type: string + FileResourcesConfig: + description: '' + required: + - name + - src + - dst + type: object + properties: + name: + $ref: '#/components/schemas/Filename' + description: '' + src: + $ref: '#/components/schemas/Filename' + description: '' + dst: + description: '' + type: string + ApplicationProbe: + description: >- + Define a Kubernetes probe See also the + + [official + documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) + required: + - path + type: object + properties: + path: + $ref: '#/components/schemas/URL' + description: '' + periodSeconds: + description: '' + type: number + failureThreshold: + description: '' + type: number + initialDelaySeconds: + description: '' + type: number + URL: + description: '' type: string - DeploymentResourcesConf: - description: "" - type: object - properties: - requests: - $ref: "#/components/schemas/CpuMemoryConfig" - description: "" - limits: - $ref: "#/components/schemas/CpuMemoryConfig" - description: "" - CpuMemoryConfig: - description: "" - type: object - properties: - cpu: - description: "" - type: string - memory: - description: "" - type: string - FileResourcesConfig: - description: "" - required: - - name - - src - - dst - type: object - properties: - name: - $ref: "#/components/schemas/Filename" - description: "" - src: - $ref: "#/components/schemas/Filename" - description: "" - dst: - description: "" - type: string - ApplicationProbe: - description: |- - Define a Kubernetes probe See also the - [official documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) - required: - - path - type: object - properties: - path: - $ref: "#/components/schemas/URL" - description: "" - periodSeconds: - description: "" - type: number - failureThreshold: - description: "" - type: number - initialDelaySeconds: - description: "" - type: number - URL: - description: "" - type: string - ApplicationConfig: - description: Place here the values to configure your application helm templates. - required: - - harness - type: object - properties: - harness: - $ref: "#/components/schemas/ApplicationHarnessConfig" - description: - Values inside this section have a special meaning to cloudharness - (e.g. enabling and configuring automatic deployment) - additionalProperties: true - HarnessMainConfig: - description: "" - required: - - local - - secured_gatekeepers - - domain - - namespace - - mainapp - - apps - type: object - properties: - local: - description: "If set to true, local DNS mapping is added to pods." - type: boolean - secured_gatekeepers: - description: - Enables/disables Gatekeepers on secured applications. Set to - false for testing/development - type: boolean - domain: - description: The root domain - type: string - example: The root domain. - namespace: - description: The K8s namespace. - type: string - mainapp: - description: Defines the app to map to the root domain - type: string - registry: - $ref: "#/components/schemas/RegistryConfig" - description: "" - tag: - description: Docker tag used to push/pull the built images. - type: string - apps: - $ref: "#/components/schemas/ApplicationsConfigsMap" - description: "" - env: - description: Environmental variables added to all pods - type: array - items: - $ref: "#/components/schemas/NameValue" - privenv: - $ref: "#/components/schemas/NameValue" - description: Private environmental variables added to all pods - backup: - $ref: "#/components/schemas/BackupConfig" - description: "" - name: - description: Base name - type: string - task-images: - $ref: "#/components/schemas/SimpleMap" - description: "" - RegistryConfig: - description: "" - required: - - name - type: object - properties: - name: - $ref: "#/components/schemas/URL" - description: The docker registry where built images are pushed - secret: - description: Optional secret used for pulling from docker registry. - type: string - SimpleMap: - description: "" - type: object - additionalProperties: - type: string - FreeObject: - description: "" - type: object - additionalProperties: true - DatabaseDeploymentConfig: - description: "" - type: object - allOf: - - type: object - properties: - type: - description: |- - Define the database type. + ApplicationConfig: + description: Place here the values to configure your application helm templates. + required: + - harness + type: object + properties: + harness: + $ref: '#/components/schemas/ApplicationHarnessConfig' + description: >- + Values inside this section have a special meaning to cloudharness (e.g. enabling and + configuring automatic deployment) + additionalProperties: true + RegistryConfig: + description: '' + required: + - name + type: object + properties: + name: + $ref: '#/components/schemas/URL' + description: The docker registry where built images are pushed + secret: + description: Optional secret used for pulling from docker registry. + type: string + FreeObject: + description: '' + type: object + additionalProperties: true + DatabaseDeploymentConfig: + description: '' + type: object + allOf: + - + type: object + properties: + type: + description: |- + Define the database type. - One of (mongo, postgres, neo4j, sqlite3) - pattern: ^(mongo|postgres|neo4j|sqlite3)$ - type: string - example: '"neo4j"' - size: - description: Specify database disk size - type: string - example: 1Gi - user: - description: database username - type: string - pass: - format: password - description: Database password - type: string - image_ref: - description: Used for referencing images from the build - type: string - example: "image_ref: myownpgimage" - mongo: - $ref: "#/components/schemas/FreeObject" - description: Mongo db specific configuration - postgres: - $ref: "#/components/schemas/FreeObject" - description: Postgres database specific configuration - neo4j: - description: Neo4j database specific configuration - resources: - $ref: "#/components/schemas/DeploymentResourcesConf" - description: Database deployment resources - - $ref: "#/components/schemas/AutoArtifactSpec" - ApplicationsConfigsMap: - description: "" - type: object - additionalProperties: - $ref: "#/components/schemas/ApplicationConfig" - NameValue: - description: "" - required: - - name - type: object - properties: - name: - description: "" - type: string - value: - description: "" - type: string - IngressConfig: - description: "" - type: object - allOf: - - type: object - properties: - ssl_redirect: - description: "" - type: boolean - letsencrypt: - description: "" - type: object - properties: + One of (mongo, postgres, neo4j, sqlite3) + pattern: ^(mongo|postgres|neo4j|sqlite3)$ + type: string + example: '"neo4j"' + size: + description: Specify database disk size + type: string + example: 1Gi + user: + description: database username + type: string + pass: + format: password + description: Database password + type: string + image_ref: + description: Used for referencing images from the build + type: string + example: 'image_ref: myownpgimage' + mongo: + $ref: '#/components/schemas/FreeObject' + description: Mongo db specific configuration + postgres: + $ref: '#/components/schemas/FreeObject' + description: Postgres database specific configuration + neo4j: + description: Neo4j database specific configuration + resources: + $ref: '#/components/schemas/DeploymentResourcesConf' + description: Database deployment resources + - + $ref: '#/components/schemas/AutoArtifactSpec' + ApplicationsConfigsMap: + description: '' + type: object + additionalProperties: + $ref: '#/components/schemas/ApplicationConfig' + NameValue: + description: '' + required: + - name + type: object + properties: + name: + description: '' + type: string + value: + description: '' + type: string + IngressConfig: + description: '' + type: object + allOf: + - + type: object + properties: + ssl_redirect: + description: '' + type: boolean + letsencrypt: + description: '' + type: object + properties: + email: + type: string + - + $ref: '#/components/schemas/AutoArtifactSpec' + BackupConfig: + description: '' + required: + - dir + - resources + type: object + properties: + active: + description: '' + type: boolean + keep_days: + description: '' + type: integer + keep_weeks: + description: '' + type: integer + keep_months: + description: '' + type: integer + schedule: + description: Cron expression + pattern: >- + /(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every + (\d+(ns|us|µs|ms|s|m|h))+)|((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7})/ + type: string + suffix: + description: The file suffix added to backup files + volumesize: + description: The volume size for backups (all backups share the same volume) + type: string + dir: + $ref: '#/components/schemas/Filename' + description: 'Target directory of backups, the mount point of the persistent volume.' + resources: + $ref: '#/components/schemas/DeploymentResourcesConf' + description: '' + UserGroup: + type: object + properties: + access: + type: object + additionalProperties: true + attributes: + $ref: '#/components/schemas/SimpleMap' + additionalProperties: true + clientRoles: + type: object + additionalProperties: true + id: + type: string + name: + type: string + path: + type: string + realmRoles: + type: array + items: + type: string + subGroups: + type: array + items: + $ref: '#/components/schemas/UserGroup' + UserCredential: + type: object + properties: + createdDate: + format: int64 + type: integer + credentialData: + type: string + id: + type: string + priority: + format: int32 + type: integer + secretData: + type: string + temporary: + type: boolean + type: + type: string + userLabel: + type: string + value: + type: string + User: + type: object + properties: + access: + type: object + additionalProperties: true + attributes: + type: object + additionalProperties: true + clientRoles: + type: object + additionalProperties: true + createdTimestamp: + format: int64 + type: integer + credentials: + type: array + items: + $ref: '#/components/schemas/UserCredential' + disableableCredentialTypes: + type: array + items: + type: string email: - type: string - - $ref: "#/components/schemas/AutoArtifactSpec" - BackupConfig: - description: "" - required: - - dir - - resources - type: object - properties: - active: - description: "" - type: boolean - keep_days: - description: "" - type: integer - keep_weeks: - description: "" - type: integer - keep_months: - description: "" - type: integer - schedule: - description: Cron expression - pattern: - "/(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every\ - \ (\\d+(ns|us|µs|ms|s|m|h))+)|((((\\d+,)+\\d+|(\\d+(\\/|-)\\d+)|\\d+|\\\ - *) ?){5,7})/" - type: string - suffix: - description: The file suffix added to backup files - volumesize: - description: The volume size for backups (all backups share the same volume) - type: string - dir: - $ref: "#/components/schemas/Filename" - description: - "Target directory of backups, the mount point of the persistent\ - \ volume." - resources: - $ref: "#/components/schemas/DeploymentResourcesConf" - description: "" - Quota: - description: "" - type: object - additionalProperties: - type: string - example: - quota-ws-max: 5 - quota-storage-max: 1G - UserGroup: - type: object - properties: - access: - type: object - additionalProperties: true - attributes: - $ref: "#/components/schemas/SimpleMap" - additionalProperties: true - clientRoles: - type: object - additionalProperties: true - id: - type: string - name: - type: string - path: - type: string - realmRoles: - type: array - items: - type: string - subGroups: - type: array - items: - $ref: "#/components/schemas/UserGroup" - UserCredential: - type: object - properties: - createdDate: - format: int64 - type: integer - credentialData: - type: string - id: - type: string - priority: - format: int32 - type: integer - secretData: - type: string - temporary: - type: boolean - type: - type: string - userLabel: - type: string - value: - type: string - User: - type: object - properties: - access: - type: object - additionalProperties: true - attributes: - type: object - additionalProperties: true - clientRoles: - type: object - additionalProperties: true - createdTimestamp: - format: int64 - type: integer - credentials: - type: array - items: - $ref: "#/components/schemas/UserCredential" - disableableCredentialTypes: - type: array - items: - type: string - email: - type: string - emailVerified: - type: boolean - enabled: - type: boolean - federationLink: - type: string - firstName: - type: string - groups: - type: array - items: - type: string - id: - type: string - lastName: - type: string - realmRoles: - type: array - items: + type: string + emailVerified: + type: boolean + enabled: + type: boolean + federationLink: + type: string + firstName: + type: string + groups: + type: array + items: + type: string + id: + type: string + lastName: + type: string + realmRoles: + type: array + items: + type: string + requiredActions: + type: array + items: + type: string + serviceAccountClientId: + type: string + username: + type: string + additionalProperties: {} + Filename: + description: '' + pattern: '^[^<>:;,?*|]+$' type: string - requiredActions: - type: array - items: + PathSpecifier: + description: '' + pattern: '^[^<>:;,?|]+$' type: string - serviceAccountClientId: - type: string - username: - type: string - additionalProperties: {} - Filename: - description: "" - pattern: "^[^<>:;,?*|]+$" - type: string - PathSpecifier: - description: "" - pattern: "^[^<>:;,?|]+$" - type: string - CDCEvent: - description: |- - A message sent to the orchestration queue. - Applications can listen to these events to react to data change events happening - on other applications. - required: - - message_type - - operation - - uid - - meta - type: object - properties: - operation: - description: the operation on the object e.g. create / update / delete - enum: - - create - - update - - delete - - other - type: string - uid: - description: the unique identifier attribute of the object - type: string - message_type: - description: the type of the message (relates to the object type) e.g. jobs - type: string - resource: - $ref: "#/components/schemas/FreeObject" - description: The target object - meta: - $ref: "#/components/schemas/CDCEventMeta" - description: "" - CDCEventMeta: - description: "" - required: - - app_name - type: object - properties: - app_name: - description: The name of the application/microservice sending the message - type: string - user: - $ref: "#/components/schemas/User" - description: "" - args: - description: the caller function arguments - type: array - items: - $ref: "#/components/schemas/FreeObject" - kwargs: - description: the caller function keyword arguments - description: - description: General description -- for human consumption - type: string - ApplicationHarnessConfig: - description: - "Define helm variables that allow CloudHarness to enable and configure\ - \ your \napplication's deployment" - required: [] - type: object - properties: - deployment: - $ref: "#/components/schemas/DeploymentAutoArtifactConfig" - description: - Defines reference deployment parameters. Values maps to k8s - spec - service: - $ref: "#/components/schemas/ServiceAutoArtifactConfig" - description: Defines automatic service parameters. - subdomain: - description: "If specified, an ingress will be created at [subdomain].[.Values.domain]" - type: string - aliases: - description: - "If specified, an ingress will be created at [alias].[.Values.domain]\ - \ for each alias" - type: array - items: - type: string - domain: - description: "If specified, an ingress will be created at [domain]" - type: string - dependencies: - $ref: "#/components/schemas/ApplicationDependenciesConfig" - description: - Application dependencies are used to define what is required - in the deployment when --include (-i) is used. Specify application names - in the list. - secured: - description: "When true, the application is shielded with a getekeeper" - type: boolean - uri_role_mapping: - description: - "Map uri/roles to secure with the Gatekeeper (if `secured:\ - \ true`)" - type: array - items: - $ref: "#/components/schemas/UriRoleMappingConfig" - secrets: - $ref: "#/components/schemas/SimpleMap" - description: |- - Define secrets will be mounted in the deployment + CDCEvent: + description: |- + A message sent to the orchestration queue. + Applications can listen to these events to react to data change events happening + on other applications. + required: + - message_type + - operation + - uid + - meta + type: object + properties: + operation: + description: the operation on the object e.g. create / update / delete + enum: + - create + - update + - delete + - other + type: string + uid: + description: the unique identifier attribute of the object + type: string + message_type: + description: the type of the message (relates to the object type) e.g. jobs + type: string + resource: + $ref: '#/components/schemas/FreeObject' + description: The target object + meta: + $ref: '#/components/schemas/CDCEventMeta' + description: '' + CDCEventMeta: + description: '' + required: + - app_name + type: object + properties: + app_name: + description: The name of the application/microservice sending the message + type: string + user: + $ref: '#/components/schemas/User' + description: '' + args: + description: the caller function arguments + type: array + items: + $ref: '#/components/schemas/FreeObject' + kwargs: + description: the caller function keyword arguments + description: + description: General description -- for human consumption + type: string + ApplicationHarnessConfig: + description: |- + Define helm variables that allow CloudHarness to enable and configure your + application's deployment + required: [] + type: object + properties: + deployment: + $ref: '#/components/schemas/DeploymentAutoArtifactConfig' + description: Defines reference deployment parameters. Values maps to k8s spec + service: + $ref: '#/components/schemas/ServiceAutoArtifactConfig' + description: Defines automatic service parameters. + subdomain: + description: 'If specified, an ingress will be created at [subdomain].[.Values.domain]' + type: string + aliases: + description: 'If specified, an ingress will be created at [alias].[.Values.domain] for each alias' + type: array + items: + type: string + domain: + description: 'If specified, an ingress will be created at [domain]' + type: string + dependencies: + $ref: '#/components/schemas/ApplicationDependenciesConfig' + description: >- + Application dependencies are used to define what is required in the deployment when + --include (-i) is used. Specify application names in the list. + secured: + description: 'When true, the application is shielded with a getekeeper' + type: boolean + uri_role_mapping: + description: 'Map uri/roles to secure with the Gatekeeper (if `secured: true`)' + type: array + items: + $ref: '#/components/schemas/UriRoleMappingConfig' + secrets: + $ref: '#/components/schemas/SimpleMap' + description: |- + Define secrets will be mounted in the deployment - Define as + Define as - ```yaml - secrets: - secret_name: 'value' + ```yaml + secrets: + secret_name: 'value' - ``` + ``` - Values if left empty are randomly generated - use_services: - description: - "Specify which services this application uses in the frontend\ - \ to create proxy ingresses. e.g. \n```\n- name: samples\n```" - type: array - items: - type: string - database: - $ref: "#/components/schemas/DatabaseDeploymentConfig" - description: "" - resources: - description: |- - Application file resources. Maps from deploy/resources folder and mounts as - configmaps - type: array - items: - $ref: "#/components/schemas/FileResourcesConfig" - readinessProbe: - $ref: "#/components/schemas/ApplicationProbe" - description: Kubernetes readiness probe configuration - startupProbe: - $ref: "#/components/schemas/ApplicationProbe" - description: "" - livenessProbe: - $ref: "#/components/schemas/ApplicationProbe" - description: Kubernetes liveness probe configuration - sourceRoot: - $ref: "#/components/schemas/Filename" - description: "" - name: - description: |- - Application's name. Do not edit, the value is automatically set from the - application directory's name - type: string - jupyterhub: - $ref: "#/components/schemas/JupyterHubConfig" - description: | - Configurations specific to jupyterhub. Edit only if your application is - configured as a jupyterhub deployment - accounts: - $ref: "#/components/schemas/ApplicationAccountsConfig" - description: Define specific test users and roles for this application - test: - $ref: "#/components/schemas/ApplicationTestConfig" - description: Enable and configure automated testing for this application. - additionalProperties: true - JupyterHubConfig: - description: "" - type: object - properties: - args: - description: arguments passed to the container - type: array - items: - type: string - extraConfig: - $ref: "#/components/schemas/SimpleMap" - description: - allows you to add Python snippets to the jupyterhub_config.py - file - spawnerExtraConfig: - $ref: "#/components/schemas/FreeObject" - description: - allows you to add values to the spawner object without the - need of creating a new hook - applicationHook: - description: - "change the hook function (advanced)\n\nSpecify the Python\ - \ name of the function (full module path, the module must be \ninstalled\ - \ in the Docker image)" - example: my_lib.change_pod_manifest - additionalProperties: true - UserRole: - type: object - properties: - attributes: - type: object - additionalProperties: true - clientRole: - type: boolean - composite: - type: boolean - containerId: - type: string - description: - type: string - id: - type: string - name: - type: string - additionalProperties: true - ApplicationAccountsConfig: - description: "" - type: object - properties: - roles: - description: - Specify roles to be created in this deployment specific for - this application - type: array - items: - type: string - users: - description: - "Defines test users to be added to the deployment, specific\ - \ for this application" - type: array - items: - $ref: "#/components/schemas/ApplicationUser" - ApplicationUser: - description: Defines a user - required: - - username - type: object - properties: - username: - description: "" - type: string - password: - format: password - description: "" - type: string - clientRoles: - description: "" - type: array - items: - type: string - realmRoles: - description: "" - type: array - items: - type: string - ApplicationTestConfig: - description: "" - required: - - unit - - e2e - - api - type: object - properties: - unit: - $ref: "#/components/schemas/UnitTestsConfig" - description: "" - api: - $ref: "#/components/schemas/ApiTestsConfig" - description: "" - e2e: - $ref: "#/components/schemas/E2ETestsConfig" - description: "" - UnitTestsConfig: - description: "" - required: - - enabled - - commands - type: object - properties: - enabled: - description: "Enables unit tests for this application (default: true)" - type: boolean - commands: - description: Commands to run unit tests - type: array - items: - type: string - example: '["pytest /usr/src/app/samples/test"]' - E2ETestsConfig: - description: "" - required: - - enabled - - smoketest - type: object - properties: - enabled: - description: - "Enables end to end testing for this application (default:\ - \ false)" - type: boolean - smoketest: - description: Specify whether to run the common smoke tests - type: boolean - ignoreConsoleErrors: - description: "" - type: boolean - ignoreRequestErrors: - description: "" - type: boolean - ApiTestsConfig: - description: "" - required: - - enabled - - autotest - - checks - type: object - properties: - enabled: - description: "Enables api tests for this application (default: false)" - type: boolean - autotest: - description: Specify whether to run the common smoke tests - type: boolean - runParams: - description: Additional schemathesis parameters - type: array - items: - type: string - checks: - description: |- - One of the Schemathesis checks: + Values if left empty are randomly generated + use_services: + description: >- + Specify which services this application uses in the frontend to create proxy + ingresses. e.g. - - not_a_server_error. The response has 5xx HTTP status; - - status_code_conformance. The response status is not defined in the API schema; - - content_type_conformance. The response content type is not defined in the API schema; - - response_schema_conformance. The response content does not conform to the schema defined for this specific response; - - response_headers_conformance. The response headers does not contain all defined headers. - type: array - items: - type: string - example: '["not_a_server_error", "status_code_conformance"]' - DeploymentAutoArtifactConfig: - description: "" - type: object - allOf: - - type: object - properties: - port: - description: Deployment port - type: string - replicas: - description: Number of replicas - type: integer - image: - description: |- - Image name to use in the deployment. Leave it blank to set from the application's - Docker file - pattern: "(?:[a-z]+/)?([a-z]+)(?::[0-9]+)?" - type: string - resources: - $ref: "#/components/schemas/DeploymentResourcesConf" - description: Deployment resources - volume: - $ref: "#/components/schemas/DeploymentVolumeSpec" - description: Volume specification - resources: - description: Deployment resources - type: string - test: - description: ssaa - type: string - - $ref: "#/components/schemas/AutoArtifactSpec" - DeploymentVolumeSpec: - description: |- - Defines a volume attached to the deployment. - Automatically created the volume claim and mounts. - type: object - allOf: - - required: - - mountpath - type: object - properties: - mountpath: - description: The mount path for the volume - type: string - size: - description: "The volume size. \n\nE.g. 5Gi" - usenfs: - description: - Set to `true` to use the nfs on the created volume and mount - as ReadWriteMany. - type: boolean - - $ref: "#/components/schemas/AutoArtifactSpec" - example: - auto: true - mountpath: /usr/src/app/persistent - name: my-files - size: 5Gi - usenfs: true + ``` + + - name: samples + + ``` + type: array + items: + type: string + database: + $ref: '#/components/schemas/DatabaseDeploymentConfig' + description: '' + resources: + description: |- + Application file resources. Maps from deploy/resources folder and mounts as + configmaps + type: array + items: + $ref: '#/components/schemas/FileResourcesConfig' + readinessProbe: + $ref: '#/components/schemas/ApplicationProbe' + description: Kubernetes readiness probe configuration + startupProbe: + $ref: '#/components/schemas/ApplicationProbe' + description: '' + livenessProbe: + $ref: '#/components/schemas/ApplicationProbe' + description: Kubernetes liveness probe configuration + sourceRoot: + $ref: '#/components/schemas/Filename' + description: '' + name: + description: |- + Application's name. Do not edit, the value is automatically set from the + application directory's name + type: string + jupyterhub: + $ref: '#/components/schemas/JupyterHubConfig' + description: | + Configurations specific to jupyterhub. Edit only if your application is + configured as a jupyterhub deployment + accounts: + $ref: '#/components/schemas/ApplicationAccountsConfig' + description: Define specific test users and roles for this application + test: + $ref: '#/components/schemas/ApplicationTestConfig' + description: Enable and configure automated testing for this application. + quotas: + $ref: '#/components/schemas/Quota' + description: '' + additionalProperties: true + JupyterHubConfig: + description: '' + type: object + properties: + args: + description: arguments passed to the container + type: array + items: + type: string + extraConfig: + $ref: '#/components/schemas/SimpleMap' + description: allows you to add Python snippets to the jupyterhub_config.py file + spawnerExtraConfig: + $ref: '#/components/schemas/FreeObject' + description: allows you to add values to the spawner object without the need of creating a new hook + applicationHook: + description: |- + change the hook function (advanced) + + Specify the Python name of the function (full module path, the module must be + installed in the Docker image) + example: my_lib.change_pod_manifest + additionalProperties: true + UserRole: + type: object + properties: + attributes: + type: object + additionalProperties: true + clientRole: + type: boolean + composite: + type: boolean + containerId: + type: string + description: + type: string + id: + type: string + name: + type: string + additionalProperties: true + ApplicationAccountsConfig: + description: '' + type: object + properties: + roles: + description: Specify roles to be created in this deployment specific for this application + type: array + items: + type: string + users: + description: 'Defines test users to be added to the deployment, specific for this application' + type: array + items: + $ref: '#/components/schemas/ApplicationUser' + ApplicationUser: + description: Defines a user + required: + - username + type: object + properties: + username: + description: '' + type: string + password: + format: password + description: '' + type: string + clientRoles: + description: '' + type: array + items: + type: string + realmRoles: + description: '' + type: array + items: + type: string + ApplicationTestConfig: + description: '' + required: + - unit + - e2e + - api + type: object + properties: + unit: + $ref: '#/components/schemas/UnitTestsConfig' + description: '' + api: + $ref: '#/components/schemas/ApiTestsConfig' + description: '' + e2e: + $ref: '#/components/schemas/E2ETestsConfig' + description: '' + UnitTestsConfig: + description: '' + required: + - enabled + - commands + type: object + properties: + enabled: + description: 'Enables unit tests for this application (default: true)' + type: boolean + commands: + description: Commands to run unit tests + type: array + items: + type: string + example: '["pytest /usr/src/app/samples/test"]' + E2ETestsConfig: + description: '' + required: + - enabled + - smoketest + type: object + properties: + enabled: + description: 'Enables end to end testing for this application (default: false)' + type: boolean + smoketest: + description: Specify whether to run the common smoke tests + type: boolean + ignoreConsoleErrors: + description: '' + type: boolean + ignoreRequestErrors: + description: '' + type: boolean + ApiTestsConfig: + description: '' + required: + - enabled + - autotest + - checks + type: object + properties: + enabled: + description: 'Enables api tests for this application (default: false)' + type: boolean + autotest: + description: Specify whether to run the common smoke tests + type: boolean + runParams: + description: Additional schemathesis parameters + type: array + items: + type: string + checks: + description: >- + One of the Schemathesis checks: + + + - not_a_server_error. The response has 5xx HTTP status; + + - status_code_conformance. The response status is not defined in the API schema; + + - content_type_conformance. The response content type is not defined in the API + schema; + + - response_schema_conformance. The response content does not conform to the schema + defined for this specific response; + + - response_headers_conformance. The response headers does not contain all defined + headers. + type: array + items: + type: string + example: '["not_a_server_error", "status_code_conformance"]' + DeploymentAutoArtifactConfig: + description: '' + type: object + allOf: + - + type: object + properties: + port: + description: Deployment port + type: string + replicas: + description: Number of replicas + type: integer + image: + description: >- + Image name to use in the deployment. Leave it blank to set from the + application's + + Docker file + pattern: '(?:[a-z]+/)?([a-z]+)(?::[0-9]+)?' + type: string + resources: + $ref: '#/components/schemas/DeploymentResourcesConf' + description: Deployment resources + volume: + $ref: '#/components/schemas/DeploymentVolumeSpec' + description: Volume specification + resources: + description: Deployment resources + type: string + test: + description: ssaa + type: string + - + $ref: '#/components/schemas/AutoArtifactSpec' + DeploymentVolumeSpec: + description: |- + Defines a volume attached to the deployment. + Automatically created the volume claim and mounts. + type: object + allOf: + - + required: + - mountpath + type: object + properties: + mountpath: + description: The mount path for the volume + type: string + size: + description: |- + The volume size. + + E.g. 5Gi + usenfs: + description: Set to `true` to use the nfs on the created volume and mount as ReadWriteMany. + type: boolean + - + $ref: '#/components/schemas/AutoArtifactSpec' + example: + auto: true + mountpath: /usr/src/app/persistent + name: my-files + size: 5Gi + usenfs: true + HarnessMainConfig: + description: '' + required: + - local + - secured_gatekeepers + - domain + - namespace + - mainapp + - apps + type: object + properties: + local: + description: 'If set to true, local DNS mapping is added to pods.' + type: boolean + secured_gatekeepers: + description: >- + Enables/disables Gatekeepers on secured applications. Set to false for + testing/development + type: boolean + domain: + description: The root domain + type: string + example: The root domain. + namespace: + description: The K8s namespace. + type: string + mainapp: + description: Defines the app to map to the root domain + type: string + registry: + $ref: '#/components/schemas/RegistryConfig' + description: '' + tag: + description: Docker tag used to push/pull the built images. + type: string + apps: + $ref: '#/components/schemas/ApplicationsConfigsMap' + description: '' + env: + description: Environmental variables added to all pods + type: array + items: + $ref: '#/components/schemas/NameValue' + privenv: + $ref: '#/components/schemas/NameValue' + description: Private environmental variables added to all pods + backup: + $ref: '#/components/schemas/BackupConfig' + description: '' + name: + description: Base name + type: string + task-images: + $ref: '#/components/schemas/SimpleMap' + description: '' + additionalProperties: true + SimpleMap: + description: '' + type: object + additionalProperties: true + Quota: + description: '' + type: object + additionalProperties: true + example: + quota-ws-max: 5 + quota-storage-max: 1G diff --git a/libraries/models/cloudharness_model/models/application_harness_config.py b/libraries/models/cloudharness_model/models/application_harness_config.py index 4a083624..ba287654 100644 --- a/libraries/models/cloudharness_model/models/application_harness_config.py +++ b/libraries/models/cloudharness_model/models/application_harness_config.py @@ -37,7 +37,7 @@ class ApplicationHarnessConfig(Model): Do not edit the class manually. """ - def __init__(self, deployment=None, service=None, subdomain=None, aliases=None, domain=None, dependencies=None, secured=None, uri_role_mapping=None, secrets=None, use_services=None, database=None, resources=None, readiness_probe=None, startup_probe=None, liveness_probe=None, source_root=None, name=None, jupyterhub=None, accounts=None, test=None): # noqa: E501 + def __init__(self, deployment=None, service=None, subdomain=None, aliases=None, domain=None, dependencies=None, secured=None, uri_role_mapping=None, secrets=None, use_services=None, database=None, resources=None, readiness_probe=None, startup_probe=None, liveness_probe=None, source_root=None, name=None, jupyterhub=None, accounts=None, test=None, quotas=None): # noqa: E501 """ApplicationHarnessConfig - a model defined in OpenAPI :param deployment: The deployment of this ApplicationHarnessConfig. # noqa: E501 @@ -57,7 +57,7 @@ def __init__(self, deployment=None, service=None, subdomain=None, aliases=None, :param uri_role_mapping: The uri_role_mapping of this ApplicationHarnessConfig. # noqa: E501 :type uri_role_mapping: List[UriRoleMappingConfig] :param secrets: The secrets of this ApplicationHarnessConfig. # noqa: E501 - :type secrets: Dict[str, str] + :type secrets: Dict[str, object] :param use_services: The use_services of this ApplicationHarnessConfig. # noqa: E501 :type use_services: List[str] :param database: The database of this ApplicationHarnessConfig. # noqa: E501 @@ -80,6 +80,8 @@ def __init__(self, deployment=None, service=None, subdomain=None, aliases=None, :type accounts: ApplicationAccountsConfig :param test: The test of this ApplicationHarnessConfig. # noqa: E501 :type test: ApplicationTestConfig + :param quotas: The quotas of this ApplicationHarnessConfig. # noqa: E501 + :type quotas: Dict[str, object] """ self.openapi_types = { 'deployment': DeploymentAutoArtifactConfig, @@ -90,7 +92,7 @@ def __init__(self, deployment=None, service=None, subdomain=None, aliases=None, 'dependencies': ApplicationDependenciesConfig, 'secured': bool, 'uri_role_mapping': List[UriRoleMappingConfig], - 'secrets': Dict[str, str], + 'secrets': Dict[str, object], 'use_services': List[str], 'database': DatabaseDeploymentConfig, 'resources': List[FileResourcesConfig], @@ -101,7 +103,8 @@ def __init__(self, deployment=None, service=None, subdomain=None, aliases=None, 'name': str, 'jupyterhub': JupyterHubConfig, 'accounts': ApplicationAccountsConfig, - 'test': ApplicationTestConfig + 'test': ApplicationTestConfig, + 'quotas': Dict[str, object] } self.attribute_map = { @@ -124,7 +127,8 @@ def __init__(self, deployment=None, service=None, subdomain=None, aliases=None, 'name': 'name', 'jupyterhub': 'jupyterhub', 'accounts': 'accounts', - 'test': 'test' + 'test': 'test', + 'quotas': 'quotas' } self._deployment = deployment @@ -147,6 +151,7 @@ def __init__(self, deployment=None, service=None, subdomain=None, aliases=None, self._jupyterhub = jupyterhub self._accounts = accounts self._test = test + self._quotas = quotas @classmethod def from_dict(cls, dikt) -> 'ApplicationHarnessConfig': @@ -344,7 +349,7 @@ def secrets(self): # noqa: E501 :return: The secrets of this ApplicationHarnessConfig. - :rtype: Dict[str, str] + :rtype: Dict[str, object] """ return self._secrets @@ -355,7 +360,7 @@ def secrets(self, secrets): # noqa: E501 :param secrets: The secrets of this ApplicationHarnessConfig. - :type secrets: Dict[str, str] + :type secrets: Dict[str, object] """ self._secrets = secrets @@ -600,3 +605,26 @@ def test(self, test): """ self._test = test + + @property + def quotas(self): + """Gets the quotas of this ApplicationHarnessConfig. + + # noqa: E501 + + :return: The quotas of this ApplicationHarnessConfig. + :rtype: Dict[str, object] + """ + return self._quotas + + @quotas.setter + def quotas(self, quotas): + """Sets the quotas of this ApplicationHarnessConfig. + + # noqa: E501 + + :param quotas: The quotas of this ApplicationHarnessConfig. + :type quotas: Dict[str, object] + """ + + self._quotas = quotas diff --git a/libraries/models/cloudharness_model/models/harness_main_config.py b/libraries/models/cloudharness_model/models/harness_main_config.py index d3b4af7f..c75db6d3 100644 --- a/libraries/models/cloudharness_model/models/harness_main_config.py +++ b/libraries/models/cloudharness_model/models/harness_main_config.py @@ -51,7 +51,7 @@ def __init__(self, local=None, secured_gatekeepers=None, domain=None, namespace= :param name: The name of this HarnessMainConfig. # noqa: E501 :type name: str :param task_images: The task_images of this HarnessMainConfig. # noqa: E501 - :type task_images: Dict[str, str] + :type task_images: Dict[str, object] """ self.openapi_types = { 'local': bool, @@ -66,7 +66,7 @@ def __init__(self, local=None, secured_gatekeepers=None, domain=None, namespace= 'privenv': NameValue, 'backup': BackupConfig, 'name': str, - 'task_images': Dict[str, str] + 'task_images': Dict[str, object] } self.attribute_map = { @@ -399,7 +399,7 @@ def task_images(self): # noqa: E501 :return: The task_images of this HarnessMainConfig. - :rtype: Dict[str, str] + :rtype: Dict[str, object] """ return self._task_images @@ -410,7 +410,7 @@ def task_images(self, task_images): # noqa: E501 :param task_images: The task_images of this HarnessMainConfig. - :type task_images: Dict[str, str] + :type task_images: Dict[str, object] """ self._task_images = task_images diff --git a/libraries/models/cloudharness_model/models/jupyter_hub_config.py b/libraries/models/cloudharness_model/models/jupyter_hub_config.py index 51316ff2..1f04fa73 100644 --- a/libraries/models/cloudharness_model/models/jupyter_hub_config.py +++ b/libraries/models/cloudharness_model/models/jupyter_hub_config.py @@ -21,7 +21,7 @@ def __init__(self, args=None, extra_config=None, spawner_extra_config=None, appl :param args: The args of this JupyterHubConfig. # noqa: E501 :type args: List[str] :param extra_config: The extra_config of this JupyterHubConfig. # noqa: E501 - :type extra_config: Dict[str, str] + :type extra_config: Dict[str, object] :param spawner_extra_config: The spawner_extra_config of this JupyterHubConfig. # noqa: E501 :type spawner_extra_config: Dict[str, object] :param application_hook: The application_hook of this JupyterHubConfig. # noqa: E501 @@ -29,7 +29,7 @@ def __init__(self, args=None, extra_config=None, spawner_extra_config=None, appl """ self.openapi_types = { 'args': List[str], - 'extra_config': Dict[str, str], + 'extra_config': Dict[str, object], 'spawner_extra_config': Dict[str, object], 'application_hook': object } @@ -87,7 +87,7 @@ def extra_config(self): # noqa: E501 :return: The extra_config of this JupyterHubConfig. - :rtype: Dict[str, str] + :rtype: Dict[str, object] """ return self._extra_config @@ -98,7 +98,7 @@ def extra_config(self, extra_config): # noqa: E501 :param extra_config: The extra_config of this JupyterHubConfig. - :type extra_config: Dict[str, str] + :type extra_config: Dict[str, object] """ self._extra_config = extra_config diff --git a/libraries/models/cloudharness_model/models/user_group.py b/libraries/models/cloudharness_model/models/user_group.py index 50d3b30d..72e135ef 100644 --- a/libraries/models/cloudharness_model/models/user_group.py +++ b/libraries/models/cloudharness_model/models/user_group.py @@ -21,7 +21,7 @@ def __init__(self, access=None, attributes=None, client_roles=None, id=None, nam :param access: The access of this UserGroup. # noqa: E501 :type access: Dict[str, object] :param attributes: The attributes of this UserGroup. # noqa: E501 - :type attributes: Dict[str, str] + :type attributes: Dict[str, object] :param client_roles: The client_roles of this UserGroup. # noqa: E501 :type client_roles: Dict[str, object] :param id: The id of this UserGroup. # noqa: E501 @@ -37,7 +37,7 @@ def __init__(self, access=None, attributes=None, client_roles=None, id=None, nam """ self.openapi_types = { 'access': Dict[str, object], - 'attributes': Dict[str, str], + 'attributes': Dict[str, object], 'client_roles': Dict[str, object], 'id': str, 'name': str, @@ -105,7 +105,7 @@ def attributes(self): # noqa: E501 :return: The attributes of this UserGroup. - :rtype: Dict[str, str] + :rtype: Dict[str, object] """ return self._attributes @@ -116,7 +116,7 @@ def attributes(self, attributes): # noqa: E501 :param attributes: The attributes of this UserGroup. - :type attributes: Dict[str, str] + :type attributes: Dict[str, object] """ self._attributes = attributes diff --git a/tools/cloudharness-test/cloudharness_test/api.py b/tools/cloudharness-test/cloudharness_test/api.py index d7cc87bd..5193902d 100644 --- a/tools/cloudharness-test/cloudharness_test/api.py +++ b/tools/cloudharness-test/cloudharness_test/api.py @@ -60,6 +60,13 @@ def run_api_tests(root_paths, helm_values: HarnessMainConfig, base_domain, inclu app_env = get_app_environment(app_config, app_domain) + schema_file = f"applications/{app_config.name}/api/openapi.yaml" + + for path in root_paths: + # use local schema if available to simplify test development + if os.path.exists(os.path.join(path, schema_file)): + app_env["APP_SCHEMA_FILE"] = schema_file + if api_config.autotest: logging.info("Running auto api tests") cmd = get_schemathesis_command(api_filename, app_config, app_domain) diff --git a/tools/cloudharness-test/cloudharness_test/apitest_init.py b/tools/cloudharness-test/cloudharness_test/apitest_init.py index 4ba65f7e..1f83eabd 100644 --- a/tools/cloudharness-test/cloudharness_test/apitest_init.py +++ b/tools/cloudharness-test/cloudharness_test/apitest_init.py @@ -1,44 +1,64 @@ import os import logging +import requests import schemathesis as st from cloudharness.auth import get_token -if "APP_URL" in os.environ: +if "APP_URL" or "APP_SCHEMA_FILE" in os.environ: + app_schema = os.environ.get("APP_SCHEMA_FILE", None) + app_url = os.environ.get("APP_URL", "http://samples.ch.local/api") + logging.info("Start schemathesis tests on %s", app_url) + if app_schema: + # Test locally with harness-test -- use local schema for convenience during test development + openapi_uri = app_schema + schema = st.from_file(openapi_uri) + else: + # remote testing: might be /api/openapi.json or /openapi.json + try: + openapi_uri = openapi_uri = app_url + "/openapi.json" + schema = st.from_uri(openapi_uri) + except st.exceptions.SchemaLoadingError as e: + # Use alternative configuration + try: + openapi_uri = app_url.replace("/api", "") + "/openapi.json" + print(requests.get(openapi_uri)) + schema = st.from_uri(openapi_uri) + except st.exceptions.SchemaLoadingError as e: + raise Exception( + f"Cannot setup api tests: {openapi_uri} not valid. Check your deployment is up and configuration") from e - app_url = os.environ["APP_URL"] + except Exception as e: + raise Exception( + f"Cannot setup api tests: {openapi_uri}: {e}") from e - try: - openapi_uri = app_url + "/openapi.json" - logging.info("Using openapi spec at %s", openapi_uri) - schema = st.from_uri(openapi_uri) - except Exception as e: - raise Exception(f"Cannot setup api tests: {openapi_uri} not reachable. Check your deployment is up and configuration") from e + logging.info("Using openapi spec at %s", openapi_uri) if "USERNAME" in os.environ and "PASSWORD" in os.environ: logging.info("Setting token from username and password") + @st.auth.register() class TokenAuth: def get(self, context): - + username = os.environ["USERNAME"] - password = os.environ["PASSWORD"] - + password = os.environ["PASSWORD"] + return get_token(username, password) def set(self, case, data, context): case.headers = case.headers or {} - case.headers["Authorization"] = f"Bearer {data}" - case.headers["Cookie"] = f"kc-access={data}" + case.headers["Authorization"] = f"Bearer {data}" + case.headers["Cookie"] = f"kc-access={data}" else: @st.auth.register() class TokenAuth: def get(self, context): - + return "" def set(self, case, data, context): case.headers = case.headers or {} - case.headers["Authorization"] = f"Bearer {data}" - case.headers["Cookie"] = f"kc-access={data}" \ No newline at end of file + case.headers["Authorization"] = f"Bearer {data}" + case.headers["Cookie"] = f"kc-access={data}" diff --git a/tools/deployment-cli-tools/ch_cli_tools/skaffold.py b/tools/deployment-cli-tools/ch_cli_tools/skaffold.py index 1e6742a5..774234b9 100644 --- a/tools/deployment-cli-tools/ch_cli_tools/skaffold.py +++ b/tools/deployment-cli-tools/ch_cli_tools/skaffold.py @@ -3,7 +3,7 @@ import json import time -from os.path import join, dirname, relpath, basename +from os.path import join, relpath, basename, exists, abspath from cloudharness_model import ApplicationTestConfig, HarnessMainConfig from cloudharness_utils.constants import APPS_PATH, DEPLOYMENT_CONFIGURATION_PATH, \ @@ -51,7 +51,7 @@ def build_artifact(image_name, context_path, requirements=None, dockerfile_path= in requirements] return artifact_spec - + base_images = set() def process_build_dockerfile(dockerfile_path, root_path, global_context=False, requirements=None, app_name=None): @@ -78,13 +78,13 @@ def process_build_dockerfile(dockerfile_path, root_path, global_context=False, r for dockerfile_path in base_dockerfiles: process_build_dockerfile(dockerfile_path, root_path, global_context=True) - - + + release_config = skaffold_conf['deploy']['helm']['releases'][0] release_config['name'] = helm_values.namespace release_config['namespace'] = helm_values.namespace release_config['artifactOverrides'][KEY_APPS] = {} - + static_images = set() for root_path in root_paths: static_dockerfiles = find_dockerfiles_paths( @@ -92,7 +92,7 @@ def process_build_dockerfile(dockerfile_path, root_path, global_context=False, r for dockerfile_path in static_dockerfiles: process_build_dockerfile(dockerfile_path, root_path) - + for root_path in root_paths: apps_path = join(root_path, APPS_PATH) @@ -138,22 +138,45 @@ def process_build_dockerfile(dockerfile_path, root_path, global_context=False, r } } - flask_main = find_file_paths(context_path, '__main__.py') - - if flask_main: + mains_candidates = find_file_paths(context_path, '__main__.py') + + def identify_unicorn_based_main(candidates): + import re + gunicorn_pattern = re.compile(r"gunicorn") + for candidate in candidates: + dockerfile_path = f"{candidate}/.." + while not exists(f"{dockerfile_path}/Dockerfile") and abspath(dockerfile_path) != abspath(root_path): + dockerfile_path += "/.." + dockerfile = f"{dockerfile_path}/Dockerfile" + if not exists(dockerfile): + continue + with open(dockerfile, 'r') as file: + if re.search(gunicorn_pattern, file.read()): + return candidate + requirements = f"{candidate}/../requirements.txt" + if not exists(requirements): + continue + with open(requirements, 'r') as file: + if re.search(gunicorn_pattern, file.read()): + return candidate + return None + + task_main_file = identify_unicorn_based_main(mains_candidates) + + if task_main_file: release_config['overrides']['apps'][app_key] = \ { 'harness': { 'deployment': { 'command': ['python'], - 'args': [f'/usr/src/app/{os.path.basename(flask_main[0])}/__main__.py'] + 'args': [f'/usr/src/app/{os.path.basename(task_main_file)}/__main__.py'] } } } - + test_config: ApplicationTestConfig = helm_values.apps[app_key].harness.test if test_config.unit.enabled and test_config.unit.commands: - + skaffold_conf['test'].append(dict( image=get_image_tag(app_name), custom=[dict(command="docker run $IMAGE " + cmd) for cmd in test_config.unit.commands] @@ -209,7 +232,7 @@ def get_image_tag(name): "/usr/src/app"), } }) - + if not os.path.exists(os.path.dirname(vscode_launch_path)): os.makedirs(os.path.dirname(vscode_launch_path)) diff --git a/tools/deployment-cli-tools/tests/resources_buggy/.dockerignore b/tools/deployment-cli-tools/tests/resources_buggy/.dockerignore new file mode 100644 index 00000000..bba89d5b --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources_buggy/.dockerignore @@ -0,0 +1,22 @@ +**/node_modules +.tox +docs +applications +/infrastructure +/blueprint +test +/tools/deployment-cli-tools +.github +.git +.vscode +/deployment +skaffold.yaml +*.egg-info +__pycache__ +.hypothesis +.coverage +.pytest_cache +/application-templates +/deployment-configuration +/cloud-harness +.openapi-generator \ No newline at end of file diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/events/deploy/values-prod.yaml b/tools/deployment-cli-tools/tests/resources_buggy/applications/events/deploy/values-prod.yaml new file mode 100644 index 00000000..d4d54b1f --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources_buggy/applications/events/deploy/values-prod.yaml @@ -0,0 +1,7 @@ +kafka: + resources: + limits: + cpu: overridden-prod + requests: + cpu: 50m + memory: 100Mi diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/events/deploy/values.yaml b/tools/deployment-cli-tools/tests/resources_buggy/applications/events/deploy/values.yaml new file mode 100644 index 00000000..300997d5 --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources_buggy/applications/events/deploy/values.yaml @@ -0,0 +1,8 @@ +kafka: + resources: + limits: + cpu: 600m + memory: overridden + requests: + cpu: 50m + memory: 100Mi diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/.dockerignore b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/.dockerignore new file mode 100644 index 00000000..669be813 --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/.dockerignore @@ -0,0 +1 @@ +*.ignored \ No newline at end of file diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/Dockerfile b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/Dockerfile new file mode 100644 index 00000000..533a9c95 --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/Dockerfile @@ -0,0 +1,4 @@ +ARG CLOUDHARNESS_FLASK +FROM $CLOUDHARNESS_FLASK + +RUN pip install gunicorn \ No newline at end of file diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/deploy/resources/aresource.txt b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/deploy/resources/aresource.txt new file mode 100644 index 00000000..e69de29b diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/deploy/templates/mytemplate.yaml b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/deploy/templates/mytemplate.yaml new file mode 100644 index 00000000..e69de29b diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/deploy/values-dev.yaml b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/deploy/values-dev.yaml new file mode 100644 index 00000000..0b7c9a77 --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/deploy/values-dev.yaml @@ -0,0 +1,11 @@ +harness: + name: "I'm useless" + subdomain: mysubdomain + dependencies: + soft: + - legacy + build: + - cloudharness-flask + - my-common +a: b +dev: true \ No newline at end of file diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/deploy/values-test.yaml b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/deploy/values-test.yaml new file mode 100644 index 00000000..85ecbfae --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/deploy/values-test.yaml @@ -0,0 +1,2 @@ +a: test +test: true \ No newline at end of file diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/deploy/values-withmongo.yaml b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/deploy/values-withmongo.yaml new file mode 100644 index 00000000..f3519b50 --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/deploy/values-withmongo.yaml @@ -0,0 +1,4 @@ +harness: + database: + auto: true + type: mongo \ No newline at end of file diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/deploy/values-withoutdb.yaml b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/deploy/values-withoutdb.yaml new file mode 100644 index 00000000..e69de29b diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/deploy/values-withpostgres.yaml b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/deploy/values-withpostgres.yaml new file mode 100644 index 00000000..4b260dbf --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/deploy/values-withpostgres.yaml @@ -0,0 +1,2 @@ +harness: + database: {auto: true, type: postgres} \ No newline at end of file diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/deploy/values.yaml b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/deploy/values.yaml new file mode 100644 index 00000000..434b042b --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/deploy/values.yaml @@ -0,0 +1,14 @@ +harness: + name: "I'm useless" + subdomain: mysubdomain + dependencies: + soft: + - legacy + build: + - cloudharness-flask + test: + unit: + commands: + - tox + - echo "hello" +a: b diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/myapp_code/__main__.py b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/myapp_code/__main__.py new file mode 100644 index 00000000..3118318a --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/myapp_code/__main__.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 + +from cloudharness.utils.server import init_flask, main + + +app = init_flask(title="Cloudharness sample application", webapp=True) + +if __name__ == '__main__': + main() diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/tasks/mytask/Dockerfile b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/tasks/mytask/Dockerfile new file mode 100644 index 00000000..e69de29b diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/venv/matplotlib/__main__.py b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/venv/matplotlib/__main__.py new file mode 100644 index 00000000..37de2b0b --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp/venv/matplotlib/__main__.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 + +from something import something_else + + +def fake_content(): + ... \ No newline at end of file diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/.dockerignore b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/.dockerignore new file mode 100644 index 00000000..669be813 --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/.dockerignore @@ -0,0 +1 @@ +*.ignored \ No newline at end of file diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/Dockerfile b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/Dockerfile new file mode 100644 index 00000000..de89f0ed --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/Dockerfile @@ -0,0 +1,4 @@ +ARG CLOUDHARNESS_FLASK +FROM $CLOUDHARNESS_FLASK + +RUN pip install -r requirements.txt \ No newline at end of file diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/deploy/resources/aresource.txt b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/deploy/resources/aresource.txt new file mode 100644 index 00000000..e69de29b diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/deploy/templates/mytemplate.yaml b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/deploy/templates/mytemplate.yaml new file mode 100644 index 00000000..e69de29b diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/deploy/values-dev.yaml b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/deploy/values-dev.yaml new file mode 100644 index 00000000..0b7c9a77 --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/deploy/values-dev.yaml @@ -0,0 +1,11 @@ +harness: + name: "I'm useless" + subdomain: mysubdomain + dependencies: + soft: + - legacy + build: + - cloudharness-flask + - my-common +a: b +dev: true \ No newline at end of file diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/deploy/values-test.yaml b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/deploy/values-test.yaml new file mode 100644 index 00000000..85ecbfae --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/deploy/values-test.yaml @@ -0,0 +1,2 @@ +a: test +test: true \ No newline at end of file diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/deploy/values-withmongo.yaml b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/deploy/values-withmongo.yaml new file mode 100644 index 00000000..f3519b50 --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/deploy/values-withmongo.yaml @@ -0,0 +1,4 @@ +harness: + database: + auto: true + type: mongo \ No newline at end of file diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/deploy/values-withoutdb.yaml b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/deploy/values-withoutdb.yaml new file mode 100644 index 00000000..e69de29b diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/deploy/values-withpostgres.yaml b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/deploy/values-withpostgres.yaml new file mode 100644 index 00000000..4b260dbf --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/deploy/values-withpostgres.yaml @@ -0,0 +1,2 @@ +harness: + database: {auto: true, type: postgres} \ No newline at end of file diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/deploy/values.yaml b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/deploy/values.yaml new file mode 100644 index 00000000..434b042b --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/deploy/values.yaml @@ -0,0 +1,14 @@ +harness: + name: "I'm useless" + subdomain: mysubdomain + dependencies: + soft: + - legacy + build: + - cloudharness-flask + test: + unit: + commands: + - tox + - echo "hello" +a: b diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/myapp_code/__main__.py b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/myapp_code/__main__.py new file mode 100644 index 00000000..3118318a --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/myapp_code/__main__.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 + +from cloudharness.utils.server import init_flask, main + + +app = init_flask(title="Cloudharness sample application", webapp=True) + +if __name__ == '__main__': + main() diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/requirements.txt b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/requirements.txt new file mode 100644 index 00000000..283e1305 --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/requirements.txt @@ -0,0 +1 @@ +gunicorn \ No newline at end of file diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/tasks/mytask/Dockerfile b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/tasks/mytask/Dockerfile new file mode 100644 index 00000000..e69de29b diff --git a/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/venv/matplotlib/__main__.py b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/venv/matplotlib/__main__.py new file mode 100644 index 00000000..37de2b0b --- /dev/null +++ b/tools/deployment-cli-tools/tests/resources_buggy/applications/myapp2/venv/matplotlib/__main__.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 + +from something import something_else + + +def fake_content(): + ... \ No newline at end of file diff --git a/tools/deployment-cli-tools/tests/resources_buggy/infrastructure/base-images/cloudharness-base/testfile b/tools/deployment-cli-tools/tests/resources_buggy/infrastructure/base-images/cloudharness-base/testfile new file mode 100644 index 00000000..e69de29b diff --git a/tools/deployment-cli-tools/tests/resources_buggy/infrastructure/common-images/my-common/Dockerfile b/tools/deployment-cli-tools/tests/resources_buggy/infrastructure/common-images/my-common/Dockerfile new file mode 100644 index 00000000..e69de29b diff --git a/tools/deployment-cli-tools/tests/test_skaffold.py b/tools/deployment-cli-tools/tests/test_skaffold.py index 98036b21..94998892 100644 --- a/tools/deployment-cli-tools/tests/test_skaffold.py +++ b/tools/deployment-cli-tools/tests/test_skaffold.py @@ -7,6 +7,7 @@ HERE = os.path.dirname(os.path.realpath(__file__)) RESOURCES = os.path.join(HERE, 'resources') +RESOURCES_BUGGY = os.path.join(HERE, 'resources_buggy') OUT = '/tmp/deployment' CLOUDHARNESS_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) @@ -70,7 +71,7 @@ def test_create_skaffold_configuration(): a for a in sk['build']['artifacts'] if a['image'] == 'reg/cloudharness/cloudharness-flask') - assert os.path.samefile(cloudharness_flask_artifact['context'], + assert os.path.samefile(cloudharness_flask_artifact['context'], join(CLOUDHARNESS_ROOT, 'infrastructure/common-images/cloudharness-flask') ) @@ -102,3 +103,75 @@ def test_create_skaffold_configuration(): shutil.rmtree(OUT) shutil.rmtree(BUILD_DIR) + + +def test_create_skaffold_configuration_with_conflicting_dependencies(tmp_path): + values = create_helm_chart( + [CLOUDHARNESS_ROOT, RESOURCES_BUGGY], + output_path=OUT, + include=['myapp'], + exclude=['events'], + domain="my.local", + namespace='test', + env='dev', + local=False, + tag=1, + registry='reg' + ) + root_paths = preprocess_build_overrides( + root_paths=[CLOUDHARNESS_ROOT, RESOURCES_BUGGY], + helm_values=values, + merge_build_path=str(tmp_path) + ) + + sk = create_skaffold_configuration( + root_paths=root_paths, + helm_values=values, + output_path=OUT + ) + + releases = sk['deploy']['helm']['releases'] + assert len(releases) == 1 # Ensure we only found 1 deployment (for myapp) + + release = releases[0] + assert 'myapp' in release['overrides']['apps'] + assert 'matplotlib' not in release['overrides']['apps'] + + myapp_config = release['overrides']['apps']['myapp'] + assert myapp_config['harness']['deployment']['args'][0] == '/usr/src/app/myapp_code/__main__.py' + + +def test_create_skaffold_configuration_with_conflicting_dependencies_requirements_file(tmp_path): + values = create_helm_chart( + [CLOUDHARNESS_ROOT, RESOURCES_BUGGY], + output_path=OUT, + include=['myapp2'], + exclude=['events'], + domain="my.local", + namespace='test', + env='dev', + local=False, + tag=1, + registry='reg' + ) + root_paths = preprocess_build_overrides( + root_paths=[CLOUDHARNESS_ROOT, RESOURCES_BUGGY], + helm_values=values, + merge_build_path=str(tmp_path) + ) + + sk = create_skaffold_configuration( + root_paths=root_paths, + helm_values=values, + output_path=OUT + ) + + releases = sk['deploy']['helm']['releases'] + assert len(releases) == 1 # Ensure we only found 1 deployment (for myapp) + + release = releases[0] + assert 'myapp2' in release['overrides']['apps'] + assert 'matplotlib' not in release['overrides']['apps'] + + myapp_config = release['overrides']['apps']['myapp2'] + assert myapp_config['harness']['deployment']['args'][0] == '/usr/src/app/myapp_code/__main__.py' \ No newline at end of file