--- title: Usability Testing for Schema v1 tags: "schema" --- ## Hypotheses - (1) Configuration Authors can eliminate the complexity of verifying the presence of a Data Value in their templates. - (2) Configuration Authors find value in being able to define defaults (especially for array items) rather than describe them in comments. - (3) Configuration Authors — when they make mistakes in authoring schema — can easily identify the mistake, understand why it's an error, and know how to fix it. - (4) Configuration Consumers — can easily identify the violation they made in writing Data Values, understand why it's a violation, and know how to fix it. - (5) Overall, Configuration Consumers find ytt easier to use when Schema features are enabled compared to when they are not. ## Solicitation > Greetings! > > Are you an intrepid `ytt` user? \ > Interested in helping us make the tool more intuitive and easy to use? > > Well, we're nearing the end of development of our first iteration of Schemas in `ytt`! > > But we won't call it "ready" until we know it's both solving the right problem and doing it the right way. > > And this is where we could use your help... we're a little too close to the situation. We could use a fresh pair of eyes and honest feedback about how easy or hard this feature is to use. > > If this sparks your interest... read further in "About the ytt Schema Preview" ## Script [Carvel use in GitHub](https://hackmd.io/zmBg0aDAQ_6ozRxkZlEtTw) ### Configuration Author #### part 1 Source: https://github.com/k14s/ytt-schema-v1-preview/tree/develop/author-k8s-deployment Tests hypotheses: 1, 2 Notes: with a schema in place, author should be able to: - delete the `signing_keys()` function, entirely (and just refer to the `data.values.jwt.policy` directly ) - remove the `if hasattr()` and simply reference `data.values.uses` directly. ##### Sample Solution `config/schema.yml` ```yaml= #@data/values-schema --- uses: 20 jwt: policy: activeKeyId: "" keys: default_jwt_signing_key: signingKey: "" ``` ```diff= diff --git a/author-k8s-deployment/config/secret.yml b/author-k8s-deployment/config/secret.yml index ad6d8f7..c0f59b7 100644 --- a/author-k8s-deployment/config/secret.yml +++ b/author-k8s-deployment/config/secret.yml @@ -2,22 +2,10 @@ #@ load("@ytt:assert", "assert") #@ load("@ytt:yaml", "yaml") -#@ def signing_keys(jwt): -#@ if not hasattr(jwt, "policy") or not hasattr(jwt.policy, "activeKeyId"): -#@ assert.fail("jwt.policy.activeKeyId is required") -#@ end -#@ -#@ if type(jwt.policy.keys) != "struct": -#@ assert.fail("jwt.policy.keys must be an object") -#@ end -#@ -#@ return jwt.policy -#@ end - #@ def jwt_policy_file_contents(): jwt: token: - policy: #@ signing_keys(data.values.jwt) + policy: #@ data.values.jwt.policy #@ end --- @@ -26,11 +14,7 @@ kind: Secret metadata: name: uaa-jwt-policy-signing-keys annotations: - #@ if hasattr(data.values, "uses"): uses_remaining: #@ str(data.values.uses) - #@ else: - uses_remaining: "20" - #@ end type: Opaque stringData: jwt_policy_signing_keys.yml: #@ yaml.encode(jwt_policy_file_contents()) ``` #### part 2 Possible `@schema/type any=True` - array items of differing type - port that is a number or string - an undefined chunk of YAML Defaults for arrays: - log destinations Nullable: - optional configuration. TODO: - a scenario where an any type is needed? - defaults in arrays? - optional field -> nullable? Example with array, some work on this below ```yaml= --- apiVersion: v1 kind: Pod metadata: name: secret-cf-pod spec: containers: - name: test-container image: nginx volumeMounts: # name must match the volume name below - name: secret-volume mountPath: /etc/secret-volume - name: internal-volumes mountPath: /etc/internal-volume volumes: - name: secret-volume secret: secretName: test-secret - name: internal-volume secret: secretName: fake-secret ``` --- ### Configuration Consumer #### part 1 (4, 5) Now from the perspective of a configuration consumer, you have a version controlled directory that contains a ytt configuration that you need to customize for your cluster. The goal of this step is to compile the configuration with the following desired values: *(we provide the destination, try to tease out the path)* ```yaml= --- apiVersion: apps/v1 kind: Deployment metadata: name: rss-website labels: app: web spec: replicas: 3 selector: matchLabels: app: web template: metadata: labels: app: web spec: containers: - name: front-end image: nginx ports: - containerPort: 80 - name: rss-reader image: sample-reader/rss-php-nginx:v1 ports: - containerPort: 88 ``` We provide ``` ├── git-repo │ ├── config.yml └ └── schema.yml ``` `git-repo/config.yml`: ```yaml= #@ load("@ytt:data", "data") --- apiVersion: apps/v1 kind: Deployment metadata: name: #@ data.values.deployment_name labels: app: #@ data.values.app spec: replicas: #@ data.values.replicas selector: matchLabels: app: #@ data.values.app template: metadata: labels: app: #@ data.values.app spec: containers: #@ data.values.containers ``` and `git-repo/schema.yml`: ```yaml= #@data/values-schema --- deployment_name: default-name app: my-app replicas: 1 containers: - name: default-container image: default-image ports: - containerPort: 5050 ``` \**User should create a data values file to provide overrides to values in schema.*\* A solution could look like`values.yml`: ```yaml= #@data/values --- deployment_name: rss-website app: web replicas: 3 containers: - name: front-end image: nginx ports: - containerPort: 80 - name: rss-reader image: sample-reader/rss-php-nginx:v1 ports: - containerPort: 88 ``` #### part 2 Now enrich the deployment by adding a service. You can do this in a new file. Make sure to include the label, `app: web`, to this service as it is in the deployment. (multiple schemas is more of an advanced use case.) Desired Outcome: ```yaml= --- apiVersion: apps/v1 kind: Deployment metadata: name: rss-website labels: app: web spec: replicas: 3 selector: matchLabels: app: web template: metadata: labels: app: web spec: containers: - name: front-end image: nginx ports: - containerPort: 80 - name: rss-reader image: sample-reader/rss-php-nginx:v1 ports: - containerPort: 88 --- apiVersion: v1 kind: Service metadata: name: rss-service spec: type: ClusterIP ports: - port: 90 protocol: TCP targetPort: 5678 selector: app: web ``` A solution could look like `service.yml`: ```yaml= #@ load("@ytt:data", "data") --- apiVersion: v1 kind: Service metadata: name: rss-service spec: type: ClusterIP ports: - port: 90 protocol: TCP targetPort: 5678 selector: app: #@ data.values.app ``` ## Scenarios/Workflows Below here is scratch work - Extract out X fields into a configurable schema + data values - Convert data values into schema - Overlay assertions: basic example where there is an overlay assertion written to verify that certain fields of the output are present and of a correct type/length/value? Try replacing assertions with a schema that performs the same checks. This should work for ytt templates using `hasattr()`. --- (1, 2) example workflow pulled from cf-4-k8s postgress configuration (without _ytt_lib dir) `postgress.yml:` ```yaml= #@ load("@ytt:assert", "assert") #@ load("@ytt:data", "data") #@ load("@ytt:overlay", "overlay") --- apiVersion: v1 kind: Secret metadata: name: cf-db-admin-secret namespace: cf-db stringData: #@ if len(data.values.cf_db.admin_password) == 0: #@ assert.fail("cf_db.admin_password cannot be empty") #@ end postgresql-password: #@ data.values.cf_db.admin_password --- apiVersion: v1 kind: Secret metadata: name: cf-db-credentials namespace: cf-db stringData: #@ if len(data.values.capi.database.user) == 0: #@ assert.fail("capi.database.user cannot be empty") #@ end ccdb-username: #@ data.values.capi.database.user #@ if len(data.values.capi.database.password) == 0: #@ assert.fail("capi.database.password cannot be empty") #@ end ccdb-password: #@ data.values.capi.database.password #@ if len(data.values.uaa.database.user) == 0: #@ assert.fail("uaa.database.user cannot be empty") #@ end uaadb-username: #@ data.values.uaa.database.user #@ if len(data.values.uaa.database.password) == 0: #@ assert.fail("uaa.database.password cannot be empty") #@ end uaadb-password: #@ data.values.uaa.database.password ``` `values.yml:` ```yaml= #@data/values --- cf_db: admin_password: the_cf_dbadmin_password capi: database: user: ccdb_user password: ccdb_password uaa: database: user: uaadb_user password: uaadb_password ``` RESULT: `ytt -f .` ```yaml= apiVersion: v1 kind: Secret metadata: name: cf-db-admin-secret namespace: cf-db stringData: postgresql-password: the_cf_dbadmin_password --- apiVersion: v1 kind: Secret metadata: name: cf-db-credentials namespace: cf-db stringData: ccdb-username: ccdb_user ccdb-password: ccdb_password uaadb-username: uaadb_user uaadb-password: uaadb_password ``` - Prompt the user to simplify the template (postgress.yml) and add a schema file to set defaults in needed fields. --- (1, 2) Using overlays to assert type/presence, can we simplify this with schemas? `config.yml:` ```yaml= #@ load("@ytt:data", "data") apiVersion: extensions/v1beta1 kind: Ingress metadata: name: #@ data.values.name + '1' annotations: ingress.kubernetes.io/rewrite-target: #@ data.values.rewriteTarget ``` `values.yml:` ```yaml= #@data/values --- name: example-ingress rewriteTarget: /mount ``` `assert1.yml:` ```yaml= #@ load("@ytt:overlay", "overlay") #@overlay/match by=overlay.subset({'kind': 'Ingress'}), expects="1+" --- metadata: annotations: #@overlay/assert ingress.kubernetes.io/rewrite-target: /mount ``` `assert2.yml:` ```yaml= #@ load("@ytt:overlay", "overlay") #@overlay/match by=overlay.subset({'kind': 'Ingress'}), expects="1+" --- metadata: annotations: #@overlay/assert via=lambda left, right: left.startswith(right) ingress.kubernetes.io/rewrite-target: / -OR- #@overlay/match by=overlay.subset({'kind': 'Ingress'}), expects="1+" --- metadata: annotations: #@overlay/assert via=lambda left, right: len(left) > 0 ingress.kubernetes.io/rewrite-target: ``` COPIED FROM INITIAL CONFIGURATION AUTHOR PASS: " Desired values to make configurable: ``` name: secret-volume ``` Please define a schema which extracts out these values from the following template: ```yaml= --- apiVersion: v1 kind: Pod metadata: name: secret-cf-pod spec: containers: - name: test-container image: nginx volumeMounts: # name must match the volume name below - name: secret-volume mountPath: /etc/secret-volume - name: secret-volume mountPath: /etc/secret-volume volumes: - name: secret-volume secret: secretName: test-secret - name: secret-volume secret: secretName: test-secret ``` `postgress.yml:` ```yaml= #@ load("@ytt:assert", "assert") #@ load("@ytt:data", "data") #@ load("@ytt:overlay", "overlay") --- apiVersion: v1 kind: Secret metadata: name: cf-db-admin-secret namespace: cf-db stringData: #@ if len(data.values.cf_db.admin_password) == 0: #@ assert.fail("cf_db.admin_password cannot be empty") #@ end postgresql-password: #@ data.values.cf_db.admin_password --- apiVersion: v1 kind: Secret metadata: name: cf-db-credentials namespace: cf-db stringData: #@ if len(data.values.uaa.database.user) == 0: #@ assert.fail("uaa.database.user cannot be empty") #@ end uaadb-username: #@ data.values.uaa.database.user #@ if len(data.values.uaa.database.password) == 0: #@ assert.fail("uaa.database.password cannot be empty") #@ end uaadb-password: #@ data.values.uaa.database.password ``` `values.yml:` ```yaml= #@data/values --- cf_db: admin_password: the_cf_dbadmin_password uaa: database: user: uaadb_user password: uaadb_password ``` " --- (4, 5) two different deployment manifests, one configured with just data values, the other with schema and data values: DV only: `config.yml:` ``` #@ load("@ytt:data", "data") --- apiVersion: apps/v1 kind: Deployment metadata: name: #@ data.values.deployment_name labels: app: web spec: replicas: #@ data.values.replicas selector: matchLabels: app: web template: metadata: labels: app: web spec: containers: #@ data.values.pod_containers ``` `values1.yml` ``` #@data/values --- replicas: 3 deployment_name: rss-site pod_containers: - name: front-end image: nginx ports: - containerPort: 80 - name: rss-reader image: nickchase/rss-php-nginx:v1 ports: - containerPort: 88 ``` With Schema: `config.yml:` ``` #@ load("@ytt:data", "data") --- apiVersion: apps/v1 kind: Deployment metadata: name: #@ data.values.deployment_name labels: app: web spec: replicas: #@ data.values.replicas selector: matchLabels: app: web template: metadata: labels: app: web spec: containers: #@ data.values.pod_containers ``` `schema.yml` ``` #@data/values-schema --- replicas: 3 deployment_name: rss-site pod_containers: - name: front-end image: nginx ports: - containerPort: 80 ``` `values1.yml` ``` #@data/values --- pod_containers: - name: front-end image: nginx ports: - containerPort: 80 - name: rss-reader image: nickchase/rss-php-nginx:v1 ports: - containerPort: 88 ``` --- hasattr() helper functions could be replaced with a schema ```yaml= #@ load("@ytt:data", "data") #@ load("jwt_policy_signing_keys.star", "signing_keys") --- apiVersion: v1 kind: Secret metadata: name: uaa-jwt-policy-signing-keys #@ if hasattr(data.values, "uses"): uses: #@ data.values.uses #@ else: uses: 20 #@ end type: Opaque stringData: jwt_policy_signing_keys.yml: jwt: token: policy: #@ signing_keys(data.values.jwt) ``` ```yaml= #@data/values --- jwt: policy: activeKeyId: "default_jwt_signing_key" keys: default_jwt_signing_key: signingKey: jwt_policy_signing_key ``` ```python load("@ytt:assert", "assert") def signing_keys(jwt): if not hasattr(jwt, "policy") or not hasattr(jwt.policy, "activeKeyId"): assert.fail("jwt.policy.activeKeyId is required") end if type(jwt.policy.keys) != "struct": assert.fail("jwt.policy.keys must be an object") end if not hasattr(jwt.policy.keys, jwt.policy.activeKeyId): assert.fail("jwt.policy.keys must contain keyId matching jwt.policy.activeKeyId") end return jwt.policy end ``` could be replaced with: ```yaml= #@ load("@ytt:data", "data") #@ load("jwt_policy_signing_keys.star", "signing_keys") --- apiVersion: v1 kind: Secret metadata: name: uaa-jwt-policy-signing-keys uses: #@ data.values.uses type: Opaque stringData: jwt_policy_signing_keys.yml: jwt: token: policy: #@ signing_keys(data.values.jwt) ``` ```yaml= #@data/values-schema --- uses: 20 jwt: policy: activeKeyId: "default_jwt_signing_key" #! allows user to add items to keys map without schema error. #! thus keys could have many keys but only one is active at a time #@schema/type any=True keys: default_jwt_signing_key: signingKey: jwt_policy_signing_key ``` ```yaml= #@data/values --- #! uses: default value is set in schema, can be overridden here jwt: policy: activeKeyId: "dev_jwt_signing_key" keys: default_jwt_signing_key: signingKey: jwt_policy_signing_key ``` ```python= load("@ytt:assert", "assert") def signing_keys(jwt): if not hasattr(jwt.policy.keys, jwt.policy.activeKeyId): assert.fail("jwt.policy.keys must contain keyId matching jwt.policy.activeKeyId value") end return jwt.policy end ```