---
# System prepended metadata

title: Usability Testing for Schema v1
tags: [schema]

---

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