diff --git a/.gitignore b/.gitignore index d6495a05c..7c9713113 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ *.out /cert-manager-operator +/http01-proxy # Log output from telepresence telepresence.log diff --git a/Makefile b/Makefile index 101accaa9..1b47966b9 100644 --- a/Makefile +++ b/Makefile @@ -60,6 +60,7 @@ endif CERT_MANAGER_VERSION ?= v1.20.3 ISTIO_CSR_VERSION ?= v0.16.0 TRUST_MANAGER_VERSION ?= v0.20.3 +HTTP01PROXY_VERSION ?= v0.1.0 # --- Test Versions --- @@ -329,10 +330,12 @@ local-run: build ## Run the operator locally against the cluster configured in ~ RELATED_IMAGE_CERT_MANAGER_ACMESOLVER=quay.io/jetstack/cert-manager-acmesolver:$(CERT_MANAGER_VERSION) \ RELATED_IMAGE_CERT_MANAGER_ISTIOCSR=quay.io/jetstack/cert-manager-istio-csr:$(ISTIO_CSR_VERSION) \ RELATED_IMAGE_CERT_MANAGER_TRUST_MANAGER=quay.io/jetstack/trust-manager:$(TRUST_MANAGER_VERSION) \ + RELATED_IMAGE_CERT_MANAGER_HTTP01PROXY=quay.io/openshift/cert-manager-http01-proxy:$(HTTP01PROXY_VERSION) \ OPERATOR_NAME=cert-manager-operator \ OPERAND_IMAGE_VERSION=$(BUNDLE_VERSION) \ ISTIOCSR_OPERAND_IMAGE_VERSION=$(ISTIO_CSR_VERSION) \ TRUSTMANAGER_OPERAND_IMAGE_VERSION=$(TRUST_MANAGER_VERSION) \ + HTTP01PROXY_OPERAND_IMAGE_VERSION=$(HTTP01PROXY_VERSION) \ OPERATOR_IMAGE_VERSION=$(BUNDLE_VERSION) \ ./cert-manager-operator start \ --config=./hack/local-run-config.yaml \ @@ -352,6 +355,10 @@ build: generate fmt vet build-operator ## Build operator binary with all checks build-operator: ## Build operator binary only (no checks or code generation). @GOFLAGS="-mod=vendor" source hack/go-fips.sh && $(GO) build $(GOBUILD_VERSION_ARGS) -o $(BIN) +.PHONY: build-http01-proxy +build-http01-proxy: ## Build HTTP01 proxy binary. + @GOFLAGS="-mod=vendor" source hack/go-fips.sh && $(GO) build $(GOBUILD_VERSION_ARGS) -o $(PROJECT_ROOT)/http01-proxy ./cmd/http01-proxy + .PHONY: run run: manifests generate fmt vet ## Run the operator from your host (for development). go run $(PACKAGE) @@ -364,6 +371,10 @@ image-build: ## Build container image with the operator. image-push: ## Push container image with the operator. $(CONTAINER_ENGINE) push $(IMG) $(CONTAINER_PUSH_ARGS) +.PHONY: image-build-http01-proxy +image-build-http01-proxy: ## Build HTTP01 proxy container image. + $(CONTAINER_ENGINE) build -t cert-manager-http01-proxy:$(HTTP01PROXY_VERSION) -f images/ci/http01proxy.Dockerfile . + # ============================================================================ # Deployment # ============================================================================ diff --git a/api/operator/v1alpha1/features.go b/api/operator/v1alpha1/features.go index 6d9c380a5..bdaad7f1f 100644 --- a/api/operator/v1alpha1/features.go +++ b/api/operator/v1alpha1/features.go @@ -21,9 +21,19 @@ var ( // For more details, // https://github.com/openshift/enhancements/blob/master/enhancements/cert-manager/trust-manager-controller.md FeatureTrustManager featuregate.Feature = "TrustManager" + + // HTTP01Proxy enables the controller for http01proxies.operator.openshift.io resource, + // which extends cert-manager-operator to deploy and manage the HTTP01 challenge proxy. + // The proxy enables cert-manager to complete HTTP01 ACME challenges for the API endpoint + // on baremetal platforms where the API VIP is not exposed via OpenShift Ingress. + // + // For more details, + // https://github.com/openshift/enhancements/pull/1929 + FeatureHTTP01Proxy featuregate.Feature = "HTTP01Proxy" ) var OperatorFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ FeatureIstioCSR: {Default: true, PreRelease: featuregate.GA}, FeatureTrustManager: {Default: false, PreRelease: "TechPreview"}, + FeatureHTTP01Proxy: {Default: false, PreRelease: featuregate.Alpha}, } diff --git a/api/operator/v1alpha1/http01proxy_types.go b/api/operator/v1alpha1/http01proxy_types.go new file mode 100644 index 000000000..742d507ba --- /dev/null +++ b/api/operator/v1alpha1/http01proxy_types.go @@ -0,0 +1,109 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func init() { + SchemeBuilder.Register(&HTTP01Proxy{}, &HTTP01ProxyList{}) +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true + +// HTTP01ProxyList is a list of HTTP01Proxy objects. +type HTTP01ProxyList struct { + metav1.TypeMeta `json:",inline"` + + // metadata is the standard list's metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + metav1.ListMeta `json:"metadata"` + Items []HTTP01Proxy `json:"items"` +} + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:path=http01proxies,scope=Namespaced,categories={cert-manager-operator},shortName=http01proxy +// +kubebuilder:printcolumn:name="Mode",type="string",JSONPath=".spec.mode" +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" +// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].message" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:metadata:labels={"app.kubernetes.io/name=http01proxy", "app.kubernetes.io/part-of=cert-manager-operator"} + +// HTTP01Proxy describes the configuration for the HTTP01 challenge proxy +// that redirects traffic from the API endpoint on port 80 to ingress routers. +// This enables cert-manager to perform HTTP01 ACME challenges for API endpoint certificates. +// The name must be `default` to make HTTP01Proxy a singleton. +// +// When an HTTP01Proxy is created, the proxy DaemonSet is deployed on control plane nodes. +// +// +kubebuilder:validation:XValidation:rule="self.metadata.name == 'default'",message="http01proxy is a singleton, .metadata.name must be 'default'" +// +operator-sdk:csv:customresourcedefinitions:displayName="HTTP01Proxy" +type HTTP01Proxy struct { + metav1.TypeMeta `json:",inline"` + + // metadata is the standard object's metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + metav1.ObjectMeta `json:"metadata,omitempty"` + + // spec is the specification of the desired behavior of the HTTP01Proxy. + // +kubebuilder:validation:Required + // +required + Spec HTTP01ProxySpec `json:"spec"` + + // status is the most recently observed status of the HTTP01Proxy. + // +kubebuilder:validation:Optional + // +optional + Status HTTP01ProxyStatus `json:"status,omitempty"` +} + +// HTTP01ProxyMode controls how the HTTP01 challenge proxy is deployed. +// +kubebuilder:validation:Enum=DefaultDeployment;CustomDeployment +type HTTP01ProxyMode string + +const ( + // HTTP01ProxyModeDefault enables the proxy with default configuration. + HTTP01ProxyModeDefault HTTP01ProxyMode = "DefaultDeployment" + + // HTTP01ProxyModeCustom enables the proxy with user-specified configuration. + HTTP01ProxyModeCustom HTTP01ProxyMode = "CustomDeployment" +) + +// HTTP01ProxySpec is the specification of the desired behavior of the HTTP01Proxy. +// +kubebuilder:validation:XValidation:rule="self.mode == 'CustomDeployment' ? has(self.customDeployment) : !has(self.customDeployment)",message="customDeployment is required when mode is CustomDeployment and forbidden otherwise" +type HTTP01ProxySpec struct { + // mode controls whether the HTTP01 challenge proxy is active and how it should be deployed. + // DefaultDeployment enables the proxy with default configuration. + // CustomDeployment enables the proxy with user-specified configuration. + // +kubebuilder:validation:Required + // +required + Mode HTTP01ProxyMode `json:"mode"` + + // customDeployment contains configuration options when mode is CustomDeployment. + // This field is only valid when mode is CustomDeployment. + // +kubebuilder:validation:Optional + // +optional + CustomDeployment *HTTP01ProxyCustomDeploymentSpec `json:"customDeployment,omitempty"` +} + +// HTTP01ProxyCustomDeploymentSpec contains configuration for custom proxy deployment. +type HTTP01ProxyCustomDeploymentSpec struct { + // internalPort specifies the internal port used by the proxy service. + // Valid values are 1024-65535. + // +kubebuilder:validation:Minimum=1024 + // +kubebuilder:validation:Maximum=65535 + // +kubebuilder:default=8888 + // +optional + InternalPort int32 `json:"internalPort,omitempty"` +} + +// HTTP01ProxyStatus is the most recently observed status of the HTTP01Proxy. +type HTTP01ProxyStatus struct { + // conditions holds information about the current state of the HTTP01 proxy deployment. + ConditionalStatus `json:",inline,omitempty"` + + // proxyImage is the name of the image and the tag used for deploying the proxy. + ProxyImage string `json:"proxyImage,omitempty"` +} diff --git a/api/operator/v1alpha1/zz_generated.deepcopy.go b/api/operator/v1alpha1/zz_generated.deepcopy.go index 883cddf1b..a0d015685 100644 --- a/api/operator/v1alpha1/zz_generated.deepcopy.go +++ b/api/operator/v1alpha1/zz_generated.deepcopy.go @@ -334,6 +334,116 @@ func (in *DeploymentConfig) DeepCopy() *DeploymentConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTP01Proxy) DeepCopyInto(out *HTTP01Proxy) { + *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 HTTP01Proxy. +func (in *HTTP01Proxy) DeepCopy() *HTTP01Proxy { + if in == nil { + return nil + } + out := new(HTTP01Proxy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HTTP01Proxy) 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 *HTTP01ProxyCustomDeploymentSpec) DeepCopyInto(out *HTTP01ProxyCustomDeploymentSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTP01ProxyCustomDeploymentSpec. +func (in *HTTP01ProxyCustomDeploymentSpec) DeepCopy() *HTTP01ProxyCustomDeploymentSpec { + if in == nil { + return nil + } + out := new(HTTP01ProxyCustomDeploymentSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTP01ProxyList) DeepCopyInto(out *HTTP01ProxyList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]HTTP01Proxy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTP01ProxyList. +func (in *HTTP01ProxyList) DeepCopy() *HTTP01ProxyList { + if in == nil { + return nil + } + out := new(HTTP01ProxyList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HTTP01ProxyList) 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 *HTTP01ProxySpec) DeepCopyInto(out *HTTP01ProxySpec) { + *out = *in + if in.CustomDeployment != nil { + in, out := &in.CustomDeployment, &out.CustomDeployment + *out = new(HTTP01ProxyCustomDeploymentSpec) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTP01ProxySpec. +func (in *HTTP01ProxySpec) DeepCopy() *HTTP01ProxySpec { + if in == nil { + return nil + } + out := new(HTTP01ProxySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTP01ProxyStatus) DeepCopyInto(out *HTTP01ProxyStatus) { + *out = *in + in.ConditionalStatus.DeepCopyInto(&out.ConditionalStatus) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTP01ProxyStatus. +func (in *HTTP01ProxyStatus) DeepCopy() *HTTP01ProxyStatus { + if in == nil { + return nil + } + out := new(HTTP01ProxyStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IstioCSR) DeepCopyInto(out *IstioCSR) { *out = *in diff --git a/bindata/http01-proxy/cert-manager-http01-proxy-clusterrole.yaml b/bindata/http01-proxy/cert-manager-http01-proxy-clusterrole.yaml new file mode 100644 index 000000000..50df01e83 --- /dev/null +++ b/bindata/http01-proxy/cert-manager-http01-proxy-clusterrole.yaml @@ -0,0 +1,37 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: cert-manager-http01-proxy + labels: + app: cert-manager-http01-proxy + app.kubernetes.io/name: cert-manager-http01-proxy + app.kubernetes.io/part-of: cert-manager-operator +rules: + - apiGroups: + - config.openshift.io + resources: + - clusterversions + - infrastructures + - ingresses + verbs: + - get + - list + - watch + - apiGroups: + - machineconfiguration.openshift.io + resources: + - machineconfigs + verbs: + - get + - create + - update + - patch + - apiGroups: + - operator.openshift.io + resources: + - machineconfigurations + verbs: + - get + - create + - update + - patch diff --git a/bindata/http01-proxy/cert-manager-http01-proxy-clusterrolebinding.yaml b/bindata/http01-proxy/cert-manager-http01-proxy-clusterrolebinding.yaml new file mode 100644 index 000000000..ceda98164 --- /dev/null +++ b/bindata/http01-proxy/cert-manager-http01-proxy-clusterrolebinding.yaml @@ -0,0 +1,16 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: cert-manager-http01-proxy + labels: + app: cert-manager-http01-proxy + app.kubernetes.io/name: cert-manager-http01-proxy + app.kubernetes.io/part-of: cert-manager-operator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cert-manager-http01-proxy +subjects: + - kind: ServiceAccount + name: cert-manager-http01-proxy + namespace: cert-manager-operator diff --git a/bindata/http01-proxy/cert-manager-http01-proxy-daemonset.yaml b/bindata/http01-proxy/cert-manager-http01-proxy-daemonset.yaml new file mode 100644 index 000000000..f9260ee3f --- /dev/null +++ b/bindata/http01-proxy/cert-manager-http01-proxy-daemonset.yaml @@ -0,0 +1,75 @@ +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: cert-manager-http01-proxy + namespace: cert-manager-operator + labels: + app: cert-manager-http01-proxy + app.kubernetes.io/name: cert-manager-http01-proxy + app.kubernetes.io/part-of: cert-manager-operator +spec: + selector: + matchLabels: + app: cert-manager-http01-proxy + updateStrategy: + type: RollingUpdate + template: + metadata: + labels: + app: cert-manager-http01-proxy + app.kubernetes.io/name: cert-manager-http01-proxy + app.kubernetes.io/part-of: cert-manager-operator + spec: + serviceAccountName: cert-manager-http01-proxy + hostNetwork: true + nodeSelector: + node-role.kubernetes.io/master: "" + tolerations: + - key: node-role.kubernetes.io/master + operator: Exists + effect: NoSchedule + - key: node-role.kubernetes.io/control-plane + operator: Exists + effect: NoSchedule + containers: + - name: http01-proxy + image: ${RELATED_IMAGE_CERT_MANAGER_HTTP01PROXY} + ports: + - name: proxy + containerPort: 8888 + hostPort: 8888 + protocol: TCP + env: + - name: PROXY_PORT + value: "8888" + livenessProbe: + tcpSocket: + port: proxy + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + tcpSocket: + port: proxy + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + add: + - NET_ADMIN + drop: + - ALL + runAsNonRoot: false + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + cpu: 100m + memory: 64Mi + priorityClassName: system-cluster-critical diff --git a/bindata/http01-proxy/cert-manager-http01-proxy-scc-rolebinding.yaml b/bindata/http01-proxy/cert-manager-http01-proxy-scc-rolebinding.yaml new file mode 100644 index 000000000..0a5603031 --- /dev/null +++ b/bindata/http01-proxy/cert-manager-http01-proxy-scc-rolebinding.yaml @@ -0,0 +1,16 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: cert-manager-http01-proxy-scc + labels: + app: cert-manager-http01-proxy + app.kubernetes.io/name: cert-manager-http01-proxy + app.kubernetes.io/part-of: cert-manager-operator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:openshift:scc:privileged +subjects: + - kind: ServiceAccount + name: cert-manager-http01-proxy + namespace: cert-manager-operator diff --git a/bindata/http01-proxy/cert-manager-http01-proxy-serviceaccount.yaml b/bindata/http01-proxy/cert-manager-http01-proxy-serviceaccount.yaml new file mode 100644 index 000000000..a738b4a01 --- /dev/null +++ b/bindata/http01-proxy/cert-manager-http01-proxy-serviceaccount.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: cert-manager-http01-proxy + namespace: cert-manager-operator + labels: + app: cert-manager-http01-proxy + app.kubernetes.io/name: cert-manager-http01-proxy + app.kubernetes.io/part-of: cert-manager-operator diff --git a/bindata/networkpolicies/http01-proxy-allow-egress-networkpolicy.yaml b/bindata/networkpolicies/http01-proxy-allow-egress-networkpolicy.yaml new file mode 100644 index 000000000..48a6c9b07 --- /dev/null +++ b/bindata/networkpolicies/http01-proxy-allow-egress-networkpolicy.yaml @@ -0,0 +1,23 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: cert-manager-http01-proxy-allow-egress + namespace: cert-manager-operator + labels: + app: cert-manager-http01-proxy + app.kubernetes.io/name: cert-manager-http01-proxy + app.kubernetes.io/part-of: cert-manager-operator +spec: + podSelector: + matchLabels: + app: cert-manager-http01-proxy + policyTypes: + - Egress + egress: + - ports: + - port: 443 + protocol: TCP + - port: 6443 + protocol: TCP + - port: 80 + protocol: TCP diff --git a/bindata/networkpolicies/http01-proxy-deny-all-networkpolicy.yaml b/bindata/networkpolicies/http01-proxy-deny-all-networkpolicy.yaml new file mode 100644 index 000000000..7b4c8c1ba --- /dev/null +++ b/bindata/networkpolicies/http01-proxy-deny-all-networkpolicy.yaml @@ -0,0 +1,16 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: cert-manager-http01-proxy-deny-all + namespace: cert-manager-operator + labels: + app: cert-manager-http01-proxy + app.kubernetes.io/name: cert-manager-http01-proxy + app.kubernetes.io/part-of: cert-manager-operator +spec: + podSelector: + matchLabels: + app: cert-manager-http01-proxy + policyTypes: + - Ingress + - Egress diff --git a/bundle/manifests/cert-manager-operator.clusterserviceversion.yaml b/bundle/manifests/cert-manager-operator.clusterserviceversion.yaml index 2832f06b5..c1d9e8388 100644 --- a/bundle/manifests/cert-manager-operator.clusterserviceversion.yaml +++ b/bundle/manifests/cert-manager-operator.clusterserviceversion.yaml @@ -365,6 +365,9 @@ spec: kind: ClusterIssuer name: clusterissuers.cert-manager.io version: v1 + - kind: HTTP01Proxy + name: http01proxies.operator.openshift.io + version: v1alpha1 - description: |- An Issuer represents a certificate issuing authority which can be referenced as part of `issuerRef` fields. @@ -495,6 +498,7 @@ spec: - apiGroups: - apps resources: + - daemonsets - deployments - replicasets verbs: @@ -568,6 +572,8 @@ spec: - config.openshift.io resources: - apiservers + - clusterversions + - ingresses verbs: - get - list @@ -646,6 +652,7 @@ spec: - operator.openshift.io resources: - certmanagers/finalizers + - http01proxies/finalizers - istiocsrs/finalizers - trustmanagers/finalizers verbs: @@ -654,6 +661,7 @@ spec: - operator.openshift.io resources: - certmanagers/status + - http01proxies/status - istiocsrs/status - trustmanagers/status verbs: @@ -663,6 +671,7 @@ spec: - apiGroups: - operator.openshift.io resources: + - http01proxies - istiocsrs - trustmanagers verbs: @@ -699,6 +708,14 @@ spec: - patch - update - watch + - apiGroups: + - security.openshift.io + resourceNames: + - privileged + resources: + - securitycontextconstraints + verbs: + - use - apiGroups: - trust.cert-manager.io resources: @@ -802,12 +819,16 @@ spec: value: quay.io/jetstack/cert-manager-istio-csr:v0.16.0 - name: RELATED_IMAGE_CERT_MANAGER_TRUST_MANAGER value: quay.io/jetstack/trust-manager:v0.20.3 + - name: RELATED_IMAGE_CERT_MANAGER_HTTP01PROXY + value: quay.io/openshift/cert-manager-http01-proxy:v0.1.0 - name: OPERAND_IMAGE_VERSION value: 1.20.3 - name: ISTIOCSR_OPERAND_IMAGE_VERSION value: 0.16.0 - name: TRUSTMANAGER_OPERAND_IMAGE_VERSION value: 0.20.3 + - name: HTTP01PROXY_OPERAND_IMAGE_VERSION + value: 0.1.0 - name: OPERATOR_IMAGE_VERSION value: 1.20.0 - name: OPERATOR_LOG_LEVEL @@ -924,5 +945,7 @@ spec: name: cert-manager-istiocsr - image: quay.io/jetstack/trust-manager:v0.20.3 name: cert-manager-trust-manager + - image: quay.io/openshift/cert-manager-http01-proxy:v0.1.0 + name: cert-manager-http01proxy replaces: cert-manager-operator.v1.19.0 version: 1.20.0 diff --git a/bundle/manifests/operator.openshift.io_http01proxies.yaml b/bundle/manifests/operator.openshift.io_http01proxies.yaml new file mode 100644 index 000000000..f2a2da8d1 --- /dev/null +++ b/bundle/manifests/operator.openshift.io_http01proxies.yaml @@ -0,0 +1,185 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + creationTimestamp: null + labels: + app.kubernetes.io/name: http01proxy + app.kubernetes.io/part-of: cert-manager-operator + name: http01proxies.operator.openshift.io +spec: + group: operator.openshift.io + names: + categories: + - cert-manager-operator + kind: HTTP01Proxy + listKind: HTTP01ProxyList + plural: http01proxies + shortNames: + - http01proxy + singular: http01proxy + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.mode + name: Mode + type: string + - jsonPath: .status.conditions[?(@.type=='Ready')].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=='Ready')].message + name: Message + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + HTTP01Proxy describes the configuration for the HTTP01 challenge proxy + that redirects traffic from the API endpoint on port 80 to ingress routers. + This enables cert-manager to perform HTTP01 ACME challenges for API endpoint certificates. + The name must be `default` to make HTTP01Proxy a singleton. + + When an HTTP01Proxy is created, the proxy DaemonSet is deployed on control plane nodes. + 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: spec is the specification of the desired behavior of the + HTTP01Proxy. + properties: + customDeployment: + description: |- + customDeployment contains configuration options when mode is CustomDeployment. + This field is only valid when mode is CustomDeployment. + properties: + internalPort: + default: 8888 + description: |- + internalPort specifies the internal port used by the proxy service. + Valid values are 1024-65535. + format: int32 + maximum: 65535 + minimum: 1024 + type: integer + type: object + mode: + description: |- + mode controls whether the HTTP01 challenge proxy is active and how it should be deployed. + DefaultDeployment enables the proxy with default configuration. + CustomDeployment enables the proxy with user-specified configuration. + enum: + - DefaultDeployment + - CustomDeployment + type: string + required: + - mode + type: object + x-kubernetes-validations: + - message: customDeployment is required when mode is CustomDeployment + and forbidden otherwise + rule: 'self.mode == ''CustomDeployment'' ? has(self.customDeployment) + : !has(self.customDeployment)' + status: + description: status is the most recently observed status of the HTTP01Proxy. + properties: + conditions: + description: conditions holds information about the current state + of the operand deployment. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + proxyImage: + description: proxyImage is the name of the image and the tag used + for deploying the proxy. + type: string + type: object + required: + - spec + type: object + x-kubernetes-validations: + - message: http01proxy is a singleton, .metadata.name must be 'default' + rule: self.metadata.name == 'default' + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/cmd/http01-proxy/main.go b/cmd/http01-proxy/main.go new file mode 100644 index 000000000..d7a6d628a --- /dev/null +++ b/cmd/http01-proxy/main.go @@ -0,0 +1,75 @@ +package main + +import ( + "context" + "log" + "net/http" + "os" + "strconv" + "strings" + "time" + + utils "github.com/openshift/cert-manager-operator/cmd/http01-proxy/pkg" +) + +const defaultPort = "8888" + +func main() { + ctx := context.Background() + + env, err := utils.GetOCPEnvDetails(ctx) + if err != nil { + log.Fatalf("Error getting OCP environment details: %v", err) + } + log.Printf("OCP API: %s, API VIP: %s, APPS VIP: %s, Platform: %s, Version: %s", + env.APIHostname, env.APIVIP, env.AppsVIP, env.PlatformType, env.ClusterVersion) + + if err := utils.SupportedOCPVersion(env.ClusterVersion); err != nil { + log.Fatalf("Detected non-supported version: %v", err) + } + + if env.AppsVIP == env.APIVIP { + log.Printf("API VIP and APPS VIP are equal, no proxy needed") + os.Exit(0) + } + + port := os.Getenv("PROXY_PORT") + if port == "" { + port = defaultPort + } + if p, err := strconv.Atoi(port); err != nil || p < 1 || p > 65535 { + log.Fatalf("Invalid PROXY_PORT %q: must be a number between 1 and 65535", port) + } + + if err := utils.CreateNFTablesRuleMachineConfig(ctx, env.Client, env.APIVIP, port); err != nil { + log.Fatalf("Error creating nft rules machineconfig: %v", err) + } + log.Println("NFTables Rules MachineConfig created/updated") + + backendServer := "http://" + env.AppsVIP + ":80" + proxy, err := utils.NewReverseProxy(backendServer) + if err != nil { + log.Fatalf("Error creating reverse proxy: %v", err) + } + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, "/.well-known/acme-challenge/") { + log.Printf("Forwarding request to APPS VIP: %s", r.URL.Path) + proxy.ServeHTTP(w, r) + } else { + http.Error(w, "Forbidden: Only /.well-known/acme-challenge/* is allowed", http.StatusForbidden) + } + }) + + server := &http.Server{ + Addr: ":" + port, + ReadHeaderTimeout: 10 * time.Second, + ReadTimeout: 15 * time.Second, + WriteTimeout: 15 * time.Second, + IdleTimeout: 60 * time.Second, + } + log.Printf("Reverse proxy listening on :%s, forwarding http01 challenges for %s to %s", port, env.APIHostname, backendServer) + if err := server.ListenAndServe(); err != nil { + log.Fatalf("Error starting proxy: %v", err) + } +} diff --git a/cmd/http01-proxy/pkg/utils.go b/cmd/http01-proxy/pkg/utils.go new file mode 100644 index 000000000..89727722c --- /dev/null +++ b/cmd/http01-proxy/pkg/utils.go @@ -0,0 +1,242 @@ +package utils + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "log" + "net" + "net/http" + "net/http/httputil" + "net/url" + "strconv" + "strings" + "text/template" + + "github.com/openshift/cert-manager-operator/cmd/http01-proxy/templates" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + yamlserializer "k8s.io/apimachinery/pkg/runtime/serializer/yaml" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" +) + +var ( + infrastructureGVR = schema.GroupVersionResource{Group: "config.openshift.io", Version: "v1", Resource: "infrastructures"} + ingressGVR = schema.GroupVersionResource{Group: "config.openshift.io", Version: "v1", Resource: "ingresses"} + clusterVersionGVR = schema.GroupVersionResource{Group: "config.openshift.io", Version: "v1", Resource: "clusterversions"} + machineConfigGVR = schema.GroupVersionResource{Group: "machineconfiguration.openshift.io", Version: "v1", Resource: "machineconfigs"} + machineConfigurationGVR = schema.GroupVersionResource{Group: "operator.openshift.io", Version: "v1", Resource: "machineconfigurations"} +) + +// OCPEnvironment holds the discovered OpenShift cluster details needed by the proxy. +type OCPEnvironment struct { + APIHostname string + AppsVIP string + APIVIP string + PlatformType string + ClusterVersion string + Client *dynamic.DynamicClient +} + +func NewReverseProxy(targetURL string) (*httputil.ReverseProxy, error) { + target, err := url.Parse(targetURL) + if err != nil { + return nil, fmt.Errorf("failed to parse target URL %q: %w", targetURL, err) + } + + proxy := httputil.NewSingleHostReverseProxy(target) + proxy.Director = func(req *http.Request) { + req.URL.Scheme = target.Scheme + req.URL.Host = target.Host + req.Header.Set("X-Proxy-Server", "cert-manager-http01-proxy") + } + proxy.ErrorHandler = func(w http.ResponseWriter, req *http.Request, err error) { + log.Printf("Error handling request: %v", err) + http.Error(w, "Backend unavailable", http.StatusBadGateway) + } + + return proxy, nil +} + +func newKubeClient() (*dynamic.DynamicClient, error) { + config, err := rest.InClusterConfig() + if err != nil { + return nil, fmt.Errorf("failed to get in-cluster config: %w", err) + } + + client, err := dynamic.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("failed to create dynamic client: %w", err) + } + + return client, nil +} + +func GetOCPEnvDetails(ctx context.Context) (*OCPEnvironment, error) { + client, err := newKubeClient() + if err != nil { + return nil, err + } + + infrastructureData, err := client.Resource(infrastructureGVR).Get(ctx, "cluster", metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to get infrastructure: %w", err) + } + platformType, found, err := unstructured.NestedString(infrastructureData.Object, "status", "platform") + if err != nil { + return nil, fmt.Errorf("failed to read platform type: %w", err) + } + if !found { + return nil, fmt.Errorf("platform type not found in infrastructure status") + } + + ingressData, err := client.Resource(ingressGVR).Get(ctx, "cluster", metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to get ingress: %w", err) + } + ingressDomain, found, err := unstructured.NestedString(ingressData.Object, "spec", "domain") + if err != nil { + return nil, fmt.Errorf("failed to read ingress domain: %w", err) + } + if !found { + return nil, fmt.Errorf("ingress domain not found in ingress spec") + } + + clusterVersionData, err := client.Resource(clusterVersionGVR).Get(ctx, "version", metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to get clusterversion: %w", err) + } + clusterVersion, found, err := unstructured.NestedString(clusterVersionData.Object, "status", "desired", "version") + if err != nil { + return nil, fmt.Errorf("failed to read cluster version: %w", err) + } + if !found { + return nil, fmt.Errorf("desired version not found in clusterversion status") + } + + suffix, ok := strings.CutPrefix(ingressDomain, "apps.") + if !ok { + return nil, fmt.Errorf("ingress domain %q does not start with \"apps.\"", ingressDomain) + } + apiHostname := "api." + suffix + + // Resolve a subdomain under the wildcard *.apps record to get the Apps VIP + appsIPs, err := resolveDNSRecord(ctx, "test."+ingressDomain) + if err != nil { + return nil, fmt.Errorf("failed to resolve apps domain: %w", err) + } + + apiIPs, err := resolveDNSRecord(ctx, apiHostname) + if err != nil { + return nil, fmt.Errorf("failed to resolve api hostname: %w", err) + } + + return &OCPEnvironment{ + APIHostname: apiHostname, + AppsVIP: appsIPs[0].String(), + APIVIP: apiIPs[0].String(), + PlatformType: platformType, + ClusterVersion: clusterVersion, + Client: client, + }, nil +} + +func CreateNFTablesRuleMachineConfig(ctx context.Context, client *dynamic.DynamicClient, apiVIP, port string) error { + data := templates.TemplateData{ + APIVIP: apiVIP, + ProxyPort: port, + } + + tmpl, err := template.New("nftables").Parse(templates.NFTRuleTemplate) + if err != nil { + return fmt.Errorf("failed to parse nftables template: %w", err) + } + var nftablesResult bytes.Buffer + if err := tmpl.Execute(&nftablesResult, data); err != nil { + return fmt.Errorf("failed to execute nftables template: %w", err) + } + data.NFTRules = base64.StdEncoding.EncodeToString(nftablesResult.Bytes()) + + tmpl, err = template.New("machineconfig").Parse(templates.MachineConfigTemplate) + if err != nil { + return fmt.Errorf("failed to parse machineconfig template: %w", err) + } + var machineConfigResult bytes.Buffer + if err := tmpl.Execute(&machineConfigResult, data); err != nil { + return fmt.Errorf("failed to execute machineconfig template: %w", err) + } + + fieldManager := "cert-manager-http01-proxy" + + if err := applyManifest(ctx, client, []byte(templates.MachineConfigurationManifest), machineConfigurationGVR, fieldManager); err != nil { + return err + } + + if err := applyManifest(ctx, client, machineConfigResult.Bytes(), machineConfigGVR, fieldManager); err != nil { + return err + } + + return nil +} + +func applyManifest(ctx context.Context, client *dynamic.DynamicClient, manifest []byte, gvr schema.GroupVersionResource, fieldManager string) error { + decoder := yamlserializer.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) + obj := &unstructured.Unstructured{} + if _, _, err := decoder.Decode(manifest, nil, obj); err != nil { + return fmt.Errorf("failed to decode %s: %w", gvr.Resource, err) + } + + objData, err := json.Marshal(obj) + if err != nil { + return fmt.Errorf("failed to marshal %s: %w", gvr.Resource, err) + } + + if _, err := client.Resource(gvr).Patch( + ctx, + obj.GetName(), + types.ApplyPatchType, + objData, + metav1.PatchOptions{FieldManager: fieldManager}, + ); err != nil { + return fmt.Errorf("failed to apply %s: %w", gvr.Resource, err) + } + + return nil +} + +func SupportedOCPVersion(runningVersion string) error { + version := strings.Split(runningVersion, ".") + if len(version) < 3 { + return fmt.Errorf("invalid OCP version %q (expecting X.Y.Z format)", runningVersion) + } + major, err := strconv.Atoi(version[0]) + if err != nil { + return fmt.Errorf("invalid OCP major version in %q: %w", runningVersion, err) + } + minor, err := strconv.Atoi(version[1]) + if err != nil { + return fmt.Errorf("invalid OCP minor version in %q: %w", runningVersion, err) + } + if major < 4 || (major == 4 && minor < 17) { + return fmt.Errorf("unsupported OCP version %q (minimum supported is 4.17+)", runningVersion) + } + return nil +} + +func resolveDNSRecord(ctx context.Context, hostname string) ([]net.IP, error) { + var r net.Resolver + ips, err := r.LookupIP(ctx, "ip4", hostname) + if err != nil { + return nil, fmt.Errorf("failed to resolve %q: %w", hostname, err) + } + if len(ips) == 0 { + return nil, fmt.Errorf("no IPs found for %q", hostname) + } + return ips, nil +} diff --git a/cmd/http01-proxy/templates/templates.go b/cmd/http01-proxy/templates/templates.go new file mode 100644 index 000000000..686bf203c --- /dev/null +++ b/cmd/http01-proxy/templates/templates.go @@ -0,0 +1,78 @@ +package templates + +type TemplateData struct { + APIVIP string + ProxyPort string + NFTRules string +} + +const NFTRuleTemplate = ` +table inet crtmgr_proxy_table +delete table inet crtmgr_proxy_table +table inet crtmgr_proxy_table { + chain crtmgr_proxy_PREROUTING { + type nat hook prerouting priority 0; + # Redirect to proxy port + ip daddr {{ .APIVIP }} tcp dport 80 redirect to {{ .ProxyPort }} + } +}` + +const MachineConfigurationManifest = ` +apiVersion: operator.openshift.io/v1 +kind: MachineConfiguration +metadata: + name: cluster +spec: + nodeDisruptionPolicy: + files: + - actions: + - restart: + serviceName: nftables.service + type: Restart + path: /etc/sysconfig/nftables.conf + units: + - actions: + - type: DaemonReload + - type: Reload + reload: + serviceName: nftables.service + name: nftables.service` + +const MachineConfigTemplate = ` +apiVersion: machineconfiguration.openshift.io/v1 +kind: MachineConfig +metadata: + labels: + machineconfiguration.openshift.io/role: master + name: 98-nftables-crtmgr-proxy +spec: + config: + ignition: + version: 3.4.0 + storage: + files: + - contents: + source: data:text/plain;charset=utf-8;base64,{{ .NFTRules }} + mode: 384 + overwrite: true + path: /etc/sysconfig/nftables.conf + systemd: + units: + - contents: | + [Unit] + Description=Netfilter Tables + Documentation=man:nft(8) + Wants=network-pre.target + Before=network-pre.target + [Service] + Type=oneshot + ProtectSystem=full + ProtectHome=true + ExecStart=/sbin/nft -f /etc/sysconfig/nftables.conf + ExecReload=/sbin/nft -f /etc/sysconfig/nftables.conf + ExecStop=/sbin/nft 'add table inet crtmgr_proxy_table; delete table inet crtmgr_proxy_table' + RemainAfterExit=yes + [Install] + WantedBy=multi-user.target + enabled: true + name: nftables.service` diff --git a/config/crd/bases/operator.openshift.io_http01proxies.yaml b/config/crd/bases/operator.openshift.io_http01proxies.yaml new file mode 100644 index 000000000..98bf30e8f --- /dev/null +++ b/config/crd/bases/operator.openshift.io_http01proxies.yaml @@ -0,0 +1,179 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + labels: + app.kubernetes.io/name: http01proxy + app.kubernetes.io/part-of: cert-manager-operator + name: http01proxies.operator.openshift.io +spec: + group: operator.openshift.io + names: + categories: + - cert-manager-operator + kind: HTTP01Proxy + listKind: HTTP01ProxyList + plural: http01proxies + shortNames: + - http01proxy + singular: http01proxy + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.mode + name: Mode + type: string + - jsonPath: .status.conditions[?(@.type=='Ready')].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=='Ready')].message + name: Message + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + HTTP01Proxy describes the configuration for the HTTP01 challenge proxy + that redirects traffic from the API endpoint on port 80 to ingress routers. + This enables cert-manager to perform HTTP01 ACME challenges for API endpoint certificates. + The name must be `default` to make HTTP01Proxy a singleton. + + When an HTTP01Proxy is created, the proxy DaemonSet is deployed on control plane nodes. + 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: spec is the specification of the desired behavior of the + HTTP01Proxy. + properties: + customDeployment: + description: |- + customDeployment contains configuration options when mode is CustomDeployment. + This field is only valid when mode is CustomDeployment. + properties: + internalPort: + default: 8888 + description: |- + internalPort specifies the internal port used by the proxy service. + Valid values are 1024-65535. + format: int32 + maximum: 65535 + minimum: 1024 + type: integer + type: object + mode: + description: |- + mode controls whether the HTTP01 challenge proxy is active and how it should be deployed. + DefaultDeployment enables the proxy with default configuration. + CustomDeployment enables the proxy with user-specified configuration. + enum: + - DefaultDeployment + - CustomDeployment + type: string + required: + - mode + type: object + x-kubernetes-validations: + - message: customDeployment is required when mode is CustomDeployment + and forbidden otherwise + rule: 'self.mode == ''CustomDeployment'' ? has(self.customDeployment) + : !has(self.customDeployment)' + status: + description: status is the most recently observed status of the HTTP01Proxy. + properties: + conditions: + description: conditions holds information about the current state + of the operand deployment. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + proxyImage: + description: proxyImage is the name of the image and the tag used + for deploying the proxy. + type: string + type: object + required: + - spec + type: object + x-kubernetes-validations: + - message: http01proxy is a singleton, .metadata.name must be 'default' + rule: self.metadata.name == 'default' + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index b88e88475..bad417696 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -11,6 +11,7 @@ resources: - bases/orders.acme.cert-manager.io-crd.yaml - bases/operator.openshift.io_istiocsrs.yaml - bases/operator.openshift.io_trustmanagers.yaml +- bases/operator.openshift.io_http01proxies.yaml - bases/customresourcedefinition_bundles.trust.cert-manager.io.yml #+kubebuilder:scaffold:crdkustomizeresource diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index af071a2b7..2f1131332 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -86,12 +86,16 @@ spec: value: quay.io/jetstack/cert-manager-istio-csr:v0.16.0 - name: RELATED_IMAGE_CERT_MANAGER_TRUST_MANAGER value: quay.io/jetstack/trust-manager:v0.20.3 + - name: RELATED_IMAGE_CERT_MANAGER_HTTP01PROXY + value: quay.io/openshift/cert-manager-http01-proxy:v0.1.0 - name: OPERAND_IMAGE_VERSION value: 1.20.3 - name: ISTIOCSR_OPERAND_IMAGE_VERSION value: 0.16.0 - name: TRUSTMANAGER_OPERAND_IMAGE_VERSION value: 0.20.3 + - name: HTTP01PROXY_OPERAND_IMAGE_VERSION + value: 0.1.0 - name: OPERATOR_IMAGE_VERSION value: 1.20.0 - name: OPERATOR_LOG_LEVEL diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 60136088a..6dd138308 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -86,6 +86,7 @@ rules: - apiGroups: - apps resources: + - daemonsets - deployments - replicasets verbs: @@ -159,6 +160,8 @@ rules: - config.openshift.io resources: - apiservers + - clusterversions + - ingresses verbs: - get - list @@ -237,6 +240,7 @@ rules: - operator.openshift.io resources: - certmanagers/finalizers + - http01proxies/finalizers - istiocsrs/finalizers - trustmanagers/finalizers verbs: @@ -245,6 +249,7 @@ rules: - operator.openshift.io resources: - certmanagers/status + - http01proxies/status - istiocsrs/status - trustmanagers/status verbs: @@ -254,6 +259,7 @@ rules: - apiGroups: - operator.openshift.io resources: + - http01proxies - istiocsrs - trustmanagers verbs: @@ -290,6 +296,14 @@ rules: - patch - update - watch +- apiGroups: + - security.openshift.io + resourceNames: + - privileged + resources: + - securitycontextconstraints + verbs: + - use - apiGroups: - trust.cert-manager.io resources: diff --git a/config/samples/operator.openshift.io_v1alpha1_http01proxy.yaml b/config/samples/operator.openshift.io_v1alpha1_http01proxy.yaml new file mode 100644 index 000000000..d5988f361 --- /dev/null +++ b/config/samples/operator.openshift.io_v1alpha1_http01proxy.yaml @@ -0,0 +1,7 @@ +apiVersion: operator.openshift.io/v1alpha1 +kind: HTTP01Proxy +metadata: + name: default + namespace: cert-manager-operator +spec: + mode: DefaultDeployment diff --git a/hack/verify-http01proxy.sh b/hack/verify-http01proxy.sh new file mode 100755 index 000000000..afd215603 --- /dev/null +++ b/hack/verify-http01proxy.sh @@ -0,0 +1,247 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Verifies that the HTTP01 Challenge Proxy feature is fully deployed and healthy. +# +# Checks: +# - CRDs present: certmanagers.operator.openshift.io, http01proxies.operator.openshift.io +# - CRs present: CertManager/cluster, HTTP01Proxy/default +# - Platform: BareMetal with distinct API and Ingress VIPs +# - Core cert-manager deployments available +# - HTTP01 Proxy DaemonSet running on all master nodes +# - RBAC: ServiceAccount, ClusterRole, ClusterRoleBinding +# - Network policies: deny-all and allow-egress +# - Proxy pod health and status conditions +# +# Usage: +# hack/verify-http01proxy.sh [--namespace NAMESPACE] [--timeout TIMEOUT] +# +# Defaults: +# NAMESPACE: cert-manager-operator +# TIMEOUT: 180s + +NAMESPACE="cert-manager-operator" +TIMEOUT="180s" +ERRORS=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --namespace|-n) NAMESPACE="$2"; shift 2;; + --timeout) TIMEOUT="$2"; shift 2;; + *) echo "Unknown arg: $1" >&2; exit 2;; + esac +done + +echo "[info] Namespace: ${NAMESPACE}" +echo "[info] Timeout: ${TIMEOUT}" +echo "" + +# ── helpers ────────────────────────────────────────────────────────────────── + +check_pass() { echo " OK"; } +check_fail() { echo " FAIL: $1"; ERRORS=$((ERRORS + 1)); } +check_warn() { echo " WARN: $1"; } + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { echo "[error] $1 not found in PATH" >&2; exit 1; } +} + +require_crd() { + local crd="$1" + echo -n "[check] CRD ${crd} ..." + if oc get crd "${crd}" >/dev/null 2>&1; then + check_pass + else + check_fail "CRD not found" + fi +} + +require_resource() { + local kind="$1" name="$2" ns="${3:--}" + if [[ "$ns" != "-" ]]; then + echo -n "[check] ${kind}/${name} -n ${ns} ..." + if oc get "$kind" "$name" -n "$ns" >/dev/null 2>&1; then + check_pass + else + check_fail "not found" + fi + else + echo -n "[check] ${kind}/${name} (cluster-scoped) ..." + if oc get "$kind" "$name" >/dev/null 2>&1; then + check_pass + else + check_fail "not found" + fi + fi +} + +wait_deploy() { + local name="$1" ns="$2" + echo "[wait] deployment/${name} -n ${ns} (${TIMEOUT})" + if ! oc -n "$ns" rollout status deploy/"$name" --timeout="$TIMEOUT" 2>/dev/null; then + check_fail "deployment/${name} not available within ${TIMEOUT}" + fi +} + +# ── preflight ──────────────────────────────────────────────────────────────── + +require_cmd oc + +echo "═══════════════════════════════════════════════════════════" +echo "[phase] Platform Validation" +echo "═══════════════════════════════════════════════════════════" + +PLATFORM=$(oc get infrastructure cluster -o jsonpath='{.status.platformStatus.type}' 2>/dev/null || echo "unknown") +echo -n "[check] Platform type ..." +if [[ "$PLATFORM" == "BareMetal" ]]; then + check_pass +else + check_fail "expected BareMetal, got ${PLATFORM}" +fi + +API_VIPS=$(oc get infrastructure cluster -o jsonpath='{.status.platformStatus.baremetal.apiServerInternalIPs}' 2>/dev/null || echo "") +echo -n "[check] API VIPs present ..." +if [[ -n "$API_VIPS" && "$API_VIPS" != "[]" ]]; then + echo " OK (${API_VIPS})" +else + check_fail "no API VIPs found" +fi + +INGRESS_VIPS=$(oc get infrastructure cluster -o jsonpath='{.status.platformStatus.baremetal.ingressIPs}' 2>/dev/null || echo "") +echo -n "[check] Ingress VIPs present ..." +if [[ -n "$INGRESS_VIPS" && "$INGRESS_VIPS" != "[]" ]]; then + echo " OK (${INGRESS_VIPS})" +else + check_fail "no Ingress VIPs found" +fi + +echo -n "[check] API and Ingress VIPs differ ..." +if [[ "$API_VIPS" != "$INGRESS_VIPS" ]]; then + check_pass +else + check_fail "API VIPs and Ingress VIPs are identical" +fi + +echo "" +echo "═══════════════════════════════════════════════════════════" +echo "[phase] CRDs" +echo "═══════════════════════════════════════════════════════════" + +require_crd certmanagers.operator.openshift.io +require_crd http01proxies.operator.openshift.io + +echo "" +echo "═══════════════════════════════════════════════════════════" +echo "[phase] Custom Resources" +echo "═══════════════════════════════════════════════════════════" + +require_resource certmanagers.operator.openshift.io cluster - +require_resource http01proxies.operator.openshift.io default "$NAMESPACE" + +echo "" +echo "═══════════════════════════════════════════════════════════" +echo "[phase] Core cert-manager Deployments" +echo "═══════════════════════════════════════════════════════════" + +wait_deploy cert-manager cert-manager +wait_deploy cert-manager-webhook cert-manager +wait_deploy cert-manager-cainjector cert-manager + +echo "" +echo "═══════════════════════════════════════════════════════════" +echo "[phase] HTTP01 Proxy DaemonSet" +echo "═══════════════════════════════════════════════════════════" + +DS_EXISTS=false +echo -n "[check] DaemonSet cert-manager-http01-proxy ..." +if oc get daemonset cert-manager-http01-proxy -n "$NAMESPACE" >/dev/null 2>&1; then + DS_EXISTS=true + check_pass +else + check_fail "DaemonSet not found" +fi + +if [[ "$DS_EXISTS" == "true" ]]; then + DESIRED=$(oc get daemonset cert-manager-http01-proxy -n "$NAMESPACE" -o jsonpath='{.status.desiredNumberScheduled}' 2>/dev/null || echo "0") + READY=$(oc get daemonset cert-manager-http01-proxy -n "$NAMESPACE" -o jsonpath='{.status.numberReady}' 2>/dev/null || echo "0") + MASTER_COUNT=$(oc get nodes -l node-role.kubernetes.io/master --no-headers 2>/dev/null | wc -l | tr -d ' ') + + echo -n "[check] DaemonSet ready (${READY}/${DESIRED}, masters: ${MASTER_COUNT}) ..." + if [[ "$READY" -gt 0 && "$READY" == "$DESIRED" ]]; then + check_pass + else + check_fail "expected ${DESIRED} ready, got ${READY}" + fi + + echo "" + echo "[info] Proxy pods:" + oc get pods -n "$NAMESPACE" -l app=cert-manager-http01-proxy -o wide 2>/dev/null || true +fi + +echo "" +echo "═══════════════════════════════════════════════════════════" +echo "[phase] RBAC" +echo "═══════════════════════════════════════════════════════════" + +require_resource serviceaccount cert-manager-http01-proxy "$NAMESPACE" +require_resource clusterrole cert-manager-http01-proxy - +require_resource clusterrolebinding cert-manager-http01-proxy - + +echo "" +echo "═══════════════════════════════════════════════════════════" +echo "[phase] Network Policies" +echo "═══════════════════════════════════════════════════════════" + +require_resource networkpolicy cert-manager-http01-proxy-deny-all "$NAMESPACE" +require_resource networkpolicy cert-manager-http01-proxy-allow-egress "$NAMESPACE" + +echo "" +echo "═══════════════════════════════════════════════════════════" +echo "[phase] HTTP01Proxy Status" +echo "═══════════════════════════════════════════════════════════" + +echo "[info] HTTP01Proxy conditions:" +oc get http01proxies.operator.openshift.io default -n "$NAMESPACE" \ + -o jsonpath='{range .status.conditions[*]} {.type}: {.status} ({.reason}) - {.message}{"\n"}{end}' 2>/dev/null || echo " (no conditions found)" + +PROXY_IMAGE=$(oc get http01proxies.operator.openshift.io default -n "$NAMESPACE" \ + -o jsonpath='{.status.proxyImage}' 2>/dev/null || echo "") +if [[ -n "$PROXY_IMAGE" ]]; then + echo "[info] Proxy image: ${PROXY_IMAGE}" +fi + +if [[ "$DS_EXISTS" == "true" ]]; then + echo "" + echo "═══════════════════════════════════════════════════════════" + echo "[phase] Proxy Pod Health" + echo "═══════════════════════════════════════════════════════════" + + NOT_RUNNING=$(oc get pods -n "$NAMESPACE" -l app=cert-manager-http01-proxy \ + --field-selector=status.phase!=Running --no-headers 2>/dev/null | wc -l | tr -d ' ') + echo -n "[check] All proxy pods Running ..." + if [[ "$NOT_RUNNING" -eq 0 ]]; then + check_pass + else + check_fail "${NOT_RUNNING} pod(s) not in Running state" + fi + + RESTART_TOTAL=$(oc get pods -n "$NAMESPACE" -l app=cert-manager-http01-proxy \ + -o jsonpath='{range .items[*]}{range .status.containerStatuses[*]}{.restartCount}{"\n"}{end}{end}' 2>/dev/null \ + | awk '{s+=$1} END {print s+0}') + echo -n "[check] No container restarts ..." + if [[ "$RESTART_TOTAL" -eq 0 ]]; then + check_pass + else + check_warn "${RESTART_TOTAL} total restart(s)" + fi +fi + +echo "" +echo "═══════════════════════════════════════════════════════════" + +if [[ "$ERRORS" -gt 0 ]]; then + echo "[FAIL] ${ERRORS} check(s) failed." + exit 1 +else + echo "[done] All checks passed." +fi diff --git a/images/ci/http01proxy.Dockerfile b/images/ci/http01proxy.Dockerfile new file mode 100644 index 000000000..fbb692694 --- /dev/null +++ b/images/ci/http01proxy.Dockerfile @@ -0,0 +1,15 @@ +FROM registry.ci.openshift.org/ocp/builder:rhel-9-golang-1.26-openshift-4.23 AS builder +WORKDIR /go/src/github.com/openshift/cert-manager-operator + +ARG GO_BUILD_TAGS=strictfipsruntime,openssl +ENV GOEXPERIMENT=strictfipsruntime +ENV CGO_ENABLED=1 + +COPY . . +RUN go build -mod=vendor -tags $GO_BUILD_TAGS -ldflags '-w -s' \ + -o /app/cert-manager-http01-proxy ./cmd/http01-proxy + +FROM registry.access.redhat.com/ubi9-minimal@sha256:062c52ff973065752b0965787649db2bcf551a6c727a00e95a3eb42cebadbdab +COPY --from=builder /app/cert-manager-http01-proxy /usr/local/bin/cert-manager-http01-proxy +USER 65532:65532 +ENTRYPOINT ["/usr/local/bin/cert-manager-http01-proxy"] diff --git a/pkg/controller/http01proxy/constants.go b/pkg/controller/http01proxy/constants.go new file mode 100644 index 000000000..beaf8adc5 --- /dev/null +++ b/pkg/controller/http01proxy/constants.go @@ -0,0 +1,63 @@ +package http01proxy + +import ( + "os" + "time" + + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/openshift/cert-manager-operator/pkg/controller/common" +) + +var ( + // infrastructureGVK is the GVK for the OpenShift Infrastructure resource. + infrastructureGVK = schema.GroupVersionKind{ + Group: "config.openshift.io", + Version: "v1", + Kind: "Infrastructure", + } +) + +const ( + http01proxyCommonName = "cert-manager-http01-proxy" + ControllerName = http01proxyCommonName + "-controller" + + controllerProcessedAnnotation = "operator.openshift.io/http01-proxy-processed" + finalizer = "http01proxy.openshift.operator.io/" + ControllerName + defaultRequeueTime = time.Second * 30 + + // CRD enforces singleton name "default". + http01proxyObjectName = "default" + + http01proxyImageNameEnvVarName = "RELATED_IMAGE_CERT_MANAGER_HTTP01PROXY" + http01proxyImageVersionEnvVarName = "HTTP01PROXY_OPERAND_IMAGE_VERSION" + + defaultInternalPort int32 = 8888 + proxyPortName = "proxy" + proxyPortEnvVar = "PROXY_PORT" +) + +var ( + controllerDefaultResourceLabels = map[string]string{ + common.ManagedResourceLabelKey: http01proxyCommonName, + "app.kubernetes.io/name": http01proxyCommonName, + "app.kubernetes.io/instance": http01proxyCommonName, + "app.kubernetes.io/version": os.Getenv(http01proxyImageVersionEnvVarName), + "app.kubernetes.io/managed-by": "cert-manager-operator", + "app.kubernetes.io/part-of": "cert-manager-operator", + } +) + +// asset names are the files present in the root bindata/ dir. +const ( + daemonsetAssetName = "http01-proxy/cert-manager-http01-proxy-daemonset.yaml" + serviceAccountAssetName = "http01-proxy/cert-manager-http01-proxy-serviceaccount.yaml" + clusterRoleAssetName = "http01-proxy/cert-manager-http01-proxy-clusterrole.yaml" + clusterRoleBindingAssetName = "http01-proxy/cert-manager-http01-proxy-clusterrolebinding.yaml" + sccRoleBindingAssetName = "http01-proxy/cert-manager-http01-proxy-scc-rolebinding.yaml" +) + +var http01ProxyNetworkPolicyAssets = []string{ + "networkpolicies/http01-proxy-deny-all-networkpolicy.yaml", + "networkpolicies/http01-proxy-allow-egress-networkpolicy.yaml", +} diff --git a/pkg/controller/http01proxy/controller.go b/pkg/controller/http01proxy/controller.go new file mode 100644 index 000000000..dbda6a092 --- /dev/null +++ b/pkg/controller/http01proxy/controller.go @@ -0,0 +1,217 @@ +package http01proxy + +import ( + "context" + "fmt" + "os" + "sync" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/go-logr/logr" + configv1 "github.com/openshift/api/config/v1" + + v1alpha1 "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + "github.com/openshift/cert-manager-operator/pkg/controller/common" +) + +// RequestEnqueueLabelValue is the label value used for filtering reconcile events. +const RequestEnqueueLabelValue = http01proxyCommonName + +// Reconciler reconciles an HTTP01Proxy object. +type Reconciler struct { + common.CtrlClient + + eventRecorder record.EventRecorder + log logr.Logger + + proxyImage string + + cachedPlatform *platformInfo + platformMu sync.Mutex +} + +// +kubebuilder:rbac:groups=operator.openshift.io,resources=http01proxies,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=operator.openshift.io,resources=http01proxies/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=operator.openshift.io,resources=http01proxies/finalizers,verbs=update +// +kubebuilder:rbac:groups=networking.k8s.io,resources=networkpolicies,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=apps,resources=daemonsets,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles;clusterrolebindings,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=config.openshift.io,resources=clusterversions;ingresses;infrastructures,verbs=get;list;watch +// +kubebuilder:rbac:groups=security.openshift.io,resources=securitycontextconstraints,resourceNames=privileged,verbs=use + +// New returns a new Reconciler instance. +func New(mgr ctrl.Manager) (*Reconciler, error) { + c, err := common.NewClient(mgr) + if err != nil { + return nil, err + } + return &Reconciler{ + CtrlClient: c, + eventRecorder: mgr.GetEventRecorderFor(ControllerName), + log: ctrl.Log.WithName(ControllerName), + proxyImage: os.Getenv(http01proxyImageNameEnvVarName), + }, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { + mapFunc := func(ctx context.Context, obj client.Object) []reconcile.Request { + r.log.V(4).Info("received reconcile event", "object", fmt.Sprintf("%T", obj), "name", obj.GetName(), "namespace", obj.GetNamespace()) + + objLabels := obj.GetLabels() + if objLabels != nil && objLabels[common.ManagedResourceLabelKey] == RequestEnqueueLabelValue { + namespace := obj.GetNamespace() + if namespace == "" { + namespace = common.OperatorNamespace + } + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Name: http01proxyObjectName, + Namespace: namespace, + }, + }, + } + } + + r.log.V(4).Info("object not of interest, ignoring", "object", fmt.Sprintf("%T", obj), "name", obj.GetName()) + return []reconcile.Request{} + } + + controllerManagedResources := predicate.NewPredicateFuncs(func(object client.Object) bool { + return object.GetLabels() != nil && object.GetLabels()[common.ManagedResourceLabelKey] == RequestEnqueueLabelValue + }) + + controllerManagedResourcePredicates := builder.WithPredicates(controllerManagedResources) + withIgnoreStatusUpdatePredicates := builder.WithPredicates(predicate.GenerationChangedPredicate{}, controllerManagedResources) + + // Infrastructure watch: invalidate platform cache and trigger reconcile when Infrastructure changes + infrastructureMapFunc := func(ctx context.Context, obj client.Object) []reconcile.Request { + if obj.GetName() != "cluster" { + return []reconcile.Request{} + } + // Invalidate platform cache when Infrastructure changes + r.platformMu.Lock() + r.cachedPlatform = nil + r.platformMu.Unlock() + r.log.V(2).Info("infrastructure/cluster changed, invalidated platform cache") + + // Trigger reconcile for the HTTP01Proxy singleton + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Name: http01proxyObjectName, + Namespace: common.OperatorNamespace, + }, + }, + } + } + + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.HTTP01Proxy{}). + Named(ControllerName). + Watches(&appsv1.DaemonSet{}, handler.EnqueueRequestsFromMapFunc(mapFunc), withIgnoreStatusUpdatePredicates). + Watches(&rbacv1.ClusterRole{}, handler.EnqueueRequestsFromMapFunc(mapFunc), controllerManagedResourcePredicates). + Watches(&rbacv1.ClusterRoleBinding{}, handler.EnqueueRequestsFromMapFunc(mapFunc), controllerManagedResourcePredicates). + Watches(&corev1.ServiceAccount{}, handler.EnqueueRequestsFromMapFunc(mapFunc), controllerManagedResourcePredicates). + Watches(&networkingv1.NetworkPolicy{}, handler.EnqueueRequestsFromMapFunc(mapFunc), controllerManagedResourcePredicates). + Watches(&configv1.Infrastructure{}, handler.EnqueueRequestsFromMapFunc(infrastructureMapFunc)). + Complete(r) +} + +// Reconcile compares the state specified by the HTTP01Proxy object against the actual cluster state. +func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + r.log.V(1).Info("reconciling", "request", req) + + if req.Namespace != common.OperatorNamespace { + r.log.V(1).Info("ignoring http01proxy in unexpected namespace", "namespace", req.Namespace, "expected", common.OperatorNamespace) + return ctrl.Result{}, nil + } + + proxy := &v1alpha1.HTTP01Proxy{} + if err := r.Get(ctx, req.NamespacedName, proxy); err != nil { + if errors.IsNotFound(err) { + r.log.V(1).Info("http01proxy object not found, skipping reconciliation", "request", req) + return ctrl.Result{}, nil + } + return ctrl.Result{}, fmt.Errorf("failed to fetch http01proxy %q during reconciliation: %w", req.NamespacedName, err) + } + + if !proxy.DeletionTimestamp.IsZero() { + r.log.V(1).Info("http01proxy is marked for deletion", "namespace", req.NamespacedName) + + if err := r.cleanUp(ctx, proxy); err != nil { + return ctrl.Result{}, fmt.Errorf("clean up failed for %q http01proxy deletion: %w", req.NamespacedName, err) + } + + if err := r.removeFinalizer(ctx, proxy); err != nil { + return ctrl.Result{}, err + } + + r.log.V(1).Info("removed finalizer, cleanup complete", "request", req.NamespacedName) + return ctrl.Result{}, nil + } + + if err := r.addFinalizer(ctx, proxy); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to update %q http01proxy with finalizers: %w", req.NamespacedName, err) + } + + return r.processReconcileRequest(ctx, proxy, req.NamespacedName) +} + +func (r *Reconciler) processReconcileRequest(ctx context.Context, proxy *v1alpha1.HTTP01Proxy, req types.NamespacedName) (ctrl.Result, error) { + if !common.ContainsAnnotation(proxy, controllerProcessedAnnotation) && len(proxy.Status.Conditions) == 0 { + r.log.V(1).Info("starting reconciliation of newly created http01proxy", "namespace", proxy.GetNamespace(), "name", proxy.GetName()) + } + + reconcileErr := r.reconcileHTTP01ProxyDeployment(ctx, proxy) + if reconcileErr != nil { + r.log.Error(reconcileErr, "failed to reconcile HTTP01Proxy deployment", "request", req) + } + + return common.HandleReconcileResult( + &proxy.Status.ConditionalStatus, + reconcileErr, + r.log.WithValues("namespace", proxy.GetNamespace(), "name", proxy.GetName()), + func(prependErr error) error { + return r.updateCondition(ctx, proxy, prependErr) + }, + defaultRequeueTime, + ) +} + +// cleanUp handles deletion of http01proxy gracefully. +func (r *Reconciler) cleanUp(ctx context.Context, proxy *v1alpha1.HTTP01Proxy) error { + r.log.V(1).Info("cleaning up http01proxy resources", "namespace", proxy.GetNamespace(), "name", proxy.GetName()) + r.eventRecorder.Eventf(proxy, corev1.EventTypeNormal, "CleanUp", "cleaning up resources for http01proxy %s/%s", proxy.GetNamespace(), proxy.GetName()) + + if err := r.deleteDaemonSet(ctx, proxy); err != nil { + return fmt.Errorf("failed to delete daemonset: %w", err) + } + if err := r.deleteServiceAccount(ctx, proxy); err != nil { + return fmt.Errorf("failed to delete serviceaccount: %w", err) + } + if err := r.deleteRBACResources(ctx); err != nil { + return fmt.Errorf("failed to delete rbac resources: %w", err) + } + if err := r.deleteNetworkPolicies(ctx, proxy); err != nil { + return fmt.Errorf("failed to delete network policies: %w", err) + } + + return nil +} diff --git a/pkg/controller/http01proxy/controller_test.go b/pkg/controller/http01proxy/controller_test.go new file mode 100644 index 000000000..81b0fb2df --- /dev/null +++ b/pkg/controller/http01proxy/controller_test.go @@ -0,0 +1,178 @@ +package http01proxy + +import ( + "context" + "fmt" + "strings" + "testing" + + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + + "github.com/go-logr/logr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + v1alpha1 "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + "github.com/openshift/cert-manager-operator/pkg/controller/common" + "github.com/openshift/cert-manager-operator/pkg/controller/common/fakes" +) + +func newTestReconciler(client *fakes.FakeCtrlClient) *Reconciler { + return &Reconciler{ + CtrlClient: client, + eventRecorder: record.NewFakeRecorder(10), + log: logr.Discard(), + proxyImage: "quay.io/test/proxy:v1", + } +} + +func TestReconcileWrongNamespace(t *testing.T) { + fakeClient := &fakes.FakeCtrlClient{} + r := newTestReconciler(fakeClient) + + result, err := r.Reconcile(context.Background(), ctrl.Request{ + NamespacedName: types.NamespacedName{Name: "default", Namespace: "wrong-namespace"}, + }) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result != (ctrl.Result{}) { + t.Errorf("expected empty result, got %v", result) + } + if fakeClient.GetCallCount() != 0 { + t.Errorf("expected no Get calls for wrong namespace, got %d", fakeClient.GetCallCount()) + } +} + +func TestReconcileNotFound(t *testing.T) { + fakeClient := &fakes.FakeCtrlClient{} + fakeClient.GetReturns(errors.NewNotFound(schema.GroupResource{Group: "operator.openshift.io", Resource: "http01proxies"}, http01proxyObjectName)) + + r := newTestReconciler(fakeClient) + + result, err := r.Reconcile(context.Background(), ctrl.Request{ + NamespacedName: types.NamespacedName{Name: http01proxyObjectName, Namespace: common.OperatorNamespace}, + }) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result != (ctrl.Result{}) { + t.Errorf("expected empty result, got %v", result) + } +} + +func TestReconcileGetError(t *testing.T) { + fakeClient := &fakes.FakeCtrlClient{} + fakeClient.GetReturns(fmt.Errorf("api server unavailable")) + + r := newTestReconciler(fakeClient) + + _, err := r.Reconcile(context.Background(), ctrl.Request{ + NamespacedName: types.NamespacedName{Name: http01proxyObjectName, Namespace: common.OperatorNamespace}, + }) + + if err == nil { + t.Fatal("expected error for Get failure") + } + if !strings.Contains(err.Error(), "failed to fetch") { + t.Errorf("error = %q, want substring %q", err.Error(), "failed to fetch") + } +} + +func TestReconcileDeletion(t *testing.T) { + fakeClient := &fakes.FakeCtrlClient{} + now := metav1.Now() + callCount := 0 + fakeClient.GetStub = func(_ context.Context, _ client.ObjectKey, obj client.Object) error { + callCount++ + proxy := obj.(*v1alpha1.HTTP01Proxy) + proxy.Name = http01proxyObjectName + proxy.Namespace = common.OperatorNamespace + proxy.DeletionTimestamp = &now + proxy.Finalizers = []string{finalizer} + return nil + } + fakeClient.DeleteReturns(nil) + fakeClient.UpdateWithRetryReturns(nil) + + r := newTestReconciler(fakeClient) + + result, err := r.Reconcile(context.Background(), ctrl.Request{ + NamespacedName: types.NamespacedName{Name: http01proxyObjectName, Namespace: common.OperatorNamespace}, + }) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result != (ctrl.Result{}) { + t.Errorf("expected empty result after deletion, got %v", result) + } + if fakeClient.DeleteCallCount() == 0 { + t.Error("expected Delete to be called during cleanup") + } +} + +func TestCleanUpCallsAllDeletes(t *testing.T) { + fakeClient := &fakes.FakeCtrlClient{} + fakeClient.DeleteReturns(nil) + + r := newTestReconciler(fakeClient) + proxy := &v1alpha1.HTTP01Proxy{} + proxy.SetName(http01proxyObjectName) + proxy.SetNamespace(common.OperatorNamespace) + + err := r.cleanUp(context.Background(), proxy) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Should delete: DaemonSet, ServiceAccount, 2 ClusterRoleBindings, 1 ClusterRole, 2 NetworkPolicies = 7 + if fakeClient.DeleteCallCount() < 7 { + t.Errorf("expected at least 7 Delete calls, got %d", fakeClient.DeleteCallCount()) + } +} + +func TestCleanUpDaemonSetDeleteFails(t *testing.T) { + fakeClient := &fakes.FakeCtrlClient{} + fakeClient.DeleteReturnsOnCall(0, fmt.Errorf("delete failed")) + + r := newTestReconciler(fakeClient) + proxy := &v1alpha1.HTTP01Proxy{} + proxy.SetName(http01proxyObjectName) + proxy.SetNamespace(common.OperatorNamespace) + + err := r.cleanUp(context.Background(), proxy) + if err == nil { + t.Fatal("expected error when DaemonSet delete fails") + } + if !strings.Contains(err.Error(), "daemonset") { + t.Errorf("error = %q, want substring %q", err.Error(), "daemonset") + } +} + +func TestCleanUpServiceAccountDeleteFails(t *testing.T) { + fakeClient := &fakes.FakeCtrlClient{} + // First delete (DaemonSet) succeeds + fakeClient.DeleteReturnsOnCall(0, nil) + // Second delete (ServiceAccount) fails + fakeClient.DeleteReturnsOnCall(1, fmt.Errorf("sa delete failed")) + + r := newTestReconciler(fakeClient) + proxy := &v1alpha1.HTTP01Proxy{} + proxy.SetName(http01proxyObjectName) + proxy.SetNamespace(common.OperatorNamespace) + + err := r.cleanUp(context.Background(), proxy) + if err == nil { + t.Fatal("expected error when ServiceAccount delete fails") + } + if !strings.Contains(err.Error(), "serviceaccount") { + t.Errorf("error = %q, want substring %q", err.Error(), "serviceaccount") + } +} diff --git a/pkg/controller/http01proxy/daemonsets.go b/pkg/controller/http01proxy/daemonsets.go new file mode 100644 index 000000000..e29e2964f --- /dev/null +++ b/pkg/controller/http01proxy/daemonsets.go @@ -0,0 +1,94 @@ +package http01proxy + +import ( + "context" + "fmt" + "maps" + "strconv" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + "github.com/openshift/cert-manager-operator/pkg/controller/common" + "github.com/openshift/cert-manager-operator/pkg/operator/assets" +) + +func (r *Reconciler) createOrApplyDaemonSet(ctx context.Context, proxy *v1alpha1.HTTP01Proxy, resourceLabels map[string]string) error { + desired, err := r.getDaemonSetObject(proxy, resourceLabels) + if err != nil { + return common.NewIrrecoverableError(err, "failed to build daemonset object") + } + + if err := r.createOrUpdateResource(ctx, desired); err != nil { + return err + } + + r.updateImageInStatus(proxy, desired) + return nil +} + +func (r *Reconciler) getDaemonSetObject(proxy *v1alpha1.HTTP01Proxy, resourceLabels map[string]string) (*appsv1.DaemonSet, error) { + ds := common.DecodeObjBytes[*appsv1.DaemonSet](codecs, appsv1.SchemeGroupVersion, assets.MustAsset(daemonsetAssetName)) + + ds.SetNamespace(proxy.GetNamespace()) + common.UpdateResourceLabels(ds, resourceLabels) + if ds.Spec.Template.Labels == nil { + ds.Spec.Template.Labels = make(map[string]string) + } + maps.Copy(ds.Spec.Template.Labels, resourceLabels) + + if r.proxyImage == "" { + return nil, fmt.Errorf("environment variable %s is not set", http01proxyImageNameEnvVarName) + } + if len(ds.Spec.Template.Spec.Containers) == 0 { + return nil, fmt.Errorf("DaemonSet asset %s has no containers defined", daemonsetAssetName) + } + ds.Spec.Template.Spec.Containers[0].Image = r.proxyImage + + port := r.getInternalPort(proxy) + r.updateDaemonSetPort(ds, port) + + return ds, nil +} + +func (r *Reconciler) updateDaemonSetPort(ds *appsv1.DaemonSet, port int32) { + container := &ds.Spec.Template.Spec.Containers[0] + + for i := range container.Ports { + if container.Ports[i].Name == proxyPortName { + container.Ports[i].ContainerPort = port + container.Ports[i].HostPort = port + } + } + + portStr := strconv.FormatInt(int64(port), 10) + envUpdated := false + for i := range container.Env { + if container.Env[i].Name == proxyPortEnvVar { + container.Env[i].Value = portStr + envUpdated = true + break + } + } + if !envUpdated { + container.Env = append(container.Env, corev1.EnvVar{ + Name: proxyPortEnvVar, + Value: portStr, + }) + } +} + +func (r *Reconciler) updateImageInStatus(proxy *v1alpha1.HTTP01Proxy, ds *appsv1.DaemonSet) { + if len(ds.Spec.Template.Spec.Containers) > 0 { + proxy.Status.ProxyImage = ds.Spec.Template.Spec.Containers[0].Image + } +} + +func (r *Reconciler) deleteDaemonSet(ctx context.Context, proxy *v1alpha1.HTTP01Proxy) error { + return r.deleteIfExists(ctx, &appsv1.DaemonSet{}, client.ObjectKey{ + Namespace: proxy.GetNamespace(), + Name: http01proxyCommonName, + }) +} diff --git a/pkg/controller/http01proxy/daemonsets_test.go b/pkg/controller/http01proxy/daemonsets_test.go new file mode 100644 index 000000000..ea1ae402b --- /dev/null +++ b/pkg/controller/http01proxy/daemonsets_test.go @@ -0,0 +1,256 @@ +package http01proxy + +import ( + "strings" + "testing" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + + "github.com/go-logr/logr" + + "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + "github.com/openshift/cert-manager-operator/pkg/controller/common" +) + +func newDefaultProxy() *v1alpha1.HTTP01Proxy { + proxy := &v1alpha1.HTTP01Proxy{ + Spec: v1alpha1.HTTP01ProxySpec{Mode: v1alpha1.HTTP01ProxyModeDefault}, + } + proxy.SetNamespace(common.OperatorNamespace) + return proxy +} + +func TestGetDaemonSetObject(t *testing.T) { + r := &Reconciler{ + log: logr.Discard(), + proxyImage: "quay.io/test/proxy:v1", + } + + t.Run("empty proxy image returns error", func(t *testing.T) { + noImage := &Reconciler{log: logr.Discard(), proxyImage: ""} + _, err := noImage.getDaemonSetObject(newDefaultProxy(), map[string]string{"app": "test"}) + if err == nil { + t.Fatal("expected error for empty proxyImage") + } + if !strings.Contains(err.Error(), http01proxyImageNameEnvVarName) { + t.Errorf("error = %q, want mention of env var %q", err.Error(), http01proxyImageNameEnvVarName) + } + }) + + t.Run("valid build with default mode", func(t *testing.T) { + ds, err := r.getDaemonSetObject(newDefaultProxy(), map[string]string{"app": "test"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if ds.GetNamespace() != common.OperatorNamespace { + t.Errorf("namespace = %q, want %q", ds.GetNamespace(), common.OperatorNamespace) + } + if len(ds.Spec.Template.Spec.Containers) == 0 { + t.Fatal("expected at least one container") + } + if ds.Spec.Template.Spec.Containers[0].Image != "quay.io/test/proxy:v1" { + t.Errorf("image = %q, want %q", ds.Spec.Template.Spec.Containers[0].Image, "quay.io/test/proxy:v1") + } + }) + + t.Run("custom mode with custom port", func(t *testing.T) { + proxy := &v1alpha1.HTTP01Proxy{ + Spec: v1alpha1.HTTP01ProxySpec{ + Mode: v1alpha1.HTTP01ProxyModeCustom, + CustomDeployment: &v1alpha1.HTTP01ProxyCustomDeploymentSpec{InternalPort: 9999}, + }, + } + proxy.SetNamespace(common.OperatorNamespace) + + ds, err := r.getDaemonSetObject(proxy, map[string]string{"app": "test"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + found := false + for _, port := range ds.Spec.Template.Spec.Containers[0].Ports { + if port.Name == proxyPortName { + if port.ContainerPort != 9999 { + t.Errorf("container port = %d, want %d", port.ContainerPort, 9999) + } + if port.HostPort != 9999 { + t.Errorf("host port = %d, want %d", port.HostPort, 9999) + } + found = true + } + } + if !found { + t.Error("expected port with name 'proxy' in container ports") + } + }) + + t.Run("labels propagated to template", func(t *testing.T) { + labels := map[string]string{"custom-label": "value"} + ds, err := r.getDaemonSetObject(newDefaultProxy(), labels) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if ds.Spec.Template.Labels["custom-label"] != "value" { + t.Error("expected custom label to be propagated to template labels") + } + }) +} + +func TestUpdateDaemonSetPort(t *testing.T) { + r := &Reconciler{} + + t.Run("updates existing proxy port", func(t *testing.T) { + ds := &appsv1.DaemonSet{ + Spec: appsv1.DaemonSetSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "proxy", + Ports: []corev1.ContainerPort{{Name: proxyPortName, ContainerPort: 8888, HostPort: 8888}}, + Env: []corev1.EnvVar{{Name: proxyPortEnvVar, Value: "8888"}}, + }}, + }, + }, + }, + } + + r.updateDaemonSetPort(ds, 9999) + + container := ds.Spec.Template.Spec.Containers[0] + portFound := false + for _, port := range container.Ports { + if port.Name == proxyPortName { + if port.ContainerPort != 9999 { + t.Errorf("container port = %d, want 9999", port.ContainerPort) + } + if port.HostPort != 9999 { + t.Errorf("host port = %d, want 9999", port.HostPort) + } + portFound = true + } + } + if !portFound { + t.Error("expected port with name 'proxy' in container ports") + } + envFound := false + for _, env := range container.Env { + if env.Name == proxyPortEnvVar { + if env.Value != "9999" { + t.Errorf("env value = %q, want %q", env.Value, "9999") + } + envFound = true + } + } + if !envFound { + t.Error("expected PROXY_PORT env var") + } + }) + + t.Run("appends env var when missing", func(t *testing.T) { + ds := &appsv1.DaemonSet{ + Spec: appsv1.DaemonSetSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "proxy", + Ports: []corev1.ContainerPort{{Name: proxyPortName, ContainerPort: 8888, HostPort: 8888}}, + Env: []corev1.EnvVar{}, + }}, + }, + }, + }, + } + + r.updateDaemonSetPort(ds, 7777) + + container := ds.Spec.Template.Spec.Containers[0] + found := false + for _, env := range container.Env { + if env.Name == proxyPortEnvVar { + if env.Value != "7777" { + t.Errorf("env value = %q, want %q", env.Value, "7777") + } + found = true + } + } + if !found { + t.Error("expected PROXY_PORT env var to be appended") + } + }) + + t.Run("does not modify non-proxy ports", func(t *testing.T) { + ds := &appsv1.DaemonSet{ + Spec: appsv1.DaemonSetSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "proxy", + Ports: []corev1.ContainerPort{ + {Name: "metrics", ContainerPort: 9090, HostPort: 9090}, + {Name: proxyPortName, ContainerPort: 8888, HostPort: 8888}, + }, + Env: []corev1.EnvVar{{Name: proxyPortEnvVar, Value: "8888"}}, + }}, + }, + }, + }, + } + + r.updateDaemonSetPort(ds, 5555) + + container := ds.Spec.Template.Spec.Containers[0] + for _, port := range container.Ports { + if port.Name == "metrics" { + if port.ContainerPort != 9090 { + t.Errorf("metrics port should be unchanged, got %d", port.ContainerPort) + } + } + } + }) +} + +func TestUpdateImageInStatus(t *testing.T) { + r := &Reconciler{} + + t.Run("sets proxy image from daemonset container", func(t *testing.T) { + proxy := &v1alpha1.HTTP01Proxy{} + ds := &appsv1.DaemonSet{ + Spec: appsv1.DaemonSetSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Image: "quay.io/proxy:v2"}}, + }, + }, + }, + } + + r.updateImageInStatus(proxy, ds) + + if proxy.Status.ProxyImage != "quay.io/proxy:v2" { + t.Errorf("ProxyImage = %q, want %q", proxy.Status.ProxyImage, "quay.io/proxy:v2") + } + }) + + t.Run("no-op when no containers", func(t *testing.T) { + proxy := &v1alpha1.HTTP01Proxy{} + proxy.Status.ProxyImage = "original" + ds := &appsv1.DaemonSet{ + Spec: appsv1.DaemonSetSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{}, + }, + }, + }, + } + + r.updateImageInStatus(proxy, ds) + + if proxy.Status.ProxyImage != "original" { + t.Errorf("ProxyImage should be unchanged, got %q", proxy.Status.ProxyImage) + } + }) +} diff --git a/pkg/controller/http01proxy/infrastructure.go b/pkg/controller/http01proxy/infrastructure.go new file mode 100644 index 000000000..e1a58fc5e --- /dev/null +++ b/pkg/controller/http01proxy/infrastructure.go @@ -0,0 +1,102 @@ +package http01proxy + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" +) + +const ( + // platformBareMetal is the platform type for baremetal clusters. + platformBareMetal = "BareMetal" +) + +// platformInfo holds the discovered platform details needed to decide +// whether the HTTP01 proxy should be deployed. +type platformInfo struct { + platformType string + apiVIPs []string + ingressVIPs []string +} + +// getOrDiscoverPlatform returns cached platform info, or fetches it on first call. +func (r *Reconciler) getOrDiscoverPlatform(ctx context.Context) (*platformInfo, error) { + r.platformMu.Lock() + defer r.platformMu.Unlock() + if r.cachedPlatform != nil { + return r.cachedPlatform, nil + } + info, err := r.discoverPlatform(ctx) + if err != nil { + return nil, err + } + r.cachedPlatform = info + return info, nil +} + +// discoverPlatform reads the Infrastructure CR and returns platform details. +func (r *Reconciler) discoverPlatform(ctx context.Context) (*platformInfo, error) { + infra := &unstructured.Unstructured{} + infra.SetGroupVersionKind(infrastructureGVK) + + if err := r.Get(ctx, types.NamespacedName{Name: "cluster"}, infra); err != nil { + return nil, fmt.Errorf("failed to get infrastructure/cluster: %w", err) + } + + platformType, found, err := unstructured.NestedString(infra.Object, "status", "platformStatus", "type") + if err != nil { + return nil, fmt.Errorf("failed to parse infrastructure status.platformStatus.type: %w", err) + } + if !found { + return nil, fmt.Errorf("infrastructure status.platformStatus.type not found") + } + + info := &platformInfo{ + platformType: platformType, + } + + switch platformType { + case platformBareMetal: + apiVIPs, _, err := unstructured.NestedStringSlice(infra.Object, "status", "platformStatus", "baremetal", "apiServerInternalIPs") + if err != nil { + return nil, fmt.Errorf("failed to parse baremetal.apiServerInternalIPs: %w", err) + } + ingressVIPs, _, err := unstructured.NestedStringSlice(infra.Object, "status", "platformStatus", "baremetal", "ingressIPs") + if err != nil { + return nil, fmt.Errorf("failed to parse baremetal.ingressIPs: %w", err) + } + info.apiVIPs = apiVIPs + info.ingressVIPs = ingressVIPs + } + + return info, nil +} + +// validatePlatform checks whether the platform supports HTTP01 proxy deployment. +// Returns a human-readable reason if the platform is not supported, or empty string if OK. +func validatePlatform(info *platformInfo) string { + if info.platformType != platformBareMetal { + return fmt.Sprintf("platform type %q is not supported; HTTP01 proxy is only supported on BareMetal platforms", info.platformType) + } + + if len(info.apiVIPs) == 0 { + return "no API server VIPs found in infrastructure status; cannot deploy HTTP01 proxy" + } + + if len(info.ingressVIPs) == 0 { + return "no ingress VIPs found in infrastructure status; cannot deploy HTTP01 proxy" + } + + // If any API VIP equals any ingress VIP, proxy is not needed + for _, apiVIP := range info.apiVIPs { + for _, ingressVIP := range info.ingressVIPs { + if apiVIP == ingressVIP { + return fmt.Sprintf("API VIP (%s) and ingress VIP (%s) are the same; HTTP01 proxy is not needed", apiVIP, ingressVIP) + } + } + } + + return "" +} diff --git a/pkg/controller/http01proxy/infrastructure_test.go b/pkg/controller/http01proxy/infrastructure_test.go new file mode 100644 index 000000000..ff07d6182 --- /dev/null +++ b/pkg/controller/http01proxy/infrastructure_test.go @@ -0,0 +1,293 @@ +package http01proxy + +import ( + "context" + "fmt" + "strings" + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/go-logr/logr" + + "github.com/openshift/cert-manager-operator/pkg/controller/common/fakes" +) + +func TestValidatePlatform(t *testing.T) { + tests := []struct { + name string + info *platformInfo + wantMsg string + wantEmpty bool + }{ + { + name: "non-baremetal platform", + info: &platformInfo{platformType: "AWS"}, + wantMsg: "not supported", + }, + { + name: "baremetal no API VIPs", + info: &platformInfo{platformType: "BareMetal", apiVIPs: nil, ingressVIPs: []string{"10.0.0.2"}}, + wantMsg: "no API server VIPs", + }, + { + name: "baremetal no ingress VIPs", + info: &platformInfo{platformType: "BareMetal", apiVIPs: []string{"10.0.0.1"}, ingressVIPs: nil}, + wantMsg: "no ingress VIPs", + }, + { + name: "baremetal overlapping VIPs", + info: &platformInfo{platformType: "BareMetal", apiVIPs: []string{"10.0.0.1"}, ingressVIPs: []string{"10.0.0.1"}}, + wantMsg: "are the same", + }, + { + name: "baremetal valid distinct VIPs", + info: &platformInfo{platformType: "BareMetal", apiVIPs: []string{"10.0.0.1"}, ingressVIPs: []string{"10.0.0.2"}}, + wantEmpty: true, + }, + { + name: "baremetal multiple distinct VIPs", + info: &platformInfo{platformType: "BareMetal", apiVIPs: []string{"10.0.0.1", "fd00::1"}, ingressVIPs: []string{"10.0.0.2", "fd00::2"}}, + wantEmpty: true, + }, + { + name: "baremetal one overlapping pair among multiple", + info: &platformInfo{platformType: "BareMetal", apiVIPs: []string{"10.0.0.1", "10.0.0.3"}, ingressVIPs: []string{"10.0.0.2", "10.0.0.1"}}, + wantMsg: "are the same", + }, + { + name: "empty platform type", + info: &platformInfo{platformType: ""}, + wantMsg: "not supported", + }, + { + name: "None platform type", + info: &platformInfo{platformType: "None"}, + wantMsg: "not supported", + }, + { + name: "baremetal empty VIP slices", + info: &platformInfo{platformType: "BareMetal", apiVIPs: []string{}, ingressVIPs: []string{"10.0.0.2"}}, + wantMsg: "no API server VIPs", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := validatePlatform(tt.info) + if tt.wantEmpty { + if got != "" { + t.Errorf("validatePlatform() = %q, want empty string", got) + } + return + } + if got == "" { + t.Error("validatePlatform() = empty string, want non-empty error message") + return + } + if !strings.Contains(got, tt.wantMsg) { + t.Errorf("validatePlatform() = %q, want substring %q", got, tt.wantMsg) + } + }) + } +} + +func TestDiscoverPlatform(t *testing.T) { + tests := []struct { + name string + getStub func(context.Context, client.ObjectKey, client.Object) error + wantErr bool + wantErrMsg string + wantPlatform string + wantAPIVIPs int + }{ + { + name: "Get error", + getStub: func(_ context.Context, _ client.ObjectKey, _ client.Object) error { + return fmt.Errorf("connection refused") + }, + wantErr: true, + wantErrMsg: "failed to get infrastructure/cluster", + }, + { + name: "missing platformStatus.type", + getStub: func(_ context.Context, _ client.ObjectKey, obj client.Object) error { + u := obj.(*unstructured.Unstructured) + u.Object = map[string]interface{}{ + "status": map[string]interface{}{}, + } + return nil + }, + wantErr: true, + wantErrMsg: "not found", + }, + { + name: "non-BareMetal platform", + getStub: func(_ context.Context, _ client.ObjectKey, obj client.Object) error { + u := obj.(*unstructured.Unstructured) + u.Object = map[string]interface{}{ + "status": map[string]interface{}{ + "platformStatus": map[string]interface{}{ + "type": "AWS", + }, + }, + } + return nil + }, + wantPlatform: "AWS", + wantAPIVIPs: 0, + }, + { + name: "BareMetal with VIPs", + getStub: func(_ context.Context, _ client.ObjectKey, obj client.Object) error { + u := obj.(*unstructured.Unstructured) + u.Object = map[string]interface{}{ + "status": map[string]interface{}{ + "platformStatus": map[string]interface{}{ + "type": "BareMetal", + "baremetal": map[string]interface{}{ + "apiServerInternalIPs": []interface{}{"10.0.0.1"}, + "ingressIPs": []interface{}{"10.0.0.2"}, + }, + }, + }, + } + return nil + }, + wantPlatform: "BareMetal", + wantAPIVIPs: 1, + }, + { + name: "BareMetal without VIP fields", + getStub: func(_ context.Context, _ client.ObjectKey, obj client.Object) error { + u := obj.(*unstructured.Unstructured) + u.Object = map[string]interface{}{ + "status": map[string]interface{}{ + "platformStatus": map[string]interface{}{ + "type": "BareMetal", + "baremetal": map[string]interface{}{}, + }, + }, + } + return nil + }, + wantPlatform: "BareMetal", + wantAPIVIPs: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeClient := &fakes.FakeCtrlClient{} + fakeClient.GetStub = tt.getStub + r := &Reconciler{ + CtrlClient: fakeClient, + log: logr.Discard(), + } + + info, err := r.discoverPlatform(context.Background()) + if tt.wantErr { + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), tt.wantErrMsg) { + t.Errorf("error = %q, want substring %q", err.Error(), tt.wantErrMsg) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if info.platformType != tt.wantPlatform { + t.Errorf("platformType = %q, want %q", info.platformType, tt.wantPlatform) + } + if len(info.apiVIPs) != tt.wantAPIVIPs { + t.Errorf("len(apiVIPs) = %d, want %d", len(info.apiVIPs), tt.wantAPIVIPs) + } + }) + } +} + +func TestGetOrDiscoverPlatform(t *testing.T) { + t.Run("returns cached platform without calling Get", func(t *testing.T) { + fakeClient := &fakes.FakeCtrlClient{} + cached := &platformInfo{platformType: "BareMetal", apiVIPs: []string{"10.0.0.1"}, ingressVIPs: []string{"10.0.0.2"}} + r := &Reconciler{ + CtrlClient: fakeClient, + log: logr.Discard(), + cachedPlatform: cached, + } + + info, err := r.getOrDiscoverPlatform(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if info != cached { + t.Error("expected cached platform to be returned") + } + if fakeClient.GetCallCount() != 0 { + t.Errorf("expected 0 Get calls with cached platform, got %d", fakeClient.GetCallCount()) + } + }) + + t.Run("discovers and caches on first call", func(t *testing.T) { + fakeClient := &fakes.FakeCtrlClient{} + fakeClient.GetStub = func(_ context.Context, _ client.ObjectKey, obj client.Object) error { + u := obj.(*unstructured.Unstructured) + u.Object = map[string]interface{}{ + "status": map[string]interface{}{ + "platformStatus": map[string]interface{}{ + "type": "AWS", + }, + }, + } + return nil + } + r := &Reconciler{ + CtrlClient: fakeClient, + log: logr.Discard(), + } + + info, err := r.getOrDiscoverPlatform(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if info.platformType != "AWS" { + t.Errorf("platformType = %q, want %q", info.platformType, "AWS") + } + if r.cachedPlatform == nil { + t.Error("expected cachedPlatform to be set") + } + + // Second call should use cache + info2, err := r.getOrDiscoverPlatform(context.Background()) + if err != nil { + t.Fatalf("unexpected error on second call: %v", err) + } + if info2 != info { + t.Error("second call should return same cached pointer") + } + if fakeClient.GetCallCount() != 1 { + t.Errorf("expected 1 Get call total (cached second time), got %d", fakeClient.GetCallCount()) + } + }) + + t.Run("does not cache on error", func(t *testing.T) { + fakeClient := &fakes.FakeCtrlClient{} + fakeClient.GetReturns(fmt.Errorf("api unavailable")) + r := &Reconciler{ + CtrlClient: fakeClient, + log: logr.Discard(), + } + + _, err := r.getOrDiscoverPlatform(context.Background()) + if err == nil { + t.Fatal("expected error") + } + if r.cachedPlatform != nil { + t.Error("cachedPlatform should remain nil on error") + } + }) +} + diff --git a/pkg/controller/http01proxy/install_http01proxy.go b/pkg/controller/http01proxy/install_http01proxy.go new file mode 100644 index 000000000..036d32bfd --- /dev/null +++ b/pkg/controller/http01proxy/install_http01proxy.go @@ -0,0 +1,57 @@ +package http01proxy + +import ( + "context" + "fmt" + "maps" + + "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + "github.com/openshift/cert-manager-operator/pkg/controller/common" +) + +func (r *Reconciler) reconcileHTTP01ProxyDeployment(ctx context.Context, proxy *v1alpha1.HTTP01Proxy) error { + info, err := r.getOrDiscoverPlatform(ctx) + if err != nil { + return common.NewRetryRequiredError(err, "failed to discover platform") + } + + if reason := validatePlatform(info); reason != "" { + r.log.V(1).Info("platform not supported for HTTP01 proxy", "reason", reason, "platformType", info.platformType) + if err := r.cleanUp(ctx, proxy); err != nil { + r.log.Error(err, "failed to clean up resources after platform validation failure") + } + return common.NewIrrecoverableError(fmt.Errorf("platform validation failed"), "%s", reason) + } + + resourceLabels := make(map[string]string) + maps.Copy(resourceLabels, controllerDefaultResourceLabels) + + if err := r.createOrApplyNetworkPolicies(ctx, proxy, resourceLabels); err != nil { + r.log.Error(err, "failed to reconcile network policy resources") + return err + } + + if err := r.createOrApplyServiceAccount(ctx, proxy, resourceLabels); err != nil { + r.log.Error(err, "failed to reconcile serviceaccount resource") + return err + } + + if err := r.createOrApplyRBACResources(ctx, proxy, resourceLabels); err != nil { + r.log.Error(err, "failed to reconcile rbac resources") + return err + } + + if err := r.createOrApplyDaemonSet(ctx, proxy, resourceLabels); err != nil { + r.log.Error(err, "failed to reconcile daemonset resource") + return err + } + + if common.AddAnnotation(proxy, controllerProcessedAnnotation, "true") { + if err := r.UpdateWithRetry(ctx, proxy); err != nil { + return fmt.Errorf("failed to update processed annotation to %s/%s: %w", proxy.GetNamespace(), proxy.GetName(), err) + } + } + + r.log.V(4).Info("finished reconciliation of http01proxy", "namespace", proxy.GetNamespace(), "name", proxy.GetName()) + return nil +} diff --git a/pkg/controller/http01proxy/networkpolicies.go b/pkg/controller/http01proxy/networkpolicies.go new file mode 100644 index 000000000..886fdeefa --- /dev/null +++ b/pkg/controller/http01proxy/networkpolicies.go @@ -0,0 +1,37 @@ +package http01proxy + +import ( + "context" + "fmt" + + networkingv1 "k8s.io/api/networking/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + "github.com/openshift/cert-manager-operator/pkg/controller/common" + "github.com/openshift/cert-manager-operator/pkg/operator/assets" +) + +func (r *Reconciler) createOrApplyNetworkPolicies(ctx context.Context, proxy *v1alpha1.HTTP01Proxy, resourceLabels map[string]string) error { + for _, assetName := range http01ProxyNetworkPolicyAssets { + np := common.DecodeObjBytes[*networkingv1.NetworkPolicy](codecs, networkingv1.SchemeGroupVersion, assets.MustAsset(assetName)) + np.SetNamespace(proxy.GetNamespace()) + common.UpdateResourceLabels(np, resourceLabels) + + if err := r.createOrUpdateResource(ctx, np); err != nil { + return fmt.Errorf("failed to reconcile network policy %s: %w", assetName, err) + } + } + return nil +} + +func (r *Reconciler) deleteNetworkPolicies(ctx context.Context, proxy *v1alpha1.HTTP01Proxy) error { + for _, assetName := range http01ProxyNetworkPolicyAssets { + np := common.DecodeObjBytes[*networkingv1.NetworkPolicy](codecs, networkingv1.SchemeGroupVersion, assets.MustAsset(assetName)) + key := client.ObjectKey{Namespace: proxy.GetNamespace(), Name: np.GetName()} + if err := r.deleteIfExists(ctx, &networkingv1.NetworkPolicy{}, key); err != nil { + return fmt.Errorf("failed to delete network policy %q: %w", key.Name, err) + } + } + return nil +} diff --git a/pkg/controller/http01proxy/rbacs.go b/pkg/controller/http01proxy/rbacs.go new file mode 100644 index 000000000..1fcccdbf7 --- /dev/null +++ b/pkg/controller/http01proxy/rbacs.go @@ -0,0 +1,54 @@ +package http01proxy + +import ( + "context" + "fmt" + + rbacv1 "k8s.io/api/rbac/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + "github.com/openshift/cert-manager-operator/pkg/controller/common" + "github.com/openshift/cert-manager-operator/pkg/operator/assets" +) + +func (r *Reconciler) createOrApplyRBACResources(ctx context.Context, proxy *v1alpha1.HTTP01Proxy, resourceLabels map[string]string) error { + cr := common.DecodeObjBytes[*rbacv1.ClusterRole](codecs, rbacv1.SchemeGroupVersion, assets.MustAsset(clusterRoleAssetName)) + common.UpdateResourceLabels(cr, resourceLabels) + if err := r.createOrUpdateResource(ctx, cr); err != nil { + return fmt.Errorf("failed to reconcile clusterrole: %w", err) + } + + crb := common.DecodeObjBytes[*rbacv1.ClusterRoleBinding](codecs, rbacv1.SchemeGroupVersion, assets.MustAsset(clusterRoleBindingAssetName)) + common.UpdateResourceLabels(crb, resourceLabels) + for i := range crb.Subjects { + if crb.Subjects[i].Kind == "ServiceAccount" { + crb.Subjects[i].Namespace = proxy.GetNamespace() + } + } + if err := r.createOrUpdateResource(ctx, crb); err != nil { + return fmt.Errorf("failed to reconcile clusterrolebinding: %w", err) + } + + sccCRB := common.DecodeObjBytes[*rbacv1.ClusterRoleBinding](codecs, rbacv1.SchemeGroupVersion, assets.MustAsset(sccRoleBindingAssetName)) + common.UpdateResourceLabels(sccCRB, resourceLabels) + for i := range sccCRB.Subjects { + if sccCRB.Subjects[i].Kind == "ServiceAccount" { + sccCRB.Subjects[i].Namespace = proxy.GetNamespace() + } + } + if err := r.createOrUpdateResource(ctx, sccCRB); err != nil { + return fmt.Errorf("failed to reconcile scc clusterrolebinding: %w", err) + } + + return nil +} + +func (r *Reconciler) deleteRBACResources(ctx context.Context) error { + for _, name := range []string{http01proxyCommonName, http01proxyCommonName + "-scc"} { + if err := r.deleteIfExists(ctx, &rbacv1.ClusterRoleBinding{}, client.ObjectKey{Name: name}); err != nil { + return fmt.Errorf("failed to delete clusterrolebinding %q: %w", name, err) + } + } + return r.deleteIfExists(ctx, &rbacv1.ClusterRole{}, client.ObjectKey{Name: http01proxyCommonName}) +} diff --git a/pkg/controller/http01proxy/serviceaccounts.go b/pkg/controller/http01proxy/serviceaccounts.go new file mode 100644 index 000000000..c9869aaa5 --- /dev/null +++ b/pkg/controller/http01proxy/serviceaccounts.go @@ -0,0 +1,26 @@ +package http01proxy + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + "github.com/openshift/cert-manager-operator/pkg/controller/common" + "github.com/openshift/cert-manager-operator/pkg/operator/assets" +) + +func (r *Reconciler) createOrApplyServiceAccount(ctx context.Context, proxy *v1alpha1.HTTP01Proxy, resourceLabels map[string]string) error { + sa := common.DecodeObjBytes[*corev1.ServiceAccount](codecs, corev1.SchemeGroupVersion, assets.MustAsset(serviceAccountAssetName)) + sa.SetNamespace(proxy.GetNamespace()) + common.UpdateResourceLabels(sa, resourceLabels) + return r.createOrUpdateResource(ctx, sa) +} + +func (r *Reconciler) deleteServiceAccount(ctx context.Context, proxy *v1alpha1.HTTP01Proxy) error { + return r.deleteIfExists(ctx, &corev1.ServiceAccount{}, client.ObjectKey{ + Namespace: proxy.GetNamespace(), + Name: http01proxyCommonName, + }) +} diff --git a/pkg/controller/http01proxy/utils.go b/pkg/controller/http01proxy/utils.go new file mode 100644 index 000000000..3b1ab3633 --- /dev/null +++ b/pkg/controller/http01proxy/utils.go @@ -0,0 +1,201 @@ +package http01proxy + +import ( + "context" + "fmt" + "reflect" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + "github.com/openshift/cert-manager-operator/pkg/controller/common" +) + +var ( + scheme = runtime.NewScheme() + codecs = serializer.NewCodecFactory(scheme) +) + +func init() { + if err := appsv1.AddToScheme(scheme); err != nil { + panic(err) + } + if err := corev1.AddToScheme(scheme); err != nil { + panic(err) + } + if err := networkingv1.AddToScheme(scheme); err != nil { + panic(err) + } + if err := rbacv1.AddToScheme(scheme); err != nil { + panic(err) + } +} + +func (r *Reconciler) updateStatus(ctx context.Context, changed *v1alpha1.HTTP01Proxy) error { + namespacedName := client.ObjectKeyFromObject(changed) + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + r.log.V(4).Info("updating http01proxy status", "request", namespacedName) + current := &v1alpha1.HTTP01Proxy{} + if err := r.Get(ctx, namespacedName, current); err != nil { + return fmt.Errorf("failed to fetch http01proxy %q for status update: %w", namespacedName, err) + } + changed.Status.DeepCopyInto(¤t.Status) + if err := r.StatusUpdate(ctx, current); err != nil { + return fmt.Errorf("failed to update http01proxy %q status: %w", namespacedName, err) + } + return nil + }) +} + +func (r *Reconciler) addFinalizer(ctx context.Context, proxy *v1alpha1.HTTP01Proxy) error { + namespacedName := client.ObjectKeyFromObject(proxy) + if !controllerutil.ContainsFinalizer(proxy, finalizer) { + if !controllerutil.AddFinalizer(proxy, finalizer) { + return fmt.Errorf("failed to create %q http01proxy object with finalizers added", namespacedName) + } + if err := r.UpdateWithRetry(ctx, proxy); err != nil { + return fmt.Errorf("failed to add finalizers on %q http01proxy with %w", namespacedName, err) + } + updated := &v1alpha1.HTTP01Proxy{} + if err := r.Get(ctx, namespacedName, updated); err != nil { + return fmt.Errorf("failed to fetch http01proxy %q after updating finalizers: %w", namespacedName, err) + } + updated.DeepCopyInto(proxy) + } + return nil +} + +func (r *Reconciler) removeFinalizer(ctx context.Context, proxy *v1alpha1.HTTP01Proxy) error { + namespacedName := client.ObjectKeyFromObject(proxy) + if controllerutil.ContainsFinalizer(proxy, finalizer) { + if !controllerutil.RemoveFinalizer(proxy, finalizer) { + return fmt.Errorf("failed to create %q http01proxy object with finalizers removed", namespacedName) + } + if err := r.UpdateWithRetry(ctx, proxy); err != nil { + return fmt.Errorf("failed to remove finalizers on %q http01proxy with %w", namespacedName, err) + } + } + return nil +} + +func hasObjectChanged(desired, fetched client.Object) (bool, error) { + if reflect.TypeOf(desired) != reflect.TypeOf(fetched) { + return false, fmt.Errorf("type mismatch: desired %T vs fetched %T", desired, fetched) + } + + var objectModified bool + switch d := desired.(type) { + case *rbacv1.ClusterRole: + f, _ := fetched.(*rbacv1.ClusterRole) + objectModified = !reflect.DeepEqual(d.Rules, f.Rules) + case *rbacv1.ClusterRoleBinding: + f, _ := fetched.(*rbacv1.ClusterRoleBinding) + objectModified = !reflect.DeepEqual(d.RoleRef, f.RoleRef) || !reflect.DeepEqual(d.Subjects, f.Subjects) + case *appsv1.DaemonSet: + f, _ := fetched.(*appsv1.DaemonSet) + objectModified = daemonSetSpecModified(d, f) + case *networkingv1.NetworkPolicy: + f, _ := fetched.(*networkingv1.NetworkPolicy) + objectModified = !reflect.DeepEqual(d.Spec, f.Spec) + case *corev1.ServiceAccount: + return common.ObjectMetadataModified(desired, fetched), nil + default: + return false, fmt.Errorf("unsupported object type: %T", desired) + } + return objectModified || common.ObjectMetadataModified(desired, fetched), nil +} + +func daemonSetSpecModified(desired, fetched *appsv1.DaemonSet) bool { + // Use deep equality on the full PodTemplateSpec to catch all drift including + // hostNetwork, tolerations, volumes, securityContext, affinity, init containers, etc. + // This ensures upgrades don't leave the live proxy on stale networking or privilege settings. + if !reflect.DeepEqual(desired.Spec.Template.Spec, fetched.Spec.Template.Spec) { + return true + } + + // Also compare template metadata (labels and annotations) + if !reflect.DeepEqual(desired.Spec.Template.Labels, fetched.Spec.Template.Labels) || + !reflect.DeepEqual(desired.Spec.Template.Annotations, fetched.Spec.Template.Annotations) { + return true + } + + // Compare selector (required for DaemonSet immutability rules) + if !reflect.DeepEqual(desired.Spec.Selector, fetched.Spec.Selector) { + return true + } + + // Compare update strategy + if !reflect.DeepEqual(desired.Spec.UpdateStrategy, fetched.Spec.UpdateStrategy) { + return true + } + + return false +} + +func (r *Reconciler) updateCondition(ctx context.Context, proxy *v1alpha1.HTTP01Proxy, prependErr error) error { + if err := r.updateStatus(ctx, proxy); err != nil { + errUpdate := fmt.Errorf("failed to update %s/%s status: %w", proxy.GetNamespace(), proxy.GetName(), err) + if prependErr != nil { + return utilerrors.NewAggregate([]error{prependErr, errUpdate}) + } + return errUpdate + } + return prependErr +} + +func (r *Reconciler) getInternalPort(proxy *v1alpha1.HTTP01Proxy) int32 { + if proxy.Spec.Mode == v1alpha1.HTTP01ProxyModeCustom && + proxy.Spec.CustomDeployment != nil && + proxy.Spec.CustomDeployment.InternalPort > 0 { + return proxy.Spec.CustomDeployment.InternalPort + } + return defaultInternalPort +} + +func (r *Reconciler) createOrUpdateResource(ctx context.Context, desired client.Object) error { + key := client.ObjectKeyFromObject(desired) + kind := desired.GetObjectKind().GroupVersionKind().Kind + + r.log.V(2).Info("creating resource", "kind", kind, "name", key) + if err := r.Create(ctx, desired); err != nil { + if !errors.IsAlreadyExists(err) { + return common.FromClientError(err, "failed to create %s %q", kind, key) + } + fetched, _ := desired.DeepCopyObject().(client.Object) + if err := r.Get(ctx, key, fetched); err != nil { + return common.FromClientError(err, "failed to get %s %q for update", kind, key) + } + changed, err := hasObjectChanged(desired, fetched) + if err != nil { + return fmt.Errorf("failed to compare %s %q: %w", kind, key, err) + } + if changed { + r.log.V(2).Info("updating resource", "kind", kind, "name", key) + desired.SetResourceVersion(fetched.GetResourceVersion()) + if err := r.Update(ctx, desired); err != nil { + return common.FromClientError(err, "failed to update %s %q", kind, key) + } + } + } + + return nil +} + +func (r *Reconciler) deleteIfExists(ctx context.Context, obj client.Object, key client.ObjectKey) error { + obj.SetName(key.Name) + obj.SetNamespace(key.Namespace) + if err := r.Delete(ctx, obj); client.IgnoreNotFound(err) != nil { + return fmt.Errorf("failed to delete %s %q: %w", obj.GetObjectKind().GroupVersionKind().Kind, key, err) + } + return nil +} diff --git a/pkg/controller/http01proxy/utils_test.go b/pkg/controller/http01proxy/utils_test.go new file mode 100644 index 000000000..6f526b5e5 --- /dev/null +++ b/pkg/controller/http01proxy/utils_test.go @@ -0,0 +1,363 @@ +package http01proxy + +import ( + "testing" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" +) + +func TestGetInternalPort(t *testing.T) { + tests := []struct { + name string + proxy *v1alpha1.HTTP01Proxy + want int32 + }{ + { + name: "DefaultDeployment mode returns default port", + proxy: &v1alpha1.HTTP01Proxy{ + Spec: v1alpha1.HTTP01ProxySpec{Mode: v1alpha1.HTTP01ProxyModeDefault}, + }, + want: defaultInternalPort, + }, + { + name: "CustomDeployment with port set", + proxy: &v1alpha1.HTTP01Proxy{ + Spec: v1alpha1.HTTP01ProxySpec{ + Mode: v1alpha1.HTTP01ProxyModeCustom, + CustomDeployment: &v1alpha1.HTTP01ProxyCustomDeploymentSpec{InternalPort: 9999}, + }, + }, + want: 9999, + }, + { + name: "CustomDeployment with nil CustomDeployment pointer", + proxy: &v1alpha1.HTTP01Proxy{ + Spec: v1alpha1.HTTP01ProxySpec{ + Mode: v1alpha1.HTTP01ProxyModeCustom, + CustomDeployment: nil, + }, + }, + want: defaultInternalPort, + }, + { + name: "CustomDeployment with port zero", + proxy: &v1alpha1.HTTP01Proxy{ + Spec: v1alpha1.HTTP01ProxySpec{ + Mode: v1alpha1.HTTP01ProxyModeCustom, + CustomDeployment: &v1alpha1.HTTP01ProxyCustomDeploymentSpec{InternalPort: 0}, + }, + }, + want: defaultInternalPort, + }, + } + + r := &Reconciler{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := r.getInternalPort(tt.proxy) + if got != tt.want { + t.Errorf("getInternalPort() = %d, want %d", got, tt.want) + } + }) + } +} + +func baseDaemonSet(image string) *appsv1.DaemonSet { + return &appsv1.DaemonSet{ + Spec: appsv1.DaemonSetSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "proxy"}}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app": "proxy"}, + Annotations: map[string]string{"version": "1"}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "proxy", + Image: image, + Ports: []corev1.ContainerPort{{Name: proxyPortName, ContainerPort: 8888, HostPort: 8888}}, + }}, + HostNetwork: true, + }, + }, + UpdateStrategy: appsv1.DaemonSetUpdateStrategy{ + Type: appsv1.RollingUpdateDaemonSetStrategyType, + }, + }, + } +} + +func TestDaemonSetSpecModified(t *testing.T) { + tests := []struct { + name string + desired *appsv1.DaemonSet + fetched *appsv1.DaemonSet + want bool + }{ + { + name: "identical specs", + desired: baseDaemonSet("img:v1"), + fetched: baseDaemonSet("img:v1"), + want: false, + }, + { + name: "different image", + desired: baseDaemonSet("img:v1"), + fetched: baseDaemonSet("img:v2"), + want: true, + }, + { + name: "different template labels", + desired: baseDaemonSet("img:v1"), + fetched: func() *appsv1.DaemonSet { + ds := baseDaemonSet("img:v1") + ds.Spec.Template.Labels = map[string]string{"app": "other"} + return ds + }(), + want: true, + }, + { + name: "different template annotations", + desired: baseDaemonSet("img:v1"), + fetched: func() *appsv1.DaemonSet { + ds := baseDaemonSet("img:v1") + ds.Spec.Template.Annotations = map[string]string{"version": "2"} + return ds + }(), + want: true, + }, + { + name: "different selector", + desired: baseDaemonSet("img:v1"), + fetched: func() *appsv1.DaemonSet { + ds := baseDaemonSet("img:v1") + ds.Spec.Selector = &metav1.LabelSelector{MatchLabels: map[string]string{"app": "other"}} + return ds + }(), + want: true, + }, + { + name: "different update strategy", + desired: baseDaemonSet("img:v1"), + fetched: func() *appsv1.DaemonSet { + ds := baseDaemonSet("img:v1") + ds.Spec.UpdateStrategy = appsv1.DaemonSetUpdateStrategy{Type: appsv1.OnDeleteDaemonSetStrategyType} + return ds + }(), + want: true, + }, + { + name: "different hostNetwork", + desired: baseDaemonSet("img:v1"), + fetched: func() *appsv1.DaemonSet { + ds := baseDaemonSet("img:v1") + ds.Spec.Template.Spec.HostNetwork = false + return ds + }(), + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := daemonSetSpecModified(tt.desired, tt.fetched) + if got != tt.want { + t.Errorf("daemonSetSpecModified() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestHasObjectChanged(t *testing.T) { + tests := []struct { + name string + desired client.Object + fetched client.Object + wantChanged bool + wantErr bool + }{ + { + name: "type mismatch returns error", + desired: &rbacv1.ClusterRole{}, + fetched: &rbacv1.ClusterRoleBinding{}, + wantErr: true, + }, + { + name: "ClusterRole identical", + desired: &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "1"}}, + Rules: []rbacv1.PolicyRule{{Verbs: []string{"get"}}}, + }, + fetched: &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "1"}}, + Rules: []rbacv1.PolicyRule{{Verbs: []string{"get"}}}, + }, + wantChanged: false, + }, + { + name: "ClusterRole rules different", + desired: &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "1"}}, + Rules: []rbacv1.PolicyRule{{Verbs: []string{"get"}}}, + }, + fetched: &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "1"}}, + Rules: []rbacv1.PolicyRule{{Verbs: []string{"list"}}}, + }, + wantChanged: true, + }, + { + name: "ClusterRole metadata different", + desired: &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "1"}}, + Rules: []rbacv1.PolicyRule{{Verbs: []string{"get"}}}, + }, + fetched: &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "2"}}, + Rules: []rbacv1.PolicyRule{{Verbs: []string{"get"}}}, + }, + wantChanged: true, + }, + { + name: "ClusterRoleBinding identical", + desired: &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "1"}}, + RoleRef: rbacv1.RoleRef{APIGroup: "rbac", Kind: "ClusterRole", Name: "x"}, + Subjects: []rbacv1.Subject{{Kind: "ServiceAccount", Name: "sa", Namespace: "ns"}}, + }, + fetched: &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "1"}}, + RoleRef: rbacv1.RoleRef{APIGroup: "rbac", Kind: "ClusterRole", Name: "x"}, + Subjects: []rbacv1.Subject{{Kind: "ServiceAccount", Name: "sa", Namespace: "ns"}}, + }, + wantChanged: false, + }, + { + name: "ClusterRoleBinding RoleRef different", + desired: &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "1"}}, + RoleRef: rbacv1.RoleRef{APIGroup: "rbac", Kind: "ClusterRole", Name: "role-a"}, + Subjects: []rbacv1.Subject{{Kind: "ServiceAccount", Name: "sa", Namespace: "ns"}}, + }, + fetched: &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "1"}}, + RoleRef: rbacv1.RoleRef{APIGroup: "rbac", Kind: "ClusterRole", Name: "role-b"}, + Subjects: []rbacv1.Subject{{Kind: "ServiceAccount", Name: "sa", Namespace: "ns"}}, + }, + wantChanged: true, + }, + { + name: "ClusterRoleBinding Subjects different", + desired: &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "1"}}, + RoleRef: rbacv1.RoleRef{APIGroup: "rbac", Kind: "ClusterRole", Name: "x"}, + Subjects: []rbacv1.Subject{{Kind: "ServiceAccount", Name: "sa1", Namespace: "ns"}}, + }, + fetched: &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "1"}}, + RoleRef: rbacv1.RoleRef{APIGroup: "rbac", Kind: "ClusterRole", Name: "x"}, + Subjects: []rbacv1.Subject{{Kind: "ServiceAccount", Name: "sa2", Namespace: "ns"}}, + }, + wantChanged: true, + }, + { + name: "DaemonSet identical", + desired: &appsv1.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "1"}}, + Spec: baseDaemonSet("img:v1").Spec, + }, + fetched: &appsv1.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "1"}}, + Spec: baseDaemonSet("img:v1").Spec, + }, + wantChanged: false, + }, + { + name: "DaemonSet spec different", + desired: &appsv1.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "1"}}, + Spec: baseDaemonSet("img:v1").Spec, + }, + fetched: &appsv1.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "1"}}, + Spec: baseDaemonSet("img:v2").Spec, + }, + wantChanged: true, + }, + { + name: "NetworkPolicy identical", + desired: &networkingv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "1"}}, + Spec: networkingv1.NetworkPolicySpec{PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}}, + }, + fetched: &networkingv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "1"}}, + Spec: networkingv1.NetworkPolicySpec{PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}}, + }, + wantChanged: false, + }, + { + name: "NetworkPolicy spec different", + desired: &networkingv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "1"}}, + Spec: networkingv1.NetworkPolicySpec{PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}}, + }, + fetched: &networkingv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "1"}}, + Spec: networkingv1.NetworkPolicySpec{PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeEgress}}, + }, + wantChanged: true, + }, + { + name: "ServiceAccount metadata identical", + desired: &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "1"}}, + }, + fetched: &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "1"}}, + }, + wantChanged: false, + }, + { + name: "ServiceAccount metadata different", + desired: &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "1"}}, + }, + fetched: &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"a": "2"}}, + }, + wantChanged: true, + }, + { + name: "unsupported type returns error", + desired: &corev1.Pod{}, + fetched: &corev1.Pod{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := hasObjectChanged(tt.desired, tt.fetched) + if tt.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tt.wantChanged { + t.Errorf("hasObjectChanged() = %v, want %v", got, tt.wantChanged) + } + }) + } +} diff --git a/pkg/features/features_test.go b/pkg/features/features_test.go index b064b67d0..98a05cc95 100644 --- a/pkg/features/features_test.go +++ b/pkg/features/features_test.go @@ -65,6 +65,7 @@ var expectedDefaultFeatureState = map[bool][]featuregate.Feature{ // list of features which are expected to be disabled at runtime. false: { featuregate.Feature("TrustManager"), + featuregate.Feature("HTTP01Proxy"), }, } @@ -87,7 +88,7 @@ func TestFeatureGates(t *testing.T) { } slices.Sort(knownOperatorFeatures) - assert.Equal(t, knownOperatorFeatures, testFeatureNames, + assert.ElementsMatch(t, knownOperatorFeatures, testFeatureNames, `the list of features known to the operator differ from what is being tested here, it could be that there was a new Feature added to the api which wasn't added to the tests. Please verify "api/operator/v1alpha1" and "pkg/features" have identical features.`) @@ -102,7 +103,7 @@ func TestFeatureGates(t *testing.T) { } }) - t.Run("all TechPreview features should be disabled by default", func(t *testing.T) { + t.Run("all pre-GA features should be disabled by default", func(t *testing.T) { feats := mutableFeatureGate.GetAll() for feat, spec := range feats { // skip "AllBeta", "AllAlpha": our operator does not use those @@ -110,9 +111,10 @@ func TestFeatureGates(t *testing.T) { continue } - assert.Equal(t, spec.PreRelease == "TechPreview", !spec.Default, - "prerelease TechPreview %q feature should default to disabled", - feat) + isPreGA := spec.PreRelease == "TechPreview" || spec.PreRelease == featuregate.Alpha || spec.PreRelease == featuregate.Beta + assert.Equal(t, isPreGA, !spec.Default, + "pre-GA %q feature (prerelease=%s) should default to disabled", + feat, spec.PreRelease) } }) diff --git a/pkg/operator/applyconfigurations/internal/internal.go b/pkg/operator/applyconfigurations/internal/internal.go index 6b910267e..671b8b064 100644 --- a/pkg/operator/applyconfigurations/internal/internal.go +++ b/pkg/operator/applyconfigurations/internal/internal.go @@ -33,6 +33,16 @@ var schemaYAML = typed.YAMLObject(`types: elementType: namedType: __untyped_deduced_ elementRelationship: separable +- name: com.github.openshift.cert-manager-operator.api.operator.v1alpha1.HTTP01Proxy + scalar: untyped + list: + elementType: + namedType: __untyped_atomic_ + elementRelationship: atomic + map: + elementType: + namedType: __untyped_deduced_ + elementRelationship: separable - name: com.github.openshift.cert-manager-operator.api.operator.v1alpha1.IstioCSR scalar: untyped list: diff --git a/pkg/operator/applyconfigurations/operator/v1alpha1/http01proxy.go b/pkg/operator/applyconfigurations/operator/v1alpha1/http01proxy.go new file mode 100644 index 000000000..d5be56c84 --- /dev/null +++ b/pkg/operator/applyconfigurations/operator/v1alpha1/http01proxy.go @@ -0,0 +1,282 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + operatorv1alpha1 "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + internal "github.com/openshift/cert-manager-operator/pkg/operator/applyconfigurations/internal" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + managedfields "k8s.io/apimachinery/pkg/util/managedfields" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// HTTP01ProxyApplyConfiguration represents a declarative configuration of the HTTP01Proxy type for use +// with apply. +// +// HTTP01Proxy describes the configuration for the HTTP01 challenge proxy +// that redirects traffic from the API endpoint on port 80 to ingress routers. +// This enables cert-manager to perform HTTP01 ACME challenges for API endpoint certificates. +// The name must be `default` to make HTTP01Proxy a singleton. +// +// When an HTTP01Proxy is created, the proxy DaemonSet is deployed on control plane nodes. +type HTTP01ProxyApplyConfiguration struct { + v1.TypeMetaApplyConfiguration `json:",inline"` + // metadata is the standard object's metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + // spec is the specification of the desired behavior of the HTTP01Proxy. + Spec *HTTP01ProxySpecApplyConfiguration `json:"spec,omitempty"` + // status is the most recently observed status of the HTTP01Proxy. + Status *HTTP01ProxyStatusApplyConfiguration `json:"status,omitempty"` +} + +// HTTP01Proxy constructs a declarative configuration of the HTTP01Proxy type for use with +// apply. +func HTTP01Proxy(name, namespace string) *HTTP01ProxyApplyConfiguration { + b := &HTTP01ProxyApplyConfiguration{} + b.WithName(name) + b.WithNamespace(namespace) + b.WithKind("HTTP01Proxy") + b.WithAPIVersion("operator.openshift.io/v1alpha1") + return b +} + +// ExtractHTTP01ProxyFrom extracts the applied configuration owned by fieldManager from +// hTTP01Proxy for the specified subresource. Pass an empty string for subresource to extract +// the main resource. Common subresources include "status", "scale", etc. +// hTTP01Proxy must be a unmodified HTTP01Proxy API object that was retrieved from the Kubernetes API. +// ExtractHTTP01ProxyFrom provides a way to perform a extract/modify-in-place/apply workflow. +// Note that an extracted apply configuration will contain fewer fields than what the fieldManager previously +// applied if another fieldManager has updated or force applied any of the previously applied fields. +func ExtractHTTP01ProxyFrom(hTTP01Proxy *operatorv1alpha1.HTTP01Proxy, fieldManager string, subresource string) (*HTTP01ProxyApplyConfiguration, error) { + b := &HTTP01ProxyApplyConfiguration{} + err := managedfields.ExtractInto(hTTP01Proxy, internal.Parser().Type("com.github.openshift.cert-manager-operator.api.operator.v1alpha1.HTTP01Proxy"), fieldManager, b, subresource) + if err != nil { + return nil, err + } + b.WithName(hTTP01Proxy.Name) + b.WithNamespace(hTTP01Proxy.Namespace) + + b.WithKind("HTTP01Proxy") + b.WithAPIVersion("operator.openshift.io/v1alpha1") + return b, nil +} + +// ExtractHTTP01Proxy extracts the applied configuration owned by fieldManager from +// hTTP01Proxy. If no managedFields are found in hTTP01Proxy for fieldManager, a +// HTTP01ProxyApplyConfiguration is returned with only the Name, Namespace (if applicable), +// APIVersion and Kind populated. It is possible that no managed fields were found for because other +// field managers have taken ownership of all the fields previously owned by fieldManager, or because +// the fieldManager never owned fields any fields. +// hTTP01Proxy must be a unmodified HTTP01Proxy API object that was retrieved from the Kubernetes API. +// ExtractHTTP01Proxy provides a way to perform a extract/modify-in-place/apply workflow. +// Note that an extracted apply configuration will contain fewer fields than what the fieldManager previously +// applied if another fieldManager has updated or force applied any of the previously applied fields. +func ExtractHTTP01Proxy(hTTP01Proxy *operatorv1alpha1.HTTP01Proxy, fieldManager string) (*HTTP01ProxyApplyConfiguration, error) { + return ExtractHTTP01ProxyFrom(hTTP01Proxy, fieldManager, "") +} + +// ExtractHTTP01ProxyStatus extracts the applied configuration owned by fieldManager from +// hTTP01Proxy for the status subresource. +func ExtractHTTP01ProxyStatus(hTTP01Proxy *operatorv1alpha1.HTTP01Proxy, fieldManager string) (*HTTP01ProxyApplyConfiguration, error) { + return ExtractHTTP01ProxyFrom(hTTP01Proxy, fieldManager, "status") +} + +func (b HTTP01ProxyApplyConfiguration) IsApplyConfiguration() {} + +// WithKind sets the Kind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Kind field is set to the value of the last call. +func (b *HTTP01ProxyApplyConfiguration) WithKind(value string) *HTTP01ProxyApplyConfiguration { + b.TypeMetaApplyConfiguration.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APIVersion field is set to the value of the last call. +func (b *HTTP01ProxyApplyConfiguration) WithAPIVersion(value string) *HTTP01ProxyApplyConfiguration { + b.TypeMetaApplyConfiguration.APIVersion = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *HTTP01ProxyApplyConfiguration) WithName(value string) *HTTP01ProxyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Name = &value + return b +} + +// WithGenerateName sets the GenerateName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GenerateName field is set to the value of the last call. +func (b *HTTP01ProxyApplyConfiguration) WithGenerateName(value string) *HTTP01ProxyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *HTTP01ProxyApplyConfiguration) WithNamespace(value string) *HTTP01ProxyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Namespace = &value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *HTTP01ProxyApplyConfiguration) WithUID(value types.UID) *HTTP01ProxyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ResourceVersion field is set to the value of the last call. +func (b *HTTP01ProxyApplyConfiguration) WithResourceVersion(value string) *HTTP01ProxyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.ResourceVersion = &value + return b +} + +// WithGeneration sets the Generation field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Generation field is set to the value of the last call. +func (b *HTTP01ProxyApplyConfiguration) WithGeneration(value int64) *HTTP01ProxyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Generation = &value + return b +} + +// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CreationTimestamp field is set to the value of the last call. +func (b *HTTP01ProxyApplyConfiguration) WithCreationTimestamp(value metav1.Time) *HTTP01ProxyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.CreationTimestamp = &value + return b +} + +// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionTimestamp field is set to the value of the last call. +func (b *HTTP01ProxyApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *HTTP01ProxyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value + return b +} + +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *HTTP01ProxyApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *HTTP01ProxyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *HTTP01ProxyApplyConfiguration) WithLabels(entries map[string]string) *HTTP01ProxyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *HTTP01ProxyApplyConfiguration) WithAnnotations(entries map[string]string) *HTTP01ProxyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the OwnerReferences field. +func (b *HTTP01ProxyApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *HTTP01ProxyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Finalizers field. +func (b *HTTP01ProxyApplyConfiguration) WithFinalizers(values ...string) *HTTP01ProxyApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) + } + return b +} + +func (b *HTTP01ProxyApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *HTTP01ProxyApplyConfiguration) WithSpec(value *HTTP01ProxySpecApplyConfiguration) *HTTP01ProxyApplyConfiguration { + b.Spec = value + return b +} + +// WithStatus sets the Status field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Status field is set to the value of the last call. +func (b *HTTP01ProxyApplyConfiguration) WithStatus(value *HTTP01ProxyStatusApplyConfiguration) *HTTP01ProxyApplyConfiguration { + b.Status = value + return b +} + +// GetKind retrieves the value of the Kind field in the declarative configuration. +func (b *HTTP01ProxyApplyConfiguration) GetKind() *string { + return b.TypeMetaApplyConfiguration.Kind +} + +// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. +func (b *HTTP01ProxyApplyConfiguration) GetAPIVersion() *string { + return b.TypeMetaApplyConfiguration.APIVersion +} + +// GetName retrieves the value of the Name field in the declarative configuration. +func (b *HTTP01ProxyApplyConfiguration) GetName() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Name +} + +// GetNamespace retrieves the value of the Namespace field in the declarative configuration. +func (b *HTTP01ProxyApplyConfiguration) GetNamespace() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Namespace +} diff --git a/pkg/operator/applyconfigurations/operator/v1alpha1/http01proxycustomdeploymentspec.go b/pkg/operator/applyconfigurations/operator/v1alpha1/http01proxycustomdeploymentspec.go new file mode 100644 index 000000000..711df3414 --- /dev/null +++ b/pkg/operator/applyconfigurations/operator/v1alpha1/http01proxycustomdeploymentspec.go @@ -0,0 +1,27 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// HTTP01ProxyCustomDeploymentSpecApplyConfiguration represents a declarative configuration of the HTTP01ProxyCustomDeploymentSpec type for use +// with apply. +// +// HTTP01ProxyCustomDeploymentSpec contains configuration for custom proxy deployment. +type HTTP01ProxyCustomDeploymentSpecApplyConfiguration struct { + // internalPort specifies the internal port used by the proxy service. + // Valid values are 1024-65535. + InternalPort *int32 `json:"internalPort,omitempty"` +} + +// HTTP01ProxyCustomDeploymentSpecApplyConfiguration constructs a declarative configuration of the HTTP01ProxyCustomDeploymentSpec type for use with +// apply. +func HTTP01ProxyCustomDeploymentSpec() *HTTP01ProxyCustomDeploymentSpecApplyConfiguration { + return &HTTP01ProxyCustomDeploymentSpecApplyConfiguration{} +} + +// WithInternalPort sets the InternalPort field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the InternalPort field is set to the value of the last call. +func (b *HTTP01ProxyCustomDeploymentSpecApplyConfiguration) WithInternalPort(value int32) *HTTP01ProxyCustomDeploymentSpecApplyConfiguration { + b.InternalPort = &value + return b +} diff --git a/pkg/operator/applyconfigurations/operator/v1alpha1/http01proxyspec.go b/pkg/operator/applyconfigurations/operator/v1alpha1/http01proxyspec.go new file mode 100644 index 000000000..4d0bcbabc --- /dev/null +++ b/pkg/operator/applyconfigurations/operator/v1alpha1/http01proxyspec.go @@ -0,0 +1,43 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + operatorv1alpha1 "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" +) + +// HTTP01ProxySpecApplyConfiguration represents a declarative configuration of the HTTP01ProxySpec type for use +// with apply. +// +// HTTP01ProxySpec is the specification of the desired behavior of the HTTP01Proxy. +type HTTP01ProxySpecApplyConfiguration struct { + // mode controls whether the HTTP01 challenge proxy is active and how it should be deployed. + // DefaultDeployment enables the proxy with default configuration. + // CustomDeployment enables the proxy with user-specified configuration. + Mode *operatorv1alpha1.HTTP01ProxyMode `json:"mode,omitempty"` + // customDeployment contains configuration options when mode is CustomDeployment. + // This field is only valid when mode is CustomDeployment. + CustomDeployment *HTTP01ProxyCustomDeploymentSpecApplyConfiguration `json:"customDeployment,omitempty"` +} + +// HTTP01ProxySpecApplyConfiguration constructs a declarative configuration of the HTTP01ProxySpec type for use with +// apply. +func HTTP01ProxySpec() *HTTP01ProxySpecApplyConfiguration { + return &HTTP01ProxySpecApplyConfiguration{} +} + +// WithMode sets the Mode field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Mode field is set to the value of the last call. +func (b *HTTP01ProxySpecApplyConfiguration) WithMode(value operatorv1alpha1.HTTP01ProxyMode) *HTTP01ProxySpecApplyConfiguration { + b.Mode = &value + return b +} + +// WithCustomDeployment sets the CustomDeployment field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CustomDeployment field is set to the value of the last call. +func (b *HTTP01ProxySpecApplyConfiguration) WithCustomDeployment(value *HTTP01ProxyCustomDeploymentSpecApplyConfiguration) *HTTP01ProxySpecApplyConfiguration { + b.CustomDeployment = value + return b +} diff --git a/pkg/operator/applyconfigurations/operator/v1alpha1/http01proxystatus.go b/pkg/operator/applyconfigurations/operator/v1alpha1/http01proxystatus.go new file mode 100644 index 000000000..97a0a87b5 --- /dev/null +++ b/pkg/operator/applyconfigurations/operator/v1alpha1/http01proxystatus.go @@ -0,0 +1,45 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// HTTP01ProxyStatusApplyConfiguration represents a declarative configuration of the HTTP01ProxyStatus type for use +// with apply. +// +// HTTP01ProxyStatus is the most recently observed status of the HTTP01Proxy. +type HTTP01ProxyStatusApplyConfiguration struct { + // conditions holds information about the current state of the HTTP01 proxy deployment. + ConditionalStatusApplyConfiguration `json:",omitempty,inline"` + // proxyImage is the name of the image and the tag used for deploying the proxy. + ProxyImage *string `json:"proxyImage,omitempty"` +} + +// HTTP01ProxyStatusApplyConfiguration constructs a declarative configuration of the HTTP01ProxyStatus type for use with +// apply. +func HTTP01ProxyStatus() *HTTP01ProxyStatusApplyConfiguration { + return &HTTP01ProxyStatusApplyConfiguration{} +} + +// WithConditions adds the given value to the Conditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Conditions field. +func (b *HTTP01ProxyStatusApplyConfiguration) WithConditions(values ...*v1.ConditionApplyConfiguration) *HTTP01ProxyStatusApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithConditions") + } + b.ConditionalStatusApplyConfiguration.Conditions = append(b.ConditionalStatusApplyConfiguration.Conditions, *values[i]) + } + return b +} + +// WithProxyImage sets the ProxyImage field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ProxyImage field is set to the value of the last call. +func (b *HTTP01ProxyStatusApplyConfiguration) WithProxyImage(value string) *HTTP01ProxyStatusApplyConfiguration { + b.ProxyImage = &value + return b +} diff --git a/pkg/operator/applyconfigurations/utils.go b/pkg/operator/applyconfigurations/utils.go index 37cba6978..ba97e423e 100644 --- a/pkg/operator/applyconfigurations/utils.go +++ b/pkg/operator/applyconfigurations/utils.go @@ -38,6 +38,14 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &operatorv1alpha1.DefaultCAPackageConfigApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("DeploymentConfig"): return &operatorv1alpha1.DeploymentConfigApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("HTTP01Proxy"): + return &operatorv1alpha1.HTTP01ProxyApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("HTTP01ProxyCustomDeploymentSpec"): + return &operatorv1alpha1.HTTP01ProxyCustomDeploymentSpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("HTTP01ProxySpec"): + return &operatorv1alpha1.HTTP01ProxySpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("HTTP01ProxyStatus"): + return &operatorv1alpha1.HTTP01ProxyStatusApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("IstioConfig"): return &operatorv1alpha1.IstioConfigApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("IstioCSR"): diff --git a/pkg/operator/assets/bindata.go b/pkg/operator/assets/bindata.go index 6183e9b47..1469589e4 100644 --- a/pkg/operator/assets/bindata.go +++ b/pkg/operator/assets/bindata.go @@ -43,6 +43,11 @@ // bindata/cert-manager-deployment/webhook/cert-manager-webhook-subjectaccessreviews-crb.yaml // bindata/cert-manager-deployment/webhook/cert-manager-webhook-svc.yaml // bindata/cert-manager-deployment/webhook/cert-manager-webhook-validatingwebhookconfiguration.yaml +// bindata/http01-proxy/cert-manager-http01-proxy-clusterrole.yaml +// bindata/http01-proxy/cert-manager-http01-proxy-clusterrolebinding.yaml +// bindata/http01-proxy/cert-manager-http01-proxy-daemonset.yaml +// bindata/http01-proxy/cert-manager-http01-proxy-scc-rolebinding.yaml +// bindata/http01-proxy/cert-manager-http01-proxy-serviceaccount.yaml // bindata/istio-csr/cert-manager-istio-csr-clusterrole.yaml // bindata/istio-csr/cert-manager-istio-csr-clusterrolebinding.yaml // bindata/istio-csr/cert-manager-istio-csr-deployment.yaml @@ -59,6 +64,8 @@ // bindata/networkpolicies/cert-manager-allow-ingress-to-metrics-networkpolicy.yaml // bindata/networkpolicies/cert-manager-allow-ingress-to-webhook-networkpolicy.yaml // bindata/networkpolicies/cert-manager-deny-all-networkpolicy.yaml +// bindata/networkpolicies/http01-proxy-allow-egress-networkpolicy.yaml +// bindata/networkpolicies/http01-proxy-deny-all-networkpolicy.yaml // bindata/networkpolicies/istio-csr-allow-egress-to-api-server-networkpolicy.yaml // bindata/networkpolicies/istio-csr-allow-ingress-to-grpc-networkpolicy.yaml // bindata/networkpolicies/istio-csr-allow-ingress-to-metrics-networkpolicy.yaml @@ -2295,6 +2302,244 @@ func certManagerDeploymentWebhookCertManagerWebhookValidatingwebhookconfiguratio return a, nil } +var _http01ProxyCertManagerHttp01ProxyClusterroleYaml = []byte(`apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: cert-manager-http01-proxy + labels: + app: cert-manager-http01-proxy + app.kubernetes.io/name: cert-manager-http01-proxy + app.kubernetes.io/part-of: cert-manager-operator +rules: + - apiGroups: + - config.openshift.io + resources: + - clusterversions + - infrastructures + - ingresses + verbs: + - get + - list + - watch + - apiGroups: + - machineconfiguration.openshift.io + resources: + - machineconfigs + verbs: + - get + - create + - update + - patch + - apiGroups: + - operator.openshift.io + resources: + - machineconfigurations + verbs: + - get + - create + - update + - patch +`) + +func http01ProxyCertManagerHttp01ProxyClusterroleYamlBytes() ([]byte, error) { + return _http01ProxyCertManagerHttp01ProxyClusterroleYaml, nil +} + +func http01ProxyCertManagerHttp01ProxyClusterroleYaml() (*asset, error) { + bytes, err := http01ProxyCertManagerHttp01ProxyClusterroleYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "http01-proxy/cert-manager-http01-proxy-clusterrole.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _http01ProxyCertManagerHttp01ProxyClusterrolebindingYaml = []byte(`apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: cert-manager-http01-proxy + labels: + app: cert-manager-http01-proxy + app.kubernetes.io/name: cert-manager-http01-proxy + app.kubernetes.io/part-of: cert-manager-operator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cert-manager-http01-proxy +subjects: + - kind: ServiceAccount + name: cert-manager-http01-proxy + namespace: cert-manager-operator +`) + +func http01ProxyCertManagerHttp01ProxyClusterrolebindingYamlBytes() ([]byte, error) { + return _http01ProxyCertManagerHttp01ProxyClusterrolebindingYaml, nil +} + +func http01ProxyCertManagerHttp01ProxyClusterrolebindingYaml() (*asset, error) { + bytes, err := http01ProxyCertManagerHttp01ProxyClusterrolebindingYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "http01-proxy/cert-manager-http01-proxy-clusterrolebinding.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _http01ProxyCertManagerHttp01ProxyDaemonsetYaml = []byte(`apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: cert-manager-http01-proxy + namespace: cert-manager-operator + labels: + app: cert-manager-http01-proxy + app.kubernetes.io/name: cert-manager-http01-proxy + app.kubernetes.io/part-of: cert-manager-operator +spec: + selector: + matchLabels: + app: cert-manager-http01-proxy + updateStrategy: + type: RollingUpdate + template: + metadata: + labels: + app: cert-manager-http01-proxy + app.kubernetes.io/name: cert-manager-http01-proxy + app.kubernetes.io/part-of: cert-manager-operator + spec: + serviceAccountName: cert-manager-http01-proxy + hostNetwork: true + nodeSelector: + node-role.kubernetes.io/master: "" + tolerations: + - key: node-role.kubernetes.io/master + operator: Exists + effect: NoSchedule + - key: node-role.kubernetes.io/control-plane + operator: Exists + effect: NoSchedule + containers: + - name: http01-proxy + image: ${RELATED_IMAGE_CERT_MANAGER_HTTP01PROXY} + ports: + - name: proxy + containerPort: 8888 + hostPort: 8888 + protocol: TCP + env: + - name: PROXY_PORT + value: "8888" + livenessProbe: + tcpSocket: + port: proxy + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + tcpSocket: + port: proxy + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + add: + - NET_ADMIN + drop: + - ALL + runAsNonRoot: false + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + cpu: 100m + memory: 64Mi + priorityClassName: system-cluster-critical +`) + +func http01ProxyCertManagerHttp01ProxyDaemonsetYamlBytes() ([]byte, error) { + return _http01ProxyCertManagerHttp01ProxyDaemonsetYaml, nil +} + +func http01ProxyCertManagerHttp01ProxyDaemonsetYaml() (*asset, error) { + bytes, err := http01ProxyCertManagerHttp01ProxyDaemonsetYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "http01-proxy/cert-manager-http01-proxy-daemonset.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _http01ProxyCertManagerHttp01ProxySccRolebindingYaml = []byte(`apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: cert-manager-http01-proxy-scc + labels: + app: cert-manager-http01-proxy + app.kubernetes.io/name: cert-manager-http01-proxy + app.kubernetes.io/part-of: cert-manager-operator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:openshift:scc:privileged +subjects: + - kind: ServiceAccount + name: cert-manager-http01-proxy + namespace: cert-manager-operator +`) + +func http01ProxyCertManagerHttp01ProxySccRolebindingYamlBytes() ([]byte, error) { + return _http01ProxyCertManagerHttp01ProxySccRolebindingYaml, nil +} + +func http01ProxyCertManagerHttp01ProxySccRolebindingYaml() (*asset, error) { + bytes, err := http01ProxyCertManagerHttp01ProxySccRolebindingYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "http01-proxy/cert-manager-http01-proxy-scc-rolebinding.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _http01ProxyCertManagerHttp01ProxyServiceaccountYaml = []byte(`apiVersion: v1 +kind: ServiceAccount +metadata: + name: cert-manager-http01-proxy + namespace: cert-manager-operator + labels: + app: cert-manager-http01-proxy + app.kubernetes.io/name: cert-manager-http01-proxy + app.kubernetes.io/part-of: cert-manager-operator +`) + +func http01ProxyCertManagerHttp01ProxyServiceaccountYamlBytes() ([]byte, error) { + return _http01ProxyCertManagerHttp01ProxyServiceaccountYaml, nil +} + +func http01ProxyCertManagerHttp01ProxyServiceaccountYaml() (*asset, error) { + bytes, err := http01ProxyCertManagerHttp01ProxyServiceaccountYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "http01-proxy/cert-manager-http01-proxy-serviceaccount.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + var _istioCsrCertManagerIstioCsrClusterroleYaml = []byte(`kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: @@ -2966,6 +3211,79 @@ func networkpoliciesCertManagerDenyAllNetworkpolicyYaml() (*asset, error) { return a, nil } +var _networkpoliciesHttp01ProxyAllowEgressNetworkpolicyYaml = []byte(`apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: cert-manager-http01-proxy-allow-egress + namespace: cert-manager-operator + labels: + app: cert-manager-http01-proxy + app.kubernetes.io/name: cert-manager-http01-proxy + app.kubernetes.io/part-of: cert-manager-operator +spec: + podSelector: + matchLabels: + app: cert-manager-http01-proxy + policyTypes: + - Egress + egress: + - ports: + - port: 443 + protocol: TCP + - port: 6443 + protocol: TCP + - port: 80 + protocol: TCP +`) + +func networkpoliciesHttp01ProxyAllowEgressNetworkpolicyYamlBytes() ([]byte, error) { + return _networkpoliciesHttp01ProxyAllowEgressNetworkpolicyYaml, nil +} + +func networkpoliciesHttp01ProxyAllowEgressNetworkpolicyYaml() (*asset, error) { + bytes, err := networkpoliciesHttp01ProxyAllowEgressNetworkpolicyYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "networkpolicies/http01-proxy-allow-egress-networkpolicy.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _networkpoliciesHttp01ProxyDenyAllNetworkpolicyYaml = []byte(`apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: cert-manager-http01-proxy-deny-all + namespace: cert-manager-operator + labels: + app: cert-manager-http01-proxy + app.kubernetes.io/name: cert-manager-http01-proxy + app.kubernetes.io/part-of: cert-manager-operator +spec: + podSelector: + matchLabels: + app: cert-manager-http01-proxy + policyTypes: + - Ingress + - Egress +`) + +func networkpoliciesHttp01ProxyDenyAllNetworkpolicyYamlBytes() ([]byte, error) { + return _networkpoliciesHttp01ProxyDenyAllNetworkpolicyYaml, nil +} + +func networkpoliciesHttp01ProxyDenyAllNetworkpolicyYaml() (*asset, error) { + bytes, err := networkpoliciesHttp01ProxyDenyAllNetworkpolicyYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "networkpolicies/http01-proxy-deny-all-networkpolicy.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + var _networkpoliciesIstioCsrAllowEgressToApiServerNetworkpolicyYaml = []byte(`apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: @@ -3772,6 +4090,11 @@ var _bindata = map[string]func() (*asset, error){ "cert-manager-deployment/webhook/cert-manager-webhook-subjectaccessreviews-crb.yaml": certManagerDeploymentWebhookCertManagerWebhookSubjectaccessreviewsCrbYaml, "cert-manager-deployment/webhook/cert-manager-webhook-svc.yaml": certManagerDeploymentWebhookCertManagerWebhookSvcYaml, "cert-manager-deployment/webhook/cert-manager-webhook-validatingwebhookconfiguration.yaml": certManagerDeploymentWebhookCertManagerWebhookValidatingwebhookconfigurationYaml, + "http01-proxy/cert-manager-http01-proxy-clusterrole.yaml": http01ProxyCertManagerHttp01ProxyClusterroleYaml, + "http01-proxy/cert-manager-http01-proxy-clusterrolebinding.yaml": http01ProxyCertManagerHttp01ProxyClusterrolebindingYaml, + "http01-proxy/cert-manager-http01-proxy-daemonset.yaml": http01ProxyCertManagerHttp01ProxyDaemonsetYaml, + "http01-proxy/cert-manager-http01-proxy-scc-rolebinding.yaml": http01ProxyCertManagerHttp01ProxySccRolebindingYaml, + "http01-proxy/cert-manager-http01-proxy-serviceaccount.yaml": http01ProxyCertManagerHttp01ProxyServiceaccountYaml, "istio-csr/cert-manager-istio-csr-clusterrole.yaml": istioCsrCertManagerIstioCsrClusterroleYaml, "istio-csr/cert-manager-istio-csr-clusterrolebinding.yaml": istioCsrCertManagerIstioCsrClusterrolebindingYaml, "istio-csr/cert-manager-istio-csr-deployment.yaml": istioCsrCertManagerIstioCsrDeploymentYaml, @@ -3788,6 +4111,8 @@ var _bindata = map[string]func() (*asset, error){ "networkpolicies/cert-manager-allow-ingress-to-metrics-networkpolicy.yaml": networkpoliciesCertManagerAllowIngressToMetricsNetworkpolicyYaml, "networkpolicies/cert-manager-allow-ingress-to-webhook-networkpolicy.yaml": networkpoliciesCertManagerAllowIngressToWebhookNetworkpolicyYaml, "networkpolicies/cert-manager-deny-all-networkpolicy.yaml": networkpoliciesCertManagerDenyAllNetworkpolicyYaml, + "networkpolicies/http01-proxy-allow-egress-networkpolicy.yaml": networkpoliciesHttp01ProxyAllowEgressNetworkpolicyYaml, + "networkpolicies/http01-proxy-deny-all-networkpolicy.yaml": networkpoliciesHttp01ProxyDenyAllNetworkpolicyYaml, "networkpolicies/istio-csr-allow-egress-to-api-server-networkpolicy.yaml": networkpoliciesIstioCsrAllowEgressToApiServerNetworkpolicyYaml, "networkpolicies/istio-csr-allow-ingress-to-grpc-networkpolicy.yaml": networkpoliciesIstioCsrAllowIngressToGrpcNetworkpolicyYaml, "networkpolicies/istio-csr-allow-ingress-to-metrics-networkpolicy.yaml": networkpoliciesIstioCsrAllowIngressToMetricsNetworkpolicyYaml, @@ -3903,6 +4228,13 @@ var _bintree = &bintree{nil, map[string]*bintree{ "cert-manager-webhook-validatingwebhookconfiguration.yaml": {certManagerDeploymentWebhookCertManagerWebhookValidatingwebhookconfigurationYaml, map[string]*bintree{}}, }}, }}, + "http01-proxy": {nil, map[string]*bintree{ + "cert-manager-http01-proxy-clusterrole.yaml": {http01ProxyCertManagerHttp01ProxyClusterroleYaml, map[string]*bintree{}}, + "cert-manager-http01-proxy-clusterrolebinding.yaml": {http01ProxyCertManagerHttp01ProxyClusterrolebindingYaml, map[string]*bintree{}}, + "cert-manager-http01-proxy-daemonset.yaml": {http01ProxyCertManagerHttp01ProxyDaemonsetYaml, map[string]*bintree{}}, + "cert-manager-http01-proxy-scc-rolebinding.yaml": {http01ProxyCertManagerHttp01ProxySccRolebindingYaml, map[string]*bintree{}}, + "cert-manager-http01-proxy-serviceaccount.yaml": {http01ProxyCertManagerHttp01ProxyServiceaccountYaml, map[string]*bintree{}}, + }}, "istio-csr": {nil, map[string]*bintree{ "cert-manager-istio-csr-clusterrole.yaml": {istioCsrCertManagerIstioCsrClusterroleYaml, map[string]*bintree{}}, "cert-manager-istio-csr-clusterrolebinding.yaml": {istioCsrCertManagerIstioCsrClusterrolebindingYaml, map[string]*bintree{}}, @@ -3922,6 +4254,8 @@ var _bintree = &bintree{nil, map[string]*bintree{ "cert-manager-allow-ingress-to-metrics-networkpolicy.yaml": {networkpoliciesCertManagerAllowIngressToMetricsNetworkpolicyYaml, map[string]*bintree{}}, "cert-manager-allow-ingress-to-webhook-networkpolicy.yaml": {networkpoliciesCertManagerAllowIngressToWebhookNetworkpolicyYaml, map[string]*bintree{}}, "cert-manager-deny-all-networkpolicy.yaml": {networkpoliciesCertManagerDenyAllNetworkpolicyYaml, map[string]*bintree{}}, + "http01-proxy-allow-egress-networkpolicy.yaml": {networkpoliciesHttp01ProxyAllowEgressNetworkpolicyYaml, map[string]*bintree{}}, + "http01-proxy-deny-all-networkpolicy.yaml": {networkpoliciesHttp01ProxyDenyAllNetworkpolicyYaml, map[string]*bintree{}}, "istio-csr-allow-egress-to-api-server-networkpolicy.yaml": {networkpoliciesIstioCsrAllowEgressToApiServerNetworkpolicyYaml, map[string]*bintree{}}, "istio-csr-allow-ingress-to-grpc-networkpolicy.yaml": {networkpoliciesIstioCsrAllowIngressToGrpcNetworkpolicyYaml, map[string]*bintree{}}, "istio-csr-allow-ingress-to-metrics-networkpolicy.yaml": {networkpoliciesIstioCsrAllowIngressToMetricsNetworkpolicyYaml, map[string]*bintree{}}, diff --git a/pkg/operator/clientset/versioned/typed/operator/v1alpha1/fake/fake_http01proxy.go b/pkg/operator/clientset/versioned/typed/operator/v1alpha1/fake/fake_http01proxy.go new file mode 100644 index 000000000..0fb97493a --- /dev/null +++ b/pkg/operator/clientset/versioned/typed/operator/v1alpha1/fake/fake_http01proxy.go @@ -0,0 +1,37 @@ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + operatorv1alpha1 "github.com/openshift/cert-manager-operator/pkg/operator/applyconfigurations/operator/v1alpha1" + typedoperatorv1alpha1 "github.com/openshift/cert-manager-operator/pkg/operator/clientset/versioned/typed/operator/v1alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeHTTP01Proxies implements HTTP01ProxyInterface +type fakeHTTP01Proxies struct { + *gentype.FakeClientWithListAndApply[*v1alpha1.HTTP01Proxy, *v1alpha1.HTTP01ProxyList, *operatorv1alpha1.HTTP01ProxyApplyConfiguration] + Fake *FakeOperatorV1alpha1 +} + +func newFakeHTTP01Proxies(fake *FakeOperatorV1alpha1, namespace string) typedoperatorv1alpha1.HTTP01ProxyInterface { + return &fakeHTTP01Proxies{ + gentype.NewFakeClientWithListAndApply[*v1alpha1.HTTP01Proxy, *v1alpha1.HTTP01ProxyList, *operatorv1alpha1.HTTP01ProxyApplyConfiguration]( + fake.Fake, + namespace, + v1alpha1.SchemeGroupVersion.WithResource("http01proxies"), + v1alpha1.SchemeGroupVersion.WithKind("HTTP01Proxy"), + func() *v1alpha1.HTTP01Proxy { return &v1alpha1.HTTP01Proxy{} }, + func() *v1alpha1.HTTP01ProxyList { return &v1alpha1.HTTP01ProxyList{} }, + func(dst, src *v1alpha1.HTTP01ProxyList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.HTTP01ProxyList) []*v1alpha1.HTTP01Proxy { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha1.HTTP01ProxyList, items []*v1alpha1.HTTP01Proxy) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/pkg/operator/clientset/versioned/typed/operator/v1alpha1/fake/fake_operator_client.go b/pkg/operator/clientset/versioned/typed/operator/v1alpha1/fake/fake_operator_client.go index aaca26cb7..ddd167035 100644 --- a/pkg/operator/clientset/versioned/typed/operator/v1alpha1/fake/fake_operator_client.go +++ b/pkg/operator/clientset/versioned/typed/operator/v1alpha1/fake/fake_operator_client.go @@ -16,6 +16,10 @@ func (c *FakeOperatorV1alpha1) CertManagers() v1alpha1.CertManagerInterface { return newFakeCertManagers(c) } +func (c *FakeOperatorV1alpha1) HTTP01Proxies(namespace string) v1alpha1.HTTP01ProxyInterface { + return newFakeHTTP01Proxies(c, namespace) +} + func (c *FakeOperatorV1alpha1) IstioCSRs(namespace string) v1alpha1.IstioCSRInterface { return newFakeIstioCSRs(c, namespace) } diff --git a/pkg/operator/clientset/versioned/typed/operator/v1alpha1/generated_expansion.go b/pkg/operator/clientset/versioned/typed/operator/v1alpha1/generated_expansion.go index df39e06da..9a6c56b17 100644 --- a/pkg/operator/clientset/versioned/typed/operator/v1alpha1/generated_expansion.go +++ b/pkg/operator/clientset/versioned/typed/operator/v1alpha1/generated_expansion.go @@ -4,6 +4,8 @@ package v1alpha1 type CertManagerExpansion interface{} +type HTTP01ProxyExpansion interface{} + type IstioCSRExpansion interface{} type TrustManagerExpansion interface{} diff --git a/pkg/operator/clientset/versioned/typed/operator/v1alpha1/http01proxy.go b/pkg/operator/clientset/versioned/typed/operator/v1alpha1/http01proxy.go new file mode 100644 index 000000000..ef636b340 --- /dev/null +++ b/pkg/operator/clientset/versioned/typed/operator/v1alpha1/http01proxy.go @@ -0,0 +1,58 @@ +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + operatorv1alpha1 "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + applyconfigurationsoperatorv1alpha1 "github.com/openshift/cert-manager-operator/pkg/operator/applyconfigurations/operator/v1alpha1" + scheme "github.com/openshift/cert-manager-operator/pkg/operator/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// HTTP01ProxiesGetter has a method to return a HTTP01ProxyInterface. +// A group's client should implement this interface. +type HTTP01ProxiesGetter interface { + HTTP01Proxies(namespace string) HTTP01ProxyInterface +} + +// HTTP01ProxyInterface has methods to work with HTTP01Proxy resources. +type HTTP01ProxyInterface interface { + Create(ctx context.Context, hTTP01Proxy *operatorv1alpha1.HTTP01Proxy, opts v1.CreateOptions) (*operatorv1alpha1.HTTP01Proxy, error) + Update(ctx context.Context, hTTP01Proxy *operatorv1alpha1.HTTP01Proxy, opts v1.UpdateOptions) (*operatorv1alpha1.HTTP01Proxy, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, hTTP01Proxy *operatorv1alpha1.HTTP01Proxy, opts v1.UpdateOptions) (*operatorv1alpha1.HTTP01Proxy, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*operatorv1alpha1.HTTP01Proxy, error) + List(ctx context.Context, opts v1.ListOptions) (*operatorv1alpha1.HTTP01ProxyList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *operatorv1alpha1.HTTP01Proxy, err error) + Apply(ctx context.Context, hTTP01Proxy *applyconfigurationsoperatorv1alpha1.HTTP01ProxyApplyConfiguration, opts v1.ApplyOptions) (result *operatorv1alpha1.HTTP01Proxy, err error) + // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). + ApplyStatus(ctx context.Context, hTTP01Proxy *applyconfigurationsoperatorv1alpha1.HTTP01ProxyApplyConfiguration, opts v1.ApplyOptions) (result *operatorv1alpha1.HTTP01Proxy, err error) + HTTP01ProxyExpansion +} + +// hTTP01Proxies implements HTTP01ProxyInterface +type hTTP01Proxies struct { + *gentype.ClientWithListAndApply[*operatorv1alpha1.HTTP01Proxy, *operatorv1alpha1.HTTP01ProxyList, *applyconfigurationsoperatorv1alpha1.HTTP01ProxyApplyConfiguration] +} + +// newHTTP01Proxies returns a HTTP01Proxies +func newHTTP01Proxies(c *OperatorV1alpha1Client, namespace string) *hTTP01Proxies { + return &hTTP01Proxies{ + gentype.NewClientWithListAndApply[*operatorv1alpha1.HTTP01Proxy, *operatorv1alpha1.HTTP01ProxyList, *applyconfigurationsoperatorv1alpha1.HTTP01ProxyApplyConfiguration]( + "http01proxies", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *operatorv1alpha1.HTTP01Proxy { return &operatorv1alpha1.HTTP01Proxy{} }, + func() *operatorv1alpha1.HTTP01ProxyList { return &operatorv1alpha1.HTTP01ProxyList{} }, + ), + } +} diff --git a/pkg/operator/clientset/versioned/typed/operator/v1alpha1/operator_client.go b/pkg/operator/clientset/versioned/typed/operator/v1alpha1/operator_client.go index 9eabd32fe..b042f2907 100644 --- a/pkg/operator/clientset/versioned/typed/operator/v1alpha1/operator_client.go +++ b/pkg/operator/clientset/versioned/typed/operator/v1alpha1/operator_client.go @@ -13,6 +13,7 @@ import ( type OperatorV1alpha1Interface interface { RESTClient() rest.Interface CertManagersGetter + HTTP01ProxiesGetter IstioCSRsGetter TrustManagersGetter } @@ -26,6 +27,10 @@ func (c *OperatorV1alpha1Client) CertManagers() CertManagerInterface { return newCertManagers(c) } +func (c *OperatorV1alpha1Client) HTTP01Proxies(namespace string) HTTP01ProxyInterface { + return newHTTP01Proxies(c, namespace) +} + func (c *OperatorV1alpha1Client) IstioCSRs(namespace string) IstioCSRInterface { return newIstioCSRs(c, namespace) } diff --git a/pkg/operator/informers/externalversions/generic.go b/pkg/operator/informers/externalversions/generic.go index 7dc954ca9..a440afd15 100644 --- a/pkg/operator/informers/externalversions/generic.go +++ b/pkg/operator/informers/externalversions/generic.go @@ -39,6 +39,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource // Group=operator.openshift.io, Version=v1alpha1 case v1alpha1.SchemeGroupVersion.WithResource("certmanagers"): return &genericInformer{resource: resource.GroupResource(), informer: f.Operator().V1alpha1().CertManagers().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("http01proxies"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Operator().V1alpha1().HTTP01Proxies().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("istiocsrs"): return &genericInformer{resource: resource.GroupResource(), informer: f.Operator().V1alpha1().IstioCSRs().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("trustmanagers"): diff --git a/pkg/operator/informers/externalversions/operator/v1alpha1/http01proxy.go b/pkg/operator/informers/externalversions/operator/v1alpha1/http01proxy.go new file mode 100644 index 000000000..ffb2f4921 --- /dev/null +++ b/pkg/operator/informers/externalversions/operator/v1alpha1/http01proxy.go @@ -0,0 +1,86 @@ +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + time "time" + + apioperatorv1alpha1 "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + versioned "github.com/openshift/cert-manager-operator/pkg/operator/clientset/versioned" + internalinterfaces "github.com/openshift/cert-manager-operator/pkg/operator/informers/externalversions/internalinterfaces" + operatorv1alpha1 "github.com/openshift/cert-manager-operator/pkg/operator/listers/operator/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// HTTP01ProxyInformer provides access to a shared informer and lister for +// HTTP01Proxies. +type HTTP01ProxyInformer interface { + Informer() cache.SharedIndexInformer + Lister() operatorv1alpha1.HTTP01ProxyLister +} + +type hTTP01ProxyInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewHTTP01ProxyInformer constructs a new informer for HTTP01Proxy type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewHTTP01ProxyInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredHTTP01ProxyInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredHTTP01ProxyInformer constructs a new informer for HTTP01Proxy type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredHTTP01ProxyInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.OperatorV1alpha1().HTTP01Proxies(namespace).List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.OperatorV1alpha1().HTTP01Proxies(namespace).Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.OperatorV1alpha1().HTTP01Proxies(namespace).List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.OperatorV1alpha1().HTTP01Proxies(namespace).Watch(ctx, options) + }, + }, client), + &apioperatorv1alpha1.HTTP01Proxy{}, + resyncPeriod, + indexers, + ) +} + +func (f *hTTP01ProxyInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredHTTP01ProxyInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *hTTP01ProxyInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apioperatorv1alpha1.HTTP01Proxy{}, f.defaultInformer) +} + +func (f *hTTP01ProxyInformer) Lister() operatorv1alpha1.HTTP01ProxyLister { + return operatorv1alpha1.NewHTTP01ProxyLister(f.Informer().GetIndexer()) +} diff --git a/pkg/operator/informers/externalversions/operator/v1alpha1/interface.go b/pkg/operator/informers/externalversions/operator/v1alpha1/interface.go index 422750840..fbcba0144 100644 --- a/pkg/operator/informers/externalversions/operator/v1alpha1/interface.go +++ b/pkg/operator/informers/externalversions/operator/v1alpha1/interface.go @@ -10,6 +10,8 @@ import ( type Interface interface { // CertManagers returns a CertManagerInformer. CertManagers() CertManagerInformer + // HTTP01Proxies returns a HTTP01ProxyInformer. + HTTP01Proxies() HTTP01ProxyInformer // IstioCSRs returns a IstioCSRInformer. IstioCSRs() IstioCSRInformer // TrustManagers returns a TrustManagerInformer. @@ -32,6 +34,11 @@ func (v *version) CertManagers() CertManagerInformer { return &certManagerInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} } +// HTTP01Proxies returns a HTTP01ProxyInformer. +func (v *version) HTTP01Proxies() HTTP01ProxyInformer { + return &hTTP01ProxyInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // IstioCSRs returns a IstioCSRInformer. func (v *version) IstioCSRs() IstioCSRInformer { return &istioCSRInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/pkg/operator/listers/operator/v1alpha1/expansion_generated.go b/pkg/operator/listers/operator/v1alpha1/expansion_generated.go index 1692896d0..e76b36ac2 100644 --- a/pkg/operator/listers/operator/v1alpha1/expansion_generated.go +++ b/pkg/operator/listers/operator/v1alpha1/expansion_generated.go @@ -6,6 +6,14 @@ package v1alpha1 // CertManagerLister. type CertManagerListerExpansion interface{} +// HTTP01ProxyListerExpansion allows custom methods to be added to +// HTTP01ProxyLister. +type HTTP01ProxyListerExpansion interface{} + +// HTTP01ProxyNamespaceListerExpansion allows custom methods to be added to +// HTTP01ProxyNamespaceLister. +type HTTP01ProxyNamespaceListerExpansion interface{} + // IstioCSRListerExpansion allows custom methods to be added to // IstioCSRLister. type IstioCSRListerExpansion interface{} diff --git a/pkg/operator/listers/operator/v1alpha1/http01proxy.go b/pkg/operator/listers/operator/v1alpha1/http01proxy.go new file mode 100644 index 000000000..951da193a --- /dev/null +++ b/pkg/operator/listers/operator/v1alpha1/http01proxy.go @@ -0,0 +1,54 @@ +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + operatorv1alpha1 "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// HTTP01ProxyLister helps list HTTP01Proxies. +// All objects returned here must be treated as read-only. +type HTTP01ProxyLister interface { + // List lists all HTTP01Proxies in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*operatorv1alpha1.HTTP01Proxy, err error) + // HTTP01Proxies returns an object that can list and get HTTP01Proxies. + HTTP01Proxies(namespace string) HTTP01ProxyNamespaceLister + HTTP01ProxyListerExpansion +} + +// hTTP01ProxyLister implements the HTTP01ProxyLister interface. +type hTTP01ProxyLister struct { + listers.ResourceIndexer[*operatorv1alpha1.HTTP01Proxy] +} + +// NewHTTP01ProxyLister returns a new HTTP01ProxyLister. +func NewHTTP01ProxyLister(indexer cache.Indexer) HTTP01ProxyLister { + return &hTTP01ProxyLister{listers.New[*operatorv1alpha1.HTTP01Proxy](indexer, operatorv1alpha1.Resource("http01proxy"))} +} + +// HTTP01Proxies returns an object that can list and get HTTP01Proxies. +func (s *hTTP01ProxyLister) HTTP01Proxies(namespace string) HTTP01ProxyNamespaceLister { + return hTTP01ProxyNamespaceLister{listers.NewNamespaced[*operatorv1alpha1.HTTP01Proxy](s.ResourceIndexer, namespace)} +} + +// HTTP01ProxyNamespaceLister helps list and get HTTP01Proxies. +// All objects returned here must be treated as read-only. +type HTTP01ProxyNamespaceLister interface { + // List lists all HTTP01Proxies in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*operatorv1alpha1.HTTP01Proxy, err error) + // Get retrieves the HTTP01Proxy from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*operatorv1alpha1.HTTP01Proxy, error) + HTTP01ProxyNamespaceListerExpansion +} + +// hTTP01ProxyNamespaceLister implements the HTTP01ProxyNamespaceLister +// interface. +type hTTP01ProxyNamespaceLister struct { + listers.ResourceIndexer[*operatorv1alpha1.HTTP01Proxy] +} diff --git a/pkg/operator/setup_manager.go b/pkg/operator/setup_manager.go index 852217a84..6fff4f7b5 100644 --- a/pkg/operator/setup_manager.go +++ b/pkg/operator/setup_manager.go @@ -20,12 +20,14 @@ import ( "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" configv1 "github.com/openshift/api/config/v1" v1alpha1 "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" "github.com/openshift/cert-manager-operator/pkg/controller/common" + "github.com/openshift/cert-manager-operator/pkg/controller/http01proxy" "github.com/openshift/cert-manager-operator/pkg/controller/istiocsr" "github.com/openshift/cert-manager-operator/pkg/controller/trustmanager" "github.com/openshift/cert-manager-operator/pkg/version" @@ -80,7 +82,7 @@ var istioCSRManagedResources = []client.Object{ // cert-manager Issuer (and ClusterIssuer, which is never listed here) must not use a // managed-resource label selector: IstioCSR reconciles user-created Issuers referenced // from the spec, which are not labeled by the operator. Those types are left out of -// ByObject so they use the manager cache’s default unfiltered informer per GVK. +// ByObject so they use the manager cache's default unfiltered informer per GVK. var trustManagerManagedResources = []client.Object{ &certmanagerv1.Certificate{}, &appsv1.Deployment{}, @@ -93,6 +95,16 @@ var trustManagerManagedResources = []client.Object{ &admissionregistrationv1.ValidatingWebhookConfiguration{}, } +// http01ProxyManagedResources defines the resources managed by the HTTP01Proxy controller. +// These resources will be watched with a label selector filter. +var http01ProxyManagedResources = []client.Object{ + &appsv1.DaemonSet{}, + &rbacv1.ClusterRole{}, + &rbacv1.ClusterRoleBinding{}, + &corev1.ServiceAccount{}, + &networkingv1.NetworkPolicy{}, +} + func init() { utilruntime.Must(clientscheme.AddToScheme(scheme)) utilruntime.Must(appsv1.AddToScheme(scheme)) @@ -115,6 +127,7 @@ type Manager struct { type ControllerConfig struct { EnableIstioCSR bool EnableTrustManager bool + EnableHTTP01Proxy bool } // NewControllerManager creates a unified manager for all enabled operand controllers. @@ -122,7 +135,7 @@ type ControllerConfig struct { func NewControllerManager(config ControllerConfig) (*Manager, error) { setupLog.Info("setting up unified operator manager") setupLog.Info("controller", "version", version.Get()) - setupLog.Info("enabled controllers", "istioCSR", config.EnableIstioCSR, "trustManager", config.EnableTrustManager) + setupLog.Info("enabled controllers", "istioCSR", config.EnableIstioCSR, "trustManager", config.EnableTrustManager, "http01Proxy", config.EnableHTTP01Proxy) cacheBuilder := newUnifiedCacheBuilder(config) @@ -130,6 +143,9 @@ func NewControllerManager(config ControllerConfig) (*Manager, error) { Scheme: scheme, NewCache: cacheBuilder, Logger: ctrl.Log.WithName("operator-manager"), + // Use a separate port for the controller-runtime metrics server to avoid + // conflicting with the library-go metrics server on :8080. + Metrics: metricsserver.Options{BindAddress: ":8085"}, }) if err != nil { return nil, fmt.Errorf("failed to create manager: %w", err) @@ -148,6 +164,12 @@ func NewControllerManager(config ControllerConfig) (*Manager, error) { } } + if config.EnableHTTP01Proxy { + if err := setupHTTP01ProxyController(mgr); err != nil { + return nil, err + } + } + return &Manager{ manager: mgr, }, nil @@ -179,6 +201,19 @@ func setupTrustManagerController(mgr ctrl.Manager) error { return nil } +// setupHTTP01ProxyController creates and registers the HTTP01Proxy controller with the manager. +func setupHTTP01ProxyController(mgr ctrl.Manager) error { + setupLog.Info("setting up controller", "name", http01proxy.ControllerName) + r, err := http01proxy.New(mgr) + if err != nil { + return fmt.Errorf("failed to create %s reconciler object: %w", http01proxy.ControllerName, err) + } + if err := r.SetupWithManager(mgr); err != nil { + return fmt.Errorf("failed to create %s controller: %w", http01proxy.ControllerName, err) + } + return nil +} + // newUnifiedCacheBuilder creates a cache builder that combines cache configurations // for all enabled controllers into a single unified cache. func newUnifiedCacheBuilder(config ControllerConfig) cache.NewCacheFunc { @@ -214,6 +249,14 @@ func buildCacheObjectList(config ControllerConfig) (map[client.Object]cache.ByOb objectList[&v1alpha1.TrustManager{}] = cache.ByObject{} } + if config.EnableHTTP01Proxy { + if err := addControllerCacheConfig(objectList, http01proxy.RequestEnqueueLabelValue, http01ProxyManagedResources); err != nil { + return nil, fmt.Errorf("failed to configure HTTP01Proxy cache: %w", err) + } + // HTTP01Proxy CR - no label filter needed + objectList[&v1alpha1.HTTP01Proxy{}] = cache.ByObject{} + } + return objectList, nil } diff --git a/pkg/operator/starter.go b/pkg/operator/starter.go index 188f73d35..5c06df05d 100644 --- a/pkg/operator/starter.go +++ b/pkg/operator/starter.go @@ -18,6 +18,7 @@ import ( "github.com/openshift/library-go/pkg/operator/status" "github.com/openshift/library-go/pkg/operator/v1helpers" + v1alpha1 "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" "github.com/openshift/cert-manager-operator/pkg/controller/certmanager" "github.com/openshift/cert-manager-operator/pkg/features" certmanoperatorclient "github.com/openshift/cert-manager-operator/pkg/operator/clientset/versioned" @@ -144,12 +145,14 @@ func RunOperator(ctx context.Context, cc *controllercmd.ControllerContext) error } istioCSREnabled := features.IsIstioCSRFeatureGateEnabled() trustManagerEnabled := featureStatus.IsTrustManagerFeatureGateEnabled() + http01ProxyEnabled := features.DefaultFeatureGate.Enabled(v1alpha1.FeatureHTTP01Proxy) - if istioCSREnabled || trustManagerEnabled { + if istioCSREnabled || trustManagerEnabled || http01ProxyEnabled { // Create unified manager for all enabled operand controllers manager, err := NewControllerManager(ControllerConfig{ EnableIstioCSR: istioCSREnabled, EnableTrustManager: trustManagerEnabled, + EnableHTTP01Proxy: http01ProxyEnabled, }) if err != nil { return fmt.Errorf("failed to create unified controller manager: %w", err) diff --git a/test/e2e/http01proxy_test.go b/test/e2e/http01proxy_test.go new file mode 100644 index 000000000..d0eb2636f --- /dev/null +++ b/test/e2e/http01proxy_test.go @@ -0,0 +1,128 @@ +//go:build e2e +// +build e2e + +package e2e + +import ( + "context" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + + "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" +) + +var _ = Describe("HTTP01 Proxy", Label("Platform:Generic"), Ordered, func() { + var ( + ctx context.Context + originalUnsupportedAddonFeatures string + unsupportedAddonFeaturesEnvVarName = "UNSUPPORTED_ADDON_FEATURES" + ) + + BeforeAll(func() { + ctx = context.Background() + + By("capturing original UNSUPPORTED_ADDON_FEATURES value") + original, err := getSubscriptionEnvVar(ctx, loader, unsupportedAddonFeaturesEnvVarName) + Expect(err).NotTo(HaveOccurred(), "failed to get original UNSUPPORTED_ADDON_FEATURES") + originalUnsupportedAddonFeatures = original + + By("enabling HTTP01Proxy feature gate via subscription env var") + err = patchSubscriptionWithEnvVars(ctx, loader, map[string]string{ + unsupportedAddonFeaturesEnvVarName: "HTTP01Proxy=true", + }) + Expect(err).NotTo(HaveOccurred(), "failed to enable HTTP01Proxy feature gate") + + By("waiting for operator to restart with feature gate enabled") + err = waitForDeploymentEnvVarAndRollout(ctx, operatorNamespace, operatorDeploymentName, + unsupportedAddonFeaturesEnvVarName, "HTTP01Proxy=true", highTimeout) + Expect(err).NotTo(HaveOccurred(), "operator did not roll out after enabling HTTP01Proxy feature gate") + + By("waiting for operator to become available after restart") + err = VerifyHealthyOperatorConditions(certmanageroperatorclient.OperatorV1alpha1()) + Expect(err).NotTo(HaveOccurred(), "operator not healthy after enabling HTTP01Proxy feature gate") + }) + + AfterAll(func() { + By("restoring original UNSUPPORTED_ADDON_FEATURES value") + err := patchSubscriptionWithEnvVars(ctx, loader, map[string]string{ + unsupportedAddonFeaturesEnvVarName: originalUnsupportedAddonFeatures, + }) + if err != nil { + fmt.Fprintf(GinkgoWriter, "failed to restore UNSUPPORTED_ADDON_FEATURES during cleanup: %v\n", err) + return + } + + By("waiting for operator to roll out after restoring feature gates") + if originalUnsupportedAddonFeatures == "" { + err = waitForDeploymentEnvVarRemovedAndRollout(ctx, operatorNamespace, operatorDeploymentName, + unsupportedAddonFeaturesEnvVarName, highTimeout) + } else { + err = waitForDeploymentEnvVarAndRollout(ctx, operatorNamespace, operatorDeploymentName, + unsupportedAddonFeaturesEnvVarName, originalUnsupportedAddonFeatures, highTimeout) + } + if err != nil { + fmt.Fprintf(GinkgoWriter, "operator did not roll out after restoring feature gates: %v\n", err) + } + }) + + BeforeEach(func() { + By("waiting for operator status to become available") + err := VerifyHealthyOperatorConditions(certmanageroperatorclient.OperatorV1alpha1()) + Expect(err).NotTo(HaveOccurred(), "operator is expected to be available") + }) + + Context("on a non-baremetal cluster", func() { + It("should reject HTTP01Proxy CR with unsupported platform condition", func() { + proxy := &v1alpha1.HTTP01Proxy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + Namespace: operatorNamespace, + }, + Spec: v1alpha1.HTTP01ProxySpec{ + Mode: v1alpha1.HTTP01ProxyModeDefault, + }, + } + + By("creating HTTP01Proxy CR") + _, err := certmanageroperatorclient.OperatorV1alpha1().HTTP01Proxies(operatorNamespace).Create(ctx, proxy, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred(), "failed to create HTTP01Proxy CR") + + DeferCleanup(func(ctx context.Context) { + By("deleting HTTP01Proxy CR") + err := certmanageroperatorclient.OperatorV1alpha1().HTTP01Proxies(operatorNamespace).Delete(ctx, "default", metav1.DeleteOptions{}) + if err != nil && !apierrors.IsNotFound(err) { + fmt.Fprintf(GinkgoWriter, "failed to delete HTTP01Proxy CR during cleanup: %v\n", err) + } + + By("waiting for HTTP01Proxy CR to be fully removed") + _ = wait.PollUntilContextTimeout(ctx, fastPollInterval, lowTimeout, true, func(ctx context.Context) (bool, error) { + _, getErr := certmanageroperatorclient.OperatorV1alpha1().HTTP01Proxies(operatorNamespace).Get(ctx, "default", metav1.GetOptions{}) + return apierrors.IsNotFound(getErr), nil + }) + }) + + By("waiting for Degraded=True and Ready=False conditions") + Eventually(func(g Gomega) { + fetched, getErr := certmanageroperatorclient.OperatorV1alpha1().HTTP01Proxies(operatorNamespace).Get(ctx, "default", metav1.GetOptions{}) + g.Expect(getErr).NotTo(HaveOccurred()) + + degraded := meta.FindStatusCondition(fetched.Status.Conditions, v1alpha1.Degraded) + g.Expect(degraded).NotTo(BeNil(), "Degraded condition not found on HTTP01Proxy") + g.Expect(degraded.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(degraded.Reason).To(Equal(v1alpha1.ReasonFailed)) + g.Expect(degraded.Message).To(ContainSubstring("not supported")) + + ready := meta.FindStatusCondition(fetched.Status.Conditions, v1alpha1.Ready) + g.Expect(ready).NotTo(BeNil(), "Ready condition not found on HTTP01Proxy") + g.Expect(ready.Status).To(Equal(metav1.ConditionFalse)) + }, lowTimeout, fastPollInterval).Should(Succeed()) + }) + }) +})