Skip to content

Commit e7b9069

Browse files
committed
CM-716: Add HTTP01 Challenge Proxy controller
Implements a new HTTP01Proxy controller that enables HTTP-01 ACME challenges for baremetal clusters where API and Ingress VIPs differ. The proxy runs as a DaemonSet with hostNetwork, intercepts HTTP-01 challenge traffic on port 80, and forwards it to the ingress VIP. Features: - Platform detection via Infrastructure/cluster resource - Validates baremetal platform with distinct API/Ingress VIPs - Deploys DaemonSet with nftables-based traffic redirection - Includes RBAC, NetworkPolicies, and ServiceAccount resources - Feature-gated with HTTP01Proxy TechPreview gate - Watches Infrastructure for platform changes and invalidates cache - Comprehensive DaemonSet drift detection for safe upgrades - Automatic cleanup on deletion or platform validation failure Architecture: - Controller reconciles HTTP01Proxy singleton in operator namespace - Platform validation runs on each reconcile with caching - Infrastructure watch invalidates cache when cluster config changes - Finalizer ensures cleanup of all managed resources - Status conditions track deployment state Security: - Least-privilege RBAC (removed unused MCO permissions) - allowPrivilegeEscalation: false in securityContext - NET_ADMIN capability required for nftables manipulation - NetworkPolicies restrict egress (though limited with hostNetwork) - Runs as root for node-level network configuration Implementation details: - Uses Server-Side Apply where appropriate - Full PodTemplateSpec comparison for drift detection - Context properly threaded from Reconcile through all helpers - Proper error handling for Infrastructure resource parsing - Full VIP list validation (all combinations checked) Closes: CM-716
1 parent 454cb88 commit e7b9069

52 files changed

Lines changed: 3223 additions & 8 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.golangci.bck.yaml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# This file contains all available configuration options
2+
# with their default values.
3+
4+
# options for analysis running
5+
run:
6+
# default concurrency is a available CPU number
7+
concurrency: 4
8+
9+
# timeout for analysis, e.g. 30s, 5m, default is 1m
10+
timeout: 5m
11+
12+
# exit code when at least one issue was found, default is 1
13+
issues-exit-code: 1
14+
15+
# include test files or not, default is true
16+
tests: true
17+
# list of build tags, all linters use it. Default is empty list.
18+
build-tags:
19+
- e2e
20+
21+
# by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules":
22+
# If invoked with -mod=readonly, the go command is disallowed from the implicit
23+
# automatic updating of go.mod described above. Instead, it fails when any changes
24+
# to go.mod are needed. This setting is most useful to check that go.mod does
25+
# not need updates, such as in a continuous integration and testing system.
26+
# If invoked with -mod=vendor, the go command assumes that the vendor
27+
# directory holds the correct copies of dependencies and ignores
28+
# the dependency descriptions in go.mod.
29+
modules-download-mode: vendor
30+
31+
# Allow multiple parallel golangci-lint instances running.
32+
# If false (default) - golangci-lint acquires file lock on start.
33+
allow-parallel-runners: false
34+
35+
36+
# output configuration options
37+
output:
38+
# print lines of code with issue, default is true
39+
print-issued-lines: true
40+
41+
# print linter name in the end of issue text, default is true
42+
print-linter-name: true
43+
44+
# add a prefix to the output file references; default is no prefix
45+
path-prefix: ""
46+
47+
# sorts results by: filepath, line and column
48+
sort-results: false
49+
50+
linters:
51+
disable-all: true
52+
enable:
53+
- errcheck
54+
- gofmt
55+
- goimports
56+
- gosec
57+
- gosimple
58+
- govet
59+
- ineffassign
60+
- misspell
61+
- staticcheck
62+
- typecheck
63+
- unused
64+
fast: false
65+
66+
linters-settings:
67+
goimports:
68+
# put imports beginning with prefix after 3rd-party packages;
69+
# it's a comma-separated list of prefixes
70+
local-prefixes: github.com/openshift/cert-manager-operator

CLAUDE.md

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
This is the Cert Manager Operator for OpenShift. It deploys and manages the upstream cert-manager project on OpenShift clusters. The operator runs in the `cert-manager-operator` namespace and deploys its operand (cert-manager) in the `cert-manager` namespace - both namespaces are hardcoded.
8+
9+
## Common Commands
10+
11+
### Building and Running
12+
13+
```bash
14+
# Build the operator binary
15+
make build
16+
17+
# Build operator binary only (skip code generation and checks)
18+
make build-operator
19+
20+
# Run operator locally (requires connection to OpenShift cluster)
21+
make deploy # Install operator manifests and CRDs
22+
oc scale --replicas=0 deploy --all -n cert-manager-operator # Scale down cluster operator
23+
make local-run # Run operator locally
24+
25+
# Build and push container image
26+
export IMG=<registry>/<repository>/cert-manager-operator:<tag>
27+
make image-build image-push
28+
```
29+
30+
### Testing
31+
32+
```bash
33+
# Run unit tests
34+
make test
35+
36+
# Run a single test
37+
go test -v ./pkg/controller/deployment/... -run TestDeploymentOverrides
38+
39+
# Run e2e tests (requires deployed operator in cluster)
40+
make test-e2e
41+
42+
# Run e2e tests with specific label filter
43+
make test-e2e E2E_GINKGO_LABEL_FILTER="Platform: isSubsetOf {AWS}"
44+
```
45+
46+
### Code Quality
47+
48+
```bash
49+
# Run linter
50+
make lint
51+
52+
# Format code
53+
make fmt
54+
55+
# Run go vet
56+
make vet
57+
58+
# Update generated code (deepcopy, clientgen, manifests, bindata)
59+
make update
60+
61+
# Verify generated code is up to date
62+
make verify
63+
```
64+
65+
### Updating cert-manager Version
66+
67+
1. Update `CERT_MANAGER_VERSION` in the Makefile
68+
2. Run `make update` to regenerate manifests and bindata
69+
70+
## Architecture
71+
72+
### Controller Structure
73+
74+
The operator uses OpenShift's library-go controller framework. The main entry point is `pkg/operator/starter.go` which initializes:
75+
76+
- **CertManagerControllerSet** (`pkg/controller/deployment/cert_manager_controller_set.go`): A collection of 8 controllers managing cert-manager components:
77+
- Controller deployment + static resources
78+
- Webhook deployment + static resources
79+
- CAInjector deployment + static resources
80+
- NetworkPolicy controllers (static and user-defined)
81+
82+
- **DefaultCertManagerController**: Ensures a cluster-scoped `CertManager` CR named `cluster` exists with defaults
83+
84+
### API Types
85+
86+
Located in `api/operator/v1alpha1/`:
87+
88+
- **CertManager**: Cluster-scoped singleton CR (must be named `cluster`) that configures cert-manager deployment. Supports customization of controller, webhook, and cainjector deployments via `controllerConfig`, `webhookConfig`, and `cainjectorConfig` fields.
89+
90+
- **IstioCSR**: Namespace-scoped singleton CR (must be named `default`) for deploying istio-csr agent. Only active when `IstioCSR` feature gate is enabled via `--unsupported-addon-features="IstioCSR=true"`.
91+
92+
### Generated Code
93+
94+
- **Bindata**: Static manifests in `bindata/` are compiled into `pkg/operator/assets/bindata.go` via `make update-bindata`
95+
- **Client/Informers/Listers**: Generated in `pkg/operator/clientset/`, `pkg/operator/informers/`, `pkg/operator/listers/`
96+
- **DeepCopy**: Generated via controller-gen for API types
97+
98+
### Deployment Manifests
99+
100+
- `bindata/cert-manager-crds/`: cert-manager CRDs
101+
- `bindata/cert-manager-deployment/`: cert-manager deployment manifests split into controller/webhook/cainjector
102+
- `config/`: Kustomize configuration for operator deployment
103+
- `bundle/`: OLM bundle manifests
104+
105+
## Key Patterns
106+
107+
### Deployment Overrides
108+
109+
The operator supports overriding cert-manager deployment specs through the CertManager CR:
110+
- `overrideArgs`: Additional CLI arguments
111+
- `overrideEnv`: Environment variables
112+
- `overrideLabels`: Pod labels
113+
- `overrideResources`: Resource requests/limits
114+
- `overrideReplicas`: Replica count
115+
- `overrideScheduling`: NodeSelector and tolerations
116+
117+
Validation logic is in `pkg/controller/deployment/deployment_overrides_validation.go`.
118+
119+
### Feature Gates
120+
121+
Feature gates are defined in `api/operator/v1alpha1/features.go` and managed via `pkg/features/features.go`. Currently supports:
122+
- `IstioCSR`: Enables istio-csr controller (Technology Preview)
123+
124+
### Environment Variables for Images
125+
126+
The operator uses `RELATED_IMAGE_*` environment variables to specify operand images:
127+
- `RELATED_IMAGE_CERT_MANAGER_CONTROLLER`
128+
- `RELATED_IMAGE_CERT_MANAGER_WEBHOOK`
129+
- `RELATED_IMAGE_CERT_MANAGER_CA_INJECTOR`
130+
- `RELATED_IMAGE_CERT_MANAGER_ACMESOLVER`
131+
- `RELATED_IMAGE_CERT_MANAGER_ISTIOCSR`
132+
133+
## Testing
134+
135+
- Unit tests use standard Go testing with testify assertions
136+
- E2E tests use Ginkgo/Gomega and require a running OpenShift cluster with the operator deployed
137+
- E2E tests are tagged with `// +build e2e` and use platform labels for filtering (e.g., `Platform: AWS`)
138+
- Test utilities are in `test/library/`
139+
140+
## Import Organization
141+
142+
Local imports should be separated from third-party packages using the prefix `github.com/openshift/cert-manager-operator` (configured in `.golangci.yaml`).

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,10 +329,12 @@ local-run: build ## Run the operator locally against the cluster configured in ~
329329
RELATED_IMAGE_CERT_MANAGER_ACMESOLVER=quay.io/jetstack/cert-manager-acmesolver:$(CERT_MANAGER_VERSION) \
330330
RELATED_IMAGE_CERT_MANAGER_ISTIOCSR=quay.io/jetstack/cert-manager-istio-csr:$(ISTIO_CSR_VERSION) \
331331
RELATED_IMAGE_CERT_MANAGER_TRUST_MANAGER=quay.io/jetstack/trust-manager:$(TRUST_MANAGER_VERSION) \
332+
RELATED_IMAGE_CERT_MANAGER_HTTP01PROXY=quay.io/bapalm/cert-mgr-http01-proxy:latest \
332333
OPERATOR_NAME=cert-manager-operator \
333334
OPERAND_IMAGE_VERSION=$(BUNDLE_VERSION) \
334335
ISTIOCSR_OPERAND_IMAGE_VERSION=$(ISTIO_CSR_VERSION) \
335336
TRUSTMANAGER_OPERAND_IMAGE_VERSION=$(TRUST_MANAGER_VERSION) \
337+
HTTP01PROXY_OPERAND_IMAGE_VERSION=0.1.0 \
336338
OPERATOR_IMAGE_VERSION=$(BUNDLE_VERSION) \
337339
./cert-manager-operator start \
338340
--config=./hack/local-run-config.yaml \

TODO-cleanup.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Cleanup Opportunities
2+
3+
Identified 2026-04-08 via codebase review. Grouped by effort level.
4+
5+
## Trivial (bundle into one PR)
6+
7+
- [ ] Typo: "name o the" → "name of the" in `istiocsr/constants.go:76`
8+
- [ ] `fmt.Errorf("%s", msg)``errors.New(msg)` in `istiocsr/utils.go:501,547`
9+
- [ ] `int32(420)``int32(0o644)` in `istiocsr/deployments.go`, `certmanager/deployment_overrides.go`
10+
- [ ] Magic string `"Kubernetes"``defaultClusterID` constant in `istiocsr/deployments.go:140`
11+
- [ ] Magic string `"default"``defaultIstioRevision` constant in `istiocsr/certificates.go:90`
12+
- [ ] Duplicate constant `roleBindingSubjectKind` in `istiocsr/rbacs.go:17` and `trustmanager/constants.go:49` → move to common
13+
- [ ] Duplicate constants `istiodCertificateCommonNameFmt` and `istiodCertificateDefaultDNSName` have same value `"istiod.%s.svc"` in `istiocsr/constants.go:52,55` → consolidate
14+
- [ ] Remove redundant `sort.Strings` in `certmanager/deployment_overrides.go:103` (already sorted in `mergeContainerArgs`)
15+
- [ ] `make(map[string]string, 0)``make(map[string]string)` in `certmanager/deployment_overrides_validation.go:89,138`
16+
17+
## Easy (individual PRs, mechanical changes)
18+
19+
- [ ] Replace 8 `decode*ObjBytes` functions in istiocsr with `common.DecodeObjBytes[T]` (~95 lines deleted)
20+
- [ ] Move `updatePodTemplateLabels` to common (identical one-liner in istiocsr + trustmanager)
21+
- [ ] Extract `UpdateContainerImage(deployment, envVar, containerName)` to common (shared pattern)
22+
- [ ] Extract `BuildDefaultResourceLabels(appName, versionEnvVar)` to common (prevents label schema divergence)
23+
- [ ] `IsEmptyString(interface{})``IsEmptyString(string)` in `test/library/utils.go:141` (unsafe type assertion)
24+
- [ ] Duplicate `MustAsset` call in `certmanager/generic_deployment_controller.go:32,74` → call once, reuse
25+
- [ ] Duplicate issuer API call in `istiocsr/deployments.go:247,371` → return from first call
26+
- [ ] Decode YAML just to get SA name in `istiocsr/rbacs.go:21` → use a constant
27+
28+
## Medium (higher impact, needs design)
29+
30+
- [ ] Move `addFinalizer`/`removeFinalizer` to common (identical in istiocsr + trustmanager, ~80 lines)
31+
- [ ] Move `updateStatus` to common (identical in istiocsr + trustmanager, needs generic interface)
32+
- [ ] Extract create-or-update reconcile pattern (8+ copies in istiocsr → generic helper with callback)
33+
- [ ] Batch status updates in istiocsr (5 separate API calls per reconcile → accumulate and flush once)
34+
- [ ] Extract e2e `addOverride*` helpers (~400 lines of copy-paste in test utils → generic mutation helper)
35+
36+
## Open PRs (in flight)
37+
38+
- [x] CNF-22825 / PR #314 — Consolidate istiocsr logging around klog/v2 (CI: 7/9 green, e2e pending)
39+
- [x] CNF-22826 / PR #242 — Consolidate context usage around context.TODO() (CI: 7/9 green, e2e pending)

api/operator/v1alpha1/features.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,19 @@ var (
2121
// For more details,
2222
// https://github.com/openshift/enhancements/blob/master/enhancements/cert-manager/trust-manager-controller.md
2323
FeatureTrustManager featuregate.Feature = "TrustManager"
24+
25+
// HTTP01Proxy enables the controller for http01proxies.operator.openshift.io resource,
26+
// which extends cert-manager-operator to deploy and manage the HTTP01 challenge proxy.
27+
// The proxy enables cert-manager to complete HTTP01 ACME challenges for the API endpoint
28+
// on baremetal platforms where the API VIP is not exposed via OpenShift Ingress.
29+
//
30+
// For more details,
31+
// https://github.com/openshift/enhancements/pull/1929
32+
FeatureHTTP01Proxy featuregate.Feature = "HTTP01Proxy"
2433
)
2534

2635
var OperatorFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
2736
FeatureIstioCSR: {Default: true, PreRelease: featuregate.GA},
2837
FeatureTrustManager: {Default: false, PreRelease: "TechPreview"},
38+
FeatureHTTP01Proxy: {Default: false, PreRelease: featuregate.Alpha},
2939
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package v1alpha1
2+
3+
import (
4+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
5+
)
6+
7+
func init() {
8+
SchemeBuilder.Register(&HTTP01Proxy{}, &HTTP01ProxyList{})
9+
}
10+
11+
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
12+
// +kubebuilder:object:root=true
13+
14+
// HTTP01ProxyList is a list of HTTP01Proxy objects.
15+
type HTTP01ProxyList struct {
16+
metav1.TypeMeta `json:",inline"`
17+
18+
// metadata is the standard list's metadata.
19+
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
20+
metav1.ListMeta `json:"metadata"`
21+
Items []HTTP01Proxy `json:"items"`
22+
}
23+
24+
// +genclient
25+
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
26+
// +kubebuilder:object:root=true
27+
// +kubebuilder:subresource:status
28+
// +kubebuilder:resource:path=http01proxies,scope=Namespaced,categories={cert-manager-operator},shortName=http01proxy
29+
// +kubebuilder:printcolumn:name="Mode",type="string",JSONPath=".spec.mode"
30+
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status"
31+
// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].message"
32+
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
33+
// +kubebuilder:metadata:labels={"app.kubernetes.io/name=http01proxy", "app.kubernetes.io/part-of=cert-manager-operator"}
34+
35+
// HTTP01Proxy describes the configuration for the HTTP01 challenge proxy
36+
// that redirects traffic from the API endpoint on port 80 to ingress routers.
37+
// This enables cert-manager to perform HTTP01 ACME challenges for API endpoint certificates.
38+
// The name must be `default` to make HTTP01Proxy a singleton.
39+
//
40+
// When an HTTP01Proxy is created, the proxy DaemonSet is deployed on control plane nodes.
41+
//
42+
// +kubebuilder:validation:XValidation:rule="self.metadata.name == 'default'",message="http01proxy is a singleton, .metadata.name must be 'default'"
43+
// +operator-sdk:csv:customresourcedefinitions:displayName="HTTP01Proxy"
44+
type HTTP01Proxy struct {
45+
metav1.TypeMeta `json:",inline"`
46+
47+
// metadata is the standard object's metadata.
48+
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
49+
metav1.ObjectMeta `json:"metadata,omitempty"`
50+
51+
// spec is the specification of the desired behavior of the HTTP01Proxy.
52+
// +kubebuilder:validation:Required
53+
// +required
54+
Spec HTTP01ProxySpec `json:"spec"`
55+
56+
// status is the most recently observed status of the HTTP01Proxy.
57+
// +kubebuilder:validation:Optional
58+
// +optional
59+
Status HTTP01ProxyStatus `json:"status,omitempty"`
60+
}
61+
62+
// HTTP01ProxyMode controls how the HTTP01 challenge proxy is deployed.
63+
// +kubebuilder:validation:Enum=DefaultDeployment;CustomDeployment
64+
type HTTP01ProxyMode string
65+
66+
const (
67+
// HTTP01ProxyModeDefault enables the proxy with default configuration.
68+
HTTP01ProxyModeDefault HTTP01ProxyMode = "DefaultDeployment"
69+
70+
// HTTP01ProxyModeCustom enables the proxy with user-specified configuration.
71+
HTTP01ProxyModeCustom HTTP01ProxyMode = "CustomDeployment"
72+
)
73+
74+
// HTTP01ProxySpec is the specification of the desired behavior of the HTTP01Proxy.
75+
// +kubebuilder:validation:XValidation:rule="self.mode == 'CustomDeployment' ? has(self.customDeployment) : !has(self.customDeployment)",message="customDeployment is required when mode is CustomDeployment and forbidden otherwise"
76+
type HTTP01ProxySpec struct {
77+
// mode controls whether the HTTP01 challenge proxy is active and how it should be deployed.
78+
// DefaultDeployment enables the proxy with default configuration.
79+
// CustomDeployment enables the proxy with user-specified configuration.
80+
// +kubebuilder:validation:Required
81+
// +required
82+
Mode HTTP01ProxyMode `json:"mode"`
83+
84+
// customDeployment contains configuration options when mode is CustomDeployment.
85+
// This field is only valid when mode is CustomDeployment.
86+
// +kubebuilder:validation:Optional
87+
// +optional
88+
CustomDeployment *HTTP01ProxyCustomDeploymentSpec `json:"customDeployment,omitempty"`
89+
}
90+
91+
// HTTP01ProxyCustomDeploymentSpec contains configuration for custom proxy deployment.
92+
type HTTP01ProxyCustomDeploymentSpec struct {
93+
// internalPort specifies the internal port used by the proxy service.
94+
// Valid values are 1024-65535.
95+
// +kubebuilder:validation:Minimum=1024
96+
// +kubebuilder:validation:Maximum=65535
97+
// +kubebuilder:default=8888
98+
// +optional
99+
InternalPort int32 `json:"internalPort,omitempty"`
100+
}
101+
102+
// HTTP01ProxyStatus is the most recently observed status of the HTTP01Proxy.
103+
type HTTP01ProxyStatus struct {
104+
// conditions holds information about the current state of the HTTP01 proxy deployment.
105+
ConditionalStatus `json:",inline,omitempty"`
106+
107+
// proxyImage is the name of the image and the tag used for deploying the proxy.
108+
ProxyImage string `json:"proxyImage,omitempty"`
109+
}

0 commit comments

Comments
 (0)