diff --git a/Makefile b/Makefile index 9bce29288f23..47cdb0b3b30e 100644 --- a/Makefile +++ b/Makefile @@ -203,6 +203,8 @@ generate-go: ## Runs Go related generate targets $(MAKE) generate-go-core $(MAKE) generate-go-kubeadm-bootstrap $(MAKE) generate-go-kubeadm-control-plane + $(MAKE) generate-go-exp-postapply + .PHONY: generate-go-core generate-go-core: $(CONTROLLER_GEN) $(CONVERSION_GEN) @@ -231,6 +233,12 @@ generate-go-kubeadm-control-plane: $(CONTROLLER_GEN) $(CONVERSION_GEN) ## Runs G object:headerFile=./hack/boilerplate/boilerplate.generatego.txt \ paths=./controlplane/kubeadm/api/... +.PHONY: generate-go-exp-postapply +generate-go-exp-postapply: $(CONTROLLER_GEN) $(CONVERSION_GEN) ## Runs Go related generate targets for the experimental postapply + $(CONTROLLER_GEN) \ + object:headerFile=./hack/boilerplate/boilerplate.generatego.txt \ + paths=./exp/postapply/api/... + .PHONY: generate-bindata generate-bindata: $(KUSTOMIZE) $(GOBINDATA) clean-bindata ## Generate code for embedding the clusterctl api manifest # Package manifest YAML into a single file. @@ -250,6 +258,7 @@ generate-manifests: ## Generate manifests e.g. CRD, RBAC etc. $(MAKE) generate-core-manifests $(MAKE) generate-kubeadm-bootstrap-manifests $(MAKE) generate-kubeadm-control-plane-manifests + $(MAKE) generate-exp-postapply-manifests .PHONY: generate-core-manifests generate-core-manifests: $(CONTROLLER_GEN) ## Generate manifests for the core provider e.g. CRD, RBAC etc. @@ -293,6 +302,15 @@ generate-kubeadm-control-plane-manifests: $(CONTROLLER_GEN) ## Generate manifest output:webhook:dir=./controlplane/kubeadm/config/webhook \ webhook +.PHONY: generate-exp-postapply-manifests +generate-exp-postapply-manifests: $(CONTROLLER_GEN) ## Generate manifests for the postapply controller e.g. CRD, RBAC etc. + $(CONTROLLER_GEN) \ + paths=./exp/postapply/api/... \ + paths=./exp/postapply/controllers/... \ + crd:trivialVersions=false,preserveUnknownFields=false \ + rbac:roleName=manager-role \ + output:crd:dir=./exp/postapply/config/crd/bases + .PHONY: modules modules: ## Runs go mod to ensure modules are up to date. go mod tidy @@ -431,6 +449,8 @@ release-manifests: $(RELEASE_DIR) $(KUSTOMIZE) ## Builds the manifests to publis $(KUSTOMIZE) build bootstrap/kubeadm/config > $(RELEASE_DIR)/bootstrap-components.yaml # Build control-plane-components. $(KUSTOMIZE) build controlplane/kubeadm/config > $(RELEASE_DIR)/control-plane-components.yaml + # Build experimental postapply related components. + $(KUSTOMIZE) build exp/postapply/config/default > $(RELEASE_DIR)/postapply-components.yaml ## Build cluster-api-components (aggregate of all of the above). cat $(RELEASE_DIR)/core-components.yaml > $(RELEASE_DIR)/cluster-api-components.yaml @@ -438,6 +458,9 @@ release-manifests: $(RELEASE_DIR) $(KUSTOMIZE) ## Builds the manifests to publis cat $(RELEASE_DIR)/bootstrap-components.yaml >> $(RELEASE_DIR)/cluster-api-components.yaml echo "---" >> $(RELEASE_DIR)/cluster-api-components.yaml cat $(RELEASE_DIR)/control-plane-components.yaml >> $(RELEASE_DIR)/cluster-api-components.yaml + echo "---" >> $(RELEASE_DIR)/cluster-api-components.yaml + cat $(RELEASE_DIR)/postapply-components.yaml >> $(RELEASE_DIR)/cluster-api-components.yaml + release-binaries: ## Builds the binaries to publish with a release RELEASE_BINARY=./cmd/clusterctl GOOS=linux GOARCH=amd64 $(MAKE) release-binary diff --git a/exp/postapply/api/v1alpha3/groupversion_info.go b/exp/postapply/api/v1alpha3/groupversion_info.go new file mode 100644 index 000000000000..5fbb0500fe7c --- /dev/null +++ b/exp/postapply/api/v1alpha3/groupversion_info.go @@ -0,0 +1,35 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1alpha3 contains API Schema definitions for the postapply v1alpha3 API group +// +kubebuilder:object:generate=true +// +groupName=postapply.cluster.x-k8s.io +package v1alpha3 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "postapply.cluster.x-k8s.io", Version: "v1alpha3"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/exp/postapply/api/v1alpha3/postapplyconfig_types.go b/exp/postapply/api/v1alpha3/postapplyconfig_types.go new file mode 100644 index 000000000000..d449aea4f597 --- /dev/null +++ b/exp/postapply/api/v1alpha3/postapplyconfig_types.go @@ -0,0 +1,76 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha3 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// PostApplyConfigSpec defines the desired state of PostApplyConfig +type PostApplyConfigSpec struct { + ClusterSelector metav1.LabelSelector `json:"clusterSelector"` + // PostApplyAddons is a list of Secrets in YAML format to be applied to remote clusters. + PostApplyAddons []*PostApplyAddon `json:"postApplyAddons,omitempty"` +} + +// ANCHOR: PostApplyAddon + +// PostApplyAddon specifies the addon's Secret parameters. +type PostApplyAddon struct { + Name string `json:"name,omitempty"` + // Namespace is the namespace of the secret. + Namespace string `json:"namespace,omitempty"` +} + +// ANCHOR_END: PostApplyAddon + +// PostApplyConfigStatus defines the observed state of PostApplyConfig +type PostApplyConfigStatus struct { + // ClusterRefList will point to the clusters that the postApplyConfig yamls successfully applied. + // +optional + ClusterRefList []*corev1.ObjectReference `json:"clusterRefList,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=postapplyconfigs,scope=Namespaced,categories=cluster-api +// +kubebuilder:subresource:status +// +kubebuilder:storageversion + +// PostApplyConfig is the Schema for the postapplyconfigs API +type PostApplyConfig struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec PostApplyConfigSpec `json:"spec,omitempty"` + Status PostApplyConfigStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// PostApplyConfigList contains a list of PostApplyConfig +type PostApplyConfigList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []PostApplyConfig `json:"items"` +} + +func init() { + SchemeBuilder.Register(&PostApplyConfig{}, &PostApplyConfigList{}) +} diff --git a/exp/postapply/api/v1alpha3/zz_generated.deepcopy.go b/exp/postapply/api/v1alpha3/zz_generated.deepcopy.go new file mode 100644 index 000000000000..7526ca578152 --- /dev/null +++ b/exp/postapply/api/v1alpha3/zz_generated.deepcopy.go @@ -0,0 +1,153 @@ +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha3 + +import ( + "k8s.io/api/core/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PostApplyAddon) DeepCopyInto(out *PostApplyAddon) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostApplyAddon. +func (in *PostApplyAddon) DeepCopy() *PostApplyAddon { + if in == nil { + return nil + } + out := new(PostApplyAddon) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PostApplyConfig) DeepCopyInto(out *PostApplyConfig) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostApplyConfig. +func (in *PostApplyConfig) DeepCopy() *PostApplyConfig { + if in == nil { + return nil + } + out := new(PostApplyConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PostApplyConfig) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PostApplyConfigList) DeepCopyInto(out *PostApplyConfigList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]PostApplyConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostApplyConfigList. +func (in *PostApplyConfigList) DeepCopy() *PostApplyConfigList { + if in == nil { + return nil + } + out := new(PostApplyConfigList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PostApplyConfigList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PostApplyConfigSpec) DeepCopyInto(out *PostApplyConfigSpec) { + *out = *in + in.ClusterSelector.DeepCopyInto(&out.ClusterSelector) + if in.PostApplyAddons != nil { + in, out := &in.PostApplyAddons, &out.PostApplyAddons + *out = make([]*PostApplyAddon, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(PostApplyAddon) + **out = **in + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostApplyConfigSpec. +func (in *PostApplyConfigSpec) DeepCopy() *PostApplyConfigSpec { + if in == nil { + return nil + } + out := new(PostApplyConfigSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PostApplyConfigStatus) DeepCopyInto(out *PostApplyConfigStatus) { + *out = *in + if in.ClusterRefList != nil { + in, out := &in.ClusterRefList, &out.ClusterRefList + *out = make([]*v1.ObjectReference, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(v1.ObjectReference) + **out = **in + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostApplyConfigStatus. +func (in *PostApplyConfigStatus) DeepCopy() *PostApplyConfigStatus { + if in == nil { + return nil + } + out := new(PostApplyConfigStatus) + in.DeepCopyInto(out) + return out +} diff --git a/exp/postapply/config/certmanager/certificate.yaml b/exp/postapply/config/certmanager/certificate.yaml new file mode 100644 index 000000000000..237c9378d911 --- /dev/null +++ b/exp/postapply/config/certmanager/certificate.yaml @@ -0,0 +1,25 @@ +# The following manifests contain a self-signed issuer CR and a certificate CR. +# More document can be found at https://docs.cert-manager.io +# WARNING: Targets CertManager 0.11 check https://docs.cert-manager.io/en/latest/tasks/upgrading/index.html for breaking changes +apiVersion: cert-manager.io/v1alpha2 +kind: Issuer +metadata: + name: selfsigned-issuer + namespace: system +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1alpha2 +kind: Certificate +metadata: + name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml + namespace: system +spec: + # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize + dnsNames: + - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc + - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local + issuerRef: + kind: Issuer + name: selfsigned-issuer + secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize diff --git a/exp/postapply/config/certmanager/kustomization.yaml b/exp/postapply/config/certmanager/kustomization.yaml new file mode 100644 index 000000000000..bebea5a595ee --- /dev/null +++ b/exp/postapply/config/certmanager/kustomization.yaml @@ -0,0 +1,5 @@ +resources: +- certificate.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/exp/postapply/config/certmanager/kustomizeconfig.yaml b/exp/postapply/config/certmanager/kustomizeconfig.yaml new file mode 100644 index 000000000000..90d7c313ca10 --- /dev/null +++ b/exp/postapply/config/certmanager/kustomizeconfig.yaml @@ -0,0 +1,16 @@ +# This configuration is for teaching kustomize how to update name ref and var substitution +nameReference: +- kind: Issuer + group: cert-manager.io + fieldSpecs: + - kind: Certificate + group: cert-manager.io + path: spec/issuerRef/name + +varReference: +- kind: Certificate + group: cert-manager.io + path: spec/commonName +- kind: Certificate + group: cert-manager.io + path: spec/dnsNames diff --git a/exp/postapply/config/crd/bases/postapply.cluster.x-k8s.io_postapplyconfigs.yaml b/exp/postapply/config/crd/bases/postapply.cluster.x-k8s.io_postapplyconfigs.yaml new file mode 100644 index 000000000000..01646708ab6f --- /dev/null +++ b/exp/postapply/config/crd/bases/postapply.cluster.x-k8s.io_postapplyconfigs.yaml @@ -0,0 +1,160 @@ + +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.2.5 + creationTimestamp: null + name: postapplyconfigs.postapply.cluster.x-k8s.io +spec: + group: postapply.cluster.x-k8s.io + names: + categories: + - cluster-api + kind: PostApplyConfig + listKind: PostApplyConfigList + plural: postapplyconfigs + singular: postapplyconfig + preserveUnknownFields: false + scope: Namespaced + subresources: + status: {} + validation: + openAPIV3Schema: + description: PostApplyConfig is the Schema for the postapplyconfigs API + 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 + 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 + metadata: + type: object + spec: + description: PostApplyConfigSpec defines the desired state of PostApplyConfig + properties: + clusterSelector: + description: A label selector is a label query over a set of resources. + The result of matchLabels and matchExpressions are ANDed. An empty + label selector matches all objects. A null label selector matches + no objects. + 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 + postApplyAddons: + description: PostApplyAddons is a list of Secrets in YAML format to + be applied to remote clusters. + items: + description: PostApplyAddon specifies the addon's Secret parameters. + properties: + name: + type: string + namespace: + description: Namespace is the namespace of the secret. + type: string + type: object + type: array + required: + - clusterSelector + type: object + status: + description: PostApplyConfigStatus defines the observed state of PostApplyConfig + properties: + clusterRefList: + description: ClusterRefList will point to the clusters that the postApplyConfig + yamls successfully applied. + items: + description: ObjectReference contains enough information to let you + inspect or modify the referred object. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of + an entire object, this string should contain a valid JSON/Go + field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part of + an object. TODO: this design is not final and this field is + subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + type: array + type: object + type: object + version: v1alpha3 + versions: + - name: v1alpha3 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/exp/postapply/config/crd/kustomization.yaml b/exp/postapply/config/crd/kustomization.yaml new file mode 100644 index 000000000000..c0f9fe3a3108 --- /dev/null +++ b/exp/postapply/config/crd/kustomization.yaml @@ -0,0 +1,21 @@ +# This kustomization.yaml is not intended to be run by itself, +# since it depends on service name and namespace that are out_calisan of this kustomize package. +# It should be run by config/default +resources: +- bases/postapply.cluster.x-k8s.io_postapplyconfigs.yaml +# +kubebuilder:scaffold:crdkustomizeresource + +patchesStrategicMerge: +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. +# patches here are for enabling the conversion webhook for each CRD +#- patches/webhook_in_postapplyconfigs.yaml +# +kubebuilder:scaffold:crdkustomizewebhookpatch + +# [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. +# patches here are for enabling the CA injection for each CRD +#- patches/cainjection_in_postapplyconfigs.yaml +# +kubebuilder:scaffold:crdkustomizecainjectionpatch + +# the following config is for teaching kustomize how to do kustomization for CRDs. +configurations: +- kustomizeconfig.yaml diff --git a/exp/postapply/config/crd/kustomizeconfig.yaml b/exp/postapply/config/crd/kustomizeconfig.yaml new file mode 100644 index 000000000000..6f83d9a94bc5 --- /dev/null +++ b/exp/postapply/config/crd/kustomizeconfig.yaml @@ -0,0 +1,17 @@ +# This file is for teaching kustomize how to substitute name and namespace reference in CRD +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: CustomResourceDefinition + group: apiextensions.k8s.io + path: spec/conversion/webhookClientConfig/service/name + +namespace: +- kind: CustomResourceDefinition + group: apiextensions.k8s.io + path: spec/conversion/webhookClientConfig/service/namespace + create: false + +varReference: +- path: metadata/annotations diff --git a/exp/postapply/config/crd/patches/cainjection_in_postapplyconfigs.yaml b/exp/postapply/config/crd/patches/cainjection_in_postapplyconfigs.yaml new file mode 100644 index 000000000000..56816a174b7f --- /dev/null +++ b/exp/postapply/config/crd/patches/cainjection_in_postapplyconfigs.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: postapplyconfigs.postapply.cluster.x-k8s.io diff --git a/exp/postapply/config/crd/patches/webhook_in_postapplyconfigs.yaml b/exp/postapply/config/crd/patches/webhook_in_postapplyconfigs.yaml new file mode 100644 index 000000000000..eb5141e9bc78 --- /dev/null +++ b/exp/postapply/config/crd/patches/webhook_in_postapplyconfigs.yaml @@ -0,0 +1,17 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: postapplyconfigs.postapply.cluster.x-k8s.io +spec: + conversion: + strategy: Webhook + webhookClientConfig: + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== + service: + namespace: system + name: webhook-service + path: /convert diff --git a/exp/postapply/config/default/kustomization.yaml b/exp/postapply/config/default/kustomization.yaml new file mode 100644 index 000000000000..ffcfba54dc0c --- /dev/null +++ b/exp/postapply/config/default/kustomization.yaml @@ -0,0 +1,74 @@ +# Adds namespace to all resources. +namespace: postapply-system + +# Value of this field is prepended to the +# names of all resources, e.g. a deployment named +# "wordpress" becomes "alices-wordpress". +# Note that it should also match with the prefix (text before '-') of the namespace +# field above. +namePrefix: postapply- + +# Labels to add to all resources and selectors. +#commonLabels: +# someName: someValue + +bases: +- ../crd +#- ../rbac +#- ../manager +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in crd/kustomization.yaml +#- ../webhook +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. +#- ../certmanager +# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. +#- ../prometheus + +patchesStrategicMerge: + # Protect the /metrics endpoint by putting it behind auth. + # Only one of manager_auth_proxy_patch.yaml and + # manager_prometheus_metrics_patch.yaml should be enabled. +#- manager_auth_proxy_patch.yaml + # If you want your controller-manager to expose the /metrics + # endpoint w/o any authn/z, uncomment the following line and + # comment manager_auth_proxy_patch.yaml. + # Only one of manager_auth_proxy_patch.yaml and + # manager_prometheus_metrics_patch.yaml should be enabled. +#- manager_prometheus_metrics_patch.yaml + +# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in crd/kustomization.yaml +#- manager_webhook_patch.yaml + +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. +# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. +# 'CERTMANAGER' needs to be enabled to use ca injection +#- webhookcainjection_patch.yaml + +# the following config is for teaching kustomize how to do var substitution +vars: +# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. +#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR +# objref: +# kind: Certificate +# group: cert-manager.io +# version: v1alpha2 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldref: +# fieldpath: metadata.namespace +#- name: CERTIFICATE_NAME +# objref: +# kind: Certificate +# group: cert-manager.io +# version: v1alpha2 +# name: serving-cert # this name should match the one in certificate.yaml +#- name: SERVICE_NAMESPACE # namespace of the service +# objref: +# kind: Service +# version: v1 +# name: webhook-service +# fieldref: +# fieldpath: metadata.namespace +#- name: SERVICE_NAME +# objref: +# kind: Service +# version: v1 +# name: webhook-service diff --git a/exp/postapply/config/default/manager_auth_proxy_patch.yaml b/exp/postapply/config/default/manager_auth_proxy_patch.yaml new file mode 100644 index 000000000000..61cb5e7cb6d9 --- /dev/null +++ b/exp/postapply/config/default/manager_auth_proxy_patch.yaml @@ -0,0 +1,25 @@ +# This patch inject a sidecar container which is a HTTP proxy for the controller manager, +# it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: kube-rbac-proxy + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.4.1 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=10" + ports: + - containerPort: 8443 + name: https + - name: manager + args: + - "--metrics-addr=127.0.0.1:8080" + - "--enable-leader-election" diff --git a/exp/postapply/config/default/manager_webhook_patch.yaml b/exp/postapply/config/default/manager_webhook_patch.yaml new file mode 100644 index 000000000000..738de350b71e --- /dev/null +++ b/exp/postapply/config/default/manager_webhook_patch.yaml @@ -0,0 +1,23 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert diff --git a/exp/postapply/config/default/webhookcainjection_patch.yaml b/exp/postapply/config/default/webhookcainjection_patch.yaml new file mode 100644 index 000000000000..7e79bf9955a2 --- /dev/null +++ b/exp/postapply/config/default/webhookcainjection_patch.yaml @@ -0,0 +1,15 @@ +# This patch add annotation to admission webhook config and +# the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: MutatingWebhookConfiguration +metadata: + name: mutating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) +--- +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingWebhookConfiguration +metadata: + name: validating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) diff --git a/exp/postapply/config/prometheus/kustomization.yaml b/exp/postapply/config/prometheus/kustomization.yaml new file mode 100644 index 000000000000..ed137168a1db --- /dev/null +++ b/exp/postapply/config/prometheus/kustomization.yaml @@ -0,0 +1,2 @@ +resources: +- monitor.yaml diff --git a/exp/postapply/config/prometheus/monitor.yaml b/exp/postapply/config/prometheus/monitor.yaml new file mode 100644 index 000000000000..e2d9b087fa59 --- /dev/null +++ b/exp/postapply/config/prometheus/monitor.yaml @@ -0,0 +1,15 @@ + +# Prometheus Monitor Service (Metrics) +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + control-plane: controller-manager + name: controller-manager-metrics-monitor + namespace: system +spec: + endpoints: + - path: /metrics + port: https + selector: + control-plane: controller-manager diff --git a/exp/postapply/config/rbac/auth_proxy_role.yaml b/exp/postapply/config/rbac/auth_proxy_role.yaml new file mode 100644 index 000000000000..618f5e4177cb --- /dev/null +++ b/exp/postapply/config/rbac/auth_proxy_role.yaml @@ -0,0 +1,13 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: proxy-role +rules: +- apiGroups: ["authentication.k8s.io"] + resources: + - tokenreviews + verbs: ["create"] +- apiGroups: ["authorization.k8s.io"] + resources: + - subjectaccessreviews + verbs: ["create"] diff --git a/exp/postapply/config/rbac/auth_proxy_role_binding.yaml b/exp/postapply/config/rbac/auth_proxy_role_binding.yaml new file mode 100644 index 000000000000..48ed1e4b85c4 --- /dev/null +++ b/exp/postapply/config/rbac/auth_proxy_role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: proxy-role +subjects: +- kind: ServiceAccount + name: default + namespace: system diff --git a/exp/postapply/config/rbac/auth_proxy_service.yaml b/exp/postapply/config/rbac/auth_proxy_service.yaml new file mode 100644 index 000000000000..6cf656be1491 --- /dev/null +++ b/exp/postapply/config/rbac/auth_proxy_service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + name: controller-manager-metrics-service + namespace: system +spec: + ports: + - name: https + port: 8443 + targetPort: https + selector: + control-plane: controller-manager diff --git a/exp/postapply/config/rbac/kustomization.yaml b/exp/postapply/config/rbac/kustomization.yaml new file mode 100644 index 000000000000..817f1fe61380 --- /dev/null +++ b/exp/postapply/config/rbac/kustomization.yaml @@ -0,0 +1,11 @@ +resources: +- role.yaml +- role_binding.yaml +- leader_election_role.yaml +- leader_election_role_binding.yaml +# Comment the following 3 lines if you want to disable +# the auth proxy (https://github.com/brancz/kube-rbac-proxy) +# which protects your /metrics endpoint. +- auth_proxy_service.yaml +- auth_proxy_role.yaml +- auth_proxy_role_binding.yaml diff --git a/exp/postapply/config/rbac/leader_election_role.yaml b/exp/postapply/config/rbac/leader_election_role.yaml new file mode 100644 index 000000000000..eaa79158fb12 --- /dev/null +++ b/exp/postapply/config/rbac/leader_election_role.yaml @@ -0,0 +1,32 @@ +# permissions to do leader election. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: leader-election-role +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - update + - patch +- apiGroups: + - "" + resources: + - events + verbs: + - create diff --git a/exp/postapply/config/rbac/leader_election_role_binding.yaml b/exp/postapply/config/rbac/leader_election_role_binding.yaml new file mode 100644 index 000000000000..eed16906f4dc --- /dev/null +++ b/exp/postapply/config/rbac/leader_election_role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: leader-election-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: leader-election-role +subjects: +- kind: ServiceAccount + name: default + namespace: system diff --git a/exp/postapply/config/rbac/postapplyconfig_editor_role.yaml b/exp/postapply/config/rbac/postapplyconfig_editor_role.yaml new file mode 100644 index 000000000000..20503383c090 --- /dev/null +++ b/exp/postapply/config/rbac/postapplyconfig_editor_role.yaml @@ -0,0 +1,26 @@ +# permissions to do edit postapplyconfigs. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: postapplyconfig-editor-role +rules: +- apiGroups: + - postapply.cluster.x-k8s.io + resources: + - postapplyconfigs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - postapply.cluster.x-k8s.io + resources: + - postapplyconfigs/status + verbs: + - get + - patch + - update diff --git a/exp/postapply/config/rbac/postapplyconfig_viewer_role.yaml b/exp/postapply/config/rbac/postapplyconfig_viewer_role.yaml new file mode 100644 index 000000000000..1d8da731a18d --- /dev/null +++ b/exp/postapply/config/rbac/postapplyconfig_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions to do viewer postapplyconfigs. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: postapplyconfig-viewer-role +rules: +- apiGroups: + - postapply.cluster.x-k8s.io + resources: + - postapplyconfigs + verbs: + - get + - list + - watch +- apiGroups: + - postapply.cluster.x-k8s.io + resources: + - postapplyconfigs/status + verbs: + - get diff --git a/exp/postapply/config/rbac/role.yaml b/exp/postapply/config/rbac/role.yaml new file mode 100644 index 000000000000..525ff7b5d1da --- /dev/null +++ b/exp/postapply/config/rbac/role.yaml @@ -0,0 +1,28 @@ + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: manager-role +rules: +- apiGroups: + - postapply.cluster.x-k8s.io + resources: + - postapplyconfigs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - postapply.cluster.x-k8s.io + resources: + - postapplyconfigs/status + verbs: + - get + - patch + - update diff --git a/exp/postapply/config/rbac/role_binding.yaml b/exp/postapply/config/rbac/role_binding.yaml new file mode 100644 index 000000000000..8f2658702c89 --- /dev/null +++ b/exp/postapply/config/rbac/role_binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: manager-role +subjects: +- kind: ServiceAccount + name: default + namespace: system diff --git a/exp/postapply/config/samples/postapply_v1alpha3_postapplyconfig.yaml b/exp/postapply/config/samples/postapply_v1alpha3_postapplyconfig.yaml new file mode 100644 index 000000000000..702a5687d0e0 --- /dev/null +++ b/exp/postapply/config/samples/postapply_v1alpha3_postapplyconfig.yaml @@ -0,0 +1,7 @@ +apiVersion: postapply.cluster.x-k8s.io/v1alpha3 +kind: PostApplyConfig +metadata: + name: postapplyconfig-sample +spec: + # Add fields here + foo: bar diff --git a/exp/postapply/config/webhook/kustomization.yaml b/exp/postapply/config/webhook/kustomization.yaml new file mode 100644 index 000000000000..9cf26134e4d5 --- /dev/null +++ b/exp/postapply/config/webhook/kustomization.yaml @@ -0,0 +1,6 @@ +resources: +- manifests.yaml +- service.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/exp/postapply/config/webhook/kustomizeconfig.yaml b/exp/postapply/config/webhook/kustomizeconfig.yaml new file mode 100644 index 000000000000..25e21e3c963f --- /dev/null +++ b/exp/postapply/config/webhook/kustomizeconfig.yaml @@ -0,0 +1,25 @@ +# the following config is for teaching kustomize where to look at when substituting vars. +# It requires kustomize v2.1.0 or newer to work properly. +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + - kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + +namespace: +- kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true +- kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true + +varReference: +- path: metadata/annotations diff --git a/exp/postapply/config/webhook/manifests.yaml b/exp/postapply/config/webhook/manifests.yaml new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/exp/postapply/config/webhook/service.yaml b/exp/postapply/config/webhook/service.yaml new file mode 100644 index 000000000000..31e0f8295919 --- /dev/null +++ b/exp/postapply/config/webhook/service.yaml @@ -0,0 +1,12 @@ + +apiVersion: v1 +kind: Service +metadata: + name: webhook-service + namespace: system +spec: + ports: + - port: 443 + targetPort: 9443 + selector: + control-plane: controller-manager diff --git a/exp/postapply/controllers/postapplyconfig_controller.go b/exp/postapply/controllers/postapplyconfig_controller.go new file mode 100644 index 000000000000..8972f7747af6 --- /dev/null +++ b/exp/postapply/controllers/postapplyconfig_controller.go @@ -0,0 +1,262 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + kerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/client-go/tools/record" + clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" + "sigs.k8s.io/cluster-api/controllers/remote" + postapplyv1 "sigs.k8s.io/cluster-api/exp/postapply/api/v1alpha3" + "sigs.k8s.io/cluster-api/util/patch" + "sigs.k8s.io/cluster-api/util/secret" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +// PostApplyDataKey is the data key in post-apply-addon secrets +var PostApplyDataKey = "addon.yaml" + +// PostApplyConfigReconciler reconciles a PostApplyConfig object +type PostApplyConfigReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + recorder record.EventRecorder +} + +// +kubebuilder:rbac:groups=postapply.cluster.x-k8s.io,resources=postapplyconfigs,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=postapply.cluster.x-k8s.io,resources=postapplyconfigs/status,verbs=get;update;patch + +func (r *PostApplyConfigReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, reterr error) { + ctx := context.Background() + + // Fetch the PostApplyConfig instance. + postApplyConf := &postapplyv1.PostApplyConfig{} + if err := r.Client.Get(ctx, req.NamespacedName, postApplyConf); err != nil { + if apierrors.IsNotFound(err) { + // Object not found, return. Created objects are automatically garbage collected. + // For additional cleanup logic use finalizers. + return ctrl.Result{}, nil + } + // Error reading the object - requeue the request. + return ctrl.Result{}, err + } + + // Initialize the patch helper. + patchHelper, err := patch.NewHelper(postApplyConf, r.Client) + if err != nil { + return ctrl.Result{}, err + } + + defer func() { + // Always attempt to Patch the PostApplyConfig object and status after each reconciliation. + if err := patchHelper.Patch(ctx, postApplyConf); err != nil { + reterr = kerrors.NewAggregate([]error{reterr, err}) + } + }() + + // Handle normal reconciliation loop. + return r.reconcile(ctx, postApplyConf) +} + +// reconcile handles cluster reconciliation. +func (r *PostApplyConfigReconciler) reconcile(ctx context.Context, postApplyConf *postapplyv1.PostApplyConfig) (ctrl.Result, error) { + logger := r.Log.WithValues("postapplyconfig", postApplyConf.Name, "namespace", postApplyConf.Namespace) + + cls, err := r.getMatchingClustersForPostApply(ctx, postApplyConf.Spec.ClusterSelector.MatchLabels) + if err != nil { + logger.Error(err, "Failed fetching clusters that matches PostApplyConfig labels", "PostApplyConfig", postApplyConf.Name) + return ctrl.Result{}, err + } + + for _, cl := range cls { + r.PostApplyToCluster(cl, postApplyConf) + } + + return ctrl.Result{}, nil +} + +// getMatchingClustersForPostApply returns all of the Cluster objects +// that have a matching label with the postApplyConfig object +func (r *PostApplyConfigReconciler) getMatchingClustersForPostApply(ctx context.Context, postApplyLabels map[string]string) ([]*clusterv1.Cluster, error) { + clusterList := &clusterv1.ClusterList{} + if err := r.Client.List(ctx, clusterList, client.MatchingLabels(postApplyLabels)); err != nil { + return nil, errors.Wrap(err, "failed to list clusters") + } + + clusters := []*clusterv1.Cluster{} + for i := range clusterList.Items { + m := &clusterList.Items[i] + if m.DeletionTimestamp.IsZero() { + clusters = append(clusters, m) + } + } + return clusters, nil +} + +func (r *PostApplyConfigReconciler) PostApplyToCluster(cluster *clusterv1.Cluster, postApplyConf *postapplyv1.PostApplyConfig) error { + logger := r.Log.WithValues("PostApplyConfig", postApplyConf.Name, "namespace", postApplyConf.Namespace) + + logger.Info("Applying PostApplyConfig to cluster", "Cluster", cluster.Name, "PostApplyConfig", postApplyConf.Name) + + // Check if this postApplyConf is applied to the cluster, if not continue + if r.IsYamlAppliedToCluster(cluster, postApplyConf.Status.ClusterRefList) { + logger.Info("PostApplyConfig applied before", "Cluster", cluster.Name, "PostApplyConfig", postApplyConf.Name) + return nil + } + + // Check if all secrets exist and in correct format. If not, return and do not apply any + err := r.checkSecretsAreCorrect(postApplyConf) + if err != nil { + r.Log.Error(err, "error in PostApply secrets") + return err + } + + err = r.postApplyToCluster(cluster, postApplyConf) + if err != nil { + r.Log.Error(err, "Failed applying secrets") + return err + } + + logger.Info("Successfully applied post-apply addon", "PostApplyConfig", postApplyConf.Name+"/"+postApplyConf.Namespace) + + if postApplyConf.Status.ClusterRefList == nil { + postApplyConf.Status.ClusterRefList = make([]*corev1.ObjectReference, 0) + } + + postApplyConf.Status.ClusterRefList = append(postApplyConf.Status.ClusterRefList, &corev1.ObjectReference{ + Kind: cluster.Kind, + Name: cluster.Name, + Namespace: cluster.Namespace, + UID: cluster.UID, + }) + return nil +} + +func (r *PostApplyConfigReconciler) IsYamlAppliedToCluster(cluster *clusterv1.Cluster, refList []*corev1.ObjectReference) bool { + for _, ref := range refList { + // TODO: Is this check enough? + if ref.Name == cluster.Name && ref.Namespace == cluster.Namespace && ref.Kind == cluster.Kind { + return true + } + } + + return false +} + +func (r *PostApplyConfigReconciler) postApplyToCluster(cluster *clusterv1.Cluster, postApplyConf *postapplyv1.PostApplyConfig) error { + c, err := remote.NewClusterClient(context.Background(), r.Client, cluster, r.Scheme) + // Failed to get remote cluster client: Kubeconfig secret may be missing for the cluster. + if err != nil { + return err + } + + for _, addon := range postApplyConf.Spec.PostApplyAddons { + typedName := types.NamespacedName{Name: addon.Name, Namespace: addon.Namespace} + addonSecret, err := secret.GetAnySecretFromNamespacedName(context.Background(), r.Client, typedName) + if err != nil { + return errors.Wrapf(err, + "failed to fetch PostApply secret %q in namespace %q", addon.Name, addon.Namespace) + } + + data, ok := addonSecret.Data[PostApplyDataKey] + if !ok { + return errors.New("wrong secret format for addon") + } + err = ApplyYAMLWithNamespace(context.Background(), c, data, "") + if err != nil { + return errors.Wrapf(err, + "failed to applying PostApply secret %q in namespace %q", addon.Name, addon.Namespace) + } + } + return nil +} + +func (r *PostApplyConfigReconciler) checkSecretsAreCorrect(postApplyConf *postapplyv1.PostApplyConfig) error { + for _, addon := range postApplyConf.Spec.PostApplyAddons { + typedName := types.NamespacedName{Name: addon.Name, Namespace: addon.Namespace} + addonSecret, err := secret.GetAnySecretFromNamespacedName(context.Background(), r.Client, typedName) + if err != nil { + return errors.Wrapf(err, + "failed to fetch PostApply secret %q in namespace %q", addon.Name, addon.Namespace) + } + + _, ok := addonSecret.Data[PostApplyDataKey] + if !ok { + return errors.New("wrong secret format for addon") + } + } + + return nil +} + +func (r *PostApplyConfigReconciler) clusterToPostApplyConfig(o handler.MapObject) []ctrl.Request { + result := []ctrl.Request{} + + cluster, ok := o.Object.(*clusterv1.Cluster) + if !ok { + r.Log.Error(nil, fmt.Sprintf("Expected a Cluster but got a %T", o.Object)) + return nil + } + + postApplyConfigList := &postapplyv1.PostApplyConfigList{} + if err := r.Client.List(context.Background(), postApplyConfigList, client.MatchingLabels(cluster.GetLabels())); err != nil { + r.Log.Error(err, "failed to list PostApplyConfigs") + return nil + } + + postApplyConfigs := []*postapplyv1.PostApplyConfig{} + for i := range postApplyConfigList.Items { + m := &postApplyConfigList.Items[i] + if m.DeletionTimestamp.IsZero() { + postApplyConfigs = append(postApplyConfigs, m) + } + } + + for _, pa := range postApplyConfigs { + name := client.ObjectKey{Namespace: pa.Namespace, Name: pa.Name} + result = append(result, ctrl.Request{NamespacedName: name}) + } + + return result +} + +func (r *PostApplyConfigReconciler) SetupWithManager(mgr ctrl.Manager) error { + err := ctrl.NewControllerManagedBy(mgr). + For(&postapplyv1.PostApplyConfig{}). + Watches( + &source.Kind{Type: &clusterv1.Cluster{}}, + &handler.EnqueueRequestsFromMapFunc{ToRequests: handler.ToRequestsFunc(r.clusterToPostApplyConfig)}, + ). + Complete(r) + if err != nil { + return errors.Wrap(err, "failed setting up with a controller manager") + } + r.recorder = mgr.GetEventRecorderFor("cluster-controller") + return err +} diff --git a/exp/postapply/controllers/postapplyconfig_helpers.go b/exp/postapply/controllers/postapplyconfig_helpers.go new file mode 100644 index 000000000000..6c8cdeb3c613 --- /dev/null +++ b/exp/postapply/controllers/postapplyconfig_helpers.go @@ -0,0 +1,134 @@ +package controllers + +import ( + "bufio" + "bytes" + "context" + "io" + + "github.com/pkg/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + utilyaml "k8s.io/apimachinery/pkg/util/yaml" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" +) + +// ForEachObjectInYAMLActionFunc is a function that is executed against each +// object found in a YAML document. +// When a non-empty namespace is provided then the object is assigned the +// namespace prior to any other actions being performed with or to the object. +type ForEachObjectInYAMLActionFunc func(context.Context, client.Client, *unstructured.Unstructured) error + +// ForEachObjectInYAML excutes actionFn for each object in the provided YAML. +// If an error is returned then no further objects are processed. +// The data may be a single YAML document or multidoc YAML. +// When a non-empty namespace is provided then all objects are assigned the +// the namespace prior to any other actions being performed with or to the +// object. +func ForEachObjectInYAML( + ctx context.Context, + c client.Client, + data []byte, + namespace string, + actionFn ForEachObjectInYAMLActionFunc) error { + + chanObj, chanErr := DecodeYAML(data) + for { + select { + case obj := <-chanObj: + if obj == nil { + return nil + } + if namespace != "" { + obj.SetNamespace(namespace) + } + if err := actionFn(ctx, c, obj); err != nil { + return err + } + case err := <-chanErr: + if err == nil { + return nil + } + return errors.Wrap(err, "received error while decoding yaml to delete from server") + } + } +} + +// ApplyYAMLWithNamespace applies the provided YAML as unstructured data with the given client. +// The data may be a single YAML document or multidoc YAML. This function is idempotent. +// When a non-empty namespace is provided then all objects are assigned the namespace prior to being created. +func ApplyYAMLWithNamespace(ctx context.Context, c client.Client, data []byte, namespace string) error { + return ForEachObjectInYAML(ctx, c, data, namespace, func(ctx context.Context, c client.Client, obj *unstructured.Unstructured) error { + // Create the object on the API server. + if err := c.Create(ctx, obj); err != nil { + // The create call is idempotent, so if the object already exists + // then do not consider it to be an error. + if !apierrors.IsAlreadyExists(err) { + return errors.Wrapf( + err, + "failed to create object %s %s/%s", + obj.GroupVersionKind(), + obj.GetNamespace(), + obj.GetName()) + } + } + return nil + }) +} + +// DecodeYAML unmarshals a YAML document or multidoc YAML as unstructured +// objects, placing each decoded object into a channel. +func DecodeYAML(data []byte) (<-chan *unstructured.Unstructured, <-chan error) { + + var ( + chanErr = make(chan error) + chanObj = make(chan *unstructured.Unstructured) + multidocReader = utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(data))) + ) + + go func() { + defer close(chanErr) + defer close(chanObj) + + // Iterate over the data until Read returns io.EOF. Every successful + // read returns a complete YAML document. + for { + buf, err := multidocReader.Read() + if err != nil { + if err == io.EOF { + return + } + chanErr <- errors.Wrap(err, "failed to read yaml data") + return + } + + // Do not use this YAML doc if it is unkind. + var typeMeta runtime.TypeMeta + if err := yaml.Unmarshal(buf, &typeMeta); err != nil { + continue + } + if typeMeta.Kind == "" { + continue + } + + // Define the unstructured object into which the YAML document will be + // unmarshaled. + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{}, + } + + // Unmarshal the YAML document into the unstructured object. + if err := yaml.Unmarshal(buf, &obj.Object); err != nil { + chanErr <- errors.Wrap(err, "failed to unmarshal yaml data") + return + } + + // Place the unstructured object into the channel. + chanObj <- obj + } + }() + + return chanObj, chanErr +} diff --git a/exp/postapply/controllers/suite_test.go b/exp/postapply/controllers/suite_test.go new file mode 100644 index 000000000000..82d754d5d54b --- /dev/null +++ b/exp/postapply/controllers/suite_test.go @@ -0,0 +1,79 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "path/filepath" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + postapplyv1alpha3 "sigs.k8s.io/cluster-api/api/v1alpha3" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + // +kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecsWithDefaultAndCustomReporters(t, + "Controller Suite", + []Reporter{envtest.NewlineReporter{}}) +} + +var _ = BeforeSuite(func(done Done) { + logf.SetLogger(zap.LoggerTo(GinkgoWriter, true)) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, + } + + var err error + cfg, err = testEnv.Start() + Expect(err).ToNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + + err = postapplyv1alpha3.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).ToNot(HaveOccurred()) + Expect(k8sClient).ToNot(BeNil()) + + close(done) +}, 60) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).ToNot(HaveOccurred()) +}) diff --git a/exp/postapply/hack/boilerplate.go.txt b/exp/postapply/hack/boilerplate.go.txt new file mode 100644 index 000000000000..b92001fb4ed4 --- /dev/null +++ b/exp/postapply/hack/boilerplate.go.txt @@ -0,0 +1,14 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ \ No newline at end of file diff --git a/features/features.go b/features/features.go index 54e93e4efb06..1f8d2b5f4c6a 100644 --- a/features/features.go +++ b/features/features.go @@ -24,11 +24,15 @@ import ( ) const ( -// Every feature gate should add method here following this template: -// -// // owner: @username -// // alpha: v1.X -// MyFeature featuregate.Feature = "MyFeature" + // Every feature gate should add method here following this template: + // + // // owner: @username + // // alpha: v1.X + // MyFeature featuregate.Feature = "MyFeature" + + // owner: @sedefsavas + // alpha: v1.X TODO: add version + PostApply featuregate.Feature = "PostApply" ) func init() { @@ -40,4 +44,5 @@ func init() { var defaultClusterAPIFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ // Every feature should be initiated here: // MyFeature: {Default: false, PreRelease: featuregate.Alpha}, + PostApply: {Default: false, PreRelease: featuregate.Alpha}, } diff --git a/main.go b/main.go index b60535944ced..b8a55ae30a82 100644 --- a/main.go +++ b/main.go @@ -29,7 +29,7 @@ import ( "k8s.io/client-go/rest" "k8s.io/klog" "k8s.io/klog/klogr" - _ "sigs.k8s.io/cluster-api/features" + "sigs.k8s.io/cluster-api/features" "sigs.k8s.io/cluster-api/util/featuregate" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/cache" @@ -39,6 +39,9 @@ import ( clusterv1alpha2 "sigs.k8s.io/cluster-api/api/v1alpha2" clusterv1alpha3 "sigs.k8s.io/cluster-api/api/v1alpha3" "sigs.k8s.io/cluster-api/controllers" + postapplyv1alpha3 "sigs.k8s.io/cluster-api/exp/postapply/api/v1alpha3" + postapply "sigs.k8s.io/cluster-api/exp/postapply/controllers" + "sigs.k8s.io/controller-runtime/pkg/healthz" // +kubebuilder:scaffold:imports ) @@ -69,6 +72,8 @@ func init() { _ = clusterv1alpha2.AddToScheme(scheme) _ = clusterv1alpha3.AddToScheme(scheme) _ = apiextensionsv1.AddToScheme(scheme) + _ = postapplyv1alpha3.AddToScheme(scheme) + // +kubebuilder:scaffold:scheme } @@ -207,6 +212,18 @@ func setupReconcilers(mgr ctrl.Manager) { setupLog.Error(err, "unable to create controller", "controller", "MachinePool") os.Exit(1) } + + if featuregate.DefaultFeatureGate.Enabled(features.PostApply) { + klog.Info("xx enabled") + if err := (&postapply.PostApplyConfigReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("PostApplyConfig"), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "PostApplyConfig") + os.Exit(1) + } + } } func setupWebhooks(mgr ctrl.Manager) { diff --git a/util/secret/secret.go b/util/secret/secret.go index abbee1fb7856..b699348619b8 100644 --- a/util/secret/secret.go +++ b/util/secret/secret.go @@ -53,3 +53,19 @@ func GetFromNamespacedName(ctx context.Context, c client.Client, clusterName typ func Name(cluster string, suffix Purpose) string { return fmt.Sprintf("%s-%s", cluster, suffix) } + +// GetAnySecretFromNamespacedName retrieves any Secret from the given +// secret name and namespace. +func GetAnySecretFromNamespacedName(ctx context.Context, c client.Client, secretName types.NamespacedName) (*corev1.Secret, error) { + secret := &corev1.Secret{} + secretKey := client.ObjectKey{ + Namespace: secretName.Namespace, + Name: secretName.Name, + } + + if err := c.Get(ctx, secretKey, secret); err != nil { + return nil, err + } + + return secret, nil +} \ No newline at end of file