diff --git a/.golangci.yaml b/.golangci.yaml index 2141ad99f57..f26ecbbe0a1 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,5 +1,7 @@ run: timeout: 5m + skip-files: + - pkg/target/matchcrd_constant.go linters-settings: gocritic: diff --git a/Makefile b/Makefile index 002665a5b55..bcb609d7dbe 100644 --- a/Makefile +++ b/Makefile @@ -258,6 +258,7 @@ manifests: __controller-gen paths="./apis/..." \ paths="./pkg/..." \ output:crd:artifacts:config=config/crd/bases + ./build/update-match-schema.sh rm -rf manifest_staging mkdir -p manifest_staging/deploy/experimental mkdir -p manifest_staging/charts/gatekeeper diff --git a/build/update-match-schema.sh b/build/update-match-schema.sh new file mode 100755 index 00000000000..14c55a6577a --- /dev/null +++ b/build/update-match-schema.sh @@ -0,0 +1,21 @@ +# This script builds a golang string constant containing the YAML code for the +# Match CRD. This is needed to auto generate the JSONSchemaProps for Match. It +# will parse the YAML for the Match CRD, found in $CRD_FILE, and output to +# $GO_FILE. + +GO_FILE="./pkg/target/matchcrd_constant.go" +SRC_FILE="./pkg/mutation/match/match_types.go" +CRD_FILE="./config/crd/bases/match.gatekeeper.sh_matchcrd.yaml" + +cat << EOF > ${GO_FILE} +package target + +// DO NOT MODIFY THIS FILE DIRECTLY! +// This file is generated from $SRC_FILE via "make manifests". + +const matchYAML = \` +EOF + +# Escape backticks in the yaml, add terminating backtick +cat ${CRD_FILE} | sed "s/\`/\`+\"\`\"+\`/g" >> ${GO_FILE} +echo "\`" >> ${GO_FILE} diff --git a/config/crd/bases/match.gatekeeper.sh_matchcrd.yaml b/config/crd/bases/match.gatekeeper.sh_matchcrd.yaml new file mode 100644 index 00000000000..c8b94c2c601 --- /dev/null +++ b/config/crd/bases/match.gatekeeper.sh_matchcrd.yaml @@ -0,0 +1,209 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.10.0 + creationTimestamp: null + name: matchcrd.match.gatekeeper.sh +spec: + group: match.gatekeeper.sh + names: + kind: DummyCRD + listKind: DummyCRDList + plural: matchcrd + singular: dummycrd + scope: Namespaced + versions: + - name: match + schema: + openAPIV3Schema: + description: DummyCRD is a "dummy" CRD to hold the Match object, which we + ultimately need to generate JSONSchemaProps. The TypeMeta and ObjectMeta + fields are required for controller-gen to generate the CRD. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + embeddedMatch: + description: Match selects which objects are in scope. + properties: + excludedNamespaces: + description: 'ExcludedNamespaces is a list of namespace names. If + defined, a constraint only applies to resources not in a listed + namespace. ExcludedNamespaces also supports a prefix or suffix based + glob. For example, `excludedNamespaces: [kube-*]` matches both + `kube-system` and `kube-public`, and `excludedNamespaces: [*-system]` + matches both `kube-system` and `gatekeeper-system`.' + items: + description: 'A string that supports globbing at its front or end. + Ex: "kube-*" will match "kube-system" or "kube-public", "*-system" + will match "kube-system" or "gatekeeper-system". The asterisk + is required for wildcard matching.' + pattern: ^(\*|\*-)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ + type: string + type: array + kinds: + items: + description: Kinds accepts a list of objects with apiGroups and + kinds fields that list the groups/kinds of objects to which the + mutation will apply. If multiple groups/kinds objects are specified, + only one match is needed for the resource to be in scope. + properties: + apiGroups: + description: APIGroups is the API groups the resources belong + to. '*' is all groups. If '*' is present, the length of the + slice must be one. Required. + items: + type: string + type: array + kinds: + items: + type: string + type: array + type: object + type: array + labelSelector: + description: 'LabelSelector is the combination of two optional fields: + `matchLabels` and `matchExpressions`. These two fields provide + different methods of selecting or excluding k8s objects based on + the label keys and values included in object metadata. All selection + expressions from both sections are ANDed to determine if an object + meets the cumulative requirements of the selector.' + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector requirement is a selector that + contains values, a key, and an operator that relates the key + and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: operator represents a key's relationship to + a set of values. Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of string values. If the + operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values + array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single + {key,value} in the matchLabels map is equivalent to an element + of matchExpressions, whose key field is "key", the operator + is "In", and the values array contains only "value". The requirements + are ANDed. + type: object + type: object + name: + description: 'Name is the name of an object. If defined, it will + match against objects with the specified name. Name also supports + a prefix or suffix glob. For example, `name: pod-*` would match + both `pod-a` and `pod-b`, and `name: *-pod` would match both `a-pod` + and `b-pod`.' + pattern: ^(\*|\*-)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ + type: string + namespaceSelector: + description: NamespaceSelector is a label selector against an object's + containing namespace or the object itself, if the object is a namespace. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector requirement is a selector that + contains values, a key, and an operator that relates the key + and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: operator represents a key's relationship to + a set of values. Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of string values. If the + operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values + array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single + {key,value} in the matchLabels map is equivalent to an element + of matchExpressions, whose key field is "key", the operator + is "In", and the values array contains only "value". The requirements + are ANDed. + type: object + type: object + namespaces: + description: 'Namespaces is a list of namespace names. If defined, + a constraint only applies to resources in a listed namespace. Namespaces + also supports a prefix or suffix based glob. For example, `namespaces: + [kube-*]` matches both `kube-system` and `kube-public`, and `namespaces: + [*-system]` matches both `kube-system` and `gatekeeper-system`.' + items: + description: 'A string that supports globbing at its front or end. + Ex: "kube-*" will match "kube-system" or "kube-public", "*-system" + will match "kube-system" or "gatekeeper-system". The asterisk + is required for wildcard matching.' + pattern: ^(\*|\*-)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ + type: string + type: array + scope: + description: Scope determines if cluster-scoped and/or namespaced-scoped + resources are matched. Accepts `*`, `Cluster`, or `Namespaced`. + (defaults to `*`) + type: string + source: + description: Source determines whether generated or original resources + are matched. Accepts `Generated`|`Original`|`All` (defaults to `All`). + A value of `Generated` will only match generated resources, while + `Original` will only match regular resources. + enum: + - All + - Generated + - Original + type: string + type: object + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadataDummy: + type: object + type: object + served: true + storage: true diff --git a/config/crd/bases/mutations.gatekeeper.sh_assignmetadata.yaml b/config/crd/bases/mutations.gatekeeper.sh_assignmetadata.yaml index 895f3c44790..7fb1ee7e389 100644 --- a/config/crd/bases/mutations.gatekeeper.sh_assignmetadata.yaml +++ b/config/crd/bases/mutations.gatekeeper.sh_assignmetadata.yaml @@ -38,7 +38,7 @@ spec: location: type: string match: - description: Match selects objects to apply mutations to. + description: Match selects which objects are in scope. properties: excludedNamespaces: description: 'ExcludedNamespaces is a list of namespace names. @@ -337,7 +337,7 @@ spec: location: type: string match: - description: Match selects objects to apply mutations to. + description: Match selects which objects are in scope. properties: excludedNamespaces: description: 'ExcludedNamespaces is a list of namespace names. @@ -636,7 +636,7 @@ spec: location: type: string match: - description: Match selects objects to apply mutations to. + description: Match selects which objects are in scope. properties: excludedNamespaces: description: 'ExcludedNamespaces is a list of namespace names. diff --git a/manifest_staging/charts/gatekeeper/crds/assignmetadata-customresourcedefinition.yaml b/manifest_staging/charts/gatekeeper/crds/assignmetadata-customresourcedefinition.yaml index 3a63eef3cb3..468b01fccdb 100644 --- a/manifest_staging/charts/gatekeeper/crds/assignmetadata-customresourcedefinition.yaml +++ b/manifest_staging/charts/gatekeeper/crds/assignmetadata-customresourcedefinition.yaml @@ -39,7 +39,7 @@ spec: location: type: string match: - description: Match selects objects to apply mutations to. + description: Match selects which objects are in scope. properties: excludedNamespaces: description: 'ExcludedNamespaces is a list of namespace names. If defined, a constraint only applies to resources not in a listed namespace. ExcludedNamespaces also supports a prefix or suffix based glob. For example, `excludedNamespaces: [kube-*]` matches both `kube-system` and `kube-public`, and `excludedNamespaces: [*-system]` matches both `kube-system` and `gatekeeper-system`.' @@ -250,7 +250,7 @@ spec: location: type: string match: - description: Match selects objects to apply mutations to. + description: Match selects which objects are in scope. properties: excludedNamespaces: description: 'ExcludedNamespaces is a list of namespace names. If defined, a constraint only applies to resources not in a listed namespace. ExcludedNamespaces also supports a prefix or suffix based glob. For example, `excludedNamespaces: [kube-*]` matches both `kube-system` and `kube-public`, and `excludedNamespaces: [*-system]` matches both `kube-system` and `gatekeeper-system`.' @@ -461,7 +461,7 @@ spec: location: type: string match: - description: Match selects objects to apply mutations to. + description: Match selects which objects are in scope. properties: excludedNamespaces: description: 'ExcludedNamespaces is a list of namespace names. If defined, a constraint only applies to resources not in a listed namespace. ExcludedNamespaces also supports a prefix or suffix based glob. For example, `excludedNamespaces: [kube-*]` matches both `kube-system` and `kube-public`, and `excludedNamespaces: [*-system]` matches both `kube-system` and `gatekeeper-system`.' diff --git a/manifest_staging/deploy/gatekeeper.yaml b/manifest_staging/deploy/gatekeeper.yaml index 88c0eab6b23..08093205b6e 100644 --- a/manifest_staging/deploy/gatekeeper.yaml +++ b/manifest_staging/deploy/gatekeeper.yaml @@ -1067,7 +1067,7 @@ spec: location: type: string match: - description: Match selects objects to apply mutations to. + description: Match selects which objects are in scope. properties: excludedNamespaces: description: 'ExcludedNamespaces is a list of namespace names. If defined, a constraint only applies to resources not in a listed namespace. ExcludedNamespaces also supports a prefix or suffix based glob. For example, `excludedNamespaces: [kube-*]` matches both `kube-system` and `kube-public`, and `excludedNamespaces: [*-system]` matches both `kube-system` and `gatekeeper-system`.' @@ -1278,7 +1278,7 @@ spec: location: type: string match: - description: Match selects objects to apply mutations to. + description: Match selects which objects are in scope. properties: excludedNamespaces: description: 'ExcludedNamespaces is a list of namespace names. If defined, a constraint only applies to resources not in a listed namespace. ExcludedNamespaces also supports a prefix or suffix based glob. For example, `excludedNamespaces: [kube-*]` matches both `kube-system` and `kube-public`, and `excludedNamespaces: [*-system]` matches both `kube-system` and `gatekeeper-system`.' @@ -1489,7 +1489,7 @@ spec: location: type: string match: - description: Match selects objects to apply mutations to. + description: Match selects which objects are in scope. properties: excludedNamespaces: description: 'ExcludedNamespaces is a list of namespace names. If defined, a constraint only applies to resources not in a listed namespace. ExcludedNamespaces also supports a prefix or suffix based glob. For example, `excludedNamespaces: [kube-*]` matches both `kube-system` and `kube-public`, and `excludedNamespaces: [*-system]` matches both `kube-system` and `gatekeeper-system`.' diff --git a/pkg/mutation/match/match.go b/pkg/mutation/match/match.go index a65af53fc18..52b8893b9d2 100644 --- a/pkg/mutation/match/match.go +++ b/pkg/mutation/match/match.go @@ -6,7 +6,6 @@ import ( "reflect" "github.com/open-policy-agent/gatekeeper/pkg/mutation/types" - "github.com/open-policy-agent/gatekeeper/pkg/util" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -20,62 +19,8 @@ var ErrMatch = errors.New("failed to run Match criteria") // Only for use in Match, not ApplyTo. const Wildcard = "*" -// Match selects objects to apply mutations to. -// +kubebuilder:object:generate=true -type Match struct { - // Source determines whether generated or original resources are matched. - // Accepts `Generated`|`Original`|`All` (defaults to `All`). A value of - // `Generated` will only match generated resources, while `Original` will only - // match regular resources. - // +kubebuilder:validation:Enum=All;Generated;Original - Source string `json:"source,omitempty"` - Kinds []Kinds `json:"kinds,omitempty"` - // Scope determines if cluster-scoped and/or namespaced-scoped resources - // are matched. Accepts `*`, `Cluster`, or `Namespaced`. (defaults to `*`) - Scope apiextensionsv1.ResourceScope `json:"scope,omitempty"` - // Namespaces is a list of namespace names. If defined, a constraint only - // applies to resources in a listed namespace. Namespaces also supports a - // prefix or suffix based glob. For example, `namespaces: [kube-*]` matches both - // `kube-system` and `kube-public`, and `namespaces: [*-system]` matches both - // `kube-system` and `gatekeeper-system`. - Namespaces []util.Wildcard `json:"namespaces,omitempty"` - // ExcludedNamespaces is a list of namespace names. If defined, a - // constraint only applies to resources not in a listed namespace. - // ExcludedNamespaces also supports a prefix or suffix based glob. For example, - // `excludedNamespaces: [kube-*]` matches both `kube-system` and - // `kube-public`, and `excludedNamespaces: [*-system]` matches both `kube-system` and - // `gatekeeper-system`. - ExcludedNamespaces []util.Wildcard `json:"excludedNamespaces,omitempty"` - // LabelSelector is the combination of two optional fields: `matchLabels` - // and `matchExpressions`. These two fields provide different methods of - // selecting or excluding k8s objects based on the label keys and values - // included in object metadata. All selection expressions from both - // sections are ANDed to determine if an object meets the cumulative - // requirements of the selector. - LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"` - // NamespaceSelector is a label selector against an object's containing - // namespace or the object itself, if the object is a namespace. - NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"` - // Name is the name of an object. If defined, it will match against objects with the specified - // name. Name also supports a prefix or suffix glob. For example, `name: pod-*` would match - // both `pod-a` and `pod-b`, and `name: *-pod` would match both `a-pod` and `b-pod`. - Name util.Wildcard `json:"name,omitempty"` -} - -// Kinds accepts a list of objects with apiGroups and kinds fields -// that list the groups/kinds of objects to which the mutation will apply. -// If multiple groups/kinds objects are specified, -// only one match is needed for the resource to be in scope. -// +kubebuilder:object:generate=true -type Kinds struct { - // APIGroups is the API groups the resources belong to. '*' is all groups. - // If '*' is present, the length of the slice must be one. - // Required. - APIGroups []string `json:"apiGroups,omitempty" protobuf:"bytes,1,rep,name=apiGroups"` - Kinds []string `json:"kinds,omitempty"` -} - // Matchable represent an object to be matched along with its metadata. +// +kubebuilder:object:generate=false type Matchable struct { Object client.Object Namespace *corev1.Namespace diff --git a/pkg/mutation/match/match_types.go b/pkg/mutation/match/match_types.go new file mode 100644 index 00000000000..fc7904bda75 --- /dev/null +++ b/pkg/mutation/match/match_types.go @@ -0,0 +1,75 @@ +// +kubebuilder:object:generate=true +// +groupName=match.gatekeeper.sh +package match + +import ( + "github.com/open-policy-agent/gatekeeper/pkg/util" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Match selects which objects are in scope. +// +kubebuilder:object:generate=true +type Match struct { + // Source determines whether generated or original resources are matched. + // Accepts `Generated`|`Original`|`All` (defaults to `All`). A value of + // `Generated` will only match generated resources, while `Original` will only + // match regular resources. + // +kubebuilder:validation:Enum=All;Generated;Original + Source string `json:"source,omitempty"` + Kinds []Kinds `json:"kinds,omitempty"` + // Scope determines if cluster-scoped and/or namespaced-scoped resources + // are matched. Accepts `*`, `Cluster`, or `Namespaced`. (defaults to `*`) + Scope apiextensionsv1.ResourceScope `json:"scope,omitempty"` + // Namespaces is a list of namespace names. If defined, a constraint only + // applies to resources in a listed namespace. Namespaces also supports a + // prefix or suffix based glob. For example, `namespaces: [kube-*]` matches both + // `kube-system` and `kube-public`, and `namespaces: [*-system]` matches both + // `kube-system` and `gatekeeper-system`. + Namespaces []util.Wildcard `json:"namespaces,omitempty"` + // ExcludedNamespaces is a list of namespace names. If defined, a + // constraint only applies to resources not in a listed namespace. + // ExcludedNamespaces also supports a prefix or suffix based glob. For example, + // `excludedNamespaces: [kube-*]` matches both `kube-system` and + // `kube-public`, and `excludedNamespaces: [*-system]` matches both `kube-system` and + // `gatekeeper-system`. + ExcludedNamespaces []util.Wildcard `json:"excludedNamespaces,omitempty"` + // LabelSelector is the combination of two optional fields: `matchLabels` + // and `matchExpressions`. These two fields provide different methods of + // selecting or excluding k8s objects based on the label keys and values + // included in object metadata. All selection expressions from both + // sections are ANDed to determine if an object meets the cumulative + // requirements of the selector. + LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"` + // NamespaceSelector is a label selector against an object's containing + // namespace or the object itself, if the object is a namespace. + NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"` + // Name is the name of an object. If defined, it will match against objects with the specified + // name. Name also supports a prefix or suffix glob. For example, `name: pod-*` would match + // both `pod-a` and `pod-b`, and `name: *-pod` would match both `a-pod` and `b-pod`. + Name util.Wildcard `json:"name,omitempty"` +} + +// Kinds accepts a list of objects with apiGroups and kinds fields +// that list the groups/kinds of objects to which the mutation will apply. +// If multiple groups/kinds objects are specified, +// only one match is needed for the resource to be in scope. +// +kubebuilder:object:generate=true +type Kinds struct { + // APIGroups is the API groups the resources belong to. '*' is all groups. + // If '*' is present, the length of the slice must be one. + // Required. + APIGroups []string `json:"apiGroups,omitempty" protobuf:"bytes,1,rep,name=apiGroups"` + Kinds []string `json:"kinds,omitempty"` +} + +// DummyCRD is a "dummy" CRD to hold the Match object, which we ultimately +// need to generate JSONSchemaProps. The TypeMeta and ObjectMeta fields are +// required for controller-gen to generate the CRD. +// +kubebuilder:resource:path="matchcrd" +type DummyCRD struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadataDummy,omitempty"` + + Match `json:"embeddedMatch,omitempty"` +} diff --git a/pkg/mutation/match/zz_generated.deepcopy.go b/pkg/mutation/match/zz_generated.deepcopy.go index 88cf2894ff7..abecee261fd 100644 --- a/pkg/mutation/match/zz_generated.deepcopy.go +++ b/pkg/mutation/match/zz_generated.deepcopy.go @@ -55,6 +55,24 @@ func (in *ApplyTo) DeepCopy() *ApplyTo { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DummyCRD) DeepCopyInto(out *DummyCRD) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Match.DeepCopyInto(&out.Match) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DummyCRD. +func (in *DummyCRD) DeepCopy() *DummyCRD { + if in == nil { + return nil + } + out := new(DummyCRD) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Kinds) DeepCopyInto(out *Kinds) { *out = *in diff --git a/pkg/target/match_schema.go b/pkg/target/match_schema.go index 314815e5e76..e3ed2e7b876 100644 --- a/pkg/target/match_schema.go +++ b/pkg/target/match_schema.go @@ -1,130 +1,49 @@ package target -import "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" +import ( + "fmt" -// This pattern is meant to match: -// -// REGULAR NAMESPACES -// - These are defined by this pattern: [a-z0-9]([-a-z0-9]*[a-z0-9])? -// - You'll see that this is the first two-thirds or so of the pattern below -// -// PREFIX OR SUFFIX BASED WILDCARDS -// - A typical namespace must end in an alphanumeric character. A prefixed wildcard -// can end in "*" (like `kube*`) or "-*" (like `kube-*`), and a suffixed wildcard -// can start with "*" (like `*system`) or "*-" (like `*-system`). -// - To implement this, we add either (\*|\*-)? as a prefix or (\*|-\*)? as a suffix. -// Using both prefixed wildcards and suffixed wildcards at once is not supported. Therefore, -// this _does not_ allow the value to start _and_ end in a wildcard (like `*-*`). -// - Crucially, this _does not_ allow the value to start or end in a dash (like `-system` or `kube-`). -// That is not a valid namespace and not a wildcard, so it's disallowed. -// -// Notably, this disallows other uses of the "*" character like: -// - * -// - k*-system -// -// See the following regexr to test this regex: https://regexr.com/6dgdj -const wildcardNSPattern = `^(\*|\*-)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$` + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/yaml" +) -func matchSchema() apiextensions.JSONSchemaProps { - // Define some repeatedly used sections - wildcardNSList := apiextensions.JSONSchemaProps{ - Type: "array", - Items: &apiextensions.JSONSchemaPropsOrArray{ - Schema: &apiextensions.JSONSchemaProps{Type: "string", Pattern: wildcardNSPattern}, - }, +var matchJSONSchemaProps apiextensions.JSONSchemaProps + +func init() { + matchCRD := &apiextensionsv1.CustomResourceDefinition{} + if err := yaml.Unmarshal([]byte(matchYAML), matchCRD); err != nil { + panic(fmt.Errorf("failed to unmarshal match yaml: %w", err)) } - nullableStringList := apiextensions.JSONSchemaProps{ - Type: "array", - Items: &apiextensions.JSONSchemaPropsOrArray{ - Schema: &apiextensions.JSONSchemaProps{Type: "string", Nullable: true}, - }, + // Sanity checks to ensure the CRD was generated properly + if len(matchCRD.Spec.Versions) != 1 { + panic(fmt.Errorf("generated match CRD does not contain any versions")) + } + if matchCRD.Spec.Versions[0].Schema.OpenAPIV3Schema == nil { + panic(fmt.Errorf("generated match CRD has nil OpenAPIV3Schema")) } - trueBool := true - labelSelectorSchema := apiextensions.JSONSchemaProps{ - Type: "object", - Properties: map[string]apiextensions.JSONSchemaProps{ - "matchLabels": { - Type: "object", - Description: "A mapping of label keys to sets of allowed label values for those keys. A selected resource will match all of these expressions.", - AdditionalProperties: &apiextensions.JSONSchemaPropsOrBool{ - Allows: true, - Schema: &apiextensions.JSONSchemaProps{Type: "string"}, - }, - XPreserveUnknownFields: &trueBool, - }, - "matchExpressions": { - Type: "array", - Description: "a list of label selection expressions. A selected resource will match all of these expressions.", - Items: &apiextensions.JSONSchemaPropsOrArray{ - Schema: &apiextensions.JSONSchemaProps{ - Type: "object", - Description: "a selector that specifies a label key, a set of label values, an operator that defines the relationship between the two that will match the selector.", - Properties: map[string]apiextensions.JSONSchemaProps{ - "key": { - Description: "the label key that the selector applies to.", - Type: "string", - }, - "operator": { - Type: "string", - Description: "the relationship between the label and value set that defines a matching selection.", - Enum: []apiextensions.JSON{ - "In", - "NotIn", - "Exists", - "DoesNotExist", - }, - }, - "values": { - Type: "array", - Description: "a set of label values.", - Items: &apiextensions.JSONSchemaPropsOrArray{ - Schema: &apiextensions.JSONSchemaProps{Type: "string"}, - }, - }, - }, - }, - }, - }, - }, + // Convert v1 JSONSchemaProps to versionless + rt := runtime.NewScheme() + if err := apiextensions.AddToScheme(rt); err != nil { + panic(fmt.Errorf("could not add apiextensions to scheme: %w", err)) + } + if err := apiextensionsv1.AddToScheme(rt); err != nil { + panic(fmt.Errorf("could not add apiextensionsv1 to scheme: %w", err)) + } + embedded := matchCRD.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["embeddedMatch"] + if err := rt.Convert(&embedded, &matchJSONSchemaProps, nil); err != nil { + panic(fmt.Errorf("could not convert match JSONSchemaProps from v1 to versionless: %w", err)) } - // Make sure to copy description changes into pkg/mutation/match/match.go's `Match` struct. - return apiextensions.JSONSchemaProps{ - Type: "object", - Properties: map[string]apiextensions.JSONSchemaProps{ - "kinds": { - Type: "array", - Items: &apiextensions.JSONSchemaPropsOrArray{ - Schema: &apiextensions.JSONSchemaProps{ - Type: "object", - Description: "The Group and Kind of objects that should be matched. If multiple groups/kinds combinations are specified, an incoming resource need only match one to be in scope.", - Properties: map[string]apiextensions.JSONSchemaProps{ - "apiGroups": nullableStringList, - "kinds": nullableStringList, - }, - }, - }, - }, - "namespaces": *propsWithDescription(&wildcardNSList, "`namespaces` is a list of namespace names. If defined, a constraint only applies to resources in a listed namespace. Namespaces also supports a prefix-based glob. For example, `namespaces: [kube-*]` matches both `kube-system` and `kube-public`."), - "excludedNamespaces": *propsWithDescription(&wildcardNSList, "`excludedNamespaces` is a list of namespace names. If defined, a constraint only applies to resources not in a listed namespace. ExcludedNamespaces also supports a prefix-based glob. For example, `excludedNamespaces: [kube-*]` matches both `kube-system` and `kube-public`."), - "labelSelector": *propsWithDescription(&labelSelectorSchema, "`labelSelector` is the combination of two optional fields: `matchLabels` and `matchExpressions`. These two fields provide different methods of selecting or excluding k8s objects based on the label keys and values included in object metadata. All selection expressions from both sections are ANDed to determine if an object meets the cumulative requirements of the selector."), - "namespaceSelector": *propsWithDescription(&labelSelectorSchema, "`namespaceSelector` is a label selector against an object's containing namespace or the object itself, if the object is a namespace."), - "scope": { - Type: "string", - Description: "`scope` determines if cluster-scoped and/or namespaced-scoped resources are matched. Accepts `*`, `Cluster`, or `Namespaced`. (defaults to `*`)", - Enum: []apiextensions.JSON{ - "*", - "Cluster", - "Namespaced", - }, - }, - "name": { - Type: "string", - Description: "`name` is the name of an object. If defined, it matches against objects with the specified name. Name also supports a prefix-based glob. For example, `name: pod-*` matches both `pod-a` and `pod-b`.", - Pattern: wildcardNSPattern, - }, - }, + // Verify conversion worked by checking properties field + if _, exists := matchJSONSchemaProps.Properties["kinds"]; !exists { + panic("converted match schema does not have 'kinds' field") } } + +func matchSchema() apiextensions.JSONSchemaProps { + return *matchJSONSchemaProps.DeepCopy() +} diff --git a/pkg/target/matchcrd_constant.go b/pkg/target/matchcrd_constant.go new file mode 100644 index 00000000000..584e32c82b3 --- /dev/null +++ b/pkg/target/matchcrd_constant.go @@ -0,0 +1,216 @@ +package target + +// DO NOT MODIFY THIS FILE DIRECTLY! +// This file is generated from ./pkg/mutation/match/match_types.go via "make manifests". + +const matchYAML = ` +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.10.0 + creationTimestamp: null + name: matchcrd.match.gatekeeper.sh +spec: + group: match.gatekeeper.sh + names: + kind: DummyCRD + listKind: DummyCRDList + plural: matchcrd + singular: dummycrd + scope: Namespaced + versions: + - name: match + schema: + openAPIV3Schema: + description: DummyCRD is a "dummy" CRD to hold the Match object, which we + ultimately need to generate JSONSchemaProps. The TypeMeta and ObjectMeta + fields are required for controller-gen to generate the CRD. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + embeddedMatch: + description: Match selects which objects are in scope. + properties: + excludedNamespaces: + description: 'ExcludedNamespaces is a list of namespace names. If + defined, a constraint only applies to resources not in a listed + namespace. ExcludedNamespaces also supports a prefix or suffix based + glob. For example, `+"`"+`excludedNamespaces: [kube-*]`+"`"+` matches both + `+"`"+`kube-system`+"`"+` and `+"`"+`kube-public`+"`"+`, and `+"`"+`excludedNamespaces: [*-system]`+"`"+` + matches both `+"`"+`kube-system`+"`"+` and `+"`"+`gatekeeper-system`+"`"+`.' + items: + description: 'A string that supports globbing at its front or end. + Ex: "kube-*" will match "kube-system" or "kube-public", "*-system" + will match "kube-system" or "gatekeeper-system". The asterisk + is required for wildcard matching.' + pattern: ^(\*|\*-)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ + type: string + type: array + kinds: + items: + description: Kinds accepts a list of objects with apiGroups and + kinds fields that list the groups/kinds of objects to which the + mutation will apply. If multiple groups/kinds objects are specified, + only one match is needed for the resource to be in scope. + properties: + apiGroups: + description: APIGroups is the API groups the resources belong + to. '*' is all groups. If '*' is present, the length of the + slice must be one. Required. + items: + type: string + type: array + kinds: + items: + type: string + type: array + type: object + type: array + labelSelector: + description: 'LabelSelector is the combination of two optional fields: + `+"`"+`matchLabels`+"`"+` and `+"`"+`matchExpressions`+"`"+`. These two fields provide + different methods of selecting or excluding k8s objects based on + the label keys and values included in object metadata. All selection + expressions from both sections are ANDed to determine if an object + meets the cumulative requirements of the selector.' + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector requirement is a selector that + contains values, a key, and an operator that relates the key + and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: operator represents a key's relationship to + a set of values. Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of string values. If the + operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values + array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single + {key,value} in the matchLabels map is equivalent to an element + of matchExpressions, whose key field is "key", the operator + is "In", and the values array contains only "value". The requirements + are ANDed. + type: object + type: object + name: + description: 'Name is the name of an object. If defined, it will + match against objects with the specified name. Name also supports + a prefix or suffix glob. For example, `+"`"+`name: pod-*`+"`"+` would match + both `+"`"+`pod-a`+"`"+` and `+"`"+`pod-b`+"`"+`, and `+"`"+`name: *-pod`+"`"+` would match both `+"`"+`a-pod`+"`"+` + and `+"`"+`b-pod`+"`"+`.' + pattern: ^(\*|\*-)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ + type: string + namespaceSelector: + description: NamespaceSelector is a label selector against an object's + containing namespace or the object itself, if the object is a namespace. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: A label selector requirement is a selector that + contains values, a key, and an operator that relates the key + and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: operator represents a key's relationship to + a set of values. Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of string values. If the + operator is In or NotIn, the values array must be non-empty. + If the operator is Exists or DoesNotExist, the values + array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single + {key,value} in the matchLabels map is equivalent to an element + of matchExpressions, whose key field is "key", the operator + is "In", and the values array contains only "value". The requirements + are ANDed. + type: object + type: object + namespaces: + description: 'Namespaces is a list of namespace names. If defined, + a constraint only applies to resources in a listed namespace. Namespaces + also supports a prefix or suffix based glob. For example, `+"`"+`namespaces: + [kube-*]`+"`"+` matches both `+"`"+`kube-system`+"`"+` and `+"`"+`kube-public`+"`"+`, and `+"`"+`namespaces: + [*-system]`+"`"+` matches both `+"`"+`kube-system`+"`"+` and `+"`"+`gatekeeper-system`+"`"+`.' + items: + description: 'A string that supports globbing at its front or end. + Ex: "kube-*" will match "kube-system" or "kube-public", "*-system" + will match "kube-system" or "gatekeeper-system". The asterisk + is required for wildcard matching.' + pattern: ^(\*|\*-)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\*|-\*)?$ + type: string + type: array + scope: + description: Scope determines if cluster-scoped and/or namespaced-scoped + resources are matched. Accepts `+"`"+`*`+"`"+`, `+"`"+`Cluster`+"`"+`, or `+"`"+`Namespaced`+"`"+`. + (defaults to `+"`"+`*`+"`"+`) + type: string + source: + description: Source determines whether generated or original resources + are matched. Accepts `+"`"+`Generated`+"`"+`|`+"`"+`Original`+"`"+`|`+"`"+`All`+"`"+` (defaults to `+"`"+`All`+"`"+`). + A value of `+"`"+`Generated`+"`"+` will only match generated resources, while + `+"`"+`Original`+"`"+` will only match regular resources. + enum: + - All + - Generated + - Original + type: string + type: object + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadataDummy: + type: object + type: object + served: true + storage: true +` diff --git a/pkg/target/target.go b/pkg/target/target.go index 70350e6d551..17472b77285 100644 --- a/pkg/target/target.go +++ b/pkg/target/target.go @@ -162,12 +162,6 @@ func unstructuredToAdmissionRequest(obj *unstructured.Unstructured) (*gkReview, return &gkReview{AdmissionRequest: req}, nil } -func propsWithDescription(props *apiextensions.JSONSchemaProps, description string) *apiextensions.JSONSchemaProps { - propCopy := props.DeepCopy() - propCopy.Description = description - return propCopy -} - func (h *K8sValidationTarget) MatchSchema() apiextensions.JSONSchemaProps { return matchSchema() } diff --git a/pkg/target/target_test.go b/pkg/target/target_test.go index a79a22b85d3..da016c67b2d 100644 --- a/pkg/target/target_test.go +++ b/pkg/target/target_test.go @@ -54,6 +54,7 @@ func TestValidateConstraint(t *testing.T) { }, "spec": { "match": { + "source": "All", "kinds": [ { "apiGroups": [""], @@ -80,6 +81,7 @@ func TestValidateConstraint(t *testing.T) { }, "spec": { "match": { + "source": "Original", "kinds": [ { "apiGroups": [""], @@ -202,6 +204,7 @@ func TestValidateConstraint(t *testing.T) { }, "spec": { "match": { + "source": "Generated", "kinds": [ { "apiGroups": [""], diff --git a/test/bats/test.bats b/test/bats/test.bats index 86a31b4f3d1..a6e40886ca2 100644 --- a/test/bats/test.bats +++ b/test/bats/test.bats @@ -444,6 +444,20 @@ __expansion_audit_test() { assert_success # with a violating deployment on cluster, test that audit produces expansion violations wait_for_process ${WAIT_TIME} ${SLEEP_TIME} "__expansion_audit_test" + run kubectl delete -f test/expansion/warn_expand_deployments + run kubectl delete -f test/expansion/deployment_no_label.yaml + + # test source field on Constraints + run kubectl apply -f test/expansion/expand_deployments.yaml + run kubectl delete --ignore-not-found -f test/expansion/loadbalancers_must_have_env.yaml + run kubectl apply -f test/expansion/loadbalancers_must_have_env_source_gen.yaml + wait_for_process ${WAIT_TIME} ${SLEEP_TIME} "constraint_enforced k8srequiredlabels loadbalancers-must-have-env-gen" + # a generated pod should be denied + run kubectl apply -f test/expansion/deployment_no_label.yaml + assert_failure + # an original pod should be accepted, as the constraint only matches generated pods + run kubectl run nginx --image=nginx --dry-run=server --output json + assert_success # cleanup run kubectl delete --ignore-not-found namespace loadbalancers @@ -451,6 +465,7 @@ __expansion_audit_test() { run kubectl delete --ignore-not-found -f test/expansion/warn_expand_deployments.yaml run kubectl delete --ignore-not-found -f test/expansion/k8srequiredlabels_ct.yaml run kubectl delete --ignore-not-found -f test/expansion/loadbalancers_must_have_env.yaml + run kubectl delete --ignore-not-found -f test/expansion/loadbalancers_must_have_env_source_gen.yaml run kubectl delete --ignore-not-found -f test/expansion/assignmeta_env.yaml run kubectl delete --ignore-not-found -f test/expansion/deployment_no_label.yaml run kubectl delete --ignore-not-found -f test/expansion/deployment_with_label.yaml diff --git a/test/expansion/loadbalancers_must_have_env_source_gen.yaml b/test/expansion/loadbalancers_must_have_env_source_gen.yaml new file mode 100644 index 00000000000..5d9060b8586 --- /dev/null +++ b/test/expansion/loadbalancers_must_have_env_source_gen.yaml @@ -0,0 +1,14 @@ +apiVersion: constraints.gatekeeper.sh/v1beta1 +kind: K8sRequiredLabels +metadata: + name: loadbalancers-must-have-env-gen +spec: + match: + scope: "Namespaced" + namespaces: [ "loadbalancers" ] + kinds: + - apiGroups: [ "" ] + kinds: [ "Pod" ] + source: "Generated" + parameters: + labels: [ "env" ] diff --git a/website/docs/expansion.md b/website/docs/expansion.md index ce561c9aa01..adf1d5cf75d 100644 --- a/website/docs/expansion.md +++ b/website/docs/expansion.md @@ -126,7 +126,7 @@ specified by the Constraint in violation. #### Match Source The `source` field on the `match` API, present in the Mutation -and `ConstraintTemplate` kinds, specifies if the config should match Generated ( +and `Constraint` kinds, specifies if the config should match Generated ( i.e. expanded) resources, Original resources, or both. The `source` field is an `enum` which accepts the following values: