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
43 changes: 43 additions & 0 deletions apis/dev/v1alpha1/project_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,49 @@ type ProjectSpec struct {
// any necessary ImageConfigs for deployment into a cluster must be created
// separately at deployment time.
ImageConfigs []pkgv1beta1.ImageConfig `json:"imageConfigs,omitempty"`

// Runtime configures the local development cluster.
Runtime Runtime `json:"runtime,omitempty"`
}

// Runtime is the spec for project runtime configuration.
type Runtime struct {
Kind RuntimeKind `json:"kind,omitempty"`
Registry RegistryConfig `json:"registry,omitempty"`
}

// RuntimeKind is the runtime configuration options for KinD.
type RuntimeKind struct {
// The KinD configuration to use for local development cluster.
Config RuntimeConfig `json:"config,omitempty"`
// Use internal addresses in the exported kubeconfig.
// Enable if running crossplane project in a container.
Internal bool `json:"internal,omitempty"`
// The docker network to start up the local development cluster in.
Network RuntimeNetwork `json:"network,omitempty"`
}

// RuntimeConfig is the spec for a runtime configuration.
type RuntimeConfig struct {
// The path to the configuration file.
Path string `json:"path,omitempty"`
}

// RuntimeNetwork is the spec for runtime network configuration.
type RuntimeNetwork struct {
Name string `json:"name,omitempty"`
}

// RegistryConfig is the configuration options for the local registry
type RegistryConfig struct {
Storage StorageConfig `json:"storage,omitempty"`
}

// StorageConfig is the configuration options for storage
type StorageConfig struct {
// The type of storage to use.
// Options: "bindMount" (default), "volume".
Type string `json:"type,omitempty"`
}

// ProjectPackageMetadata holds metadata about the project, which will become
Expand Down
52 changes: 52 additions & 0 deletions cmd/crossplane/project/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"maps"
"os"
"path/filepath"
"strings"
"time"

"github.com/google/go-containerregistry/pkg/name"
Expand All @@ -31,6 +32,7 @@ import (
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"sigs.k8s.io/controller-runtime/pkg/scheme"
"sigs.k8s.io/kind/pkg/apis/config/v1alpha4"
"sigs.k8s.io/yaml"

"github.com/crossplane/crossplane-runtime/v2/pkg/errors"
Expand All @@ -44,6 +46,7 @@ import (
"github.com/crossplane/cli/v2/internal/async"
"github.com/crossplane/cli/v2/internal/config"
"github.com/crossplane/cli/v2/internal/dependency"
"github.com/crossplane/cli/v2/internal/docker"
"github.com/crossplane/cli/v2/internal/project"
"github.com/crossplane/cli/v2/internal/project/controlplane"
"github.com/crossplane/cli/v2/internal/project/functions"
Expand All @@ -69,6 +72,9 @@ type runCmd struct {

ControlPlaneName string `help:"Name of the dev control plane. Defaults to project name."`
CrossplaneVersion string `help:"Version of Crossplane to install."`
DockerNetwork string `help:"The docker network to start up the dev control plane in. Defaults to kind. This is an experimental feature in KinD."`
Internal bool `help:"Use internal addresses in the exported kubeconfig. Enable if running crossplane project in a container."`
KindConfig string `help:"The path to the KinD configuration which should be used to create the local development cluster."`
RegistryDir string `help:"Directory for local registry images."`
ClusterAdmin bool `default:"true" help:"Grant Crossplane the cluster-admin role." negatable:""`
DefaultMRAP bool `default:"true" help:"Install the default wildcard ManagedResourceActivationPolicy in the dev control plane." negatable:""`
Expand All @@ -81,6 +87,8 @@ type runCmd struct {

initResources []runtime.RawExtension
extraResources []runtime.RawExtension
kindConfig *v1alpha4.Cluster
storageType docker.StorageType
}

func (c *runCmd) Help() string {
Expand Down Expand Up @@ -130,13 +138,39 @@ func (c *runCmd) AfterApply() error {
}
}

if len(strings.TrimSpace(c.KindConfig)) == 0 {
c.KindConfig = c.proj.Spec.Runtime.Kind.Config.Path
}

if len(strings.TrimSpace(c.KindConfig)) > 0 {
kindCfgBytes, err := afero.ReadFile(c.projFS, c.KindConfig)
if err != nil {
return errors.Wrapf(err, "failed to load kind configuration from %q", c.KindConfig)
}

kindCfg := &v1alpha4.Cluster{}
if err := yaml.Unmarshal(kindCfgBytes, kindCfg); err != nil {
return errors.Wrapf(err, "failed to unmarshal KinD configuration from %q", c.KindConfig)
}

c.kindConfig = kindCfg
}

return nil
}

// Run executes the run command.
func (c *runCmd) Run(logger logging.Logger, sp terminal.SpinnerPrinter, cfg *config.Config) error { //nolint:gocyclo // Main command orchestration.
ctx := context.Background()

if !c.Internal {
c.Internal = c.proj.Spec.Runtime.Kind.Internal
}

if c.DockerNetwork == "" && len(strings.TrimSpace(c.proj.Spec.Runtime.Kind.Network.Name)) > 0 {
c.DockerNetwork = c.proj.Spec.Runtime.Kind.Network.Name
}

if c.Repository != "" {
ref, err := name.NewRepository(c.Repository)
if err != nil {
Expand All @@ -149,6 +183,11 @@ func (c *runCmd) Run(logger logging.Logger, sp terminal.SpinnerPrinter, cfg *con
c.ControlPlaneName = "crossplane-" + c.proj.Name
}

c.storageType = docker.StorageTypeBindMount
if c.proj.Spec.Runtime.Registry.Storage.Type == string(docker.StorageTypeVolume) {
c.storageType = docker.StorageTypeVolume
}

concurrency := max(1, c.MaxConcurrency)

schemasFS := afero.NewBasePathFs(c.projFS, c.proj.Spec.Paths.Schemas)
Expand Down Expand Up @@ -222,6 +261,10 @@ func (c *runCmd) Run(logger logging.Logger, sp terminal.SpinnerPrinter, cfg *con
controlplane.WithClusterAdmin(c.ClusterAdmin),
controlplane.WithDefaultMRAP(c.DefaultMRAP),
controlplane.WithLogger(logger),
controlplane.WithDockerNetwork(c.DockerNetwork),
controlplane.WithInternal(c.Internal),
controlplane.WithKindConfig(c.kindConfig),
controlplane.WithStorageType(c.storageType),
)
if ctpErr != nil {
ch.SendEvent("Setting up control plane", async.EventStatusFailure)
Expand Down Expand Up @@ -271,6 +314,15 @@ func (c *runCmd) Run(logger logging.Logger, sp terminal.SpinnerPrinter, cfg *con
return errors.Wrap(err, "failed to sideload packages")
}

storage := devCtp.Storage()
if storage == nil {
return errors.Errorf("registry storage is not initialized")
}

if err := storage.Sync(ctx, devCtp.RegistryContainerID()); err != nil {
return errors.Wrapf(err, "failed to sync registry data to volume")
}

// Apply init resources.
if len(c.initResources) > 0 {
logger.Debug("Applying init resources")
Expand Down
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ require (
github.com/google/ko v0.18.1
github.com/invopop/jsonschema v0.14.0
github.com/kubernetes-sigs/kro v0.9.2
github.com/moby/go-archive v0.3.3
github.com/moby/moby/api v1.56.0
github.com/moby/moby/client v0.5.1
github.com/muesli/termenv v0.16.0
Expand Down Expand Up @@ -222,6 +223,10 @@ require (
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/moby/sys/sequential v0.7.0 // indirect
github.com/moby/sys/user v0.4.1 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
Expand Down
14 changes: 14 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -591,10 +591,24 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME=
github.com/moby/go-archive v0.3.3/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
github.com/moby/moby/api v1.56.0 h1:GQzua3NA599ASSIICx0iFgiJeO9YkdDARvQsm23ZZuQ=
github.com/moby/moby/api v1.56.0/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk=
github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw=
github.com/moby/moby/client v0.5.1/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/mount v0.3.5 h1:eS3fsZTjHaBihwjp4/+5Z3jxqLXYsbwxqpVSfFv3M00=
github.com/moby/sys/mount v0.3.5/go.mod h1:WUQDO+/uCiCIkIztx8SrwIDVn2dtMFRBebRhpDFT71M=
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o=
github.com/moby/sys/user v0.4.1 h1:RgjRlaDKi/Xmyrz4t8lyzXT6v2ooFeO/7xtchmhVWE0=
github.com/moby/sys/user v0.4.1/go.mod h1:E9QsW5WRe1kUAf7kW8hXKwu1uhsZEAdPLYHYSDudF4Y=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
Expand Down
53 changes: 53 additions & 0 deletions internal/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import (
"slices"
"strings"

archive "github.com/moby/go-archive"

"github.com/docker/cli/cli/config"
"github.com/google/go-containerregistry/pkg/name"
"github.com/moby/moby/api/pkg/stdcopy"
Expand Down Expand Up @@ -214,6 +216,31 @@ func StartContainerByID(ctx context.Context, id string) error {
return errors.Wrap(err, "failed to start container")
}

// CopyDirectoryToContainer copies a directory tree to an existing container directory.
func CopyDirectoryToContainer(ctx context.Context, id, source, destination string) error {
cli, err := NewClient()
if err != nil {
return err
}

tarball, err := archive.TarWithOptions(source, &archive.TarOptions{
IncludeSourceDir: true,
})
if err != nil {
return errors.Wrapf(err, "failed to archive %s", source)
}
defer tarball.Close() //nolint:errcheck // Best-effort close after streaming the archive.

if _, err := cli.CopyToContainer(ctx, id, client.CopyToContainerOptions{
DestinationPath: filepath.Clean(destination),
Content: tarball,
}); err != nil {
return errors.Wrapf(err, "failed to copy directory to container path %s", destination)
}

return nil
}

type startContainerConfig struct {
containerConfig *container.Config
hostConfig *container.HostConfig
Expand Down Expand Up @@ -249,6 +276,16 @@ func StartWithBindMount(hostPath, containerPath string) StartContainerOption {
}
}

// StartWithVolume adds a volume when starting a container
func StartWithVolume(path string) StartContainerOption {
return func(cfg *startContainerConfig) {
if cfg.containerConfig.Volumes == nil {
cfg.containerConfig.Volumes = map[string]struct{}{}
}
cfg.containerConfig.Volumes[path] = struct{}{}
}
}

// StartWithNetworkID adds a network to which a container should be added.
func StartWithNetworkID(nid string) StartContainerOption {
return func(cfg *startContainerConfig) {
Expand Down Expand Up @@ -588,6 +625,22 @@ func TarFromContainer(ctx context.Context, cid, path string) ([]byte, error) {
return io.ReadAll(resp.Content)
}

// TarDirectory tars a directory.
func TarDirectory(dir string) ([]byte, error) {
rd, err := archive.TarWithOptions(dir, &archive.TarOptions{}) //archive.TarResourceRebaseOpts(base, base))
if err != nil {
return nil, errors.Wrapf(err, "failed to archive %s", dir)
}
defer rd.Close() //nolint:errcheck // Best-effort close after draining the archive.

b, err := io.ReadAll(rd)
if err != nil {
return nil, errors.Wrap(err, "failed to read directory tarball")
}

return b, nil
}

// NewClient creates a new Docker client configured from environment variables.
func NewClient() (*client.Client, error) {
cli, err := client.New(client.FromEnv)
Expand Down
95 changes: 95 additions & 0 deletions internal/docker/storage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package docker

import (
"context"
"fmt"

"github.com/crossplane/crossplane-runtime/v2/pkg/errors"
)

type StorageType string

const (
StorageTypeBindMount StorageType = "bindMount"
StorageTypeVolume StorageType = "volume"
)

type Storage interface {
// ContainerOptions returns the required docker start options for its storage type.
ContainerOptions() []StartContainerOption

// Sync ensures the specified directory is synced to the container.
Sync(ctx context.Context, containerId string) error

// Returns the destination directory of the storage.
DestDir() string
}

var (
_ Storage = (*DockerVolumeStorage)(nil)
_ Storage = (*BindMountStorage)(nil)
)

// BindMountStorage provides storage operations for bind-mounts, which mounts a directory on the host machine to a registry container.
type BindMountStorage struct {
sourceDir string
destDir string
}

func NewBindMountStorage(sourceDir, destDir string) *BindMountStorage {
return &BindMountStorage{
sourceDir: sourceDir,
destDir: destDir,
}
}

func (s BindMountStorage) ContainerOptions() []StartContainerOption {
return []StartContainerOption{
StartWithBindMount(s.sourceDir, s.destDir),
}
}

func (s BindMountStorage) Sync(_ context.Context, _ string) error {
// The source directory is mounted, no need for sync
return nil
}

func (s BindMountStorage) DestDir() string {
return s.destDir
}

// DockerVolumeStorage provides storage operations for volume-based registry storage.
// This is necessary for DinD scenarios, where bind-mount is unusable cause the bound file-system is
// the file system of the docker daemon, not the running cli process.
type DockerVolumeStorage struct {
sourceDir string
destDir string
initFiles []byte
}

func NewVolumeStorage(sourceDir, destDir string, initFiles []byte) *DockerVolumeStorage {
return &DockerVolumeStorage{
sourceDir: sourceDir,
destDir: destDir,
initFiles: initFiles,
}
}

func (s DockerVolumeStorage) ContainerOptions() []StartContainerOption {
return []StartContainerOption{
StartWithVolume(s.destDir),
StartWithCopyFiles(s.initFiles, s.destDir),
}
}

func (s DockerVolumeStorage) Sync(ctx context.Context, containerId string) error {
if err := CopyDirectoryToContainer(ctx, containerId, s.sourceDir, s.destDir); err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to copy directory %q to %q", s.sourceDir, containerId))
}

return nil
}

func (s DockerVolumeStorage) DestDir() string {
return s.destDir
}
Loading