diff --git a/artifacthub/library/general/storageclass/1.1.2/README.md b/artifacthub/library/general/storageclass/1.1.2/README.md new file mode 100644 index 000000000..f398931a9 --- /dev/null +++ b/artifacthub/library/general/storageclass/1.1.2/README.md @@ -0,0 +1,17 @@ +# StorageClass + +The `StorageClass` constraint blocks the creation of PVCs or StatefulSets +where the specified storage class doesn't exist on the cluster, or that no +storage class at all is specified. + +This policy helps prevent workloads from getting stuck indefinitely waiting +for a storage class to provision the persistent storage that will never +happen. This often causes users to get confused as to why their pods are stuck +pending, and requires deleting the StatefulSet and any PVCs it has created along +with redeploying the workload in order to fix. Blocking it up front makes it +much easier to fix before there is a mess to clean up. + +Optionally accepts an `allowedStorageClasses` parameter to restrict PVCs and +StatefulSets to a subset list of allowed storage classes. + +> Please note that this policy requires Gatekeeper v3.9.0 or later. diff --git a/artifacthub/library/general/storageclass/1.1.2/artifacthub-pkg.yml b/artifacthub/library/general/storageclass/1.1.2/artifacthub-pkg.yml new file mode 100644 index 000000000..206bba06e --- /dev/null +++ b/artifacthub/library/general/storageclass/1.1.2/artifacthub-pkg.yml @@ -0,0 +1,22 @@ +version: 1.1.2 +name: k8sstorageclass +displayName: Storage Class +createdAt: "2023-11-06T20:56:52Z" +description: Requires storage classes to be specified when used. Only Gatekeeper 3.9+ is supported. +digest: d9c7ffbcd5192a9b77d4c7fe14397efb43d942ef0a286437b8199f615e24488f +license: Apache-2.0 +homeURL: https://open-policy-agent.github.io/gatekeeper-library/website/storageclass +keywords: + - gatekeeper + - open-policy-agent + - policies +readme: |- + # Storage Class + Requires storage classes to be specified when used. Only Gatekeeper 3.9+ is supported. +install: |- + ### Usage + ```shell + kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/artifacthub/library/general/storageclass/1.1.2/template.yaml + ``` +provider: + name: Gatekeeper Library diff --git a/artifacthub/library/general/storageclass/1.1.2/kustomization.yaml b/artifacthub/library/general/storageclass/1.1.2/kustomization.yaml new file mode 100644 index 000000000..7d70d11b7 --- /dev/null +++ b/artifacthub/library/general/storageclass/1.1.2/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - template.yaml diff --git a/artifacthub/library/general/storageclass/1.1.2/samples/storageclass-allowlist/constraint.yaml b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass-allowlist/constraint.yaml new file mode 100644 index 000000000..ddd6acaa5 --- /dev/null +++ b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass-allowlist/constraint.yaml @@ -0,0 +1,15 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sStorageClass +metadata: + name: allowed-storageclass +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["PersistentVolumeClaim"] + - apiGroups: ["apps"] + kinds: ["StatefulSet"] + parameters: + includeStorageClassesInMessage: true + allowedStorageClasses: + - allowed-storage-class diff --git a/artifacthub/library/general/storageclass/1.1.2/samples/storageclass-allowlist/example_allowed.yaml b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass-allowlist/example_allowed.yaml new file mode 100644 index 000000000..7982eca54 --- /dev/null +++ b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass-allowlist/example_allowed.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: allowed-storage-class-pvc +spec: + accessModes: + - ReadWriteOnce + volumeMode: Filesystem + resources: + requests: + storage: 8Gi + storageClassName: allowed-storage-class diff --git a/artifacthub/library/general/storageclass/1.1.2/samples/storageclass-allowlist/example_disallowed.yaml b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass-allowlist/example_disallowed.yaml new file mode 100644 index 000000000..7f679bb30 --- /dev/null +++ b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass-allowlist/example_disallowed.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: disallowed-storage-class-pvc +spec: + accessModes: + - ReadWriteOnce + volumeMode: Filesystem + resources: + requests: + storage: 8Gi + storageClassName: disallowed-storage-class diff --git a/artifacthub/library/general/storageclass/1.1.2/samples/storageclass-allowlist/example_inventory_allowed_storageclass.yaml b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass-allowlist/example_inventory_allowed_storageclass.yaml new file mode 100644 index 000000000..930822f4a --- /dev/null +++ b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass-allowlist/example_inventory_allowed_storageclass.yaml @@ -0,0 +1,7 @@ +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: allowed-storage-class +provisioner: foo +parameters: +allowVolumeExpansion: true diff --git a/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/constraint.yaml b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/constraint.yaml new file mode 100644 index 000000000..6b49e5dad --- /dev/null +++ b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/constraint.yaml @@ -0,0 +1,13 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sStorageClass +metadata: + name: storageclass +spec: + match: + kinds: + - apiGroups: [""] + kinds: ["PersistentVolumeClaim"] + - apiGroups: ["apps"] + kinds: ["StatefulSet"] + parameters: + includeStorageClassesInMessage: true diff --git a/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/example_allowed_pvc.yaml b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/example_allowed_pvc.yaml new file mode 100644 index 000000000..a8608f53e --- /dev/null +++ b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/example_allowed_pvc.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: ok +spec: + accessModes: + - ReadWriteOnce + volumeMode: Filesystem + resources: + requests: + storage: 8Gi + storageClassName: somestorageclass diff --git a/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/example_allowed_ss.yaml b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/example_allowed_ss.yaml new file mode 100644 index 000000000..b1bd5db79 --- /dev/null +++ b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/example_allowed_ss.yaml @@ -0,0 +1,30 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: volumeclaimstorageclass +spec: + selector: + matchLabels: + app: volumeclaimstorageclass + serviceName: volumeclaimstorageclass + replicas: 1 + template: + metadata: + labels: + app: volumeclaimstorageclass + spec: + containers: + - name: main + image: registry.k8s.io/nginx-slim:0.8 + volumeMounts: + - name: data + mountPath: /usr/share/nginx/html + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + storageClassName: "somestorageclass" + resources: + requests: + storage: 1Gi diff --git a/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/example_disallowed_pvc_badname.yaml b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/example_disallowed_pvc_badname.yaml new file mode 100644 index 000000000..ec75200c4 --- /dev/null +++ b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/example_disallowed_pvc_badname.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: badstorageclass +spec: + accessModes: + - ReadWriteOnce + volumeMode: Filesystem + resources: + requests: + storage: 8Gi + storageClassName: badstorageclass diff --git a/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/example_disallowed_pvc_nonamename.yaml b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/example_disallowed_pvc_nonamename.yaml new file mode 100644 index 000000000..2f9cc12ec --- /dev/null +++ b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/example_disallowed_pvc_nonamename.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: nostorageclass +spec: + accessModes: + - ReadWriteOnce + volumeMode: Filesystem + resources: + requests: + storage: 8Gi diff --git a/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/example_disallowed_ssvct_badnamename.yaml b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/example_disallowed_ssvct_badnamename.yaml new file mode 100644 index 000000000..58c98fd7d --- /dev/null +++ b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/example_disallowed_ssvct_badnamename.yaml @@ -0,0 +1,30 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: badvolumeclaimstorageclass +spec: + selector: + matchLabels: + app: badvolumeclaimstorageclass + serviceName: badvolumeclaimstorageclass + replicas: 1 + template: + metadata: + labels: + app: badvolumeclaimstorageclass + spec: + containers: + - name: main + image: registry.k8s.io/nginx-slim:0.8 + volumeMounts: + - name: data + mountPath: /usr/share/nginx/html + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + storageClassName: "badstorageclass" + resources: + requests: + storage: 1Gi diff --git a/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/example_disallowed_ssvct_nonamename.yaml b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/example_disallowed_ssvct_nonamename.yaml new file mode 100644 index 000000000..ece7c23dd --- /dev/null +++ b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/example_disallowed_ssvct_nonamename.yaml @@ -0,0 +1,29 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: novolumeclaimstorageclass +spec: + selector: + matchLabels: + app: novolumeclaimstorageclass + serviceName: novolumeclaimstorageclass + replicas: 1 + template: + metadata: + labels: + app: novolumeclaimstorageclass + spec: + containers: + - name: main + image: registry.k8s.io/nginx-slim:0.8 + volumeMounts: + - name: data + mountPath: /usr/share/nginx/html + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi diff --git a/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/example_inventory_allowed_storageclass.yaml b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/example_inventory_allowed_storageclass.yaml new file mode 100644 index 000000000..0cdee0aa3 --- /dev/null +++ b/artifacthub/library/general/storageclass/1.1.2/samples/storageclass/example_inventory_allowed_storageclass.yaml @@ -0,0 +1,7 @@ +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: somestorageclass +provisioner: foo +parameters: +allowVolumeExpansion: true diff --git a/artifacthub/library/general/storageclass/1.1.2/suite.yaml b/artifacthub/library/general/storageclass/1.1.2/suite.yaml new file mode 100644 index 000000000..9cccc2ebb --- /dev/null +++ b/artifacthub/library/general/storageclass/1.1.2/suite.yaml @@ -0,0 +1,53 @@ +kind: Suite +apiVersion: test.gatekeeper.sh/v1alpha1 +metadata: + name: storageclass +tests: +- name: storageclass + template: template.yaml + constraint: samples/storageclass/constraint.yaml + cases: + - name: example-allowed-pvc + object: samples/storageclass/example_allowed_pvc.yaml + inventory: + - samples/storageclass/example_inventory_allowed_storageclass.yaml + assertions: + - violations: no + - name: example-allowed-ss + object: samples/storageclass/example_allowed_ss.yaml + inventory: + - samples/storageclass/example_inventory_allowed_storageclass.yaml + assertions: + - violations: no + - name: example-disallowed-pvc-badname + object: samples/storageclass/example_disallowed_pvc_badname.yaml + assertions: + - violations: yes + - name: example-disallowed-ssvct-badnamename + object: samples/storageclass/example_disallowed_ssvct_badnamename.yaml + assertions: + - violations: yes + - name: example-disallowed-pvc-nonamename + object: samples/storageclass/example_disallowed_pvc_nonamename.yaml + assertions: + - violations: yes + - name: example-disallowed-ssvct-nonamename + object: samples/storageclass/example_disallowed_ssvct_nonamename.yaml + assertions: + - violations: yes +- name: storageclass-allowlist + template: template.yaml + constraint: samples/storageclass-allowlist/constraint.yaml + cases: + - name: allowed-storage-class-pvc + object: samples/storageclass-allowlist/example_allowed.yaml + inventory: + - samples/storageclass-allowlist/example_inventory_allowed_storageclass.yaml + assertions: + - violations: no + - name: disallowed-storage-class-pvc + object: samples/storageclass-allowlist/example_disallowed.yaml + inventory: + - samples/storageclass-allowlist/example_inventory_allowed_storageclass.yaml + assertions: + - violations: yes diff --git a/artifacthub/library/general/storageclass/1.1.2/sync.yaml b/artifacthub/library/general/storageclass/1.1.2/sync.yaml new file mode 100644 index 000000000..9d7fd5e16 --- /dev/null +++ b/artifacthub/library/general/storageclass/1.1.2/sync.yaml @@ -0,0 +1,11 @@ +apiVersion: config.gatekeeper.sh/v1alpha1 +kind: Config +metadata: + name: config + namespace: "gatekeeper-system" +spec: + sync: + syncOnly: + - group: "storage.k8s.io" + version: "v1" + kind: "StorageClass" diff --git a/artifacthub/library/general/storageclass/1.1.2/template.yaml b/artifacthub/library/general/storageclass/1.1.2/template.yaml new file mode 100644 index 000000000..4cd85d797 --- /dev/null +++ b/artifacthub/library/general/storageclass/1.1.2/template.yaml @@ -0,0 +1,155 @@ +apiVersion: templates.gatekeeper.sh/v1 +kind: ConstraintTemplate +metadata: + name: k8sstorageclass + annotations: + metadata.gatekeeper.sh/title: "Storage Class" + metadata.gatekeeper.sh/version: 1.1.2 + metadata.gatekeeper.sh/requires-sync-data: | + "[ + [ + { + "groups":["storage.k8s.io"], + "versions": ["v1"], + "kinds": ["StorageClass"] + } + ] + ]" + description: >- + Requires storage classes to be specified when used. Only Gatekeeper 3.9+ and non-ephemeral containers are supported. +spec: + crd: + spec: + names: + kind: K8sStorageClass + validation: + openAPIV3Schema: + type: object + description: >- + Requires storage classes to be specified when used. + properties: + includeStorageClassesInMessage: + type: boolean + default: true + allowedStorageClasses: + type: array + description: "An optional allow-list of storage classes. If specified, any storage class not in the `allowedStorageClasses` parameter is disallowed." + items: + type: string + targets: + - target: admission.k8s.gatekeeper.sh + rego: | + package k8sstorageclass + + is_pvc(obj) { + obj.apiVersion == "v1" + obj.kind == "PersistentVolumeClaim" + } + + is_statefulset(obj) { + obj.apiVersion == "apps/v1" + obj.kind == "StatefulSet" + } + + violation[{"msg": msg}] { + not data.inventory.cluster["storage.k8s.io/v1"]["StorageClass"] + msg := sprintf("StorageClasses not synced. Gatekeeper may be misconfigured. Please have a cluster-admin consult the documentation.", []) + } + + storageclass_allowed(name) { + data.inventory.cluster["storage.k8s.io/v1"]["StorageClass"][name] + # support both direct use of * and as the default value + object.get(input.parameters, "allowedStorageClasses", ["*"])[_] == "*" + } + + storageclass_allowed(name) { + data.inventory.cluster["storage.k8s.io/v1"]["StorageClass"][name] + input.parameters.allowedStorageClasses[_] == name + } + + violation[{"msg": pvc_storageclass_badname_msg}] { + is_pvc(input.review.object) + not storageclass_allowed(input.review.object.spec.storageClassName) + } + pvc_storageclass_badname_msg := sprintf("pvc did not specify a valid storage class name <%v>. Must be one of [%v]", args) { + input.parameters.includeStorageClassesInMessage + object.get(input.parameters, "allowedStorageClasses", null) == null + args := [ + input.review.object.spec.storageClassName, + concat(", ", [n | data.inventory.cluster["storage.k8s.io/v1"]["StorageClass"][n]]) + ] + } else := sprintf("pvc did not specify an allowed and valid storage class name <%v>. Must be one of [%v]", args) { + input.parameters.includeStorageClassesInMessage + object.get(input.parameters, "allowedStorageClasses", null) != null + sc := {n | data.inventory.cluster["storage.k8s.io/v1"]["StorageClass"][n]} & {x | x = object.get(input.parameters, "allowedStorageClasses", [])[_]} + args := [ + input.review.object.spec.storageClassName, + concat(", ", sc) + ] + } else := sprintf( + "pvc did not specify a valid storage class name <%v>.", + [input.review.object.spec.storageClassName] + ) + + violation[{"msg": pvc_storageclass_noname_msg}] { + is_pvc(input.review.object) + not input.review.object.spec.storageClassName + } + pvc_storageclass_noname_msg := sprintf("pvc did not specify a storage class name. Must be one of [%v]", args) { + input.parameters.includeStorageClassesInMessage + args := [ + concat(", ", [n | data.inventory.cluster["storage.k8s.io/v1"]["StorageClass"][n]]) + ] + } else := sprintf( + "pvc did not specify a storage class name.", + [] + ) + + violation[{"msg": statefulset_vct_badname_msg(vct)}] { + is_statefulset(input.review.object) + vct := input.review.object.spec.volumeClaimTemplates[_] + not storageclass_allowed(vct.spec.storageClassName) + } + statefulset_vct_badname_msg(vct) := msg { + input.parameters.includeStorageClassesInMessage + object.get(input.parameters, "allowedStorageClasses", null) == null + msg := sprintf( + "statefulset did not specify a valid storage class name <%v>. Must be one of [%v]", [ + vct.spec.storageClassName, + concat(", ", [n | data.inventory.cluster["storage.k8s.io/v1"]["StorageClass"][n]]) + ]) + } + statefulset_vct_badname_msg(vct) := msg { + input.parameters.includeStorageClassesInMessage + object.get(input.parameters, "allowedStorageClasses", null) != null + sc := {n | data.inventory.cluster["storage.k8s.io/v1"]["StorageClass"][n]} & {x | x = object.get(input.parameters, "allowedStorageClasses", [])[_]} + msg := sprintf( + "statefulset did not specify an allowed and valid storage class name <%v>. Must be one of [%v]", [ + vct.spec.storageClassName, + concat(", ", sc) + ]) + } + statefulset_vct_badname_msg(vct) := msg { + not input.parameters.includeStorageClassesInMessage + msg := sprintf( + "statefulset did not specify a valid storage class name <%v>.", [ + vct.spec.storageClassName + ]) + } + + violation[{"msg": statefulset_vct_noname_msg}] { + is_statefulset(input.review.object) + vct := input.review.object.spec.volumeClaimTemplates[_] + not vct.spec.storageClassName + } + statefulset_vct_noname_msg := sprintf("statefulset did not specify a storage class name. Must be one of [%v]", args) { + input.parameters.includeStorageClassesInMessage + args := [ + concat(", ", [n | data.inventory.cluster["storage.k8s.io/v1"]["StorageClass"][n]]) + ] + } else := sprintf( + "statefulset did not specify a storage class name.", + [] + ) + + # We might want to consider adding validation of pod generic ephemerals. diff --git a/library/general/storageclass/template.yaml b/library/general/storageclass/template.yaml index b1bf0748c..4cd85d797 100644 --- a/library/general/storageclass/template.yaml +++ b/library/general/storageclass/template.yaml @@ -4,7 +4,7 @@ metadata: name: k8sstorageclass annotations: metadata.gatekeeper.sh/title: "Storage Class" - metadata.gatekeeper.sh/version: 1.1.1 + metadata.gatekeeper.sh/version: 1.1.2 metadata.gatekeeper.sh/requires-sync-data: | "[ [ @@ -16,7 +16,7 @@ metadata: ] ]" description: >- - Requires storage classes to be specified when used. Only Gatekeeper 3.9+ is supported. + Requires storage classes to be specified when used. Only Gatekeeper 3.9+ and non-ephemeral containers are supported. spec: crd: spec: @@ -152,4 +152,4 @@ spec: [] ) - #FIXME pod generic ephemeral might be good to validate some day too. + # We might want to consider adding validation of pod generic ephemerals. diff --git a/src/general/storageclass/constraint.tmpl b/src/general/storageclass/constraint.tmpl index 8b46ac7e5..b192cc8f4 100644 --- a/src/general/storageclass/constraint.tmpl +++ b/src/general/storageclass/constraint.tmpl @@ -4,7 +4,7 @@ metadata: name: k8sstorageclass annotations: metadata.gatekeeper.sh/title: "Storage Class" - metadata.gatekeeper.sh/version: 1.1.1 + metadata.gatekeeper.sh/version: 1.1.2 metadata.gatekeeper.sh/requires-sync-data: | "[ [ @@ -16,7 +16,7 @@ metadata: ] ]" description: >- - Requires storage classes to be specified when used. Only Gatekeeper 3.9+ is supported. + Requires storage classes to be specified when used. Only Gatekeeper 3.9+ and non-ephemeral containers are supported. spec: crd: spec: diff --git a/src/general/storageclass/src.rego b/src/general/storageclass/src.rego index 186abd925..7029501b6 100644 --- a/src/general/storageclass/src.rego +++ b/src/general/storageclass/src.rego @@ -111,4 +111,4 @@ statefulset_vct_noname_msg := sprintf("statefulset did not specify a storage cla [] ) -#FIXME pod generic ephemeral might be good to validate some day too. +# We might want to consider adding validation of pod generic ephemerals. diff --git a/website/docs/validation/storageclass.md b/website/docs/validation/storageclass.md index 048cbe87c..02ec3537b 100644 --- a/website/docs/validation/storageclass.md +++ b/website/docs/validation/storageclass.md @@ -6,7 +6,7 @@ title: Storage Class # Storage Class ## Description -Requires storage classes to be specified when used. Only Gatekeeper 3.9+ is supported. +Requires storage classes to be specified when used. Only Gatekeeper 3.9+ and non-ephemeral containers are supported. ## Template ```yaml @@ -16,7 +16,7 @@ metadata: name: k8sstorageclass annotations: metadata.gatekeeper.sh/title: "Storage Class" - metadata.gatekeeper.sh/version: 1.1.1 + metadata.gatekeeper.sh/version: 1.1.2 metadata.gatekeeper.sh/requires-sync-data: | "[ [ @@ -28,7 +28,7 @@ metadata: ] ]" description: >- - Requires storage classes to be specified when used. Only Gatekeeper 3.9+ is supported. + Requires storage classes to be specified when used. Only Gatekeeper 3.9+ and non-ephemeral containers are supported. spec: crd: spec: @@ -164,7 +164,7 @@ spec: [] ) - #FIXME pod generic ephemeral might be good to validate some day too. + # We might want to consider adding validation of pod generic ephemerals. ```