Skip to content
Draft
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
40 changes: 40 additions & 0 deletions cmd/crossplane/project/help/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
```
32 changes: 25 additions & 7 deletions cmd/crossplane/project/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"fmt"
"maps"
"os"
"path/filepath"
"time"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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),
)
Expand Down
43 changes: 29 additions & 14 deletions internal/project/controlplane/controlplane.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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) {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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")
Expand All @@ -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
}
}
Expand All @@ -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 {
Expand All @@ -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",
},
Expand Down