From 459a2f908025944efc97e754d0a211136dad1570 Mon Sep 17 00:00:00 2001 From: peefy Date: Mon, 28 Aug 2023 11:47:09 +0800 Subject: [PATCH 1/2] chore: use kpm error event in the ReadFromOCISource function. --- pkg/source/oci.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/source/oci.go b/pkg/source/oci.go index fb1ec13..6558b85 100644 --- a/pkg/source/oci.go +++ b/pkg/source/oci.go @@ -48,10 +48,9 @@ func ReadFromOCISource(src string) (string, error) { localPath := ociOpts.AddStoragePathSuffix(tmpDir) // 2. Pull the tarball from OCI. - err = oci.Pull(localPath, ociOpts.Reg, ociOpts.Repo, ociOpts.Tag) - - if err != nil { - return src, err + e = oci.Pull(localPath, ociOpts.Reg, ociOpts.Repo, ociOpts.Tag) + if e != nil { + return src, errors.New(e.Error()) } // 3. Get the (*.tar) file path. From 71fa70c84c2b0972a63e2531f6cb0c6eced53678 Mon Sep 17 00:00:00 2001 From: peefy Date: Mon, 28 Aug 2023 15:40:43 +0800 Subject: [PATCH 2/2] feat: add more validation and mutation examples with e2e tests. --- example_test.go | 83 ++++++++++ examples/abstraction/web-service/config.yaml | 13 +- examples/mutation/add-capabilities/kcl.mod | 4 + .../mutation/add-capabilities/kcl.mod.lock | 0 examples/mutation/add-capabilities/main.k | 7 + .../mutation/add-capabilities/suite/good.yaml | 29 ++++ .../conditionally-add-annotations/config.yaml | 13 +- .../conditionally-add-labels/config.yaml | 13 +- examples/mutation/set-annotations/config.yaml | 2 +- examples/mutation/set-labels/config.yaml | 2 +- examples/mutation/set-replicas/config.yaml | 2 +- examples/validation/external-ips/config.yaml | 28 ---- examples/validation/external-ips/kcl.mod | 4 + examples/validation/external-ips/kcl.mod.lock | 0 examples/validation/external-ips/main.k | 10 ++ .../validation/external-ips/suite/bad.yaml | 29 ++++ .../validation/external-ips/suite/config.yaml | 14 ++ .../validation/external-ips/suite/good.yaml | 29 ++++ examples/validation/https-only/config.yaml | 27 ---- examples/validation/https-only/kcl.mod | 4 + examples/validation/https-only/kcl.mod.lock | 0 examples/validation/https-only/main.k | 9 ++ examples/validation/https-only/suite/bad.yaml | 36 +++++ .../validation/https-only/suite/config.yaml | 15 ++ .../validation/https-only/suite/good.yaml | 38 +++++ examples/validation/nginx-ingress/README.md | 1 + .../restrict-ingress-annotations/kcl.mod | 4 + .../restrict-ingress-annotations/kcl.mod.lock | 0 .../restrict-ingress-annotations/main.k | 24 +++ .../suite/bad.yaml | 145 ++++++++++++++++++ .../suite/good.yaml | 65 ++++++++ .../restrict-ingress-paths/kcl.mod | 4 + .../restrict-ingress-paths/kcl.mod.lock | 0 .../restrict-ingress-paths/main.k | 18 +++ .../restrict-ingress-paths/suite/bad.yaml | 81 ++++++++++ .../restrict-ingress-paths/suite/good.yaml | 37 +++++ .../validation/replica-limits/config.yaml | 29 ---- examples/validation/replica-limits/kcl.mod | 4 + .../validation/replica-limits/kcl.mod.lock | 0 examples/validation/replica-limits/main.k | 13 ++ .../validation/replica-limits/suite/bad.yaml | 37 +++++ .../replica-limits/suite/config.yaml | 15 ++ .../validation/replica-limits/suite/good.yaml | 37 +++++ .../required-annotations/config.yaml | 35 ----- .../validation/required-annotations/kcl.mod | 4 + .../required-annotations/kcl.mod.lock | 0 .../validation/required-annotations/main.k | 17 ++ .../required-annotations/suite/bad.yaml | 35 +++++ .../required-annotations/suite/config.yaml | 16 ++ .../required-annotations/suite/good.yaml | 42 +++++ .../required-labels/config-oci.yaml | 24 --- .../validation/required-labels/config.yaml | 35 ----- examples/validation/required-labels/kcl.mod | 4 + .../validation/required-labels/kcl.mod.lock | 0 examples/validation/required-labels/main.k | 16 ++ .../validation/required-labels/suite/bad.yaml | 35 +++++ .../required-labels/suite/config.yaml | 16 ++ .../required-labels/suite/good.yaml | 42 +++++ pkg/api/v1alpha1/kclrun_types.go | 18 +++ pkg/config/config.go | 22 +-- pkg/edit/bootstrap.go | 1 + pkg/edit/wraper.go | 5 + pkg/kio/filter.go | 5 +- tests/abstraction/web-service/kcl.yaml | 2 +- tests/abstraction/web-service/main.k | 2 +- tests/mutation/set-annotations/kcl.yaml | 2 +- tests/mutation/set-annotations/main.k | 2 +- tests/validation/replica-limits/kcl.yaml | 2 +- tests/validation/replica-limits/main.k | 2 +- 69 files changed, 1081 insertions(+), 228 deletions(-) create mode 100644 example_test.go create mode 100644 examples/mutation/add-capabilities/kcl.mod create mode 100644 examples/mutation/add-capabilities/kcl.mod.lock create mode 100644 examples/mutation/add-capabilities/main.k create mode 100644 examples/mutation/add-capabilities/suite/good.yaml delete mode 100644 examples/validation/external-ips/config.yaml create mode 100644 examples/validation/external-ips/kcl.mod create mode 100644 examples/validation/external-ips/kcl.mod.lock create mode 100644 examples/validation/external-ips/main.k create mode 100644 examples/validation/external-ips/suite/bad.yaml create mode 100644 examples/validation/external-ips/suite/config.yaml create mode 100644 examples/validation/external-ips/suite/good.yaml delete mode 100644 examples/validation/https-only/config.yaml create mode 100644 examples/validation/https-only/kcl.mod create mode 100644 examples/validation/https-only/kcl.mod.lock create mode 100644 examples/validation/https-only/main.k create mode 100644 examples/validation/https-only/suite/bad.yaml create mode 100644 examples/validation/https-only/suite/config.yaml create mode 100644 examples/validation/https-only/suite/good.yaml create mode 100644 examples/validation/nginx-ingress/README.md create mode 100644 examples/validation/nginx-ingress/restrict-ingress-annotations/kcl.mod create mode 100644 examples/validation/nginx-ingress/restrict-ingress-annotations/kcl.mod.lock create mode 100644 examples/validation/nginx-ingress/restrict-ingress-annotations/main.k create mode 100644 examples/validation/nginx-ingress/restrict-ingress-annotations/suite/bad.yaml create mode 100644 examples/validation/nginx-ingress/restrict-ingress-annotations/suite/good.yaml create mode 100644 examples/validation/nginx-ingress/restrict-ingress-paths/kcl.mod create mode 100644 examples/validation/nginx-ingress/restrict-ingress-paths/kcl.mod.lock create mode 100644 examples/validation/nginx-ingress/restrict-ingress-paths/main.k create mode 100644 examples/validation/nginx-ingress/restrict-ingress-paths/suite/bad.yaml create mode 100644 examples/validation/nginx-ingress/restrict-ingress-paths/suite/good.yaml delete mode 100644 examples/validation/replica-limits/config.yaml create mode 100644 examples/validation/replica-limits/kcl.mod create mode 100644 examples/validation/replica-limits/kcl.mod.lock create mode 100644 examples/validation/replica-limits/main.k create mode 100644 examples/validation/replica-limits/suite/bad.yaml create mode 100644 examples/validation/replica-limits/suite/config.yaml create mode 100644 examples/validation/replica-limits/suite/good.yaml delete mode 100644 examples/validation/required-annotations/config.yaml create mode 100644 examples/validation/required-annotations/kcl.mod create mode 100644 examples/validation/required-annotations/kcl.mod.lock create mode 100644 examples/validation/required-annotations/main.k create mode 100644 examples/validation/required-annotations/suite/bad.yaml create mode 100644 examples/validation/required-annotations/suite/config.yaml create mode 100644 examples/validation/required-annotations/suite/good.yaml delete mode 100644 examples/validation/required-labels/config-oci.yaml delete mode 100644 examples/validation/required-labels/config.yaml create mode 100644 examples/validation/required-labels/kcl.mod create mode 100644 examples/validation/required-labels/kcl.mod.lock create mode 100644 examples/validation/required-labels/main.k create mode 100644 examples/validation/required-labels/suite/bad.yaml create mode 100644 examples/validation/required-labels/suite/config.yaml create mode 100644 examples/validation/required-labels/suite/good.yaml create mode 100644 pkg/api/v1alpha1/kclrun_types.go diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..ef8dc4c --- /dev/null +++ b/example_test.go @@ -0,0 +1,83 @@ +package main + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + "testing" + + "kcl-lang.io/krm-kcl/pkg/options" + + pkg "kcl-lang.io/kpm/pkg/package" +) + +type fields struct { + InputPath string + OutputPath string +} + +type suite struct { + name string + fields fields + wantErr bool +} + +func TestRunExamples(t *testing.T) { + var tests []suite + filepath.Walk("./examples", func(path string, info fs.FileInfo, err error) error { + if !strings.HasSuffix(path, "kcl.mod") { + return nil + } + dir := filepath.Dir(path) + + kPkg, err := pkg.LoadKclPkg(dir) + if err != nil { + return err + } + suiteDir := filepath.Join(dir, "suite") + goodSuite := filepath.Join(suiteDir, "good.yaml") + badSuite := filepath.Join(suiteDir, "bad.yaml") + + tests = append(tests, suite{ + kPkg.GetPkgName() + "-good-suite", + fields{ + InputPath: goodSuite, + }, + false, + }) + // Bad test suite is optional + if FileExists(badSuite) { + tests = append(tests, suite{ + dir + "-bad-suite", + fields{ + InputPath: badSuite, + }, + true, + }) + } + return nil + }) + fmt.Printf("%d total suites checked\n", len(tests)) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &options.RunOptions{ + InputPath: tt.fields.InputPath, + OutputPath: tt.fields.OutputPath, + } + if err := o.Run(); (err != nil) != tt.wantErr { + t.Errorf("TestRunHttps() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +// FileExists mark whether the path exists. +func FileExists(path string) bool { + fi, err := os.Lstat(path) + if err != nil || fi.IsDir() { + return false + } + return true +} diff --git a/examples/abstraction/web-service/config.yaml b/examples/abstraction/web-service/config.yaml index fffd089..a9cf4b3 100644 --- a/examples/abstraction/web-service/config.yaml +++ b/examples/abstraction/web-service/config.yaml @@ -2,12 +2,11 @@ apiVersion: krm.kcl.dev/v1alpha1 kind: KCLRun metadata: name: web-service - metadata: - annotations: - krm.kcl.dev/version: 0.0.1 - krm.kcl.dev/type: abstraction - documentation: >- - Web service application abstraction + annotations: + krm.kcl.dev/version: 0.0.1 + krm.kcl.dev/type: abstraction + documentation: >- + Web service application abstraction spec: params: name: app @@ -23,7 +22,7 @@ spec: name: app source: | resource = option("resource_list") - params = resource.functionConfig.spec.params + params = option("params") # Convert the `App` model into Kubernetes Deployment and Service Manifests kubernetesRender = lambda a { diff --git a/examples/mutation/add-capabilities/kcl.mod b/examples/mutation/add-capabilities/kcl.mod new file mode 100644 index 0000000..7b9f5f5 --- /dev/null +++ b/examples/mutation/add-capabilities/kcl.mod @@ -0,0 +1,4 @@ +[package] +name = "add-capabilities" +edition = "*" +version = "0.0.1" diff --git a/examples/mutation/add-capabilities/kcl.mod.lock b/examples/mutation/add-capabilities/kcl.mod.lock new file mode 100644 index 0000000..e69de29 diff --git a/examples/mutation/add-capabilities/main.k b/examples/mutation/add-capabilities/main.k new file mode 100644 index 0000000..bba74bc --- /dev/null +++ b/examples/mutation/add-capabilities/main.k @@ -0,0 +1,7 @@ +capabilities = option("params").capabilities or ["SETUID", "SETFCAP"] +items = [item | { + if item.kind == "Pod": + spec.containers: [{ + "securityContext": {"capabilities": {"add" += [c] if c not in (container?.securityContext?.capabilities?.drop or []) else [] for c in capabilities}} + } for container in item.spec.containers] +} for item in option("items")] diff --git a/examples/mutation/add-capabilities/suite/good.yaml b/examples/mutation/add-capabilities/suite/good.yaml new file mode 100644 index 0000000..b7834ac --- /dev/null +++ b/examples/mutation/add-capabilities/suite/good.yaml @@ -0,0 +1,29 @@ +apiVersion: krm.kcl.dev/v1alpha1 +kind: KCLRun +metadata: + name: add-capabilities + annotations: + krm.kcl.dev/version: 0.0.1 + krm.kcl.dev/type: mutation + documentation: >- + In the earlier Pod Security Policy controller, it was possible to configure a policy + to add capabilities to containers within a Pod. This made it easier to assign some basic defaults + rather than blocking Pods or to simply provide capabilities for certain workloads if not specified. + This policy mutates Pods to add the capabilities SETFCAP and SETUID so long as they are not listed + as dropped capabilities first. +spec: + params: + capabilities: + - SETUID + source: ./examples/mutation/add-capabilities/main.k +--- +apiVersion: v1 +kind: Pod +metadata: + name: nginx +spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 diff --git a/examples/mutation/conditionally-add-annotations/config.yaml b/examples/mutation/conditionally-add-annotations/config.yaml index 41b6d2f..039a4eb 100644 --- a/examples/mutation/conditionally-add-annotations/config.yaml +++ b/examples/mutation/conditionally-add-annotations/config.yaml @@ -2,12 +2,11 @@ apiVersion: krm.kcl.dev/v1alpha1 kind: KCLRun metadata: name: conditionally-add-annotations - metadata: - annotations: - krm.kcl.dev/version: 0.0.1 - krm.kcl.dev/type: mutation - documentation: >- - Conditionally add annotations + annotations: + krm.kcl.dev/version: 0.0.1 + krm.kcl.dev/type: mutation + documentation: >- + Conditionally add annotations spec: params: toMatch: @@ -16,7 +15,7 @@ spec: configmanagement.gke.io/managed: disabled source: | resource = option("resource_list") - params = resource.functionConfig.spec.params + params = option("params") toMatch = params.toMatch toAdd = params.toAdd items = [item | { diff --git a/examples/mutation/conditionally-add-labels/config.yaml b/examples/mutation/conditionally-add-labels/config.yaml index a073971..6bb0a89 100644 --- a/examples/mutation/conditionally-add-labels/config.yaml +++ b/examples/mutation/conditionally-add-labels/config.yaml @@ -2,12 +2,11 @@ apiVersion: krm.kcl.dev/v1alpha1 kind: KCLRun metadata: name: conditionally-add-labels - metadata: - annotations: - krm.kcl.dev/version: 0.0.1 - krm.kcl.dev/type: mutation - documentation: >- - Conditionally add labels + annotations: + krm.kcl.dev/version: 0.0.1 + krm.kcl.dev/type: mutation + documentation: >- + Conditionally add labels spec: params: toMatch: @@ -16,7 +15,7 @@ spec: configmanagement.gke.io/managed: disabled source: | resource = option("resource_list") - params = resource.functionConfig.spec.params + params = option("params") toMatch = params.toMatch toAdd = params.toAdd items = [item | { diff --git a/examples/mutation/set-annotations/config.yaml b/examples/mutation/set-annotations/config.yaml index 13a1b4d..edf40a7 100644 --- a/examples/mutation/set-annotations/config.yaml +++ b/examples/mutation/set-annotations/config.yaml @@ -8,7 +8,7 @@ spec: config.kubernetes.io/local-config: "true" source: | resource = option("resource_list") - params = resource.functionConfig.spec.params + params = option("params") # Use `k = v` to override existing annotations annotations = {k = v for k, v in params.annotations} items = [item | { diff --git a/examples/mutation/set-labels/config.yaml b/examples/mutation/set-labels/config.yaml index 9c8fc38..4f968ab 100644 --- a/examples/mutation/set-labels/config.yaml +++ b/examples/mutation/set-labels/config.yaml @@ -8,7 +8,7 @@ spec: config.kubernetes.io/local-config: "true" source: | resource = option("resource_list") - params = resource.functionConfig.spec.params + params = option("params") # Use `k = v` to override existing labels labels = {k = v for k, v in params.labels} items = [item | { diff --git a/examples/mutation/set-replicas/config.yaml b/examples/mutation/set-replicas/config.yaml index 5832e12..f1cc46a 100644 --- a/examples/mutation/set-replicas/config.yaml +++ b/examples/mutation/set-replicas/config.yaml @@ -7,7 +7,7 @@ spec: replicas: 5 source: | resource = option("resource_list") - replicas = resource.functionConfig.spec.params.replicas + replicas = option("params").replicas setReplicas = lambda items: [], replicas: int { [item | { if item.kind == "Deployment": diff --git a/examples/validation/external-ips/config.yaml b/examples/validation/external-ips/config.yaml deleted file mode 100644 index ff0d402..0000000 --- a/examples/validation/external-ips/config.yaml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: krm.kcl.dev/v1alpha1 -kind: KCLRun -metadata: - name: external-ips - metadata: - annotations: - krm.kcl.dev/version: 0.0.1 - krm.kcl.dev/type: validation - documentation: >- - Restricts Service externalIPs to an allowed list of IP addresses. - More info: https://kubernetes.io/docs/concepts/services-networking/service/#external-ips -spec: - params: - allowedIps: ["127.0.0.1"] - source: | - # Construct resource and params - resource = option("resource_list") - params = resource.functionConfig.spec.params - # Define the validation function - validate_external_ips = lambda item, allowedIps: [str] { - if allowedIps and item.kind == "Service" and item.spec.externalIPs: - assert all ip in item.spec.externalIPs { - ip in allowedIps - }, "Service external IPs must be in ${allowedIps}" - item - } - # Validate All resource - items = [validate_external_ips(i, params.allowedIps or []) for i in resource.items] diff --git a/examples/validation/external-ips/kcl.mod b/examples/validation/external-ips/kcl.mod new file mode 100644 index 0000000..3f1bc6a --- /dev/null +++ b/examples/validation/external-ips/kcl.mod @@ -0,0 +1,4 @@ +[package] +name = "external-ips" +edition = "*" +version = "0.0.1" diff --git a/examples/validation/external-ips/kcl.mod.lock b/examples/validation/external-ips/kcl.mod.lock new file mode 100644 index 0000000..e69de29 diff --git a/examples/validation/external-ips/main.k b/examples/validation/external-ips/main.k new file mode 100644 index 0000000..4b8b671 --- /dev/null +++ b/examples/validation/external-ips/main.k @@ -0,0 +1,10 @@ +# Define the validation function +validate_external_ips = lambda item, allowedIps: [str] { + if allowedIps and item.kind == "Service" and item.spec.externalIPs: + assert all ip in item.spec.externalIPs { + ip in allowedIps + }, "Service external IPs must be in ${allowedIps} for ${item.kind}: ${item.metadata.name}" + item +} +# Validate All resource +items = [validate_external_ips(i, option("params")?.allowedIps or []) for i in option("items")] diff --git a/examples/validation/external-ips/suite/bad.yaml b/examples/validation/external-ips/suite/bad.yaml new file mode 100644 index 0000000..d0a0c68 --- /dev/null +++ b/examples/validation/external-ips/suite/bad.yaml @@ -0,0 +1,29 @@ +apiVersion: krm.kcl.dev/v1alpha1 +kind: KCLRun +metadata: + name: external-ips + annotations: + krm.kcl.dev/version: 0.0.1 + krm.kcl.dev/type: validation + documentation: >- + Restricts Service externalIPs to an allowed list of IP addresses. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#external-ips +spec: + params: + allowedIps: ["198.51.100.32"] + source: ./examples/validation/external-ips/main.k +--- +apiVersion: v1 +kind: Service +metadata: + name: my-service +spec: + selector: + app.kubernetes.io/name: MyApp + ports: + - name: http + protocol: TCP + port: 80 + targetPort: 49152 + externalIPs: + - 127.0.0.1 diff --git a/examples/validation/external-ips/suite/config.yaml b/examples/validation/external-ips/suite/config.yaml new file mode 100644 index 0000000..91d0533 --- /dev/null +++ b/examples/validation/external-ips/suite/config.yaml @@ -0,0 +1,14 @@ +apiVersion: krm.kcl.dev/v1alpha1 +kind: KCLRun +metadata: + name: external-ips + annotations: + krm.kcl.dev/version: 0.0.1 + krm.kcl.dev/type: validation + documentation: >- + Restricts Service externalIPs to an allowed list of IP addresses. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#external-ips +spec: + params: + allowedIps: ["198.51.100.32"] + source: oci://ghcr.io/kcl-lang/external-ips diff --git a/examples/validation/external-ips/suite/good.yaml b/examples/validation/external-ips/suite/good.yaml new file mode 100644 index 0000000..7cfba3f --- /dev/null +++ b/examples/validation/external-ips/suite/good.yaml @@ -0,0 +1,29 @@ +apiVersion: krm.kcl.dev/v1alpha1 +kind: KCLRun +metadata: + name: external-ips + annotations: + krm.kcl.dev/version: 0.0.1 + krm.kcl.dev/type: validation + documentation: >- + Restricts Service externalIPs to an allowed list of IP addresses. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#external-ips +spec: + params: + allowedIps: ["198.51.100.32"] + source: ./examples/validation/external-ips/main.k +--- +apiVersion: v1 +kind: Service +metadata: + name: my-service +spec: + selector: + app.kubernetes.io/name: MyApp + ports: + - name: http + protocol: TCP + port: 80 + targetPort: 49152 + externalIPs: + - 198.51.100.32 diff --git a/examples/validation/https-only/config.yaml b/examples/validation/https-only/config.yaml deleted file mode 100644 index 5b12af4..0000000 --- a/examples/validation/https-only/config.yaml +++ /dev/null @@ -1,27 +0,0 @@ -apiVersion: krm.kcl.dev/v1alpha1 -kind: KCLRun -metadata: - name: https-only - metadata: - annotations: - krm.kcl.dev/version: 0.0.1 - krm.kcl.dev/type: validation - documentation: >- - Requires Ingress resources to be HTTPS only. Ingress resources must - include the `kubernetes.io/ingress.allow-http` annotation, set to `false`. - By default a valid TLS {} configuration is required, this can be made - optional by setting the `tlsOptional` parameter to `true`. - More info: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls -spec: - source: | - # Construct resource and params - resource = option("resource_list") - params = resource.functionConfig.spec.params - # Define the validation function - validate_https_only = lambda item { - if item.kind == "Ingress" and item.spec.tls: - assert item.metadata.annotations["kubernetes.io/ingress.allow-http"] == "false", "Ingress should be https. The kubernetes.io/ingress.allow-http=false annotation is required for ${item.metadata.name}" - item - } - # Validate All resource - items = [validate_https_only(i) for i in resource.items] diff --git a/examples/validation/https-only/kcl.mod b/examples/validation/https-only/kcl.mod new file mode 100644 index 0000000..c1ba616 --- /dev/null +++ b/examples/validation/https-only/kcl.mod @@ -0,0 +1,4 @@ +[package] +name = "https-only" +edition = "*" +version = "0.0.1" diff --git a/examples/validation/https-only/kcl.mod.lock b/examples/validation/https-only/kcl.mod.lock new file mode 100644 index 0000000..e69de29 diff --git a/examples/validation/https-only/main.k b/examples/validation/https-only/main.k new file mode 100644 index 0000000..88c56ab --- /dev/null +++ b/examples/validation/https-only/main.k @@ -0,0 +1,9 @@ +params = option("params") +# Define the validation function +validate_https_only = lambda item { + if item.kind == "Ingress" and item.spec.tls: + assert item.metadata.annotations["kubernetes.io/ingress.allow-http"] == "false", "Ingress should be https. The `kubernetes.io/ingress.allow-http: \"false\"` annotation is required for ${item.kind}: ${item.metadata.name}" + item +} +# Validate All resource +items = [validate_https_only(i) for i in option("items")] diff --git a/examples/validation/https-only/suite/bad.yaml b/examples/validation/https-only/suite/bad.yaml new file mode 100644 index 0000000..8f03ec9 --- /dev/null +++ b/examples/validation/https-only/suite/bad.yaml @@ -0,0 +1,36 @@ +apiVersion: krm.kcl.dev/v1alpha1 +kind: KCLRun +metadata: + name: https-only + annotations: + krm.kcl.dev/version: 0.0.1 + krm.kcl.dev/type: validation + documentation: >- + Requires Ingress resources to be HTTPS only. Ingress resources must + include the `kubernetes.io/ingress.allow-http` annotation, set to `false`. + By default a valid TLS {} configuration is required, this can be made + optional by setting the `tlsOptional` parameter to `true`. + More info: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls +spec: + source: ./examples/validation/https-only/main.k +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: tls-example-ingress +spec: + tls: + - hosts: + - https-example.foo.com + secretName: testsecret-tls + rules: + - host: https-example.foo.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: service1 + port: + number: 80 diff --git a/examples/validation/https-only/suite/config.yaml b/examples/validation/https-only/suite/config.yaml new file mode 100644 index 0000000..cf2eeb6 --- /dev/null +++ b/examples/validation/https-only/suite/config.yaml @@ -0,0 +1,15 @@ +apiVersion: krm.kcl.dev/v1alpha1 +kind: KCLRun +metadata: + name: https-only + annotations: + krm.kcl.dev/version: 0.0.1 + krm.kcl.dev/type: validation + documentation: >- + Requires Ingress resources to be HTTPS only. Ingress resources must + include the `kubernetes.io/ingress.allow-http` annotation, set to `false`. + By default a valid TLS {} configuration is required, this can be made + optional by setting the `tlsOptional` parameter to `true`. + More info: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls +spec: + source: oci://ghcr.io/kcl-lang/https-only diff --git a/examples/validation/https-only/suite/good.yaml b/examples/validation/https-only/suite/good.yaml new file mode 100644 index 0000000..888f5f5 --- /dev/null +++ b/examples/validation/https-only/suite/good.yaml @@ -0,0 +1,38 @@ +apiVersion: krm.kcl.dev/v1alpha1 +kind: KCLRun +metadata: + name: https-only + annotations: + krm.kcl.dev/version: 0.0.1 + krm.kcl.dev/type: validation + documentation: >- + Requires Ingress resources to be HTTPS only. Ingress resources must + include the `kubernetes.io/ingress.allow-http` annotation, set to `false`. + By default a valid TLS {} configuration is required, this can be made + optional by setting the `tlsOptional` parameter to `true`. + More info: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls +spec: + source: ./examples/validation/https-only/main.k +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: tls-example-ingress + annotations: + kubernetes.io/ingress.allow-http: "false" +spec: + tls: + - hosts: + - https-example.foo.com + secretName: testsecret-tls + rules: + - host: https-example.foo.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: service1 + port: + number: 80 diff --git a/examples/validation/nginx-ingress/README.md b/examples/validation/nginx-ingress/README.md new file mode 100644 index 0000000..4eed374 --- /dev/null +++ b/examples/validation/nginx-ingress/README.md @@ -0,0 +1 @@ ++ Reference: https://github.com/kyverno/policies/tree/main/nginx-ingress diff --git a/examples/validation/nginx-ingress/restrict-ingress-annotations/kcl.mod b/examples/validation/nginx-ingress/restrict-ingress-annotations/kcl.mod new file mode 100644 index 0000000..8009d83 --- /dev/null +++ b/examples/validation/nginx-ingress/restrict-ingress-annotations/kcl.mod @@ -0,0 +1,4 @@ +[package] +name = "restrict-ingress-annotations" +edition = "*" +version = "0.0.1" diff --git a/examples/validation/nginx-ingress/restrict-ingress-annotations/kcl.mod.lock b/examples/validation/nginx-ingress/restrict-ingress-annotations/kcl.mod.lock new file mode 100644 index 0000000..e69de29 diff --git a/examples/validation/nginx-ingress/restrict-ingress-annotations/main.k b/examples/validation/nginx-ingress/restrict-ingress-annotations/main.k new file mode 100644 index 0000000..7ec9177 --- /dev/null +++ b/examples/validation/nginx-ingress/restrict-ingress-annotations/main.k @@ -0,0 +1,24 @@ +""" +This policy mitigates CVE-2021-25746 by restricting `metadata.annotations` to safe values. +See: https://github.com/kubernetes/ingress-nginx/blame/main/internal/ingress/inspector/rules.go. +This issue has been fixed in NGINX Ingress v1.2.0. For NGINX Ingress version 1.0.5+ the +"annotation-value-word-blocklist" configuration setting is also recommended. +Please refer to the CVE for details. +""" +import regex + +invalid_anno_value_patterns = ["\\s*alias\\s*.*;", "\\s*root\\s*.*;", "/etc/(passwd|shadow|group|nginx|ingress-controller)", "/var/run/secrets", ".*_by_lua.*"] +msg = "spec.metadata.annotations.values, invalid annotation value patterns ${invalid_anno_value_patterns}" +validate_restrict_ingress_paths = lambda item { + if item.kind == "Ingress": + values = [v for _, v in item.metadata.annotations] + if values: + assert all v in values { + not any pattern in invalid_anno_value_patterns { + regex.match(v, pattern) + } + }, msg + item +} +# Validate All resource +items = [validate_restrict_ingress_paths(i) for i in option("items")] diff --git a/examples/validation/nginx-ingress/restrict-ingress-annotations/suite/bad.yaml b/examples/validation/nginx-ingress/restrict-ingress-annotations/suite/bad.yaml new file mode 100644 index 0000000..a9015db --- /dev/null +++ b/examples/validation/nginx-ingress/restrict-ingress-annotations/suite/bad.yaml @@ -0,0 +1,145 @@ +apiVersion: krm.kcl.dev/v1alpha1 +kind: KCLRun +metadata: + name: restrict-ingress-annotations + annotations: + krm.kcl.dev/version: 0.0.1 + krm.kcl.dev/type: validation + documentation: >- + This policy mitigates CVE-2021-25746 by restricting `metadata.annotations` to safe values. + See: https://github.com/kubernetes/ingress-nginx/blame/main/internal/ingress/inspector/rules.go. + This issue has been fixed in NGINX Ingress v1.2.0. For NGINX Ingress version 1.0.5+ the + "annotation-value-word-blocklist" configuration setting is also recommended. + Please refer to the CVE for details. +spec: + source: ./examples/validation/nginx-ingress/restrict-ingress-annotations/main.k +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: cafe-ingress-with-snippets + annotations: + nginx.org/bad: "alias; " +spec: + rules: + - host: cafe.example.com + http: + paths: + - path: /tea + pathType: Prefix + backend: + service: + name: tea-svc + port: + number: 80 + - path: /coffee + pathType: Prefix + backend: + service: + name: coffee-svc + port: + number: 80 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: cafe-ingress-with-snippets + annotations: + nginx.org/bad: " root ;" +spec: + rules: + - host: cafe.example.com + http: + paths: + - path: /tea + pathType: Prefix + backend: + service: + name: tea-svc + port: + number: 80 + - path: /coffee + pathType: Prefix + backend: + service: + name: coffee-svc + port: + number: 80 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: etc-passwd + annotations: + nginx.org/bad: "/etc/passwd" +spec: + rules: + - host: cafe.example.com + http: + paths: + - path: /tea + pathType: Prefix + backend: + service: + name: tea-svc + port: + number: 80 + - path: /coffee + pathType: Prefix + backend: + service: + name: coffee-svc + port: + number: 80 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: var-run-secrets + annotations: + nginx.org/bad: "/var/run/secrets" +spec: + rules: + - host: cafe.example.com + http: + paths: + - path: /tea + pathType: Prefix + backend: + service: + name: tea-svc + port: + number: 80 + - path: /coffee + pathType: Prefix + backend: + service: + name: coffee-svc + port: + number: 80 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: lua + annotations: + nginx.org/bad: "*! _by_lua 8010-191091" +spec: + rules: + - host: cafe.example.com + http: + paths: + - path: /tea + pathType: Prefix + backend: + service: + name: tea-svc + port: + number: 80 + - path: /coffee + pathType: Prefix + backend: + service: + name: coffee-svc + port: + number: 80 diff --git a/examples/validation/nginx-ingress/restrict-ingress-annotations/suite/good.yaml b/examples/validation/nginx-ingress/restrict-ingress-annotations/suite/good.yaml new file mode 100644 index 0000000..1f47dbc --- /dev/null +++ b/examples/validation/nginx-ingress/restrict-ingress-annotations/suite/good.yaml @@ -0,0 +1,65 @@ +apiVersion: krm.kcl.dev/v1alpha1 +kind: KCLRun +metadata: + name: restrict-ingress-annotations + annotations: + krm.kcl.dev/version: 0.0.1 + krm.kcl.dev/type: validation + documentation: >- + This policy mitigates CVE-2021-25746 by restricting `metadata.annotations` to safe values. + See: https://github.com/kubernetes/ingress-nginx/blame/main/internal/ingress/inspector/rules.go. + This issue has been fixed in NGINX Ingress v1.2.0. For NGINX Ingress version 1.0.5+ the + "annotation-value-word-blocklist" configuration setting is also recommended. + Please refer to the CVE for details. +spec: + source: ./examples/validation/nginx-ingress/restrict-ingress-annotations/main.k +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: cafe-ingress-with-snippets + annotations: + nginx.org/good: "value" +spec: + rules: + - host: cafe.example.com + http: + paths: + - path: /tea + pathType: Prefix + backend: + service: + name: tea-svc + port: + number: 80 + - path: /coffee + pathType: Prefix + backend: + service: + name: coffee-svc + port: + number: 80 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: cafe-ingress +spec: + rules: + - host: cafe.example.com + http: + paths: + - path: /tea + pathType: Prefix + backend: + service: + name: tea-svc + port: + number: 80 + - path: /coffee + pathType: Prefix + backend: + service: + name: coffee-svc + port: + number: 80 diff --git a/examples/validation/nginx-ingress/restrict-ingress-paths/kcl.mod b/examples/validation/nginx-ingress/restrict-ingress-paths/kcl.mod new file mode 100644 index 0000000..1394a1e --- /dev/null +++ b/examples/validation/nginx-ingress/restrict-ingress-paths/kcl.mod @@ -0,0 +1,4 @@ +[package] +name = "restrict-ingress-paths" +edition = "*" +version = "0.0.1" diff --git a/examples/validation/nginx-ingress/restrict-ingress-paths/kcl.mod.lock b/examples/validation/nginx-ingress/restrict-ingress-paths/kcl.mod.lock new file mode 100644 index 0000000..e69de29 diff --git a/examples/validation/nginx-ingress/restrict-ingress-paths/main.k b/examples/validation/nginx-ingress/restrict-ingress-paths/main.k new file mode 100644 index 0000000..0dad0b0 --- /dev/null +++ b/examples/validation/nginx-ingress/restrict-ingress-paths/main.k @@ -0,0 +1,18 @@ +""" +This policy mitigates CVE-2021-25745 by restricting `spec.rules[].http.paths[].path` to safe values. +Additional paths can be added as required. This issue has been fixed in NGINX Ingress v1.2.0. +Please refer to the CVE for details. +""" +invalid_paths = ["/etc", "/var/run/secrets", "/root", "/var/run/kubernetes/serviceaccount", "/etc/kubernetes/admin.conf"] +msg = "spec.rules[].http.paths[].path value is not allowed, invalid values ${invalid_paths}" +validate_restrict_ingress_paths = lambda item { + if item.kind == "Ingress": + paths = [p.path for r in item.spec.rules for p in r.http.paths] + if paths: + assert all path in paths { + path not in invalid_paths + }, msg + item +} +# Validate All resource +items = [validate_restrict_ingress_paths(i) for i in option("items")] diff --git a/examples/validation/nginx-ingress/restrict-ingress-paths/suite/bad.yaml b/examples/validation/nginx-ingress/restrict-ingress-paths/suite/bad.yaml new file mode 100644 index 0000000..e6444c7 --- /dev/null +++ b/examples/validation/nginx-ingress/restrict-ingress-paths/suite/bad.yaml @@ -0,0 +1,81 @@ +apiVersion: krm.kcl.dev/v1alpha1 +kind: KCLRun +metadata: + name: restrict-ingress-paths + annotations: + krm.kcl.dev/version: 0.0.1 + krm.kcl.dev/type: validation + documentation: >- + This policy mitigates CVE-2021-25745 by restricting `spec.rules[].http.paths[].path` to safe values. + Additional paths can be added as required. This issue has been fixed in NGINX Ingress v1.2.0. + Please refer to the CVE for details. +spec: + source: ./examples/validation/nginx-ingress/restrict-ingress-paths/main.k +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: bad-path-root +spec: + rules: + - host: cafe.example.com + http: + paths: + - path: /root + pathType: Prefix + backend: + service: + name: tea-svc + port: + number: 80 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: bad-path-secrets +spec: + rules: + - host: cafe.example.com + http: + paths: + - path: /var/run/secrets + pathType: Prefix + backend: + service: + name: tea-svc + port: + number: 80 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: bad-path-etc +spec: + rules: + - host: cafe.example.com + http: + paths: + - path: /etc/kubernetes/admin.conf + pathType: Prefix + backend: + service: + name: tea-svc + port: + number: 80 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: bad-path-serviceaccount +spec: + rules: + - host: cafe.example.com + http: + paths: + - path: /var/run/kubernetes/serviceaccount + pathType: Prefix + backend: + service: + name: tea-svc + port: + number: 80 diff --git a/examples/validation/nginx-ingress/restrict-ingress-paths/suite/good.yaml b/examples/validation/nginx-ingress/restrict-ingress-paths/suite/good.yaml new file mode 100644 index 0000000..43532e3 --- /dev/null +++ b/examples/validation/nginx-ingress/restrict-ingress-paths/suite/good.yaml @@ -0,0 +1,37 @@ +apiVersion: krm.kcl.dev/v1alpha1 +kind: KCLRun +metadata: + name: restrict-ingress-paths + annotations: + krm.kcl.dev/version: 0.0.1 + krm.kcl.dev/type: validation + documentation: >- + This policy mitigates CVE-2021-25745 by restricting `spec.rules[].http.paths[].path` to safe values. + Additional paths can be added as required. This issue has been fixed in NGINX Ingress v1.2.0. + Please refer to the CVE for details. +spec: + source: ./examples/validation/nginx-ingress/restrict-ingress-paths/main.k +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: good-paths +spec: + rules: + - host: cafe.example.com + http: + paths: + - path: /tea + pathType: Prefix + backend: + service: + name: tea-svc + port: + number: 80 + - path: /coffee + pathType: Prefix + backend: + service: + name: coffee-svc + port: + number: 80 diff --git a/examples/validation/replica-limits/config.yaml b/examples/validation/replica-limits/config.yaml deleted file mode 100644 index bd4e40c..0000000 --- a/examples/validation/replica-limits/config.yaml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: krm.kcl.dev/v1alpha1 -kind: KCLRun -metadata: - name: replica-limits - metadata: - annotations: - krm.kcl.dev/version: 0.0.1 - krm.kcl.dev/type: validation - documentation: >- - Requires that objects with the field `spec.replicas` (Deployments, - ReplicaSets, etc.) specify a number of replicas within defined ranges. -spec: - params: - min_replicas: 0 - max_replicas: 5 - source: | - # Construct resource and params - resource = option("resource_list") - params = resource.functionConfig.spec.params - min_replicas: int = params.min_replicas or 0 - max_replicas: int = params.max_replicas or 99999 - # Define the validation function - validate_replica_limit = lambda item, min, max { - replicas = item.spec.replicas or 0 - assert min < replicas < max, "The provided number of replicas ${replicas} is not allowed for ${item.kind}: ${item.metadata.name}. Allowed range: ${min} - ${max}" - item - } - # Validate All resource - items = [validate_replica_limit(i, min_replicas, max_replicas) for i in resource.items] diff --git a/examples/validation/replica-limits/kcl.mod b/examples/validation/replica-limits/kcl.mod new file mode 100644 index 0000000..47adcb8 --- /dev/null +++ b/examples/validation/replica-limits/kcl.mod @@ -0,0 +1,4 @@ +[package] +name = "replica-limits" +edition = "*" +version = "0.0.1" diff --git a/examples/validation/replica-limits/kcl.mod.lock b/examples/validation/replica-limits/kcl.mod.lock new file mode 100644 index 0000000..e69de29 diff --git a/examples/validation/replica-limits/main.k b/examples/validation/replica-limits/main.k new file mode 100644 index 0000000..fd0962e --- /dev/null +++ b/examples/validation/replica-limits/main.k @@ -0,0 +1,13 @@ +# Construct resource and params +resource = option("resource_list") +params = option("params") +min_replicas: int = params.min_replicas or 0 +max_replicas: int = params.max_replicas or 99999 +# Define the validation function +validate_replica_limit = lambda item, min, max { + replicas = item.spec.replicas or 0 + assert min < replicas < max, "The provided number of replicas ${replicas} is not allowed for ${item.kind}: ${item.metadata.name}. Allowed range: ${min} - ${max}" + item +} +# Validate All resource +items = [validate_replica_limit(i, min_replicas, max_replicas) for i in resource.items] diff --git a/examples/validation/replica-limits/suite/bad.yaml b/examples/validation/replica-limits/suite/bad.yaml new file mode 100644 index 0000000..8291bc8 --- /dev/null +++ b/examples/validation/replica-limits/suite/bad.yaml @@ -0,0 +1,37 @@ +apiVersion: krm.kcl.dev/v1alpha1 +kind: KCLRun +metadata: + name: replica-limits + annotations: + krm.kcl.dev/version: 0.0.1 + krm.kcl.dev/type: validation + documentation: >- + Requires that objects with the field `spec.replicas` (Deployments, + ReplicaSets, etc.) specify a number of replicas within defined ranges. +spec: + params: + min_replicas: 0 + max_replicas: 5 + source: ./examples/validation/replica-limits/main.k +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 6 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 diff --git a/examples/validation/replica-limits/suite/config.yaml b/examples/validation/replica-limits/suite/config.yaml new file mode 100644 index 0000000..86eee46 --- /dev/null +++ b/examples/validation/replica-limits/suite/config.yaml @@ -0,0 +1,15 @@ +apiVersion: krm.kcl.dev/v1alpha1 +kind: KCLRun +metadata: + name: replica-limits + annotations: + krm.kcl.dev/version: 0.0.1 + krm.kcl.dev/type: validation + documentation: >- + Requires that objects with the field `spec.replicas` (Deployments, + ReplicaSets, etc.) specify a number of replicas within defined ranges. +spec: + params: + min_replicas: 0 + max_replicas: 5 + source: oci://ghcr.io/kcl-lang/replica-limits diff --git a/examples/validation/replica-limits/suite/good.yaml b/examples/validation/replica-limits/suite/good.yaml new file mode 100644 index 0000000..29eb4bd --- /dev/null +++ b/examples/validation/replica-limits/suite/good.yaml @@ -0,0 +1,37 @@ +apiVersion: krm.kcl.dev/v1alpha1 +kind: KCLRun +metadata: + name: replica-limits + annotations: + krm.kcl.dev/version: 0.0.1 + krm.kcl.dev/type: validation + documentation: >- + Requires that objects with the field `spec.replicas` (Deployments, + ReplicaSets, etc.) specify a number of replicas within defined ranges. +spec: + params: + min_replicas: 0 + max_replicas: 5 + source: ./examples/validation/replica-limits/main.k +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 diff --git a/examples/validation/required-annotations/config.yaml b/examples/validation/required-annotations/config.yaml deleted file mode 100644 index 677023d..0000000 --- a/examples/validation/required-annotations/config.yaml +++ /dev/null @@ -1,35 +0,0 @@ -apiVersion: krm.kcl.dev/v1alpha1 -kind: KCLRun -metadata: - name: required-annotations - metadata: - annotations: - krm.kcl.dev/version: 0.0.1 - krm.kcl.dev/type: validation - documentation: >- - Requires resources to contain specified annotations, with values matching provided regular expressions. -spec: - params: - requires: - - key: config.kubernetes.io/local-config - # Optional: If specified, a regular expression the annotation's value must match. - allowedRegex: "true" - source: | - # Construct resource and params - resource = option("resource_list") - params = resource.functionConfig.spec.params - requires = params.requires or [] - # Define the validation function - validate_required_annotations = lambda item, requires { - if requires: - requires_map = {r.key: r.allowedRegex or "" for r in requires} - assert all k, v in item.metadata.annotations { - k in requires_map - }, "must provide annotations with the regex ${requires_map}" - assert all k, v in item.metadata.annotations { - regex.match(v, requires_map[k]) if requires_map[k] - }, "must provide annotations with the regex ${requires_map}" - item - } - # Validate All resource - items = [validate_required_annotations(i, requires) for i in resource.items] diff --git a/examples/validation/required-annotations/kcl.mod b/examples/validation/required-annotations/kcl.mod new file mode 100644 index 0000000..1394a1e --- /dev/null +++ b/examples/validation/required-annotations/kcl.mod @@ -0,0 +1,4 @@ +[package] +name = "restrict-ingress-paths" +edition = "*" +version = "0.0.1" diff --git a/examples/validation/required-annotations/kcl.mod.lock b/examples/validation/required-annotations/kcl.mod.lock new file mode 100644 index 0000000..e69de29 diff --git a/examples/validation/required-annotations/main.k b/examples/validation/required-annotations/main.k new file mode 100644 index 0000000..d201e8a --- /dev/null +++ b/examples/validation/required-annotations/main.k @@ -0,0 +1,17 @@ +import regex + +resource = option("resource_list") +requires = option("params").requires or [] +# Define the validation function +validate_required_annotations = lambda item, requires { + if requires and item.kind != "KCLRun": + requires_map = {r.key: r.allowedRegex or "" for r in requires} + annotations = item.metadata.annotations or {} + if annotations: + assert all k, v in annotations { + regex.match(v, requires_map[k]) if requires_map[k] + }, "must provide annotations with the regex ${requires_map}, annotations ${annotations} is not allowed for ${item.kind}: ${item.metadata.name}" + item +} +# Validate All resource +items = [validate_required_annotations(i, requires) for i in option("items")] diff --git a/examples/validation/required-annotations/suite/bad.yaml b/examples/validation/required-annotations/suite/bad.yaml new file mode 100644 index 0000000..ae36ca5 --- /dev/null +++ b/examples/validation/required-annotations/suite/bad.yaml @@ -0,0 +1,35 @@ +apiVersion: krm.kcl.dev/v1alpha1 +kind: KCLRun +metadata: + name: required-annotations + annotations: + krm.kcl.dev/version: 0.0.1 + krm.kcl.dev/type: validation + documentation: >- + Requires resources to contain specified annotations, with values matching provided regular expressions. +spec: + params: + requires: + - key: config.kubernetes.io/local-config + # Optional: If specified, a regular expression the annotation's value must match. + allowedRegex: "true" + source: ./examples/validation/required-annotations/main.k +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: bad-required-annotations + annotations: + config.kubernetes.io/local-config: "false" +spec: + rules: + - host: cafe.example.com + http: + paths: + - path: /root + pathType: Prefix + backend: + service: + name: tea-svc + port: + number: 80 diff --git a/examples/validation/required-annotations/suite/config.yaml b/examples/validation/required-annotations/suite/config.yaml new file mode 100644 index 0000000..dd51f3f --- /dev/null +++ b/examples/validation/required-annotations/suite/config.yaml @@ -0,0 +1,16 @@ +apiVersion: krm.kcl.dev/v1alpha1 +kind: KCLRun +metadata: + name: required-annotations + annotations: + krm.kcl.dev/version: 0.0.1 + krm.kcl.dev/type: validation + documentation: >- + Requires resources to contain specified annotations, with values matching provided regular expressions. +spec: + params: + requires: + - key: config.kubernetes.io/local-config + # Optional: If specified, a regular expression the annotation's value must match. + allowedRegex: "true" + source: oci://ghcr.io/kcl-lang/required-annotations diff --git a/examples/validation/required-annotations/suite/good.yaml b/examples/validation/required-annotations/suite/good.yaml new file mode 100644 index 0000000..82e9386 --- /dev/null +++ b/examples/validation/required-annotations/suite/good.yaml @@ -0,0 +1,42 @@ +apiVersion: krm.kcl.dev/v1alpha1 +kind: KCLRun +metadata: + name: required-annotations + annotations: + krm.kcl.dev/version: 0.0.1 + krm.kcl.dev/type: validation + documentation: >- + Requires resources to contain specified annotations, with values matching provided regular expressions. +spec: + params: + requires: + - key: config.kubernetes.io/local-config + # Optional: If specified, a regular expression the annotation's value must match. + allowedRegex: "true" + source: ./examples/validation/required-annotations/main.k +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: good-required-annotations + annotations: + config.kubernetes.io/local-config: "true" +spec: + rules: + - host: cafe.example.com + http: + paths: + - path: /tea + pathType: Prefix + backend: + service: + name: tea-svc + port: + number: 80 + - path: /coffee + pathType: Prefix + backend: + service: + name: coffee-svc + port: + number: 80 diff --git a/examples/validation/required-labels/config-oci.yaml b/examples/validation/required-labels/config-oci.yaml deleted file mode 100644 index 02c5410..0000000 --- a/examples/validation/required-labels/config-oci.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: config.kubernetes.io/v1 -kind: ResourceList -items: -- apiVersion: apps/v1 - kind: Deployment - metadata: - labels: - config.kubernetes.io/local-config: "true" - spec: - replicas: 2 -- kind: Service - metadata: - labels: - config.kubernetes.io/local-config: "true" -functionConfig: - apiVersion: krm.kcl.dev/v1alpha1 - kind: KCLRun - spec: - params: - requires: - - key: config.kubernetes.io/local-config - # Optional: If specified, a regular expression the annotation's value must match. - allowedRegex: "true" - source: oci://docker.io/kcllang/required-labels diff --git a/examples/validation/required-labels/config.yaml b/examples/validation/required-labels/config.yaml deleted file mode 100644 index a34545a..0000000 --- a/examples/validation/required-labels/config.yaml +++ /dev/null @@ -1,35 +0,0 @@ -apiVersion: krm.kcl.dev/v1alpha1 -kind: KCLRun -metadata: - name: required-labels - metadata: - annotations: - krm.kcl.dev/version: 0.0.1 - krm.kcl.dev/type: validation - documentation: >- - Requires resources to contain specified labels, with values matching provided regular expressions. -spec: - params: - requires: - - key: config.kubernetes.io/local-config - # Optional: If specified, a regular expression the annotation's value must match. - allowedRegex: "true" - source: | - # Construct resource and params - resource = option("resource_list") - params = resource.functionConfig.spec.params - requires = params.requires or [] - # Define the validation function - validate_required_labels = lambda item, requires { - if requires: - requires_map = {r.key: r.allowedRegex or "" for r in requires} - assert all k, v in item.metadata.labels { - k in requires_map - }, "must provide labels with the regex ${requires_map}" - assert all k, v in item.metadata.labels { - regex.match(v, requires_map[k]) if requires_map[k] - }, "must provide labels with the regex ${requires_map}" - item - } - # Validate All resource - items = [validate_required_labels(i, requires) for i in resource.items] diff --git a/examples/validation/required-labels/kcl.mod b/examples/validation/required-labels/kcl.mod new file mode 100644 index 0000000..1394a1e --- /dev/null +++ b/examples/validation/required-labels/kcl.mod @@ -0,0 +1,4 @@ +[package] +name = "restrict-ingress-paths" +edition = "*" +version = "0.0.1" diff --git a/examples/validation/required-labels/kcl.mod.lock b/examples/validation/required-labels/kcl.mod.lock new file mode 100644 index 0000000..e69de29 diff --git a/examples/validation/required-labels/main.k b/examples/validation/required-labels/main.k new file mode 100644 index 0000000..746bb57 --- /dev/null +++ b/examples/validation/required-labels/main.k @@ -0,0 +1,16 @@ +import regex + +requires = option("params").requires or [] +# Define the validation function +validate_required_labels = lambda item, requires { + if requires: + requires_map = {r.key: r.allowedRegex or "" for r in requires} + labels = item.metadata.labels or {} + if labels: + assert all k, v in labels { + regex.match(v, requires_map[k]) if requires_map[k] + }, "must provide labels with the regex ${requires_map}" + item +} +# Validate All resource +items = [validate_required_labels(i, requires) for i in option("items")] diff --git a/examples/validation/required-labels/suite/bad.yaml b/examples/validation/required-labels/suite/bad.yaml new file mode 100644 index 0000000..393c1d0 --- /dev/null +++ b/examples/validation/required-labels/suite/bad.yaml @@ -0,0 +1,35 @@ +apiVersion: krm.kcl.dev/v1alpha1 +kind: KCLRun +metadata: + name: required-labels + annotations: + krm.kcl.dev/version: 0.0.1 + krm.kcl.dev/type: validation + documentation: >- + Requires resources to contain specified labels, with values matching provided regular expressions. +spec: + params: + requires: + - key: config.kubernetes.io/local-config + # Optional: If specified, a regular expression the annotation's value must match. + allowedRegex: "true" + source: ./examples/validation/required-labels/main.k +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: bad-required-labels + labels: + config.kubernetes.io/local-config: "false" +spec: + rules: + - host: cafe.example.com + http: + paths: + - path: /root + pathType: Prefix + backend: + service: + name: tea-svc + port: + number: 80 diff --git a/examples/validation/required-labels/suite/config.yaml b/examples/validation/required-labels/suite/config.yaml new file mode 100644 index 0000000..08ef9c8 --- /dev/null +++ b/examples/validation/required-labels/suite/config.yaml @@ -0,0 +1,16 @@ +apiVersion: krm.kcl.dev/v1alpha1 +kind: KCLRun +metadata: + name: required-labels + annotations: + krm.kcl.dev/version: 0.0.1 + krm.kcl.dev/type: validation + documentation: >- + Requires resources to contain specified labels, with values matching provided regular expressions. +spec: + params: + requires: + - key: config.kubernetes.io/local-config + # Optional: If specified, a regular expression the annotation's value must match. + allowedRegex: "true" + source: oci://ghcr.io/kcl-lang/required-labels diff --git a/examples/validation/required-labels/suite/good.yaml b/examples/validation/required-labels/suite/good.yaml new file mode 100644 index 0000000..a996bb8 --- /dev/null +++ b/examples/validation/required-labels/suite/good.yaml @@ -0,0 +1,42 @@ +apiVersion: krm.kcl.dev/v1alpha1 +kind: KCLRun +metadata: + name: required-labels + annotations: + krm.kcl.dev/version: 0.0.1 + krm.kcl.dev/type: validation + documentation: >- + Requires resources to contain specified labels, with values matching provided regular expressions. +spec: + params: + requires: + - key: config.kubernetes.io/local-config + # Optional: If specified, a regular expression the annotation's value must match. + allowedRegex: "true" + source: ./examples/validation/required-labels/main.k +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: good-required-labels + labels: + config.kubernetes.io/local-config: "true" +spec: + rules: + - host: cafe.example.com + http: + paths: + - path: /tea + pathType: Prefix + backend: + service: + name: tea-svc + port: + number: 80 + - path: /coffee + pathType: Prefix + backend: + service: + name: coffee-svc + port: + number: 80 diff --git a/pkg/api/v1alpha1/kclrun_types.go b/pkg/api/v1alpha1/kclrun_types.go new file mode 100644 index 0000000..bd28f22 --- /dev/null +++ b/pkg/api/v1alpha1/kclrun_types.go @@ -0,0 +1,18 @@ +package v1alpha1 + +const ( + // KCLRunGroup represents the API group for the KCLRun resource. + KCLRunGroup = "krm.kcl.dev" + + // KCLRunVersion represents the API version for the KCLRun resource. + KCLRunVersion = "v1alpha1" + + // KCLRunAPIVersion is a combination of the API group and version for the KCLRun resource. + KCLRunAPIVersion = KCLRunGroup + "/" + KCLRunVersion + + // KCLRunKind represents the kind of resource for the KCLRun resource. + KCLRunKind = "KCLRun" + + // SourceKey is the key for the source field in a ConfigMap. + SourceKey = "source" +) diff --git a/pkg/config/config.go b/pkg/config/config.go index 2588995..5e76594 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -8,31 +8,17 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/kustomize/kyaml/yaml" + "kcl-lang.io/krm-kcl/pkg/api/v1alpha1" "kcl-lang.io/krm-kcl/pkg/edit" ) const ( - // KCLRunGroup represents the API group for the KCLRun resource. - KCLRunGroup = "krm.kcl.dev" - - // KCLRunVersion represents the API version for the KCLRun resource. - KCLRunVersion = "v1alpha1" - - // KCLRunAPIVersion is a combination of the API group and version for the KCLRun resource. - KCLRunAPIVersion = KCLRunGroup + "/" + KCLRunVersion - - // KCLRunKind represents the kind of resource for the KCLRun resource. - KCLRunKind = "KCLRun" - // ConfigMapAPIVersion represents the API version for the ConfigMap resource. ConfigMapAPIVersion = "v1" // ConfigMapKind represents the kind of resource for the ConfigMap resource. ConfigMapKind = "ConfigMap" - // SourceKey is the key for the source field in a ConfigMap. - SourceKey = "source" - // DefaultProgramName is the default name for the KCL function program. DefaultProgramName = "kcl-function-run" ) @@ -68,19 +54,19 @@ func (r *KCLRun) Config(fnCfg *fn.KubeObject) error { r.Namespace = cm.Namespace r.Spec.Params = map[string]interface{}{} for k, v := range cm.Data { - if k == SourceKey { + if k == v1alpha1.SourceKey { r.Spec.Source = v } r.Spec.Params[k] = v } - case fnCfgAPIVersion == KCLRunAPIVersion && fnCfgKind == KCLRunKind: + case fnCfgAPIVersion == v1alpha1.KCLRunAPIVersion && fnCfgKind == v1alpha1.KCLRunKind: if err := fnCfg.As(r); err != nil { return err } default: return fmt.Errorf("`functionConfig` must be either %v or %v, but we got: %v", schema.FromAPIVersionAndKind(ConfigMapAPIVersion, ConfigMapKind).String(), - schema.FromAPIVersionAndKind(KCLRunAPIVersion, KCLRunKind).String(), + schema.FromAPIVersionAndKind(v1alpha1.KCLRunAPIVersion, v1alpha1.KCLRunKind).String(), schema.FromAPIVersionAndKind(fnCfg.GetAPIVersion(), fnCfg.GetKind()).String()) } diff --git a/pkg/edit/bootstrap.go b/pkg/edit/bootstrap.go index 14cacae..4a332cb 100644 --- a/pkg/edit/bootstrap.go +++ b/pkg/edit/bootstrap.go @@ -125,6 +125,7 @@ func constructOptions(resourceList *yaml.RNode) ([]kcl.Option, error) { return nil, errors.Wrap(err) } opts := []kcl.Option{ + // resource_list kcl.WithOptions(fmt.Sprintf("%s=%s", resourceListOptionName, resourceListOptionKCLValue)), // resource.items kcl.WithOptions(fmt.Sprintf("%s=%s", itemsOptionName, itemsOptionKCLValue)), diff --git a/pkg/edit/wraper.go b/pkg/edit/wraper.go index 93ae46f..f08751c 100644 --- a/pkg/edit/wraper.go +++ b/pkg/edit/wraper.go @@ -1,6 +1,7 @@ package edit import ( + "kcl-lang.io/krm-kcl/pkg/api/v1alpha1" "sigs.k8s.io/kustomize/kyaml/errors" "sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/yaml" @@ -10,6 +11,10 @@ import ( func WrapResources(nodes []*yaml.RNode, fc *yaml.RNode) (*yaml.RNode, error) { var ynodes []*yaml.Node for _, rnode := range nodes { + // Filter KCLRun resources + if rnode.GetApiVersion() == v1alpha1.KCLRunAPIVersion && rnode.GetKind() == v1alpha1.KCLRunKind { + continue + } ynodes = append(ynodes, rnode.YNode()) } m := map[string]interface{}{ diff --git a/pkg/kio/filter.go b/pkg/kio/filter.go index c8a9ea5..0698280 100644 --- a/pkg/kio/filter.go +++ b/pkg/kio/filter.go @@ -1,6 +1,7 @@ package kio import ( + "kcl-lang.io/krm-kcl/pkg/api/v1alpha1" "kcl-lang.io/krm-kcl/pkg/config" "kcl-lang.io/krm-kcl/pkg/edit" @@ -16,7 +17,7 @@ type Filter struct { // Filter checks each input and ensures that all containers have cpu and memory // reservations set, otherwise it returns an error. func (f Filter) Filter(in []*yaml.RNode) ([]*yaml.RNode, error) { - // Whether has fnCfg in the + // Whether has fnCfg in the `functionConfig` field input resource list hasFnCfg := f.rw.FunctionConfig != nil configs, idxs, err := f.parseConfigs(in) if err != nil { @@ -49,7 +50,7 @@ func (f *Filter) parseConfigs(in []*yaml.RNode) ([]*config.KCLRun, []int, error) // If KCLRun is not found in the function config, find it in the input manifests if f.rw.FunctionConfig == nil { for idx, i := range in { - if i.GetApiVersion() == config.KCLRunAPIVersion && i.GetKind() == config.KCLRunKind { + if i.GetApiVersion() == v1alpha1.KCLRunAPIVersion && i.GetKind() == v1alpha1.KCLRunKind { // Parse the input function config. config, err := f.parseConfig(i) f.rw.FunctionConfig = i diff --git a/tests/abstraction/web-service/kcl.yaml b/tests/abstraction/web-service/kcl.yaml index 69ff71a..0924a17 100644 --- a/tests/abstraction/web-service/kcl.yaml +++ b/tests/abstraction/web-service/kcl.yaml @@ -31,7 +31,7 @@ kcl_options: name: app source: | resource = option("resource_list") - params = resource.functionConfig.spec.params + params = option("params") # Convert the `App` model into Kubernetes Deployment and Service Manifests kubernetesRender = lambda a: {} { diff --git a/tests/abstraction/web-service/main.k b/tests/abstraction/web-service/main.k index 580280f..cbca437 100644 --- a/tests/abstraction/web-service/main.k +++ b/tests/abstraction/web-service/main.k @@ -1,6 +1,6 @@ items = lambda { resource = option("resource_list") - params = resource.functionConfig.spec.params + params = option("params") # Convert the `App` model into Kubernetes Deployment and Service Manifests kubernetesRender = lambda a { diff --git a/tests/mutation/set-annotations/kcl.yaml b/tests/mutation/set-annotations/kcl.yaml index 7b8f130..8be573e 100644 --- a/tests/mutation/set-annotations/kcl.yaml +++ b/tests/mutation/set-annotations/kcl.yaml @@ -38,7 +38,7 @@ kcl_options: source: | resource = option("resource_list") items = resource.items - params = resource.functionConfig.spec.params + params = option("params") # Use `k = v` to override existing annotations annotations = {k = v for k, v in params.annotations} [item | { diff --git a/tests/mutation/set-annotations/main.k b/tests/mutation/set-annotations/main.k index 6ee0027..7caf654 100644 --- a/tests/mutation/set-annotations/main.k +++ b/tests/mutation/set-annotations/main.k @@ -1,7 +1,7 @@ items = lambda { resource = option("resource_list") items = resource.items - params = resource.functionConfig.spec.params + params = option("params") # Use `k = v` to override existing annotations annotations = {k = v for k, v in params.annotations} [item | { diff --git a/tests/validation/replica-limits/kcl.yaml b/tests/validation/replica-limits/kcl.yaml index 03ac9cc..9e989c7 100644 --- a/tests/validation/replica-limits/kcl.yaml +++ b/tests/validation/replica-limits/kcl.yaml @@ -46,7 +46,7 @@ kcl_options: # Construct resource and params resource = option("resource_list") items = resource.items - params = resource.functionConfig.spec.params + params = option("params") min_replicas: int = params.min_replicas or 0 max_replicas: int = params.max_replicas or 99999 # Define the validation function diff --git a/tests/validation/replica-limits/main.k b/tests/validation/replica-limits/main.k index 9e9778e..02578c5 100644 --- a/tests/validation/replica-limits/main.k +++ b/tests/validation/replica-limits/main.k @@ -2,7 +2,7 @@ items = lambda { # Construct resource and params resource = option("resource_list") items = resource.items - params = resource.functionConfig.spec.params + params = option("params") min_replicas: int = params.min_replicas or 0 max_replicas: int = params.max_replicas or 99999 # Define the validation function