v2
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
---
|
||||
- name: Reset validation result
|
||||
ansible.builtin.set_fact:
|
||||
wikideck_test_invalid_accepted: false
|
||||
|
||||
- name: Try invalid deployment settings
|
||||
vars:
|
||||
wikideck_image: "{{ invalid_case.image | default('registry.example.com/wikideck:1.0.0') }}"
|
||||
wikideck_name: "{{ invalid_case.name | default('wikideck') }}"
|
||||
wikideck_storage_access_modes: "{{ invalid_case.access_modes | default(['ReadWriteOnce']) }}"
|
||||
wikideck_ingress_enabled: "{{ invalid_case.ingress | default(false) }}"
|
||||
wikideck_ingress_host: "{{ invalid_case.host | default('wikideck.example.com') }}"
|
||||
wikideck_public_origin: "{{ invalid_case.origin | default('') }}"
|
||||
wikideck_resources: "{{ invalid_case.resources | default({'requests': {'cpu': '100m', 'memory': '128Mi'}, 'limits': {'cpu': '1', 'memory': '512Mi'}}) }}"
|
||||
wikideck_offline: "{{ invalid_case.offline | default(false) }}"
|
||||
wikideck_image_pull_secrets: "{{ invalid_case.pull_secrets | default([]) }}"
|
||||
wikideck_network_policy_ingress_from: "{{ invalid_case.ingress_from | default([{'podSelector': {}}]) }}"
|
||||
block:
|
||||
- name: Run role input validation
|
||||
ansible.builtin.include_tasks: ../tasks/validate.yml
|
||||
|
||||
- name: Record unexpectedly accepted inputs
|
||||
ansible.builtin.set_fact:
|
||||
wikideck_test_invalid_accepted: true
|
||||
rescue:
|
||||
- name: Confirm invalid inputs failed validation
|
||||
ansible.builtin.debug:
|
||||
msg: "Rejected invalid settings: {{ invalid_case.label }}"
|
||||
|
||||
- name: Assert invalid configuration was rejected
|
||||
ansible.builtin.assert:
|
||||
that: not wikideck_test_invalid_accepted
|
||||
fail_msg: "Validation incorrectly accepted {{ invalid_case.label }}"
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
localhost
|
||||
|
||||
[local]
|
||||
localhost ansible_connection=local
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
# Offline tests: no Kubernetes credentials or API calls.
|
||||
- name: Validate role configuration and rendered Kubernetes resources
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: false
|
||||
vars_files:
|
||||
- ../defaults/main.yml
|
||||
tasks:
|
||||
- name: Test rendering variants
|
||||
ansible.builtin.include_tasks: render_case.yml
|
||||
vars:
|
||||
wikideck_image: "{{ render_case.image | default('registry.example.com/wikideck:1.0.0') }}"
|
||||
wikideck_name: "{{ render_case.name | default('wikideck') }}"
|
||||
wikideck_namespace: "{{ render_case.namespace | default('wikideck') }}"
|
||||
wikideck_offline: "{{ render_case.offline | default(false) }}"
|
||||
wikideck_ingress_enabled: "{{ render_case.ingress | default(false) }}"
|
||||
wikideck_ingress_host: wikideck.example.com
|
||||
wikideck_public_origin: "{{ 'https://wikideck.example.com' if render_case.ingress | default(false) else '' }}"
|
||||
wikideck_ingress_class_name: "{{ render_case.ingress_class | default('') }}"
|
||||
wikideck_ingress_annotations: "{{ render_case.annotations | default({}) }}"
|
||||
wikideck_storage_class: "{{ render_case.storage_class | default(none) }}"
|
||||
wikideck_existing_claim: "{{ render_case.existing_claim | default('') }}"
|
||||
wikideck_image_pull_secrets: "{{ render_case.pull_secrets | default([]) }}"
|
||||
wikideck_network_policy_ingress_from: "{{ render_case.ingress_from | default([{'podSelector': {}}]) }}"
|
||||
wikideck_network_policy_dns_to: "{{ render_case.dns_to | default([{'namespaceSelector': {}}]) }}"
|
||||
wikideck_network_policy_https_to: "{{ render_case.https_to | default([{'ipBlock': {'cidr': '0.0.0.0/0'}}]) }}"
|
||||
loop:
|
||||
- label: defaults
|
||||
- label: TLS ingress and existing storage
|
||||
ingress: true
|
||||
ingress_class: nginx
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
existing_claim: existing-chain
|
||||
storage_class: fast-encrypted
|
||||
pull_secrets: [registry-credentials]
|
||||
name: custom-deck
|
||||
namespace: games
|
||||
- label: offline and no storage class
|
||||
offline: true
|
||||
storage_class: ""
|
||||
- label: default-deny all traffic
|
||||
ingress_from: []
|
||||
dns_to: []
|
||||
https_to: []
|
||||
- label: DNS-only offline mode
|
||||
offline: true
|
||||
- label: digest-pinned image
|
||||
image: registry.example.com/wikideck@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
loop_control:
|
||||
loop_var: render_case
|
||||
label: "{{ render_case.label }}"
|
||||
|
||||
- name: Test invalid settings are rejected
|
||||
ansible.builtin.include_tasks: invalid_case.yml
|
||||
loop:
|
||||
- label: missing image
|
||||
image: ""
|
||||
- label: untagged image
|
||||
image: registry.example.com/wikideck
|
||||
- label: registry port is not an image tag
|
||||
image: registry.example.com:5000/wikideck
|
||||
- label: malformed image digest
|
||||
image: registry.example.com/wikideck@sha256:abcd
|
||||
- label: invalid resource name
|
||||
name: Invalid_Name
|
||||
- label: unsafe multi-writer storage
|
||||
access_modes: [ReadWriteMany]
|
||||
- label: missing ingress origin
|
||||
ingress: true
|
||||
- label: mismatched ingress origin
|
||||
ingress: true
|
||||
origin: https://another.example.com
|
||||
- label: insecure public origin
|
||||
origin: http://wikideck.example.com
|
||||
- label: origin with path
|
||||
origin: https://wikideck.example.com/path
|
||||
- label: invalid ingress hostname
|
||||
ingress: true
|
||||
host: invalid_host
|
||||
origin: https://wikideck.example.com
|
||||
- label: missing resource limits
|
||||
resources:
|
||||
requests: {cpu: 100m, memory: 128Mi}
|
||||
- label: ambiguous boolean
|
||||
offline: "false"
|
||||
- label: wrong list type
|
||||
pull_secrets: not-a-list
|
||||
- label: empty peer allows all traffic
|
||||
ingress_from: [{}]
|
||||
loop_control:
|
||||
loop_var: invalid_case
|
||||
label: "{{ invalid_case.label }}"
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
- name: Validate rendering case inputs
|
||||
ansible.builtin.include_tasks: ../tasks/validate.yml
|
||||
|
||||
- name: Reset rendered resources
|
||||
ansible.builtin.set_fact:
|
||||
wikideck_test_manifests: {}
|
||||
|
||||
- name: Render and parse all Kubernetes templates
|
||||
ansible.builtin.set_fact:
|
||||
wikideck_test_manifests: >-
|
||||
{{ wikideck_test_manifests | combine({item: lookup('ansible.builtin.template',
|
||||
playbook_dir ~ '/../templates/' ~ item ~ '.yml.j2') | from_yaml}) }}
|
||||
loop: [deployment, service, serviceaccount, pvc, ingress, networkpolicy]
|
||||
|
||||
- name: Check safety and wiring invariants
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- deployment.spec.replicas == 1
|
||||
- deployment.spec.strategy.type == 'Recreate'
|
||||
- deployment.spec.selector.matchLabels == deployment.spec.template.metadata.labels
|
||||
- wikideck_test_manifests.service.spec.selector == deployment.spec.selector.matchLabels
|
||||
- wikideck_test_manifests.networkpolicy.spec.podSelector.matchLabels == deployment.spec.selector.matchLabels
|
||||
- wikideck_test_manifests.service.spec.type == 'ClusterIP'
|
||||
- wikideck_test_manifests.service.spec.ports[0].targetPort == 'http'
|
||||
- pod.serviceAccountName == wikideck_name
|
||||
- not pod.automountServiceAccountToken
|
||||
- not wikideck_test_manifests.serviceaccount.automountServiceAccountToken
|
||||
- pod.securityContext.runAsNonRoot and pod.securityContext.runAsUser == 1000
|
||||
- pod.securityContext.fsGroup == 1000
|
||||
- pod.securityContext.seccompProfile.type == 'RuntimeDefault'
|
||||
- container.securityContext.readOnlyRootFilesystem
|
||||
- not container.securityContext.allowPrivilegeEscalation
|
||||
- container.securityContext.capabilities.drop == ['ALL']
|
||||
- container.image == wikideck_image
|
||||
- container.resources == wikideck_resources
|
||||
- container.ports[0].containerPort == 3001
|
||||
- container.startupProbe.httpGet.path == '/healthz'
|
||||
- container.readinessProbe.httpGet.path == '/healthz'
|
||||
- container.livenessProbe.httpGet.path == '/healthz'
|
||||
- pod.terminationGracePeriodSeconds >= 30
|
||||
- pod.volumes[0].persistentVolumeClaim.claimName == expected_claim
|
||||
- pod.volumes[1].emptyDir.medium == 'Memory'
|
||||
- container.volumeMounts[0].mountPath == '/app/data'
|
||||
- env.HOST == '0.0.0.0' and env.PORT == '3001'
|
||||
- env.WIKIDECK_DATA_DIR == '/app/data' and env.NODE_ENV == 'production'
|
||||
- env.WIKIDECK_OFFLINE == ('1' if wikideck_offline else '0')
|
||||
- (env.PUBLIC_ORIGIN | default('')) == wikideck_public_origin
|
||||
- wikideck_test_manifests.pvc.spec.accessModes == wikideck_storage_access_modes
|
||||
- wikideck_test_manifests.ingress.spec.tls[0].secretName == wikideck_tls_secret_name
|
||||
- wikideck_test_manifests.ingress.spec.rules[0].host == wikideck_ingress_host
|
||||
- wikideck_test_manifests.ingress.spec.rules[0].http.paths[0].backend.service.name == wikideck_name
|
||||
- wikideck_test_manifests.ingress.metadata.annotations == wikideck_ingress_annotations
|
||||
- (wikideck_test_manifests.ingress.spec.ingressClassName | default('')) == wikideck_ingress_class_name
|
||||
- (pod.imagePullSecrets | default([]) | map(attribute='name') | list) == wikideck_image_pull_secrets
|
||||
- wikideck_test_manifests.networkpolicy.spec.policyTypes == ['Ingress', 'Egress']
|
||||
- wikideck_test_manifests.networkpolicy.spec.ingress | length == (1 if wikideck_network_policy_ingress_from | length > 0 else 0)
|
||||
- wikideck_test_manifests.networkpolicy.spec.egress | length == expected_egress_count | int
|
||||
vars:
|
||||
deployment: "{{ wikideck_test_manifests.deployment }}"
|
||||
pod: "{{ deployment.spec.template.spec }}"
|
||||
container: "{{ pod.containers[0] }}"
|
||||
env: "{{ dict(container.env | map(attribute='name') | zip(container.env | map(attribute='value'))) }}"
|
||||
expected_claim: "{{ wikideck_existing_claim if wikideck_existing_claim | length > 0 else wikideck_name ~ '-data' }}"
|
||||
expected_egress_count: >-
|
||||
{{ (1 if wikideck_network_policy_dns_to | length > 0 else 0)
|
||||
+ (1 if not wikideck_offline and wikideck_network_policy_https_to | length > 0 else 0) }}
|
||||
|
||||
- name: Check omitted default StorageClass
|
||||
ansible.builtin.assert:
|
||||
that: wikideck_test_manifests.pvc.spec.storageClassName is not defined
|
||||
when: wikideck_storage_class is none
|
||||
|
||||
- name: Check explicit StorageClass including empty string
|
||||
ansible.builtin.assert:
|
||||
that: wikideck_test_manifests.pvc.spec.storageClassName == wikideck_storage_class
|
||||
when: wikideck_storage_class is not none
|
||||
+10
-5
@@ -1,5 +1,10 @@
|
||||
---
|
||||
- hosts: localhost
|
||||
remote_user: root
|
||||
roles:
|
||||
- ansible-role-wikideck
|
||||
---
|
||||
# Live-cluster smoke test. Supply wikideck_image with --extra-vars.
|
||||
- name: Deploy Wikideck to the selected test cluster
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Apply the role from this checkout
|
||||
ansible.builtin.include_role:
|
||||
name: "{{ playbook_dir }}/.." # noqa: role-name[path] -- smoke-test the checkout without installation
|
||||
|
||||
Reference in New Issue
Block a user