From 50ccdffb0e0e080140655dd39c791db430e1b457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Reegn?= Date: Tue, 12 Nov 2024 16:26:54 +0100 Subject: [PATCH 1/4] Add boolean template function 'namespaced' Example usage: ``` kubectl-slice -f test.yaml -o test --prune -t '{{ if namespaced .}}namespaces/{{.metadata.namespace}}{{else}}cluster{{end}}/{{.kind}}.{{.metadata.name}}.yaml' ``` --- slice/template/funcs.go | 90 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/slice/template/funcs.go b/slice/template/funcs.go index 5a93ba7..c3b6e69 100644 --- a/slice/template/funcs.go +++ b/slice/template/funcs.go @@ -39,6 +39,96 @@ var Functions = template.FuncMap{ "dottodash": jsonDotToDash, "dottounder": jsonDotToUnder, "index": mapValueByIndex, + "namespaced": namespaced, +} + +var clusterScoped = map[string]map[string]bool{ + "v1": { + // "Namespace": true, + "Node": true, + "PersistentVolume": true, + }, + "admissionregistration.k8s.io/v1": { + "MutatingWebhookConfiguration": true, + "ValidatingAdmissionPolicy": true, + "ValidatingAdmissionPolicyBinding": true, + "ValidatingWebhookConfiguration": true, + }, + "apiextensions.k8s.io/v1": { + "CustomResourceDefinition": true, + }, + "apiregistration.k8s.io/v1": { + "APIService": true, + }, + "authentication.k8s.io/v1": { + "SelfSubjectReview": true, + "TokenReview": true, + }, + "authorization.k8s.io/v1": { + "SelfSubjectAccessReview": true, + "SelfSubjectRulesReview": true, + }, + "certificates.k8s.io/v1": { + "CertificateSigningRequest": true, + }, + "flowcontrol.apiserver.k8s.io/v1": { + "FlowSchema": true, + "PriorityLevelConfiguration": true, + }, + "networking.k8s.io/v1": { + "IngressClass": true, + }, + "node.k8s.io/v1": { + "RuntimeClass": true, + }, + "rbac.authorization.k8s.io/v1": { + "ClusterRole": true, + "ClusterRoleBinding": true, + }, + "scheduling.k8s.io/v1": { + "PriorityClass": true, + }, + "storage.k8s.io/v1": { + "CSIDriver": true, + "CSINode": true, + "StorageClass": true, + "VolumeAttachment": true, + }, +} + +func namespaced(manifest map[string]interface{}) (bool, error) { + var apiVersion string + var kind string + switch v := manifest["apiVersion"].(type) { + case string: + apiVersion = v + default: + return false, fmt.Errorf("apiVersion is not a string") + } + switch v := manifest["kind"].(type) { + case string: + kind = v + default: + return false, fmt.Errorf("kind is not a string") + } + fmt.Println("Reached A") + if v, ok := clusterScoped[apiVersion]; ok { + if _, ok := v[kind]; ok { + return false, nil + } + } + fmt.Println("Reached B") + // best effort, assume cluster scoped if unknown gvk + // and resource doesn't have a namespace declared + switch v := manifest["metadata"].(type) { + case map[string]interface{}: + if _, ok := v["namespace"]; ok { + return true, nil + } + default: + return false, fmt.Errorf("metadata is not a map") + } + return false, nil } // mapValueByIndex returns the value of the map at the given index From 95220582b66db7780203332158125318c7e5e57a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Reegn?= Date: Tue, 12 Nov 2024 16:31:05 +0100 Subject: [PATCH 2/4] Allow both namespace scoped and cluster scoped definitions --- slice/template/funcs.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/slice/template/funcs.go b/slice/template/funcs.go index c3b6e69..0d07a76 100644 --- a/slice/template/funcs.go +++ b/slice/template/funcs.go @@ -111,13 +111,11 @@ func namespaced(manifest map[string]interface{}) (bool, error) { default: return false, fmt.Errorf("kind is not a string") } - fmt.Println("Reached A") if v, ok := clusterScoped[apiVersion]; ok { - if _, ok := v[kind]; ok { - return false, nil + if clusterScoped, ok := v[kind]; ok { + return !clusterScoped, nil } } - fmt.Println("Reached B") // best effort, assume cluster scoped if unknown gvk // and resource doesn't have a namespace declared switch v := manifest["metadata"].(type) { From 93facf0790cc6bf00fde0a7c7b35c2c610620a2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Reegn?= Date: Tue, 12 Nov 2024 16:46:22 +0100 Subject: [PATCH 3/4] Add more builtin api-s --- slice/template/funcs.go | 102 ++++++++++++++++++++++++++++------------ 1 file changed, 73 insertions(+), 29 deletions(-) diff --git a/slice/template/funcs.go b/slice/template/funcs.go index 0d07a76..a274462 100644 --- a/slice/template/funcs.go +++ b/slice/template/funcs.go @@ -42,57 +42,101 @@ var Functions = template.FuncMap{ "namespaced": namespaced, } -var clusterScoped = map[string]map[string]bool{ +// kubernetes built-in stable API-s shouldn't need external definition +// extracted from vanilla k8s cluster using kubectl api-resources +var namespaceScoped = map[string]map[string]bool{ "v1": { - // "Namespace": true, - "Node": true, - "PersistentVolume": true, + "Binding": true, + "ConfigMap": true, + "Endpoints": true, + "Event": true, + "LimitRange": true, + "Namespace": false, + "Node": false, + "PersistentVolume": false, + "PersistentVolumeClaim": true, + "Pod": true, + "PodTemplate": true, + "ReplicationController": true, + "ResourceQuota": true, + "Secret": true, + "Service": true, + "ServiceAccount": true, }, "admissionregistration.k8s.io/v1": { - "MutatingWebhookConfiguration": true, - "ValidatingAdmissionPolicy": true, - "ValidatingAdmissionPolicyBinding": true, - "ValidatingWebhookConfiguration": true, + "MutatingWebhookConfiguration": false, + "ValidatingAdmissionPolicy": false, + "ValidatingAdmissionPolicyBinding": false, + "ValidatingWebhookConfiguration": false, }, "apiextensions.k8s.io/v1": { - "CustomResourceDefinition": true, + "CustomResourceDefinition": false, }, "apiregistration.k8s.io/v1": { - "APIService": true, + "APIService": false, + }, + "apps/v1": { + "ControllerRevision": true, + "DaemonSet": true, + "Deployment": true, + "ReplicaSet": true, + "StatefulSet": true, }, "authentication.k8s.io/v1": { - "SelfSubjectReview": true, - "TokenReview": true, + "SelfSubjectReview": false, + "TokenReview": false, }, "authorization.k8s.io/v1": { - "SelfSubjectAccessReview": true, - "SelfSubjectRulesReview": true, + "LocalSubjectAccessReview": true, + "SelfSubjectAccessReview": false, + "SelfSubjectRulesReview": false, + }, + "autoscaling/v2": { + "HorizontalPodAutoscaler": true, + }, + "batch/v1": { + "CronJob": true, + "Job": true, }, "certificates.k8s.io/v1": { - "CertificateSigningRequest": true, + "CertificateSigningRequest": false, + }, + "coordination.k8s.io/v1": { + "Lease": true, + }, + "discovery.k8s.io/v1": { + "EndpointSlice": true, }, "flowcontrol.apiserver.k8s.io/v1": { - "FlowSchema": true, - "PriorityLevelConfiguration": true, + "FlowSchema": false, + "PriorityLevelConfiguration": false, }, "networking.k8s.io/v1": { - "IngressClass": true, + "Ingress": true, + "IngressClass": false, + "NetworkPolicy": true, }, "node.k8s.io/v1": { - "RuntimeClass": true, + "RuntimeClass": false, + }, + "policy/v1": { + "PodDisruptionBudget": true, }, "rbac.authorization.k8s.io/v1": { - "ClusterRole": true, - "ClusterRoleBinding": true, + "ClusterRole": false, + "ClusterRoleBinding": false, + "Role": true, + "RoleBinding": true, }, "scheduling.k8s.io/v1": { - "PriorityClass": true, + "PriorityClass": false, }, "storage.k8s.io/v1": { - "CSIDriver": true, - "CSINode": true, - "StorageClass": true, - "VolumeAttachment": true, + "CSIDriver": false, + "CSINode": false, + "StorageClass": false, + "VolumeAttachment": false, + "CSIStorageCapacity": true, }, } @@ -111,9 +155,9 @@ func namespaced(manifest map[string]interface{}) (bool, error) { default: return false, fmt.Errorf("kind is not a string") } - if v, ok := clusterScoped[apiVersion]; ok { - if clusterScoped, ok := v[kind]; ok { - return !clusterScoped, nil + if v, ok := namespaceScoped[apiVersion]; ok { + if namespaced, ok := v[kind]; ok { + return namespaced, nil } } // best effort, assume cluster scoped if unknown gvk From 69f5aa8c98bb7f0827f63c14126cf027bbf4aaa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Reegn?= Date: Tue, 12 Nov 2024 16:58:47 +0100 Subject: [PATCH 4/4] Add some test cases --- slice/template/funcs_test.go | 96 +++++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/slice/template/funcs_test.go b/slice/template/funcs_test.go index 417d398..9a25177 100644 --- a/slice/template/funcs_test.go +++ b/slice/template/funcs_test.go @@ -275,7 +275,7 @@ func Test_jsonReplace(t *testing.T) { } func Test_env(t *testing.T) { - var letters = []rune("abcdefghijklmnopqrstuvwxyz") + letters := []rune("abcdefghijklmnopqrstuvwxyz") randSeq := func(n int) string { rnd := rand.New(rand.NewSource(time.Now().UnixNano())) @@ -417,7 +417,6 @@ func Test_jsonLowerAndUpper(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - lowered, err := jsonLower(tt.args.val) requireErrorIf(t, tt.wantErr, err) @@ -613,3 +612,96 @@ func requireErrorIf(t *testing.T, wantErr bool, err error) { require.NoError(t, err) } } + +func Test_namespaced(t *testing.T) { + tests := []struct { + name string + input map[string]interface{} + want bool + wantErr bool + }{ + { + name: "builtin cluster scoped", + input: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": map[string]interface{}{ + "name": "test", + }, + }, + want: false, + wantErr: false, + }, + { + name: "builtin cluster scoped with namespace", + input: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": map[string]interface{}{ + "name": "test", + "namespace": "test", + }, + }, + want: false, + wantErr: false, + }, + { + name: "builtin namespaced", + input: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "test", + "namespace": "test", + }, + }, + want: true, + wantErr: false, + }, + { + name: "builtin namespaced without namespace", + input: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "test", + }, + }, + want: true, + wantErr: false, + }, + { + name: "generic object with namespace", + input: map[string]interface{}{ + "apiVersion": "generic/v1", + "kind": "Generic", + "metadata": map[string]interface{}{ + "name": "test", + "namespace": "test", + }, + }, + want: true, + wantErr: false, + }, + { + name: "generic object without namespace", + input: map[string]interface{}{ + "apiVersion": "generic/v1", + "kind": "Generic", + "metadata": map[string]interface{}{ + "name": "test", + }, + }, + want: false, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := namespaced(tt.input) + requireErrorIf(t, tt.wantErr, err) + require.Equal(t, tt.want, got) + }) + } +}