Testing

Overview

This article contains test cases to verify the successful setup of the Humanitec Operator in your infrastructure. They are especially helpful after setting up the Humanitec Operator on a new Kubernetes cluster, or after connecting it to additional secret stores to ensure the connection to these stores works as expected.

We currently provide these tests:

  • Air-gapped : This test covers the toolchain “Humanitec Operator” - “secret store” only. You can use it when the Platform Orchestrator does not have access to your target Kubernetes cluster, e.g. when using a private cluster. Duration: 10-15 min.
  • End-to-end : This test covers the toolchain “Platform Orchestrator” - “Humanitec Operator” - “secret store”. You can use it when the Platform Orchestrator has access to your target Kubernetes cluster. It covers all of the objectives of the “Air-gapped” test, so you do not need to execute both. Duration: 20-25 min.

Prerequisites (all tests)

  • The Humanitec Operator is installed on the target cluster.
  • You have access to the target cluster via kubectl.
  • At least one secret store is configured in the Humanitec Operator, e.g. HashiCorp Vault . Select one target secret store to use in the tests if you have more than one.
  • You have write access to the target secret store to create demo secrets, e.g. via its respective CLI such as aws for AWS Secrets Manager, az for Azure Key Vault, gcloud for Google Cloud Secret Manager, or the vault CLI for HashiCorp Vault. The test cases will use the CLI tools, but you may choose to access your secret store using a different interface such as the UI.

Setup (all tests)

Perform these common setup steps before executing any of the tests.

  1. Prepare your local environment. Replace all <placeholder values> with the values of your setup.
export SECRET_STORE_ID=<my-secret-store-id>
export APP_ID=my-app
export APP_NAME="My-App"

# Only when using AWS Secrets Manager
export SECRETS_MANAGER_REGION=<region> # E.g. eu-north-1

# Only when using Azure Key Vault
export KEY_VAULT_NAME=<my-keyvault-name>

# Only when using Google Secret Manager
export GSM_PROJECT_ID=<my-Google-Cloud-Secret-Manager-Project-ID>

# Only when using HashiCorp Vault
export SECRET_PATH=<my-secret-engine-path> # e.g. "secret"
  1. Create secrets in your secret store. While not all secrets are used in all tests, we create all of them for simplification. We will delete them again when cleaning up.

aws secretsmanager create-secret \
  --name mysql-username --secret-string "super secret username" \
  --region ${SECRETS_MANAGER_REGION}
aws secretsmanager create-secret \
  --name mysql-password --secret-string "super secret password" \
  --region ${SECRETS_MANAGER_REGION}
aws secretsmanager create-secret \
  --name my-secret-shared-value --secret-string "super secret Shared Value" \
  --region ${SECRETS_MANAGER_REGION}

az keyvault secret set --name mysql-username \
  --vault-name ${KEY_VAULT_NAME} --value "super secret username"
az keyvault secret set --name mysql-password \
  --vault-name ${KEY_VAULT_NAME} --value "super secret password"
az keyvault secret set --name my-secret-shared-value \
  --vault-name ${KEY_VAULT_NAME} --value "super secret Shared Value"

echo -n "super secret username" | \
    gcloud secrets create mysql-username --project ${GSM_PROJECT_ID} --data-file=-
echo -n "super secret password" | \
    gcloud secrets create mysql-password --project ${GSM_PROJECT_ID} --data-file=-
echo -n "super secret Shared Value" | \
    gcloud secrets create my-secret-shared-value --project ${GSM_PROJECT_ID} --data-file=-

Expected output:

Created version [1] of the secret [mysql-username].
Created version [1] of the secret [mysql-password].
Created version [1] of the secret [my-secret-shared-value].

vault kv put ${SECRET_PATH}/mysql-username value="super secret username"
vault kv put ${SECRET_PATH}/mysql-password value="super secret password"
vault kv put ${SECRET_PATH}/my-secret-shared-value value="super secret Shared Value"

Expected output:

======== Secret Path ========
secret/data/mysql-username
======= Metadata =======
Key                Value
---                -----
created_time       2030-11-27T09:49:12.793035144Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            1

# (...likewise for mysql-password and my-secret-shared-value)

Test: Air-gapped

This test lets you create custom resources based on the Humanitec Operator CRDs straight on the target cluster, bypassing the need for the Platform Orchestrator. The Humanitec Operator will act on those custom resources and create the final Kubernetes manifests. No outbound network connectivity is required for the target cluster except accessing the secret store.

The custom resources contain secret references mapped to the secret store. The Humanitec Operator must resolve those secret references and read a secret value from the secret store as well as write a resource cookie to the default store.

Objectives

  • Verify the secret mapping and resource provisioning functionality of the Humanitec Operator on the target cluster.
  • Verify read access on the selected secret store for the Humanitec Operator.
  • Verify write access on the default secret store for the Humanitec Operator.

Prerequisites

Test Steps

  1. Create a namespace for the Application.
kubectl create namespace ${APP_ID}

Expected output:

namespace/my-app created
  1. Create a SecretMapping custom resource.

cat << EOF | kubectl apply -n ${APP_ID} -f -
apiVersion: humanitec.io/v1alpha1
kind: SecretMapping
metadata:
  name: secretmapping-shared-secrets
spec:
  secretData:
  - remoteRef:
      key: my-secret-shared-value
      version: ""
    secretKey: my-secret-shared-value
    secretStoreName: ${SECRET_STORE_ID}
  secretName: shared-secrets
EOF

cat << EOF | kubectl apply -n ${APP_ID} -f -
apiVersion: humanitec.io/v1alpha1
kind: SecretMapping
metadata:
  name: secretmapping-shared-secrets
spec:
  secretData:
  - remoteRef:
      key: my-secret-shared-value
      version: ""
    secretKey: my-secret-shared-value
    secretStoreName: ${SECRET_STORE_ID}
  secretName: shared-secrets
EOF

cat << EOF | kubectl apply -n ${APP_ID} -f -
apiVersion: humanitec.io/v1alpha1
kind: SecretMapping
metadata:
  name: secretmapping-shared-secrets
spec:
  secretData:
  - remoteRef:
      key: my-secret-shared-value
      version: ""
    secretKey: my-secret-shared-value
    secretStoreName: ${SECRET_STORE_ID}
  secretName: shared-secrets
EOF

cat << EOF | kubectl apply -n ${APP_ID} -f -
apiVersion: humanitec.io/v1alpha1
kind: SecretMapping
metadata:
  name: secretmapping-shared-secrets
spec:
  secretData:
  - remoteRef:
      key: my-secret-shared-value/.value
      version: ""
    secretKey: my-secret-shared-value
    secretStoreName: ${SECRET_STORE_ID}
  secretName: shared-secrets
EOF

Expected output:

secretmapping.humanitec.io/secretmapping-shared-secrets created
  1. Check the status of the SecretMapping resource:
kubectl get secretmappings -n ${APP_ID} secretmapping-shared-secrets \
  -o jsonpath='{.status.conditions[0]}' | jq

Expected output:

{
  "lastTransitionTime": "2030-11-28T09:24:41Z",
  "message": "Secret applied",
  "reason": "SecretApplied",
  "status": "True",
  "type": "SecretReady"
}
  1. The Operator should have generated a Kubernetes Secret based on the SecretMapping containing the actual secret value. Check the content of that Kubernetes Secret. This step verifies that the Humanitec Operator has read access to the target secret store.
kubectl get secret -n ${APP_ID} shared-secrets \
  -o jsonpath='{.data.my-secret-shared-value}' | base64 -d

Expected output:

super secret Shared Value
  1. Create a Resource custom resource. Its purpose is to write a resource cookie to the default secret store. It uses the convenience config Resource Type and the Template Driver , so no physical resource needs to be created. Its matching criteria match the Application you created earlier.
cat << EOF | kubectl apply -n ${APP_ID} -f - 
apiVersion: humanitec.io/v1alpha1
kind: Resource
metadata:
  name: demo-resource
spec:
  dependsOn: []
  driver: builtin://template
  driverInputs:
    driver:
      values:
        templates:
          cookie: my demo resource cookie
  driverType: humanitec/template
  guresid: demo-resource-cookie
  id: demo-resource-cookie
  type: config
EOF

Expected output:

resource.humanitec.io/demo-resource created
  1. Check the status of the Resource custom resource:
kubectl get resource -n ${APP_ID} demo-resource \
  -o jsonpath='{.status.conditions[0]}' | jq

Expected output:

{
  "lastTransitionTime": "2030-11-28T09:58:50Z",
  "message": "Resource provisioned",
  "reason": "ResourceProvisioned",
  "status": "True",
  "type": "Provisioned"
}
  1. Check for the existence of a new resource cookie in the secret store. You should see the value of the new secret, proving that the Humanitec Operator has write access to the default secret store.

aws secretsmanager get-secret-value \
  --secret-id resources/active/demo-resource-cookie/cookies/humanitec/template \
  --region ${SECRETS_MANAGER_REGION} \
  --query SecretString \
  --output text | base64 -d | tr -d '"'

az keyvault secret show \
  --name resources--active--demo-resource-cookie--cookies--humanitec--template \
  --vault-name ${KEY_VAULT_NAME} \
  --query "value" | tr -d '"' | base64 -d | tr -d '"'

gcloud secrets versions access 1 \
  --secret=resources--active--demo-resource-cookie--cookies--humanitec--template | \
  base64 -d | tr -d '"'

vault kv get -mount=${SECRET_PATH} \
  -field=value resources/active/demo-resource-cookie/cookies/humanitec/template | \
  base64 -d | tr -d '"'

Expected output:

my demo resource cookie

Cleanup

Remove all of the objects you created during the test run.

  1. Delete the namespace, which will also delete all of the objects it contains.
kubectl delete namespace ${APP_ID}
  1. Perform the cleanup steps for all tests .

Test: End-to-end

This test lets you deploy an Application via the Platform Orchestrator. The Application uses secret references in two ways: through a Resource Definition using the Echo Driver, and through a Shared Value on the Application level. The Humanitec Operator must resolve all of these secret references to their actual values, reading them from the secret store.

A specific Resource makes the Operator write a resource cookie to the default secret store.

Objectives

  • Verify the Workload deployment functionality of the Platform Orchestrator / Humanitec Operator toolchain on the target cluster.
  • Verify that the Operator can inject secrets from the secret store into the deployed Workload.
  • Verify read access on the selected secret store for the Humanitec Operator.
  • Verify write access on the default secret store for the Humanitec Operator.
  • Verify that the Operator can successfully perform requests to Humanitec-hosted Drivers .

Prerequisites

Test Steps

  1. Prepare your local environment. Replace all <placeholder values> with the values of your setup.
export HUMANITEC_ORG=<my-org-id>
export HUMANITEC_TOKEN=<my-API-token-for-my-org> # Only when using the API, not required for CLI
export K8S_RESDEF=<my-target-k8s-cluster-resource-definition-id>

Expected result: all required environment variables are set.

  1. Create an Application in the Platform Orchestrator.

humctl create application ${APP_ID} -n ${APP_NAME}

Expected output similar to:

id      name
my-app  My-App

curl "https://api.humanitec.io/orgs/${HUMANITEC_ORG}/apps" \
  -X POST \
  -H "Authorization: Bearer ${HUMANITEC_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
  "id": "'${APP_ID}'",
  "name": "'${APP_NAME}'"
}'

Expected output similar to:

{"id":"my-app","name":"My-App","org_id":"my-org","created_at":"2030-11-27T09:25:59.856902846Z","created_by":"s-d3e60a0e-8a53-40e9-a686-34548b7e06f0","envs":[{"id":"development","name":"Development","type":"development"}]}
  1. Add a secret Shared Value to the Application. It will read its real value from the secret store using a secret reference.

humctl create value my-secret-shared-value "my-secret-shared-value/.value" \
  --app ${APP_ID} \
  --description "My secret Application Shared Value" \
  --is-secret-ref \
  --secret-store ${SECRET_STORE_ID}

Expected output similar to:

key                     value   description                             source  is secret
my-secret-shared-value          My secret Application Shared Value      app     true

curl https://api.humanitec.io/orgs/${HUMANITEC_ORG}/apps/${APP_ID}/values \
  -X POST \
  -H "Authorization: Bearer ${HUMANITEC_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
  "description": "My secret Application Shared Value",
  "is_secret": true,
  "key": "my-secret-shared-value",
  "secret_ref": {
    "store": "'${SECRET_STORE_ID}'",
    "ref": "my-secret-shared-value/.value"
  }
}'

Expected output similar to:

{"created_at":"2030-11-27T09:26:07.716583767Z","description":"My secret Application Shared Value","is_secret":true,"key":"my-secret-shared-value","secret_key":"my-secret-shared-value/.value","secret_store_id":"my-vault-store","secret_version":null,"source":"app","updated_at":"2030-11-27T09:26:07.716583767Z","value":""}

humctl create value my-secret-shared-value "my-secret-shared-value" \
  --app ${APP_ID} \
  --description "My secret Application Shared Value" \
  --is-secret-ref \
  --secret-store ${SECRET_STORE_ID}

Expected output similar to:

key                     value   description                             source  is secret
my-secret-shared-value          My secret Application Shared Value      app     true

curl https://api.humanitec.io/orgs/${HUMANITEC_ORG}/apps/${APP_ID}/values \
  -X POST \
  -H "Authorization: Bearer ${HUMANITEC_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
  "description": "My secret Application Shared Value",
  "is_secret": true,
  "key": "my-secret-shared-value",
  "secret_ref": {
    "store": "'${SECRET_STORE_ID}'",
    "ref": "my-secret-shared-value"
  }
}'

Expected output similar to:

{"created_at":"2030-11-27T09:26:07.716583767Z","description":"My secret Application Shared Value","is_secret":true,"key":"my-secret-shared-value","secret_key":"my-secret-shared-value","secret_store_id":"my-store","secret_version":null,"source":"app","updated_at":"2030-11-27T09:26:07.716583767Z","value":""}
  1. Add matching criteria to the Resource Definition of the target cluster to match this Application. The command captures and outputs the ID of the new matching criteria in the variable CRITERIA_ID for later use.

export CRITERIA_ID=$(humctl api post /orgs/${HUMANITEC_ORG}/resources/defs/${K8S_RESDEF}/criteria \
  -d '{
    "env_type": "development",
    "app_id": "'${APP_ID}'"
  }' | jq --raw-output '.id' )
echo $CRITERIA_ID

export CRITERIA_ID=$(curl -s "https://api.humanitec.io/orgs/${HUMANITEC_ORG}/resources/defs/${K8S_RESDEF}/criteria" \
  -X POST \
  -H "Authorization: Bearer ${HUMANITEC_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "env_type": "development",
    "app_id": "'${APP_ID}'"
  }' | jq --raw-output '.id' )
echo $CRITERIA_ID

Expected output similar to:

d159797fd0709872
  1. Create this Resource Definition. Its purpose is to use a Humanitec-hosted Driver and prove the Operator has been properly configured to authenticate to them. It uses the Terraform Driver to pass through some values, and its matching criteria match the Application you created earlier.

  1. Create a file defining the Resource Definition:
cat << EOF > tf-myconfig.yaml
apiVersion: entity.humanitec.io/v1b1
kind: Definition
metadata:
  id: tf-myconfig
entity:
  name: TF MyConfig
  type: config
  driver_type: humanitec/terraform
  driver_inputs:
    values:
      variables:
        db_host: products.mysql.example.com
        db_name: example-db
        db_port: 3306
      script: |
        variable "db_host" {
          type = string
        }
        variable "db_name" {
          type = string
        }
        variable "db_port" {
          type = number
          default = 3306
        }

        output "host" {
          value = var.db_host
        }
        output "name" {
          value = var.db_name
        }
        output "port" {
          value = var.db_port
        }
  criteria:
  - app_id: ${APP_ID}
EOF
  1. Use the humctl create command to create the Resource Definition:
humctl create -f tf-myconfig.yaml
rm tf-myconfig.yaml

Expected output:

id              type    driver_type           driver_account
tf-myconfig     config  humanitec/terraform   -

curl https://api.humanitec.io/orgs/${HUMANITEC_ORG}/resources/defs \
  -H "Authorization: Bearer ${HUMANITEC_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
  "id": "tf-myconfig",
  "name": "TF MyConfig",
  "type": "config",
  "driver_type": "humanitec/terraform",
  "driver_inputs": {
    "values": {
      "variables": {
        "db_host": "products.mysql.example.com",
        "db_name": "example-db",
        "db_port": 3306
      },
      "script": "variable \"db_host\" {\n  type = string\n}\nvariable \"db_name\" {\n  type = string\n}\nvariable \"db_port\" {\n  type = number\n  default = 3306\n}\n\noutput \"host\" {\n  value = var.db_host\n}\noutput \"name\" {\n  value = var.db_name\n}\noutput \"port\" {\n  value = var.db_port\n}\n"
    }
  },
  "criteria": [
    {
      "app_id":"'${APP_ID}'"
    }
  ]
}'

Expected output similar to:

{"active_version_id":"f64ed7e0-bd9f-40b7-8cf0-6653311f0aa1","created_at":"2024-10-16T12:27:30.007617271Z","created_by":"s-e73faac2-f621-4777-846e-ba85b2b8b11d","criteria":[{"app_id":"my-app","class":"default","id":"be1c13ce3ce4bef1"}],"driver_inputs":{"values":{"script":"variable \"db_host\" {\n  type = string\n}\nvariable \"db_name\" {\n  type = string\n}\nvariable \"db_port\" {\n  type = number\n  default = 3306\n}\n\noutput \"host\" {\n  value = var.db_host\n}\noutput \"name\" {\n  value = var.db_name\n}\noutput \"port\" {\n  value = var.db_port\n}\n","variables":{"db_host":"products.mysql.example.com","db_name":"example-db","db_port":3306}}},"driver_type":"humanitec/terraform","id":"tf-myconfig","is_default":false,"is_deleted":false,"name":"TF MyConfig","org_id":"my-org","type":"config","updated_at":"2024-10-16T12:27:30.007617271Z","updated_by":"s-e73faac2-f621-4777-846e-ba85b2b8b11d"}
  1. Create this Resource Definition. Its purpose is to read values from the secret store using secret references and to reference the Terraform Resource Definition previously created. It uses the Echo Driver to simulate a MySQL resource, and its matching criteria match the Application you created earlier.

  1. Create a file defining the Resource Definition:
cat << EOF > echo-mysql.yaml
apiVersion: entity.humanitec.io/v1b1
kind: Definition
metadata:
  id: echo-mysql
entity:
  name: Echo MySQL
  type: mysql
  driver_type: humanitec/echo
  driver_inputs:
    secret_refs:
      password:
        ref: mysql-password/.value
        store: ${SECRET_STORE_ID}
      username:
        ref: mysql-username/.value
        store: ${SECRET_STORE_ID}
    values:
      host: \${resources.config.outputs.host}
      name: \${resources.config.outputs.name}
      port: \${resources.config.outputs.port}
  criteria:
  - app_id: ${APP_ID}
EOF
  1. Use the humctl create command to create the Resource Definition:
humctl create -f echo-mysql.yaml
rm echo-mysql.yaml

Expected output:

id              type    driver_type     driver_account
echo-mysql      mysql   humanitec/echo  -

curl https://api.humanitec.io/orgs/${HUMANITEC_ORG}/resources/defs \
  -H "Authorization: Bearer ${HUMANITEC_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
  "id": "echo-mysql",
  "name": "Echo MySQL",
  "type": "mysql",
  "driver_type": "humanitec/echo",
  "driver_inputs": {
    "values": {
      "host": "${resources.config.outputs.host}",
      "name": "${resources.config.outputs.name}",
      "port": "${resources.config.outputs.port}"
    },
    "secret_refs": {
      "username": {
        "store": "'${SECRET_STORE_ID}'",
        "ref": "mysql-username/.value"
      },
      "password": {
        "store": "'${SECRET_STORE_ID}'",
        "ref": "mysql-password/.value"
      }
    }
  },
  "criteria": [
    {
      "app_id":"'${APP_ID}'"
    }
  ]
}'

Expected output similar to:

{"active_version_id":"a485063c-f4cd-4a3e-a4ae-3eeb36ad0e69","created_at":"2024-10-16T13:31:03.310310552Z","created_by":"s-e73faac2-f621-4777-846e-ba85b2b8b11d","criteria":[{"app_id":"my-app","class":"default","id":"c13a211373380c77"}],"driver_inputs":{"secret_refs":{"password":{"store":"'${SECRET_STORE_ID}'","ref":"mysql-password/.value"},"username":{"store":"'${SECRET_STORE_ID}'","ref":"mysql-username/.value"}},"values":{"host":"${resources.config.outputs.host}","name":"${{resources.config.outputs.name}","port":"${resources.config.outputs.port}"}},"driver_type":"humanitec/echo","id":"echo-mysql","is_default":false,"is_deleted":false,"name":"Echo MySQL","org_id":"my-org","type":"mysql","updated_at":"2024-10-16T13:31:03.310310552Z","updated_by":"s-e73faac2-f621-4777-846e-ba85b2b8b11d"}

  1. Create a file defining the Resource Definition:
cat << EOF > echo-mysql.yaml
apiVersion: entity.humanitec.io/v1b1
kind: Definition
metadata:
  id: echo-mysql
entity:
  name: Echo MySQL
  type: mysql
  driver_type: humanitec/echo
  driver_inputs:
    secret_refs:
      password:
        ref: mysql-password
        store: ${SECRET_STORE_ID}
      username:
        ref: mysql-username
        store: ${SECRET_STORE_ID}
    values:
      host: \${resources.config.outputs.host}
      name: \${resources.config.outputs.name}
      port: \${resources.config.outputs.port}
  criteria:
  - app_id: ${APP_ID}
EOF
  1. Use the humctl create command to create the Resource Definition:
humctl create -f echo-mysql.yaml
rm echo-mysql.yaml

Expected output:

id              type    driver_type     driver_account
echo-mysql      mysql   humanitec/echo  -

curl https://api.humanitec.io/orgs/${HUMANITEC_ORG}/resources/defs \
  -H "Authorization: Bearer ${HUMANITEC_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
  "id": "echo-mysql",
  "name": "Echo MySQL",
  "type": "mysql",
  "driver_type": "humanitec/echo",
  "driver_inputs": {
    "values": {
      "host": "${resources.config.outputs.host}",
      "name": "${resources.config.outputs.name}",
      "port": "${resources.config.outputs.port}"
    },
    "secret_refs": {
      "username": {
        "store": "'${SECRET_STORE_ID}'",
        "ref": "mysql-username"
      },
      "password": {
        "store": "'${SECRET_STORE_ID}'",
        "ref": "mysql-password"
      }
    }
  },
  "criteria": [
    {
      "app_id":"'${APP_ID}'"
    }
  ]
}'

Expected output similar to:

{"active_version_id":"a485063c-f4cd-4a3e-a4ae-3eeb36ad0e69","created_at":"2024-10-16T13:31:03.310310552Z","created_by":"s-e73faac2-f621-4777-846e-ba85b2b8b11d","criteria":[{"app_id":"my-app","class":"default","id":"c13a211373380c77"}],"driver_inputs":{"secret_refs":{"password":{"store":"'${SECRET_STORE_ID}'","ref":"mysql-password"},"username":{"store":"'${SECRET_STORE_ID}'","ref":"mysql-username"}},"values":{"host":"${resources.config.outputs.host}","name":"${{resources.config.outputs.name}","port":"${resources.config.outputs.port}"}},"driver_type":"humanitec/echo","id":"echo-mysql","is_default":false,"is_deleted":false,"name":"Echo MySQL","org_id":"my-org","type":"mysql","updated_at":"2024-10-16T13:31:03.310310552Z","updated_by":"s-e73faac2-f621-4777-846e-ba85b2b8b11d"}
  1. Create this Resource Definition. Its purpose is to write a resource cookie to the default secret store. It uses the convenience config Resource Type and the Template Driver , so no physical resource needs to be created. Its matching criteria match the Application you created earlier.

  1. Create a file defining the Resource Definition:
cat << EOF > cookie-config.yaml
apiVersion: entity.humanitec.io/v1b1
kind: Definition
metadata:
  id: cookie-config
entity:
  criteria:
  - app_id: ${APP_ID}
    class: cookie
  driver_inputs:
    values:
      templates:
        cookie: my demo resource cookie
  driver_type: humanitec/template
  name: Cookie config
  type: config
EOF
  1. Use the humctl create command to create the Resource Definition:
humctl create -f cookie-config.yaml
rm cookie-config.yaml

curl https://api.humanitec.io/orgs/${HUMANITEC_ORG}/resources/defs \
  -H "Authorization: Bearer ${HUMANITEC_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
  "id": "cookie-config",
  "name": "Cookie config",
  "type": "config",
  "driver_type": "humanitec/template",
  "driver_inputs": {
    "values": {
      "templates": {
        "cookie": "my demo resource cookie"
      }
    }
  },
  "criteria": [
    {
      "app_id":"'${APP_ID}'",
      "class": "cookie"
    }
  ]
}'

Expected output similar to:

{"org_id":"my-org","id":"cookie-config","name":"Cookie config","type":"config","driver_type":"humanitec/template","driver_inputs":{"values":{"templates":{"cookie":"my demo resource cookie"}}},"created_by":"s-d3e60a0e-8a53-40e9-a666-74548b7e06e0","created_at":"2030-11-27T17:07:07.792263908Z","criteria":[{"app_id":"my-app","class":"default","id":"d344a2d9a45ec68e"}]}
  1. Create this Score file for the Application Workload.

    • It requests a Resource of type environment. This Resource provides access to the Application’s Shared Value which is injected into an environment variable.
    • It requests a Resource of type mysql and injects two of its outputs into a simple container which logs its environment variables. This Resource request will be matched to the Resource Definition of type mysql you created earlier.
    • That Resource, in turn, will request the config Resource using the Terraform Driver you created earlier.
    • It requests a Resource of type config and class cookie. This Resource request will be matched to the Resource Definition of type config handling the resource cookie you created earlier.
    • You may replace the image with another suitable image you might want to use as long as it has the printenv command. Provide the fully qualified path to an internal image registry as required.
cat << EOF > score.yaml
apiVersion: score.dev/v1b1
metadata:
  name: ${APP_ID}
containers:
  hello-world:
    image: busybox
    command: ["/bin/sh"]
    args: ["-c", "while true; do printenv && sleep 60; done"]
    variables:
      SECRET_SHARED_VALUE: "\${resources.env.my-secret-shared-value}"
      MYSQL_USERNAME: "\${resources.my-sql.username}"
      MYSQL_PASSWORD: "\${resources.my-sql.password}"
resources:
  env:
    type: environment
  my-sql:
    type: mysql
  my-config:
    type: config
    class: cookie
EOF

Expected result: the Score file score.yaml has been created.

  1. Deploy the Application.
humctl score deploy \
  --org $HUMANITEC_ORG \
  --app ${APP_ID} \
  --env development \
  -f score.yaml

Expected output similar to:

Delta ID                                    Pipeline ID     Pipeline Run ID                         Pipeline Run Status
4d0ee72d6eca8a84a4cffbbf883ef3e707542973    default         41d90b0b-5613-49e5-b2e1-843620c407a6    queued
  1. Now observe the resulting objects on the target Kubernetes cluster. A workload custom resource should have been created in a new namespace. First obtain the namespace.
export APP_NAMESPACE=$(kubectl get workload -A \
  -l app.humanitec.io/app-id=${APP_ID} \
  -o custom-columns=NAMESPACE:metadata.namespace \
  --no-headers)
echo $APP_NAMESPACE

Expected output similar to:

my-app-development-z36a
  1. Check the status of the custom resource of type Workload in the Application namespace and its status.
kubectl get workload -n $APP_NAMESPACE \
  -o jsonpath='{.items[0].status.conditions[0]}' | jq

Expected result similar to:

{
  "lastTransitionTime": "2030-11-27T09:27:01Z",
  "message": "Workload rolled out",
  "reason": "WorkloadRolledOut",
  "status": "True",
  "type": "Ready"
}

You should see "message": "Workload rolled out" and "status": "True" in the output.

  1. Check for the custom resources of type Resource in the Application namespace and their status.
kubectl get resource -n $APP_NAMESPACE \
  -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,ID:.spec.id,PROVISIONED:.status.provisioned

Expected result similar to:

NAMESPACE                              NAME                                                ID                                   PROVISIONED
a61741cc-5dec-4eb4-b2cd-80c1343d6031   resource-39e2aae3561cba6f6cf332363e76b9b3be798378   modules.my-app                       True
a61741cc-5dec-4eb4-b2cd-80c1343d6031   resource-4f33f1e60e014c4b837826464e71d95d57f795e3   modules.my-app.externals.my-config   True
a61741cc-5dec-4eb4-b2cd-80c1343d6031   resource-5f13f05f00b6ee3888ce0532ddbd4fc45ff6ca94   base-env                             True
a61741cc-5dec-4eb4-b2cd-80c1343d6031   resource-c37c6e88f32902c930c3cc46c75af456db507692   modules.my-app.externals.my-sql      True
a61741cc-5dec-4eb4-b2cd-80c1343d6031   resource-c37c6e88f32902c930c3cc46c75af456db507692   modules.my-app.externals.my-sql      True

You should see five resources with a PROVISIONED status of True in the output.

  1. Check for the custom resource of type SecretMapping in the Application namespace and its status.
kubectl get secretmapping -n $APP_NAMESPACE -o jsonpath='{.items[0].status.conditions[0]}' | jq

Expected output similar to:

{
  "deploymentID": "179b70b68f7f5f3b",
  "lastTransitionTime": "2030-11-27T09:26:58Z",
  "message": "Secret applied",
  "reason": "SecretApplied",
  "status": "True",
  "type": "SecretReady"
}

You should see "message": "Secret applied" and "status": "True" in the output.

  1. Get the ID of the Active Resource that was created for the config type Resource Definition named cookie-config.
export CONFIG_RESOURCE_ID=$(kubectl get resource -n $APP_NAMESPACE \
  -o jsonpath='{.items[?(@.metadata.annotations.app\.humanitec\.io/definition-id == "cookie-config")].metadata.name}' | sed 's/resource-//')
echo $CONFIG_RESOURCE_ID

Expected output similar to:

5f33b1e60e014c4c837826464e81d95d57f795e3
  1. Check for the existence of a new resource cookie in the secret store. You should see the value of the new secret, proving that the Humanitec Operator has write access to the default secret store.

aws secretsmanager get-secret-value \
  --secret-id resources/active/${CONFIG_RESOURCE_ID}/cookies/humanitec/template \
  --region ${SECRETS_MANAGER_REGION} \
  --query SecretString \
  --output text | base64 -d | tr -d '"'

az keyvault secret show \
  --name resources--active--${CONFIG_RESOURCE_ID}--cookies--humanitec--template \
  --vault-name ${KEY_VAULT_NAME} \
  --query "value" | tr -d '"' | base64 -d | tr -d '"'

gcloud secrets versions access 1 \
  --secret=resources--active--${CONFIG_RESOURCE_ID}--cookies--humanitec--template | \
  base64 -d | tr -d '"'

vault kv get -mount=${SECRET_PATH} -field=value \
  resources/active/${CONFIG_RESOURCE_ID}/cookies/humanitec/template | \
  base64 -d | tr -d '"'

Expected output:

my demo resource cookie
  1. Check for Kubernetes Secrets in the Application namespace.
kubectl get secret -n $APP_NAMESPACE --show-labels

Expected output similar to:

...
resource-secret-c37c6e88f32902c930c3cc46c75af456db507692   Opaque   2      5m   humanitec.io/created-by=operator
shared-secrets                                             Opaque   1      5m   humanitec.io/created-by=operator
...

In the list of secrets returned, you should see those two:

  • The “resource-secret-*” Secret with two DATA items. This Secret contains the secret values for the MySQL Resource.
  • The “shared-secrets” Secret with one DATA item. This Secret contains the secret value of the Application Shared Value.
  1. Finally, check the log output of the running container for correct values of the environment variables SECRET_SHARED_VALUE, MYSQL_USERNAME, and MYSQL_PASSWORD. It should show all the values you entered into the secret store secrets earlier, proving that mapping and reading secrets from the store works as expected.
kubectl logs --tail=50 -n \
  $(kubectl get pods -n $APP_NAMESPACE \
  --no-headers \
  -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name) | \
  grep -E -e '^SECRET_SHARED_VALUE|^MYSQL_USERNAME|^MYSQL_PASSWORD'

Expected output:

...
MYSQL_USERNAME=super secret username
MYSQL_PASSWORD=super secret password
SECRET_SHARED_VALUE=super secret Shared Value
...

Cleanup

Remove all of the objects you created during the test run.

  1. Delete the Application from the Platform Orchestrator. This will make the Humanitec Orchestrator remove all associated Kubernetes objects.

humctl delete application ${APP_ID} 

curl "https://api.humanitec.io/orgs/${HUMANITEC_ORG}/apps/${APP_ID}" \
  -X DELETE \
  -H "Authorization: Bearer ${HUMANITEC_TOKEN}"
  1. Remove the Score file.
rm score.yaml
  1. Remove the Resource Definitions.

humctl delete resource-definition tf-myconfig
humctl delete resource-definition echo-mysql
humctl delete resource-definition cookie-config

curl https://api.humanitec.io/orgs/${HUMANITEC_ORG}/resources/defs/tf-myconfig \
  -X DELETE \
  -H "Authorization: Bearer ${HUMANITEC_TOKEN}"
curl https://api.humanitec.io/orgs/${HUMANITEC_ORG}/resources/defs/echo-mysql \
  -X DELETE \
  -H "Authorization: Bearer ${HUMANITEC_TOKEN}"
curl https://api.humanitec.io/orgs/${HUMANITEC_ORG}/resources/defs/cookie-config \
  -X DELETE \
  -H "Authorization: Bearer ${HUMANITEC_TOKEN}"
  1. Remove the matching criteria from the cluster Resource Definition using the CRITERIA_ID captured on creation.

    humctl api delete /orgs/${HUMANITEC_ORG}/resources/defs/${K8S_RESDEF}/criteria/${CRITERIA_ID}
    

    curl "https://api.humanitec.io/orgs/${HUMANITEC_ORG}/resources/defs/${K8S_RESDEF}/criteria/${CRITERIA_ID}" \
      -X DELETE \
      -H "Authorization: Bearer ${HUMANITEC_TOKEN}"
    

  2. Perform the cleanup steps for all tests .

Cleanup (all tests)

Perform these common cleanup steps after executing any of the tests above.

  1. Delete the demo secrets from your secret store.

aws secretsmanager delete-secret \
  --secret-id mysql-username --region ${SECRETS_MANAGER_REGION} \
  --force-delete-without-recovery
aws secretsmanager delete-secret \
  --secret-id mysql-password --region ${SECRETS_MANAGER_REGION} \
  --force-delete-without-recovery
aws secretsmanager delete-secret \
  --secret-id my-secret-shared-value --region ${SECRETS_MANAGER_REGION} \
  --force-delete-without-recovery

az keyvault secret delete -n mysql-username --vault-name ${KEY_VAULT_NAME}
az keyvault secret delete -n mysql-password --vault-name ${KEY_VAULT_NAME}
az keyvault secret delete -n my-secret-shared-value --vault-name ${KEY_VAULT_NAME}

gcloud secrets delete mysql-username --project ${GSM_PROJECT_ID} --quiet
gcloud secrets delete mysql-password --project ${GSM_PROJECT_ID} --quiet
gcloud secrets delete my-secret-shared-value --project ${GSM_PROJECT_ID} --quiet

vault kv metadata delete ${SECRET_PATH}/mysql-username
vault kv metadata delete ${SECRET_PATH}/mysql-password
vault kv metadata delete ${SECRET_PATH}/my-secret-shared-value

Troubleshooting

The test cases contain a number of verification steps to be executed along the way. If at some point the expected result should not be met, utilize these sources to identify the root cause:

  • Check the status of the custom resource objects (Resource, SecretMapping, Workload) on the Kubernetes cluster that is currently being worked on. The message field will provide helpful information.
kubectl get resource,secretmapping,workload -A \
-o custom-columns=KIND:kind,NAMESPACE:.metadata.namespace,NAME:.metadata.name,STATUS:.status.conditions[0].status,MESSAGE:.status.conditions[0].message
  • Check the Humanitec Operator logs for errors:
kubectl logs -n humanitec-operator-system \
  -l app.kubernetes.io/instance=humanitec-operator \
  --tail=-1
Top