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/bundle/manifests/cert-manager-operator.clusterserviceversion.yaml b/bundle/manifests/cert-manager-operator.clusterserviceversion.yaml index 2832f06b5..cd8782f66 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. @@ -616,6 +619,18 @@ spec: - patch - update - watch + - apiGroups: + - machineconfiguration.openshift.io + resources: + - machineconfigs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - networking.k8s.io resources: @@ -646,6 +661,7 @@ spec: - operator.openshift.io resources: - certmanagers/finalizers + - http01proxies/finalizers - istiocsrs/finalizers - trustmanagers/finalizers verbs: @@ -654,6 +670,7 @@ spec: - operator.openshift.io resources: - certmanagers/status + - http01proxies/status - istiocsrs/status - trustmanagers/status verbs: @@ -663,6 +680,7 @@ spec: - apiGroups: - operator.openshift.io resources: + - http01proxies - istiocsrs - trustmanagers verbs: @@ -802,12 +820,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 +946,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/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..fad33d278 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -207,6 +207,18 @@ rules: - patch - update - watch +- apiGroups: + - machineconfiguration.openshift.io + resources: + - machineconfigs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - networking.k8s.io resources: @@ -237,6 +249,7 @@ rules: - operator.openshift.io resources: - certmanagers/finalizers + - http01proxies/finalizers - istiocsrs/finalizers - trustmanagers/finalizers verbs: @@ -245,6 +258,7 @@ rules: - operator.openshift.io resources: - certmanagers/status + - http01proxies/status - istiocsrs/status - trustmanagers/status verbs: @@ -254,6 +268,7 @@ rules: - apiGroups: - operator.openshift.io resources: + - http01proxies - istiocsrs - trustmanagers verbs: 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/pkg/controller/http01proxy/constants.go b/pkg/controller/http01proxy/constants.go new file mode 100644 index 000000000..a0796d1ef --- /dev/null +++ b/pkg/controller/http01proxy/constants.go @@ -0,0 +1,34 @@ +package http01proxy + +import ( + "time" + + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + infrastructureGVK = schema.GroupVersionKind{ + Group: "config.openshift.io", + Version: "v1", + Kind: "Infrastructure", + } + + machineConfigGVK = schema.GroupVersionKind{ + Group: "machineconfiguration.openshift.io", + Version: "v1", + Kind: "MachineConfig", + } +) + +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 + + http01proxyObjectName = "default" + + machineConfigName = "98-nftables-crtmgr-http01-dnat" +) diff --git a/pkg/controller/http01proxy/controller.go b/pkg/controller/http01proxy/controller.go new file mode 100644 index 000000000..16ee18bd4 --- /dev/null +++ b/pkg/controller/http01proxy/controller.go @@ -0,0 +1,159 @@ +package http01proxy + +import ( + "context" + "fmt" + "sync" + + corev1 "k8s.io/api/core/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/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "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" +) + +// Reconciler reconciles HTTP01Proxy objects and manages the nftables MachineConfig on baremetal clusters. +type Reconciler struct { + common.CtrlClient + + eventRecorder record.EventRecorder + log logr.Logger + + 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=config.openshift.io,resources=infrastructures,verbs=get;list;watch +// +kubebuilder:rbac:groups=machineconfiguration.openshift.io,resources=machineconfigs,verbs=get;list;watch;create;update;patch;delete + +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), + }, nil +} + +// SetupWithManager registers the controller with the manager and sets up watches for HTTP01Proxy and Infrastructure resources. +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { + infrastructureMapFunc := func(ctx context.Context, obj client.Object) []reconcile.Request { + if obj.GetName() != "cluster" { + return []reconcile.Request{} + } + r.platformMu.Lock() + r.cachedPlatform = nil + r.platformMu.Unlock() + r.log.V(2).Info("infrastructure/cluster changed, invalidated platform cache") + + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Name: http01proxyObjectName, + Namespace: common.OperatorNamespace, + }, + }, + } + } + + builder := ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.HTTP01Proxy{}). + Named(ControllerName) + + // Only watch Infrastructure if the CRD exists (MicroShift does not serve config.openshift.io). + if _, err := mgr.GetRESTMapper().RESTMapping(infrastructureGVK.GroupKind(), infrastructureGVK.Version); err == nil { + builder = builder.Watches(&configv1.Infrastructure{}, handler.EnqueueRequestsFromMapFunc(infrastructureMapFunc)) + } else { + r.log.V(1).Info("Infrastructure CRD not available, skipping watch") + } + + return builder.Complete(r) +} + +// Reconcile handles a single reconciliation loop for an HTTP01Proxy resource. +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, + ) +} + +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.deleteMachineConfig(ctx); err != nil { + return fmt.Errorf("failed to delete MachineConfig: %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..868047277 --- /dev/null +++ b/pkg/controller/http01proxy/controller_test.go @@ -0,0 +1,197 @@ +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(), + } +} + +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() + fakeClient.GetStub = func(_ context.Context, _ client.ObjectKey, obj client.Object) error { + 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 TestCleanUpDeletesMachineConfig(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) + } + + if fakeClient.DeleteCallCount() != 1 { + t.Errorf("expected 1 Delete call (MachineConfig), got %d", fakeClient.DeleteCallCount()) + } +} + +func TestCleanUpMachineConfigDeleteFails(t *testing.T) { + fakeClient := &fakes.FakeCtrlClient{} + fakeClient.DeleteReturnsOnCall(0, fmt.Errorf("mc 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 MachineConfig delete fails") + } + if !strings.Contains(err.Error(), "MachineConfig") { + t.Errorf("error = %q, want substring %q", err.Error(), "MachineConfig") + } +} + +func TestCleanUpMachineConfigAlreadyGone(t *testing.T) { + fakeClient := &fakes.FakeCtrlClient{} + fakeClient.DeleteReturns(errors.NewNotFound( + schema.GroupResource{Group: "machineconfiguration.openshift.io", Resource: "machineconfigs"}, + machineConfigName, + )) + + r := newTestReconciler(fakeClient) + proxy := &v1alpha1.HTTP01Proxy{} + proxy.SetName(http01proxyObjectName) + proxy.SetNamespace(common.OperatorNamespace) + + err := r.cleanUp(context.Background(), proxy) + if err != nil { + t.Fatalf("cleanUp should succeed when MachineConfig is already gone, got: %v", err) + } +} + +func TestReconcileDeletionWithoutFinalizer(t *testing.T) { + fakeClient := &fakes.FakeCtrlClient{} + now := metav1.Now() + fakeClient.GetStub = func(_ context.Context, _ client.ObjectKey, obj client.Object) error { + proxy := obj.(*v1alpha1.HTTP01Proxy) + proxy.Name = http01proxyObjectName + proxy.Namespace = common.OperatorNamespace + proxy.DeletionTimestamp = &now + proxy.Finalizers = []string{} + return 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, got %v", result) + } +} diff --git a/pkg/controller/http01proxy/infrastructure.go b/pkg/controller/http01proxy/infrastructure.go new file mode 100644 index 000000000..b8f1333c5 --- /dev/null +++ b/pkg/controller/http01proxy/infrastructure.go @@ -0,0 +1,106 @@ +package http01proxy + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" +) + +const ( + platformBareMetal = "BareMetal" + platformUnknown = "Unknown" +) + +// 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 { + if errors.IsNotFound(err) || meta.IsNoMatchError(err) { + return &platformInfo{platformType: platformUnknown}, 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" + } + + for _, apiVIP := range info.apiVIPs { + for _, ingressVIP := range info.ingressVIPs { + if apiVIP == ingressVIP { + return "API VIP and ingress VIP are the same; HTTP01 proxy is not needed" + } + } + } + + return "" +} diff --git a/pkg/controller/http01proxy/infrastructure_test.go b/pkg/controller/http01proxy/infrastructure_test.go new file mode 100644 index 000000000..b54822455 --- /dev/null +++ b/pkg/controller/http01proxy/infrastructure_test.go @@ -0,0 +1,292 @@ +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..e3f4bff0a --- /dev/null +++ b/pkg/controller/http01proxy/install_http01proxy.go @@ -0,0 +1,38 @@ +package http01proxy + +import ( + "context" + "fmt" + + "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", "platformType", info.platformType) + if err := r.cleanUp(ctx, proxy); err != nil { + return common.NewRetryRequiredError(err, "failed to clean up resources after platform validation failure") + } + return common.NewIrrecoverableError(fmt.Errorf("platform validation failed"), "%s", reason) + } + + if err := r.createOrApplyMachineConfig(ctx, info); err != nil { + r.log.Error(err, "failed to reconcile DNAT MachineConfig") + 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/machineconfig.go b/pkg/controller/http01proxy/machineconfig.go new file mode 100644 index 000000000..e8b98f6b1 --- /dev/null +++ b/pkg/controller/http01proxy/machineconfig.go @@ -0,0 +1,170 @@ +package http01proxy + +import ( + "bytes" + "context" + "encoding/base64" + "fmt" + "reflect" + "text/template" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/yaml" +) + +const nftRulesTemplate = `table inet crtmgr_http01_dnat +delete table inet crtmgr_http01_dnat +table inet crtmgr_http01_dnat { + chain prerouting { + type nat hook prerouting priority 0; + ip daddr {{ .APIVIP }} tcp dport 80 dnat ip to {{ .IngressVIP }}:80 + } + chain postrouting { + type nat hook postrouting priority 100; + ip daddr {{ .IngressVIP }} tcp dport 80 masquerade + } +} +` + +const machineConfigTemplate = `apiVersion: machineconfiguration.openshift.io/v1 +kind: MachineConfig +metadata: + labels: + machineconfiguration.openshift.io/role: master + name: {{ .Name }} +spec: + config: + ignition: + version: 3.4.0 + storage: + files: + - contents: + source: data:text/plain;charset=utf-8;base64,{{ .NFTRulesBase64 }} + mode: 384 + overwrite: true + path: /etc/sysconfig/nftables-crtmgr-http01.conf + systemd: + units: + - contents: | + [Unit] + Description=cert-manager HTTP01 DNAT nftables rules + Wants=network-pre.target + Before=network-pre.target + [Service] + Type=oneshot + ProtectSystem=full + ProtectHome=true + ExecStartPre=/sbin/sysctl -w net.ipv4.ip_forward=1 + ExecStart=/sbin/nft -f /etc/sysconfig/nftables-crtmgr-http01.conf + ExecStart=/bin/bash -c '/usr/sbin/iptables -C FORWARD -p tcp -d {{ .IngressVIP }}/32 --dport 80 -j ACCEPT 2>/dev/null || /usr/sbin/iptables -I FORWARD 1 -p tcp -d {{ .IngressVIP }}/32 --dport 80 -j ACCEPT' + ExecStart=/bin/bash -c '/usr/sbin/iptables -C FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || /usr/sbin/iptables -I FORWARD 1 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT' + ExecReload=/sbin/nft -f /etc/sysconfig/nftables-crtmgr-http01.conf + ExecStop=/sbin/nft 'add table inet crtmgr_http01_dnat; delete table inet crtmgr_http01_dnat' + ExecStop=/bin/bash -c '/usr/sbin/iptables -D FORWARD -p tcp -d {{ .IngressVIP }}/32 --dport 80 -j ACCEPT 2>/dev/null; true' + ExecStop=/bin/bash -c '/usr/sbin/iptables -D FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; true' + RemainAfterExit=yes + [Install] + WantedBy=multi-user.target + enabled: true + name: crtmgr-http01-dnat.service +` + +var ( + nftRulesTmpl = template.Must(template.New("nft").Parse(nftRulesTemplate)) + machineConfigTmpl = template.Must(template.New("mc").Parse(machineConfigTemplate)) +) + +type nftRulesRenderData struct { + APIVIP string + IngressVIP string +} + +type machineConfigRenderData struct { + Name string + NFTRulesBase64 string + IngressVIP string +} + +func renderMachineConfig(apiVIP, ingressVIP string) (*unstructured.Unstructured, error) { + var nftBuf bytes.Buffer + if err := nftRulesTmpl.Execute(&nftBuf, nftRulesRenderData{APIVIP: apiVIP, IngressVIP: ingressVIP}); err != nil { + return nil, fmt.Errorf("failed to render nftables rules: %w", err) + } + + mcData := machineConfigRenderData{ + Name: machineConfigName, + NFTRulesBase64: base64.StdEncoding.EncodeToString(nftBuf.Bytes()), + IngressVIP: ingressVIP, + } + + var mcBuf bytes.Buffer + if err := machineConfigTmpl.Execute(&mcBuf, mcData); err != nil { + return nil, fmt.Errorf("failed to render MachineConfig template: %w", err) + } + + obj := &unstructured.Unstructured{} + if err := yaml.NewYAMLOrJSONDecoder(&mcBuf, mcBuf.Len()).Decode(&obj.Object); err != nil { + return nil, fmt.Errorf("failed to decode rendered MachineConfig: %w", err) + } + + return obj, nil +} + +func (r *Reconciler) createOrApplyMachineConfig(ctx context.Context, info *platformInfo) error { + if len(info.apiVIPs) == 0 { + return fmt.Errorf("no API VIPs available for MachineConfig") + } + if len(info.ingressVIPs) == 0 { + return fmt.Errorf("no ingress VIPs available for MachineConfig") + } + + desired, err := renderMachineConfig(info.apiVIPs[0], info.ingressVIPs[0]) + if err != nil { + return fmt.Errorf("failed to render DNAT MachineConfig: %w", err) + } + + existing := &unstructured.Unstructured{} + existing.SetGroupVersionKind(machineConfigGVK) + err = r.Get(ctx, types.NamespacedName{Name: machineConfigName}, existing) + if err != nil { + if !errors.IsNotFound(err) { + return fmt.Errorf("failed to get MachineConfig %q: %w", machineConfigName, err) + } + r.log.V(2).Info("creating MachineConfig", "name", machineConfigName) + if err := r.Create(ctx, desired); err != nil { + return fmt.Errorf("failed to create MachineConfig %q: %w", machineConfigName, err) + } + return nil + } + + desiredSpec, _, _ := unstructured.NestedMap(desired.Object, "spec") + existingSpec, _, _ := unstructured.NestedMap(existing.Object, "spec") + if reflect.DeepEqual(desiredSpec, existingSpec) { + r.log.V(4).Info("MachineConfig unchanged, skipping update", "name", machineConfigName) + return nil + } + + desired.SetResourceVersion(existing.GetResourceVersion()) + r.log.V(2).Info("updating MachineConfig", "name", machineConfigName) + if err := r.Update(ctx, desired); err != nil { + return fmt.Errorf("failed to update MachineConfig %q: %w", machineConfigName, err) + } + return nil +} + +func (r *Reconciler) deleteMachineConfig(ctx context.Context) error { + mc := &unstructured.Unstructured{} + mc.SetGroupVersionKind(machineConfigGVK) + mc.SetName(machineConfigName) + if err := r.Delete(ctx, mc); err != nil { + if errors.IsNotFound(err) || meta.IsNoMatchError(err) { + return nil + } + return fmt.Errorf("failed to delete MachineConfig %q: %w", machineConfigName, err) + } + r.log.V(2).Info("deleted MachineConfig", "name", machineConfigName) + return nil +} diff --git a/pkg/controller/http01proxy/machineconfig_test.go b/pkg/controller/http01proxy/machineconfig_test.go new file mode 100644 index 000000000..df92122a0 --- /dev/null +++ b/pkg/controller/http01proxy/machineconfig_test.go @@ -0,0 +1,430 @@ +package http01proxy + +import ( + "context" + "encoding/base64" + "fmt" + "strings" + "testing" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openshift/cert-manager-operator/pkg/controller/common/fakes" +) + +var machineConfigGR = schema.GroupResource{Group: "machineconfiguration.openshift.io", Resource: "machineconfigs"} + +func extractNFTRules(t *testing.T, mc *unstructured.Unstructured) string { + t.Helper() + files, found, err := unstructured.NestedSlice(mc.Object, "spec", "config", "storage", "files") + if err != nil || !found || len(files) == 0 { + t.Fatalf("storage files not found: found=%v err=%v", found, err) + } + file := files[0].(map[string]interface{}) + contents := file["contents"].(map[string]interface{}) + source := contents["source"].(string) + b64Data := strings.TrimPrefix(source, "data:text/plain;charset=utf-8;base64,") + decoded, err := base64.StdEncoding.DecodeString(b64Data) + if err != nil { + t.Fatalf("failed to decode base64 nftables rules: %v", err) + } + return string(decoded) +} + +func extractUnitContents(t *testing.T, mc *unstructured.Unstructured) string { + t.Helper() + units, found, err := unstructured.NestedSlice(mc.Object, "spec", "config", "systemd", "units") + if err != nil || !found || len(units) == 0 { + t.Fatalf("systemd units not found: found=%v err=%v", found, err) + } + unit := units[0].(map[string]interface{}) + return unit["contents"].(string) +} + +func TestRenderMachineConfig(t *testing.T) { + mc, err := renderMachineConfig("10.46.97.1", "10.46.97.48") + if err != nil { + t.Fatalf("renderMachineConfig() error: %v", err) + } + + if mc.GetKind() != "MachineConfig" { + t.Errorf("kind = %q, want MachineConfig", mc.GetKind()) + } + if mc.GetName() != machineConfigName { + t.Errorf("name = %q, want %q", mc.GetName(), machineConfigName) + } + + labels := mc.GetLabels() + if labels["machineconfiguration.openshift.io/role"] != "master" { + t.Errorf("role label = %q, want master", labels["machineconfiguration.openshift.io/role"]) + } + + nftRules := extractNFTRules(t, mc) + + if !strings.Contains(nftRules, "10.46.97.48") { + t.Error("nftables rules should contain the ingress VIP") + } + if !strings.Contains(nftRules, "ip daddr 10.46.97.1") { + t.Error("nftables DNAT rule should match API VIP as destination") + } + if !strings.Contains(nftRules, "dnat ip to 10.46.97.48:80") { + t.Error("nftables rules should contain DNAT rule with 'dnat ip to' for inet table") + } + if !strings.Contains(nftRules, "masquerade") { + t.Error("nftables rules should contain masquerade rule") + } + if !strings.Contains(nftRules, "crtmgr_http01_dnat") { + t.Error("nftables rules should reference the table name") + } + if !strings.Contains(nftRules, "table inet crtmgr_http01_dnat") { + t.Error("nftables rules should use inet (dual-stack) family") + } + if !strings.Contains(nftRules, "hook prerouting") { + t.Error("nftables rules should have prerouting chain") + } + if !strings.Contains(nftRules, "hook postrouting") { + t.Error("nftables rules should have postrouting chain") + } + + units, _, _ := unstructured.NestedSlice(mc.Object, "spec", "config", "systemd", "units") + unit := units[0].(map[string]interface{}) + unitContents := unit["contents"].(string) + + if !strings.Contains(unitContents, "nft -f /etc/sysconfig/nftables-crtmgr-http01.conf") { + t.Error("unit should load nftables rules from config file") + } + if !strings.Contains(unitContents, "sysctl -w net.ipv4.ip_forward=1") { + t.Error("unit should enable ip_forward via sysctl") + } + if !strings.Contains(unitContents, "iptables -C FORWARD") { + t.Error("unit should check for existing iptables FORWARD rule before inserting") + } + if !strings.Contains(unitContents, "iptables -I FORWARD 1 -p tcp -d 10.46.97.48/32 --dport 80 -j ACCEPT") { + t.Error("unit should insert iptables FORWARD ACCEPT rule for ingress VIP") + } + if !strings.Contains(unitContents, "conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT") { + t.Error("unit should have iptables FORWARD rule for established connections") + } + if !strings.Contains(unitContents, "ExecStop=/sbin/nft") { + t.Error("unit ExecStop should clean up nftables table") + } + if !strings.Contains(unitContents, "iptables -D FORWARD -p tcp -d 10.46.97.48/32 --dport 80 -j ACCEPT") { + t.Error("unit ExecStop should remove iptables FORWARD rule") + } + if !strings.Contains(unitContents, "iptables -D FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT") { + t.Error("unit ExecStop should remove conntrack FORWARD rule") + } + if !strings.Contains(unitContents, "Type=oneshot") { + t.Error("unit should be Type=oneshot") + } + if !strings.Contains(unitContents, "RemainAfterExit=yes") { + t.Error("unit should have RemainAfterExit=yes") + } + if unit["name"] != "crtmgr-http01-dnat.service" { + t.Errorf("unit name = %q, want crtmgr-http01-dnat.service", unit["name"]) + } + if unit["enabled"] != true { + t.Errorf("unit enabled = %v, want true", unit["enabled"]) + } +} + +func TestRenderMachineConfigDifferentVIP(t *testing.T) { + mc, err := renderMachineConfig("192.168.1.1", "192.168.1.100") + if err != nil { + t.Fatalf("renderMachineConfig() error: %v", err) + } + + nftRules := extractNFTRules(t, mc) + if !strings.Contains(nftRules, "192.168.1.100") { + t.Error("nftables rules should contain the provided VIP") + } + if !strings.Contains(nftRules, "dnat ip to 192.168.1.100:80") { + t.Error("nftables DNAT rule should use the provided VIP") + } + + unitContents := extractUnitContents(t, mc) + if !strings.Contains(unitContents, "192.168.1.100") { + t.Error("systemd unit iptables rules should reference the provided VIP") + } +} + +func TestRenderMachineConfigIgnitionVersion(t *testing.T) { + mc, err := renderMachineConfig("10.0.0.254", "10.0.0.1") + if err != nil { + t.Fatalf("renderMachineConfig() error: %v", err) + } + + version, found, err := unstructured.NestedString(mc.Object, "spec", "config", "ignition", "version") + if err != nil || !found { + t.Fatalf("ignition version not found: found=%v err=%v", found, err) + } + if version != "3.4.0" { + t.Errorf("ignition version = %q, want 3.4.0", version) + } +} + +func TestRenderMachineConfigFilePermissions(t *testing.T) { + mc, err := renderMachineConfig("10.0.0.254", "10.0.0.1") + if err != nil { + t.Fatalf("renderMachineConfig() error: %v", err) + } + + files, _, _ := unstructured.NestedSlice(mc.Object, "spec", "config", "storage", "files") + file := files[0].(map[string]interface{}) + + path, ok := file["path"].(string) + if !ok || path != "/etc/sysconfig/nftables-crtmgr-http01.conf" { + t.Errorf("file path = %q, want /etc/sysconfig/nftables-crtmgr-http01.conf", path) + } + + mode, ok := file["mode"].(float64) + if !ok || mode != 384 { // 0600 octal + t.Errorf("file mode = %v, want 384 (0600)", mode) + } +} + +func TestCreateOrApplyMachineConfigNoVIPs(t *testing.T) { + fakeClient := &fakes.FakeCtrlClient{} + r := newTestReconciler(fakeClient) + + err := r.createOrApplyMachineConfig(context.Background(), &platformInfo{ + apiVIPs: []string{}, + ingressVIPs: []string{"10.0.0.1"}, + }) + if err == nil { + t.Fatal("expected error for empty apiVIPs") + } + if !strings.Contains(err.Error(), "no API VIPs") { + t.Errorf("error = %q, want substring 'no API VIPs'", err.Error()) + } + + err = r.createOrApplyMachineConfig(context.Background(), &platformInfo{ + apiVIPs: []string{"10.0.0.254"}, + ingressVIPs: []string{}, + }) + if err == nil { + t.Fatal("expected error for empty ingressVIPs") + } + if !strings.Contains(err.Error(), "no ingress VIPs") { + t.Errorf("error = %q, want substring 'no ingress VIPs'", err.Error()) + } + if fakeClient.GetCallCount() != 0 { + t.Errorf("expected 0 Get calls, got %d", fakeClient.GetCallCount()) + } +} + +func TestCreateOrApplyMachineConfigCreate(t *testing.T) { + fakeClient := &fakes.FakeCtrlClient{} + fakeClient.GetReturns(errors.NewNotFound(machineConfigGR, machineConfigName)) + fakeClient.CreateReturns(nil) + + r := newTestReconciler(fakeClient) + + err := r.createOrApplyMachineConfig(context.Background(), &platformInfo{ + apiVIPs: []string{"10.0.0.253"}, + ingressVIPs: []string{"10.0.0.2"}, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if fakeClient.CreateCallCount() != 1 { + t.Errorf("expected 1 Create call, got %d", fakeClient.CreateCallCount()) + } + + _, created, _ := fakeClient.CreateArgsForCall(0) + u := created.(*unstructured.Unstructured) + if u.GetName() != machineConfigName { + t.Errorf("created MachineConfig name = %q, want %q", u.GetName(), machineConfigName) + } +} + +func TestCreateOrApplyMachineConfigCreateError(t *testing.T) { + fakeClient := &fakes.FakeCtrlClient{} + fakeClient.GetReturns(errors.NewNotFound(machineConfigGR, machineConfigName)) + fakeClient.CreateReturns(fmt.Errorf("forbidden")) + + r := newTestReconciler(fakeClient) + + err := r.createOrApplyMachineConfig(context.Background(), &platformInfo{ + apiVIPs: []string{"10.0.0.253"}, + ingressVIPs: []string{"10.0.0.2"}, + }) + if err == nil { + t.Fatal("expected error when Create fails") + } + if !strings.Contains(err.Error(), "failed to create MachineConfig") { + t.Errorf("error = %q, want substring 'failed to create MachineConfig'", err.Error()) + } +} + +func TestCreateOrApplyMachineConfigGetError(t *testing.T) { + fakeClient := &fakes.FakeCtrlClient{} + fakeClient.GetReturns(fmt.Errorf("connection refused")) + + r := newTestReconciler(fakeClient) + + err := r.createOrApplyMachineConfig(context.Background(), &platformInfo{ + apiVIPs: []string{"10.0.0.253"}, + ingressVIPs: []string{"10.0.0.2"}, + }) + if err == nil { + t.Fatal("expected error when Get fails with non-NotFound") + } + if !strings.Contains(err.Error(), "failed to get MachineConfig") { + t.Errorf("error = %q, want substring 'failed to get MachineConfig'", err.Error()) + } + if fakeClient.CreateCallCount() != 0 { + t.Error("should not attempt Create when Get fails") + } +} + +func TestCreateOrApplyMachineConfigUpdateWhenSpecDiffers(t *testing.T) { + fakeClient := &fakes.FakeCtrlClient{} + fakeClient.GetStub = func(_ context.Context, _ client.ObjectKey, obj client.Object) error { + u := obj.(*unstructured.Unstructured) + u.SetGroupVersionKind(machineConfigGVK) + u.SetName(machineConfigName) + u.SetResourceVersion("12345") + // Existing spec has a different VIP + unstructured.SetNestedField(u.Object, "old-ignition-data", "spec", "config", "ignition", "version") + return nil + } + fakeClient.UpdateReturns(nil) + + r := newTestReconciler(fakeClient) + + err := r.createOrApplyMachineConfig(context.Background(), &platformInfo{ + apiVIPs: []string{"10.0.0.253"}, + ingressVIPs: []string{"10.0.0.2"}, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if fakeClient.UpdateCallCount() != 1 { + t.Errorf("expected 1 Update call, got %d", fakeClient.UpdateCallCount()) + } + if fakeClient.CreateCallCount() != 0 { + t.Error("should not Create when MachineConfig already exists") + } + + _, updated, _ := fakeClient.UpdateArgsForCall(0) + u := updated.(*unstructured.Unstructured) + if u.GetResourceVersion() != "12345" { + t.Errorf("Update should preserve resourceVersion, got %q", u.GetResourceVersion()) + } +} + +func TestCreateOrApplyMachineConfigUpdateError(t *testing.T) { + fakeClient := &fakes.FakeCtrlClient{} + fakeClient.GetStub = func(_ context.Context, _ client.ObjectKey, obj client.Object) error { + u := obj.(*unstructured.Unstructured) + u.SetGroupVersionKind(machineConfigGVK) + u.SetName(machineConfigName) + u.SetResourceVersion("12345") + unstructured.SetNestedField(u.Object, "stale", "spec", "config", "ignition", "version") + return nil + } + fakeClient.UpdateReturns(fmt.Errorf("conflict")) + + r := newTestReconciler(fakeClient) + + err := r.createOrApplyMachineConfig(context.Background(), &platformInfo{ + apiVIPs: []string{"10.0.0.253"}, + ingressVIPs: []string{"10.0.0.2"}, + }) + if err == nil { + t.Fatal("expected error when Update fails") + } + if !strings.Contains(err.Error(), "failed to update MachineConfig") { + t.Errorf("error = %q, want substring 'failed to update MachineConfig'", err.Error()) + } +} + +func TestCreateOrApplyMachineConfigNoOpWhenUnchanged(t *testing.T) { + desired, err := renderMachineConfig("10.0.0.253", "10.0.0.2") + if err != nil { + t.Fatalf("renderMachineConfig() error: %v", err) + } + desiredSpec, _, _ := unstructured.NestedMap(desired.Object, "spec") + + fakeClient := &fakes.FakeCtrlClient{} + fakeClient.GetStub = func(_ context.Context, _ client.ObjectKey, obj client.Object) error { + u := obj.(*unstructured.Unstructured) + u.SetGroupVersionKind(machineConfigGVK) + u.SetName(machineConfigName) + u.SetResourceVersion("99") + unstructured.SetNestedField(u.Object, desiredSpec, "spec") + return nil + } + + r := newTestReconciler(fakeClient) + + err = r.createOrApplyMachineConfig(context.Background(), &platformInfo{ + apiVIPs: []string{"10.0.0.253"}, + ingressVIPs: []string{"10.0.0.2"}, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if fakeClient.UpdateCallCount() != 0 { + t.Error("should not Update when spec is unchanged") + } + if fakeClient.CreateCallCount() != 0 { + t.Error("should not Create when MachineConfig already exists") + } +} + +func TestDeleteMachineConfigSuccess(t *testing.T) { + fakeClient := &fakes.FakeCtrlClient{} + fakeClient.DeleteReturns(nil) + + r := newTestReconciler(fakeClient) + + err := r.deleteMachineConfig(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if fakeClient.DeleteCallCount() != 1 { + t.Errorf("expected 1 Delete call, got %d", fakeClient.DeleteCallCount()) + } + + _, deleted, _ := fakeClient.DeleteArgsForCall(0) + u := deleted.(*unstructured.Unstructured) + if u.GetName() != machineConfigName { + t.Errorf("deleted object name = %q, want %q", u.GetName(), machineConfigName) + } + if u.GetObjectKind().GroupVersionKind() != machineConfigGVK { + t.Errorf("deleted object GVK = %v, want %v", u.GetObjectKind().GroupVersionKind(), machineConfigGVK) + } +} + +func TestDeleteMachineConfigNotFound(t *testing.T) { + fakeClient := &fakes.FakeCtrlClient{} + fakeClient.DeleteReturns(errors.NewNotFound(machineConfigGR, machineConfigName)) + + r := newTestReconciler(fakeClient) + + err := r.deleteMachineConfig(context.Background()) + if err != nil { + t.Fatalf("deleteMachineConfig should succeed when MachineConfig is NotFound, got: %v", err) + } +} + +func TestDeleteMachineConfigError(t *testing.T) { + fakeClient := &fakes.FakeCtrlClient{} + fakeClient.DeleteReturns(fmt.Errorf("permission denied")) + + r := newTestReconciler(fakeClient) + + err := r.deleteMachineConfig(context.Background()) + if err == nil { + t.Fatal("expected error when Delete fails") + } + if !strings.Contains(err.Error(), "failed to delete MachineConfig") { + t.Errorf("error = %q, want substring 'failed to delete MachineConfig'", err.Error()) + } +} diff --git a/pkg/controller/http01proxy/utils.go b/pkg/controller/http01proxy/utils.go new file mode 100644 index 000000000..a591be081 --- /dev/null +++ b/pkg/controller/http01proxy/utils.go @@ -0,0 +1,71 @@ +package http01proxy + +import ( + "context" + "fmt" + + 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" +) + +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 (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 +} 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/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..989d274a2 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{}, @@ -115,6 +117,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 +125,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 +133,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 +154,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 +191,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 +239,10 @@ func buildCacheObjectList(config ControllerConfig) (map[client.Object]cache.ByOb objectList[&v1alpha1.TrustManager{}] = cache.ByObject{} } + if config.EnableHTTP01Proxy { + 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..8ec96a1c2 --- /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 [apigroup:operator.openshift.io]", 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()) + }) + }) +})