---
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
```