From 8df0a1a2c08ec863e8ef3fa987f04e2be860d047 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Wed, 10 Jun 2026 16:17:39 +0200 Subject: [PATCH] feat(run): add support for private CA trust with containerd certs management Signed-off-by: Nicolas --- cmd/crossplane/project/help/run.md | 40 +++++++++++++++++ cmd/crossplane/project/run.go | 32 +++++++++++--- internal/project/controlplane/controlplane.go | 43 +++++++++++++------ 3 files changed, 94 insertions(+), 21 deletions(-) diff --git a/cmd/crossplane/project/help/run.md b/cmd/crossplane/project/help/run.md index 4c68d104..67d94a07 100644 --- a/cmd/crossplane/project/help/run.md +++ b/cmd/crossplane/project/help/run.md @@ -24,6 +24,40 @@ You can provide resources to apply around the project install: - `--extra-resources` applies one or more files *after* installing the Configuration and its dependencies (useful for things like `ProviderConfig`). +If your environment requires private CA trust for image pulls, you can mount a +host directory that contains containerd registry certs using +`--containerd-certs-dir`. The directory is mounted at `/etc/containerd/certs.d` +in the control-plane node. + +The certificates must already be present on your host before running this +command, and `--containerd-certs-dir` must point to a directory tree that +follows the containerd `certs.d` layout expected by the Kind node. + +`hosts.toml` is optional. Use it when you need custom registry host behavior +(for example mirrors or explicit endpoint/capability settings). For CA-only +trust, `ca.crt` is often sufficient. + +See containerd registry host configuration documentation: +https://github.com/containerd/containerd/blob/main/docs/hosts.md + +Example host directory structure: + +```text +/certs/containerd-certs/ + _default/ + hosts.toml (optional) + ca.crt + registry-1.docker.io/ + hosts.toml (optional) + ca.crt + ghcr.io/ + hosts.toml (optional) + ca.crt +``` + +Use `_default` for fallback trust/rules that should apply when a registry- +specific directory is not present. + ## Examples Build and run the project on the default local development control plane: @@ -50,3 +84,9 @@ Apply `imageconfig.yaml` before installing the Configuration, and ```shell crossplane project run --init-resources=imageconfig.yaml --extra-resources=providerconfig.yaml ``` + +Run with private CA trust material mounted into the control-plane node: + +```shell +crossplane project run --containerd-certs-dir=/certs/containerd-certs +``` diff --git a/cmd/crossplane/project/run.go b/cmd/crossplane/project/run.go index 8ecc5a64..b617bdee 100644 --- a/cmd/crossplane/project/run.go +++ b/cmd/crossplane/project/run.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "maps" + "os" "path/filepath" "time" @@ -65,13 +66,14 @@ type runCmd struct { MaxConcurrency uint `default:"8" help:"Max concurrent builds."` CacheDir string `env:"CROSSPLANE_XPKG_CACHE" help:"Directory for cached xpkg package contents." name:"cache-dir"` - ControlPlaneName string `help:"Name of the dev control plane. Defaults to project name."` - CrossplaneVersion string `help:"Version of Crossplane to install."` - RegistryDir string `help:"Directory for local registry images."` - ClusterAdmin bool `default:"true" help:"Grant Crossplane the cluster-admin role." negatable:""` - Timeout time.Duration `default:"5m" help:"Max wait for project readiness."` - InitResources []string `help:"Resources to apply before installing." type:"path"` - ExtraResources []string `help:"Resources to apply after installing." type:"path"` + ControlPlaneName string `help:"Name of the dev control plane. Defaults to project name."` + CrossplaneVersion string `help:"Version of Crossplane to install."` + RegistryDir string `help:"Directory for local registry images."` + ContainerdCertsDir string `help:"Host directory mounted into the control-plane node at /etc/containerd/certs.d for private CA trust." type:"path" name:"containerd-certs-dir"` + ClusterAdmin bool `default:"true" help:"Grant Crossplane the cluster-admin role." negatable:""` + Timeout time.Duration `default:"5m" help:"Max wait for project readiness."` + InitResources []string `help:"Resources to apply before installing." type:"path"` + ExtraResources []string `help:"Resources to apply after installing." type:"path"` proj *devv1alpha1.Project projFS afero.Fs @@ -127,6 +129,21 @@ func (c *runCmd) AfterApply() error { } } + if c.ContainerdCertsDir != "" { + absDir, err := filepath.Abs(c.ContainerdCertsDir) + if err != nil { + return errors.Wrap(err, "failed to resolve --containerd-certs-dir path") + } + info, err := os.Stat(absDir) + if err != nil { + return errors.Wrapf(err, "failed to access --containerd-certs-dir path %q", absDir) + } + if !info.IsDir() { + return errors.Errorf("--containerd-certs-dir must be a directory: %q", absDir) + } + c.ContainerdCertsDir = absDir + } + return nil } @@ -199,6 +216,7 @@ func (c *runCmd) Run(logger logging.Logger, sp terminal.SpinnerPrinter) error { controlplane.WithName(c.ControlPlaneName), controlplane.WithCrossplaneVersion(c.CrossplaneVersion), controlplane.WithRegistryDir(c.RegistryDir), + controlplane.WithContainerdCertsDir(c.ContainerdCertsDir), controlplane.WithClusterAdmin(c.ClusterAdmin), controlplane.WithLogger(logger), ) diff --git a/internal/project/controlplane/controlplane.go b/internal/project/controlplane/controlplane.go index 23776f59..a5415ff0 100644 --- a/internal/project/controlplane/controlplane.go +++ b/internal/project/controlplane/controlplane.go @@ -203,11 +203,12 @@ func (l *localDevControlPlane) Sideload(ctx context.Context, imgMap project.Imag type Option func(*config) type config struct { - name string - crossplaneVersion string - registryDir string - clusterAdmin bool - log logging.Logger + name string + crossplaneVersion string + registryDir string + containerdCertsDir string + clusterAdmin bool + log logging.Logger } // WithName sets the name of the local dev control plane. @@ -231,6 +232,14 @@ func WithRegistryDir(d string) Option { } } +// WithContainerdCertsDir sets a host directory mounted into the control-plane node at +// /etc/containerd/certs.d. +func WithContainerdCertsDir(d string) Option { + return func(c *config) { + c.containerdCertsDir = d + } +} + // WithClusterAdmin sets whether to grant Crossplane cluster admin privileges. func WithClusterAdmin(enabled bool) Option { return func(c *config) { @@ -268,7 +277,7 @@ func EnsureLocalDevControlPlane(ctx context.Context, opts ...Option) (DevControl cfg.name = cfg.name[:nameLen] cfg.log.Debug("Ensuring kind cluster", "name", cfg.name) - kubeconfig, err := ensureKindCluster(cfg.name) + kubeconfig, err := ensureKindCluster(cfg.name, cfg.containerdCertsDir) if err != nil { return nil, err } @@ -379,7 +388,7 @@ func TeardownLocalDevControlPlane(ctx context.Context, name string, registryDir return nil } -func ensureKindCluster(clusterName string) (clientcmd.ClientConfig, error) { +func ensureKindCluster(clusterName, certsDir string) (clientcmd.ClientConfig, error) { provider := kind.NewProvider() kubeconfigFile, err := os.CreateTemp("", "crossplane-*.kubeconfig") @@ -399,7 +408,7 @@ func ensureKindCluster(clusterName string) (clientcmd.ClientConfig, error) { return nil, errors.Wrap(err, "failed to get kubeconfig for kind cluster") } } else { - if err := createNewKindCluster(provider, clusterName, kubeconfigFile.Name()); err != nil { + if err := createNewKindCluster(provider, clusterName, kubeconfigFile.Name(), certsDir); err != nil { return nil, err } } @@ -417,8 +426,8 @@ func ensureKindCluster(clusterName string) (clientcmd.ClientConfig, error) { return kubeconfig, nil } -func createNewKindCluster(provider *kind.Provider, clusterName, kubeconfigPath string) error { - cfg := createKindClusterConfig() +func createNewKindCluster(provider *kind.Provider, clusterName, kubeconfigPath, certsDir string) error { + cfg := createKindClusterConfig(certsDir) cfgBytes, err := yaml.Marshal(cfg) if err != nil { @@ -439,15 +448,21 @@ func createNewKindCluster(provider *kind.Provider, clusterName, kubeconfigPath s return nil } -func createKindClusterConfig() *v1alpha4.Cluster { +func createKindClusterConfig(certsDir string) *v1alpha4.Cluster { + node := v1alpha4.Node{Role: v1alpha4.ControlPlaneRole} + if certsDir != "" { + node.ExtraMounts = append(node.ExtraMounts, v1alpha4.Mount{ + ContainerPath: "/etc/containerd/certs.d", + HostPath: certsDir, + }) + } + return &v1alpha4.Cluster{ TypeMeta: v1alpha4.TypeMeta{ APIVersion: "kind.x-k8s.io/v1alpha4", Kind: "Cluster", }, - Nodes: []v1alpha4.Node{{ - Role: v1alpha4.ControlPlaneRole, - }}, + Nodes: []v1alpha4.Node{node}, ContainerdConfigPatches: []string{ "[plugins.\"io.containerd.grpc.v1.cri\".registry]\nconfig_path = \"/etc/containerd/certs.d\"\n", },