Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ flowchart LR
subgraph FS[flux-system namespace]
fluxctrl[Flux Controllers]
pullsecret([image-pull-secret])
caconfigmap([custom-ca-configmap])
end
end

Expand All @@ -45,6 +46,7 @@ flowchart LR
spflux -- creates --> helmrel
helmrel -- installs --> fluxctrl
spflux -- copies secrets --> pullsecret
spflux -- copies configmap --> caconfigmap
mcpapi -- represents --> mcp
```

Expand Down Expand Up @@ -146,6 +148,10 @@ metadata:
spec:
# Optional: Reconciliation interval
pollInterval: "5m"
# Optional: ConfigMapKeySelector for a custom ca bundle (configmap will be copied to ManagedControlPlane)
caBundleRef:
name: "custom-ca-bundle"
key: "ca-bundle.crt"
# The Flux versions that can be installed
versions:
- version: "2.8.3"
Expand All @@ -170,12 +176,21 @@ spec:
tag: v1.8.1
```

| Field | Type | Description |
| ------------------- | -------- | ---------------------------------------------- |
| `spec.pollInterval` | duration | How often to reconcile resources (default: 1m) |
| `spec.versions` | array | The versions of Flux that can be installed |
| Field | Type | Description |
| ------------------- | -------------------- | ------------------------------------------------------------------ |
| `spec.pollInterval` | duration | How often to reconcile resources (default: 1m) |
| `spec.certSecretRef`| object | SecretRef for chart registry trust establishment |
| `spec.caBundleRef` | ConfigMapKeySelector | A configmap with a ca bundle used by Flux to verify certificates |
| `spec.versions` | array | The versions of Flux that can be installed |

A version item is defined as follows:
A **caBundleRef** is defined as follows:

| Field | Type | Description |
|-------|------|-------------|
| `name` | string | The name of the configmap which holds the ca bundle |
| `key` | string | The key in the configmap under which the ca bundle is stored |

A **version** item is defined as follows:

| Field | Type | Description |
| ----------------- | ------ | --------------------------------------------- |
Expand Down Expand Up @@ -210,7 +225,7 @@ For air-gapped or enterprise environments, see the [Image Localization Guide](do
| Status reporting & error messages | ✅ | |
| Operation annotations | ⚠️ | `openmcp.cloud/operation: ignore` is processed by [opencontrolplane-runtime](https://github.com/openmcp-project/opencontrolplane-runtime). `openmcp.cloud/operation: reconcile` is not processed. |
| API stability policy | ✅ | |
| Custom CA support | ⚠️ | Private-registry pull secrets supported; custom CA bundle propagation to Flux components is not implemented. |
| Custom CA support | | |
| Release artifacts (image + OCM) | ✅ | |
| Testing | ✅ | |
| Ownership and maintenance docs | ✅ | |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,31 @@ spec:
spec:
description: spec defines the desired state of ProviderConfig
properties:
caBundleRef:
description: |-
CABundleRef is a reference to a config map containing a certificate bundle.
It will be installed on the ManagedControlPlane and configured for the Flux controllers.
properties:
key:
description: The key to select.
type: string
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
optional:
description: Specify whether the ConfigMap or its key must be
defined
type: boolean
required:
- key
type: object
x-kubernetes-map-type: atomic
pollInterval:
default: 1m
description: PollInterval determines how often to reconcile resources
Expand Down
6 changes: 6 additions & 0 deletions api/v1alpha1/providerconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package v1alpha1
import (
"time"

corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
Expand All @@ -35,6 +36,11 @@ type ProviderConfigSpec struct {
// +kubebuilder:default:="1m"
// +kubebuilder:validation:Format=duration
PollInterval *metav1.Duration `json:"pollInterval,omitempty"`

// CABundleRef is a reference to a config map containing a certificate bundle.
// It will be installed on the ManagedControlPlane and configured for the Flux controllers.
// +kubebuilder:validation:Optional
CABundleRef *corev1.ConfigMapKeySelector `json:"caBundleRef,omitempty"`
}

// FluxVersion defines a version of Flux that can be installed
Expand Down
6 changes: 6 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 59 additions & 0 deletions docs/configuration/image-localization.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ In air-gapped environments, you typically need to:
1. **Mirror the Flux Helm chart** to your internal OCI registry
2. **Mirror Flux controller images** to your internal container registry
3. **Configure authentication** for both chart and image pulls
4. **Add custom CA certificates** if you use private PKI with self-signed certificates

The Flux service provider handles this through:

- **`chartPullSecret`**: Credentials for pulling the Helm chart from a private OCI registry
- **`values.imagePullSecrets`**: Credentials for pulling Flux controller images (specified in Helm values)
- **`values`**: Custom Helm values for image location overrides
- **`caBundleRef`**: PEM-encoded custom ca bundle if your private OCI registry uses self-signed certificates

## Secret Flow

Expand All @@ -24,6 +26,7 @@ flowchart TB
subgraph PC[Platform Cluster]
subgraph SPN[Service Provider Namespace]
chartsecret([chart-pull-secret])
caconfigmap([custom-ca-configmap])
imgsecret1([image-pull-secret-1])
imgsecret2([image-pull-secret-2])
end
Expand All @@ -40,15 +43,18 @@ flowchart TB
subgraph FS[flux-system namespace]
imgcopy1([image-pull-secret-1])
imgcopy2([image-pull-secret-2])
caconfigmapcopy([custom-ca-configmap])
fluxctrl[Flux Controllers]
fluxctrl -. uses .-> imgcopy1
fluxctrl -. uses .-> imgcopy2
fluxctrl -. uses .-> caconfigmapcopy
end
end

chartsecret -- copied to --> chartsecretcopy
imgsecret1 -- copied to --> imgcopy1
imgsecret2 -- copied to --> imgcopy2
caconfigmap -- copied to --> caconfigmapcopy
helmrel -- installs --> fluxctrl
```

Expand All @@ -62,6 +68,13 @@ kind: ProviderConfig
metadata:
name: flux-provider-config
spec:
# ConfigMapKeySelector pointing to a configmap which holds a PEM-encoded custom CA bundle.
# Must exist in the service provider's namespace on the platform cluster
# The configmap will be automatically copied from the service provider's namespace
# and configured for the Flux controllers.
caBundleRef:
name: "custom-ca-bundle"
key: "ca-bundle.crt"
versions:
- version: "2.8.3"
chartVersion: "2.18.2"
Expand Down Expand Up @@ -112,6 +125,33 @@ kubectl create secret docker-registry image-registry-credentials \
--docker-password=<password>
```

### Creating Custom CA ConfigMap

Concatenate all your custom CA certificates into a single PEM file. Each certificate must use the standard PEM format.

```shell
cat /path/to/ca1.crt /path/to/ca2.crt > ca-bundle.crt
```

The resulting file should look like this:

```text title="ca-bundle.crt"
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAMSO...
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJANPQ...
-----END CERTIFICATE-----
```

Create the configmap

```shell
kubectl create configmap custom-ca-bundle \
--from-file=ca-bundle.crt=ca-bundle.crt \
--namespace=openmcp-system
```

## How It Works

### Chart Pull Secret
Expand All @@ -126,6 +166,14 @@ kubectl create secret docker-registry image-registry-credentials \
2. These secrets are copied from the service provider's namespace on the platform cluster to `flux-system` on the ManagedControlPlane
3. The Helm values are passed through to Flux, which configures the controller pods with these secrets

### Custom CA Bundle
1. The configmap specified in `caBundleRef` is copied from the service provider's namespace on the platform cluster to `flux-system` on the ManagedControlPlane
2. For each Flux controller, the Helm values are adjusted so that it mounts the provided `caBundleRef.key` and sets the `SSL_CERT_DIR` environment variable to add the bundle to the pool of known certificates
3. The Helm values are passed through to Flux, and each Flux controller is able to verify certificates signed by the provided custom CA

[!CAUTION]
The custom CA certificate is not propagated to the OpenControlPlane cluster nodes. If you want to pull images from the same OCI registry you must add the custom CA certificate to the cluster nodes yourself.

## Complete Example

### Air-Gapped Setup
Expand All @@ -138,6 +186,9 @@ metadata:
spec:
chartUrl: "oci://harbor.corp.internal/charts/flux2"
chartPullSecret: "harbor-credentials"
caBundleRef:
name: "harbor-ca-bundle"
key: "harbor-ca-bundle.crt"
values:
# Image pull secrets - will be copied to ManagedControlPlane
imagePullSecrets:
Expand Down Expand Up @@ -192,6 +243,14 @@ kubectl get secrets -n mcp--<tenant-id> | grep -E "chart|image"
kubectl get secrets -n flux-system | grep -E "image"
```

### Check ConfigMap Copying
Verify configmaps are copied to the correct namespaces:

```bash
# ManagedControlPlane - flux-system namespace
kubectl get cm -n flux-system | grep -E "ca"
```

### Check OCIRepository Secret Reference

```bash
Expand Down
34 changes: 31 additions & 3 deletions internal/controller/flux_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import (
const conditionReasonError = "ReconcileError"

// ErrManagedResources is an end-user facing error if errors are present inside Flux.Status.ManagedResources
var ErrManagedResources error = errors.New("resources contain reconcile errors")
var ErrManagedResources = errors.New("resources contain reconcile errors")

// FluxReconciler reconciles a Flux object
type FluxReconciler struct {
Expand Down Expand Up @@ -117,6 +117,9 @@ func userErrorMessage(err error) string {
if errors.Is(err, flux.ErrSecretCleanup) {
errorMessages = append(errorMessages, flux.ErrSecretCleanup.Error())
}
if errors.Is(err, flux.ErrConfigMapCleanup) {
errorMessages = append(errorMessages, flux.ErrConfigMapCleanup.Error())
}
return strings.Join(errorMessages, "; ")
}

Expand Down Expand Up @@ -172,6 +175,23 @@ func (r *FluxReconciler) createObjectManager(obj *apiv1alpha1.Flux, pc *apiv1alp
})
}

if pc.Spec.CABundleRef != nil {
// add custom ca volume, volumeMount and envVar to helm values
fluxVersion.Values, err = flux.AddCAToHelmValues(fluxVersion.Values, pc.Spec.CABundleRef)
if err != nil {
return nil, fmt.Errorf("failed to add ca volume to helm values: %w", err)
}

// Sync ca configmap from platform cluster to MCP
flux.ManageCaConfigMap(mcpCluster, pc.Spec.CABundleRef.LocalObjectReference, flux.ConfigMapCopyConfig{
SourceClient: r.PlatformCluster.Client(),
SourceNamespace: r.PodNamespace,
TargetNamespace: fluxNamespace,
TargetName: flux.CustomCABundleConfigMapName,
})

}

// Configure Flux resources (OCIRepository and HelmRelease)
flux.ManageFluxResources(flux.ManageFluxResourcesParams{
Cluster: platformCluster,
Expand All @@ -194,10 +214,18 @@ func (r *FluxReconciler) createObjectManager(obj *apiv1alpha1.Flux, pc *apiv1alp
Name: prefixedChartPullSecret,
},
})
controlPlaneCleaner := flux.NewSecretCleaner(mcpCluster, fluxNamespace, helmValues.ImagePullSecrets)
controlPlaneSecretCleaner := flux.NewSecretCleaner(mcpCluster, fluxNamespace, helmValues.ImagePullSecrets)

configMapsToKeep := []corev1.LocalObjectReference{}
if pc.Spec.CABundleRef != nil {
configMapsToKeep = append(configMapsToKeep, corev1.LocalObjectReference{Name: flux.CustomCABundleConfigMapName})
}

controlPlaneConfigMapCleaner := flux.NewConfigMapCleaner(mcpCluster, fluxNamespace, configMapsToKeep)

mgr.AddCleaner(platformCleaner)
mgr.AddCleaner(controlPlaneCleaner)
mgr.AddCleaner(controlPlaneSecretCleaner)
mgr.AddCleaner(controlPlaneConfigMapCleaner)

return mgr, nil
}
Expand Down
18 changes: 16 additions & 2 deletions internal/controller/flux_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,19 +296,33 @@ func Test_updateStatusError(t *testing.T) {
wantMessage: ErrManagedResources.Error(),
},
{
name: "cleanup error",
name: "secret cleanup error",
obj: &apiv1alpha1.Flux{},
resourceErrors: false,
err: flux.ErrSecretCleanup,
wantMessage: flux.ErrSecretCleanup.Error(),
},
{
name: "combined resource and cleanup error",
name: "combined resource and secret cleanup error",
obj: &apiv1alpha1.Flux{},
resourceErrors: true,
err: flux.ErrSecretCleanup,
wantMessage: fmt.Sprintf("%s; %s", ErrManagedResources.Error(), flux.ErrSecretCleanup.Error()),
},
{
name: "configmap cleanup error",
obj: &apiv1alpha1.Flux{},
resourceErrors: false,
err: flux.ErrConfigMapCleanup,
wantMessage: flux.ErrConfigMapCleanup.Error(),
},
{
name: "combined resource and configmap cleanup error",
obj: &apiv1alpha1.Flux{},
resourceErrors: true,
err: flux.ErrConfigMapCleanup,
wantMessage: fmt.Sprintf("%s; %s", ErrManagedResources.Error(), flux.ErrConfigMapCleanup.Error()),
},
{
name: "resource error and no end-user error",
obj: &apiv1alpha1.Flux{},
Expand Down
Loading
Loading