diff --git a/pkg/cmd/delete/root.go b/pkg/cmd/delete/root.go index aca8d014..953a8420 100644 --- a/pkg/cmd/delete/root.go +++ b/pkg/cmd/delete/root.go @@ -4,6 +4,8 @@ import ( "fmt" "github.com/cnoe-io/idpbuilder/pkg/cmd/helpers" + "github.com/cnoe-io/idpbuilder/pkg/kind" + "github.com/cnoe-io/idpbuilder/pkg/util" "github.com/spf13/cobra" "sigs.k8s.io/kind/pkg/cluster" ) @@ -32,11 +34,12 @@ func preDeleteE(cmd *cobra.Command, args []string) error { func deleteE(cmd *cobra.Command, args []string) error { logger := helpers.CmdLogger logger.Info("deleting cluster", "clusterName", name) - detectOpt, err := cluster.DetectNodeProvider() + detectOpt, err := util.DetectKindNodeProvider() if err != nil { return err } - provider := cluster.NewProvider(detectOpt) + + provider := cluster.NewProvider(cluster.ProviderWithLogger(kind.KindLoggerFromLogr(&logger)), detectOpt) if err := provider.Delete(name, ""); err != nil { return fmt.Errorf("failed to delete cluster %s: %w", name, err) } diff --git a/pkg/kind/cluster.go b/pkg/kind/cluster.go index c921945a..42e3abb3 100644 --- a/pkg/kind/cluster.go +++ b/pkg/kind/cluster.go @@ -11,14 +11,12 @@ import ( "strings" "github.com/cnoe-io/idpbuilder/api/v1alpha1" - "github.com/cnoe-io/idpbuilder/pkg/runtime" "github.com/cnoe-io/idpbuilder/pkg/util" "github.com/go-logr/logr" "sigs.k8s.io/controller-runtime/pkg/log" kindv1alpha4 "sigs.k8s.io/kind/pkg/apis/config/v1alpha4" "sigs.k8s.io/kind/pkg/cluster" "sigs.k8s.io/kind/pkg/cluster/nodes" - "sigs.k8s.io/kind/pkg/cluster/nodeutils" kindexec "sigs.k8s.io/kind/pkg/exec" "sigs.k8s.io/yaml" ) @@ -34,7 +32,6 @@ var ( type Cluster struct { provider IProvider - runtime runtime.IRuntime name string kubeVersion string kubeConfigPath string @@ -122,22 +119,15 @@ func (c *Cluster) getConfig() ([]byte, error) { } func NewCluster(name, kubeVersion, kubeConfigPath, kindConfigPath, extraPortsMapping string, cfg v1alpha1.BuildCustomizationSpec, cliLogger logr.Logger) (*Cluster, error) { - detectOpt, err := cluster.DetectNodeProvider() + detectOpt, err := util.DetectKindNodeProvider() if err != nil { return nil, err } - provider := cluster.NewProvider(detectOpt, cluster.ProviderWithLogger(kindLoggerFromLogr(&cliLogger))) - - rt, err := runtime.DetectRuntime() - if err != nil { - return nil, err - } - setupLog.Info("Runtime detected", "provider", rt.Name()) + provider := cluster.NewProvider(cluster.ProviderWithLogger(KindLoggerFromLogr(&cliLogger)), detectOpt) return &Cluster{ provider: provider, - runtime: rt, name: name, kindConfigPath: kindConfigPath, kubeVersion: kubeVersion, @@ -161,31 +151,6 @@ func (c *Cluster) Exists() (bool, error) { return false, nil } -func (c *Cluster) RunsOnRightPort(ctx context.Context) (bool, error) { - allNodes, err := c.provider.ListNodes(c.name) - if err != nil { - return false, err - } - - cpNodes, err := nodeutils.ControlPlaneNodes(allNodes) - if err != nil { - return false, err - } - - var cpNodeName string - for _, cpNode := range cpNodes { - if strings.Contains(cpNode.String(), c.name) { - cpNodeName = cpNode.String() - } - } - if cpNodeName == "" { - return false, nil - } - - return c.runtime.ContainerWithPort(ctx, cpNodeName, c.cfg.Port) - -} - func (c *Cluster) Reconcile(ctx context.Context, recreate bool) error { clusterExitsts, err := c.Exists() if err != nil { @@ -195,18 +160,11 @@ func (c *Cluster) Reconcile(ctx context.Context, recreate bool) error { if clusterExitsts { if recreate { setupLog.Info("Existing cluster found. Deleting.", "cluster", c.name) - c.provider.Delete(c.name, "") - } else { - rightPort, err := c.RunsOnRightPort(ctx) + err := c.provider.Delete(c.name, "") if err != nil { - return err - } - - if !rightPort { - return fmt.Errorf("can't serve port %s. cluster %s is already running on a different port", c.cfg.Port, c.name) + return fmt.Errorf("deleting cluster %w", err) } - - // reuse if there is no port conflict + } else { setupLog.Info("Cluster already exists", "cluster", c.name) return nil } diff --git a/pkg/kind/cluster_test.go b/pkg/kind/cluster_test.go index 67f4b283..564c6307 100644 --- a/pkg/kind/cluster_test.go +++ b/pkg/kind/cluster_test.go @@ -7,13 +7,11 @@ import ( "testing" "github.com/cnoe-io/idpbuilder/api/v1alpha1" - runtime "github.com/cnoe-io/idpbuilder/pkg/runtime" "github.com/docker/docker/api/types" "github.com/docker/docker/client" "github.com/go-logr/logr" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" - "sigs.k8s.io/kind/pkg/cluster/constants" "sigs.k8s.io/kind/pkg/cluster/nodes" "sigs.k8s.io/kind/pkg/exec" ) @@ -204,7 +202,6 @@ func (m *mockProvider) ListNodes(name string) ([]nodes.Node, error) { type mockRuntime struct { mock.Mock - runtime.IRuntime } func (m *mockRuntime) ContainerWithPort(ctx context.Context, name string, port string) (bool, error) { @@ -257,44 +254,3 @@ func (n *NodeMock) CommandContext(ctx context.Context, cmd string, args ...strin mockArgs := n.Called(nil) return mockArgs.Get(0).(exec.Cmd) } - -func TestRunsOnWrongPort(t *testing.T) { - // Mock node - mockNode := &NodeMock{} - mockNode.On("Role").Return(constants.ControlPlaneNodeRoleValue, nil) - mockNode.On("String").Return("test-cluster") - - mockNodes := []nodes.Node{ - mockNode, - } - - // Mock provider - mockProvider := &mockProvider{} - mockProvider.On("ListNodes", "test-cluster").Return(mockNodes, nil) - - cluster := &Cluster{ - name: "test-cluster", - provider: mockProvider, - cfg: v1alpha1.BuildCustomizationSpec{ - Port: "8080", - }, - } - - // Mock runtime - mockRuntime1 := &mockRuntime{} - mockRuntime1.On("ContainerWithPort", context.Background(), "test-cluster", "8080").Return(true, nil) - cluster.runtime = mockRuntime1 - - result, err := cluster.RunsOnRightPort(context.Background()) - assert.NoError(t, err) - assert.True(t, result) - - // Mock Docker client - mockRuntime2 := &mockRuntime{} - mockRuntime2.On("ContainerWithPort", context.Background(), "test-cluster", "8080").Return(false, nil) - cluster.runtime = mockRuntime2 - result, err = cluster.RunsOnRightPort(context.Background()) - - assert.NoError(t, err) - assert.False(t, result) -} diff --git a/pkg/kind/kindlogger.go b/pkg/kind/kindlogger.go index 026bcd56..0b583595 100644 --- a/pkg/kind/kindlogger.go +++ b/pkg/kind/kindlogger.go @@ -35,9 +35,9 @@ func (l *kindLogger) V(level kindlog.Level) kindlog.InfoLogger { return newKindInfoLogger(l.cliLogger, int(level)) } -// this is a wrapper of logr.Logger made specifically for kind's InfoLogger. +// KindLoggerFromLogr is a wrapper of logr.Logger made specifically for kind's InfoLogger. // https://github.com/kubernetes-sigs/kind/blob/1a8f0473a0785e0975e26739524513e8ee696be3/pkg/log/types.go -func kindLoggerFromLogr(logrLogger *logr.Logger) *kindLogger { +func KindLoggerFromLogr(logrLogger *logr.Logger) *kindLogger { return &kindLogger{ cliLogger: logrLogger, } diff --git a/pkg/runtime/docker.go b/pkg/runtime/docker.go deleted file mode 100644 index 83a7c59b..00000000 --- a/pkg/runtime/docker.go +++ /dev/null @@ -1,86 +0,0 @@ -package runtime - -import ( - "context" - "fmt" - - "github.com/docker/docker/api/types" - dockerClient "github.com/docker/docker/client" -) - -type DockerRuntime struct { - client *dockerClient.Client - name string -} - -func NewDockerRuntime(name string) (IRuntime, error) { - client, err := dockerClient.NewClientWithOpts( - dockerClient.WithAPIVersionNegotiation(), - ) - if err != nil { - return nil, err - } - - return &DockerRuntime{ - client: client, - name: name, - }, nil -} - -func (p *DockerRuntime) Name() string { - return p.name -} - -func (p *DockerRuntime) GetContainerByName(ctx context.Context, name string) (*types.Container, error) { - gotContainers, err := p.client.ContainerList(ctx, types.ContainerListOptions{}) - if err != nil { - return nil, err - } - - if len(gotContainers) == 0 { - return nil, fmt.Errorf("control plane container %s exists but is not in a running state", name) - } - - var gotContainer *types.Container - for _, container := range gotContainers { - for _, containerName := range container.Names { - // internal docker container name includes a fwd slash in the name - if containerName == fmt.Sprintf("/%s", name) { - gotContainer = &container - break - } - } - if gotContainer != nil { - break - } - } - - return gotContainer, nil -} - -func (p *DockerRuntime) IsUsingPort(container *types.Container, port uint16) bool { - for _, p := range container.Ports { - if p.PublicPort == port { - return true - } - } - return false -} - -func (p *DockerRuntime) ContainerWithPort(ctx context.Context, name, port string) (bool, error) { - container, err := p.GetContainerByName(ctx, name) - if err != nil { - return false, err - } - - if container == nil { - return false, nil - } - - userPort, err := toUint16(port) - if err != nil { - return false, err - } - - return p.IsUsingPort(container, userPort), nil -} diff --git a/pkg/runtime/finch.go b/pkg/runtime/finch.go deleted file mode 100644 index 7f6341af..00000000 --- a/pkg/runtime/finch.go +++ /dev/null @@ -1,89 +0,0 @@ -package runtime - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "os/exec" - - "sigs.k8s.io/controller-runtime/pkg/log" -) - -type Container struct { - NetworkSettings NetworkSettings `json:"NetworkSettings"` - State State `json:"State"` -} - -type NetworkSettings struct { - Ports map[string][]PortBinding `json:"Ports"` -} - -type State struct { - Status string `json:"Status"` -} - -type PortBinding struct { - HostIp string `json:"HostIp"` - HostPort string `json:"HostPort"` -} - -type FinchRuntime struct { - cmd *exec.Cmd -} - -func NewFinchRuntime() (IRuntime, error) { - return &FinchRuntime{ - cmd: exec.Command("finch"), - }, nil -} - -func (p *FinchRuntime) Name() string { - return "finch" -} - -func (f *FinchRuntime) ContainerWithPort(ctx context.Context, name string, port string) (bool, error) { - logger := log.FromContext(ctx) - // add arguments to inspect the container - f.cmd.Args = append([]string{"finch"}, "container", "inspect", name) - - var stdout, stderr bytes.Buffer - f.cmd.Stdout = &stdout - f.cmd.Stderr = &stderr - - // Execute the command - logger.V(1).Info("inspect existing cluster for the configuration", "container", name, "port", port) - err := f.cmd.Run() - if err != nil { - return false, err - } - - var containers []Container - err = json.Unmarshal(stdout.Bytes(), &containers) - if err != nil { - return false, fmt.Errorf("%v: %s", err, stderr.String()) - } - - if len(containers) > 1 { - return false, errors.New("more than one container for the kind control plane") - } - - if containers[0].State.Status != Running { - return false, fmt.Errorf("control plane container %s exists but is not in a running state", name) - } - - for _, container := range containers { - for _, bindings := range container.NetworkSettings.Ports { - for _, binding := range bindings { - if binding.HostPort == port { - logger.V(1).Info("existing cluster matches the configuration", "container", name, "port", port) - return true, nil - } - } - } - } - - logger.V(1).Info("existing cluster does not match the configuration", "container", name, "port", port) - return false, nil -} diff --git a/pkg/runtime/provider.go b/pkg/runtime/provider.go deleted file mode 100644 index d3100001..00000000 --- a/pkg/runtime/provider.go +++ /dev/null @@ -1,17 +0,0 @@ -package runtime - -import ( - "context" -) - -const ( - Running = "running" -) - -type IRuntime interface { - // get runtime name - Name() string - - // checks whether the container has the following - ContainerWithPort(ctx context.Context, name, port string) (bool, error) -} diff --git a/pkg/runtime/util.go b/pkg/runtime/util.go deleted file mode 100644 index 1ea8abcc..00000000 --- a/pkg/runtime/util.go +++ /dev/null @@ -1,35 +0,0 @@ -package runtime - -import ( - "errors" - "os" - "strconv" -) - -func DetectRuntime() (rt IRuntime, err error) { - switch p := os.Getenv("KIND_EXPERIMENTAL_PROVIDER"); p { - case "", "docker": - return NewDockerRuntime("docker") - case "podman": - return NewDockerRuntime("podman") - case "finch": - return NewFinchRuntime() - default: - return nil, errors.New("runtime unknown or not supported") - } -} - -func toUint16(portString string) (uint16, error) { - // Convert port string to uint16 - port, err := strconv.ParseUint(portString, 10, 16) - if err != nil { - return 0, err - } - - // Port validation - if port > 65535 { - return 0, errors.New("invalid port number") - } - - return uint16(port), nil -} diff --git a/pkg/util/util.go b/pkg/util/util.go index 7782de48..86f4fad3 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -10,6 +10,7 @@ import ( mathrand "math/rand" "net" "net/http" + "os" "path/filepath" "strings" "time" @@ -17,6 +18,7 @@ import ( "github.com/cnoe-io/idpbuilder/api/v1alpha1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/kind/pkg/cluster" ) const ( @@ -148,3 +150,20 @@ func GetHttpClient() *http.Client { } return &http.Client{Transport: tr, Timeout: 30 * time.Second} } + +// DetectKindNodeProvider follows the kind CLI convention where: +// 1. if KIND_EXPERIMENTAL_PROVIDER env var is specified, it uses the value: +// 2. if env var is not specified, use the first available supported engine. +// https://github.com/kubernetes-sigs/kind/blob/ac81e7b64e06670132dae3486e64e531953ad58c/pkg/cluster/provider.go#L100-L114 +func DetectKindNodeProvider() (cluster.ProviderOption, error) { + switch p := os.Getenv("KIND_EXPERIMENTAL_PROVIDER"); p { + case "podman": + return cluster.ProviderWithPodman(), nil + case "docker": + return cluster.ProviderWithDocker(), nil + case "nerdctl", "finch", "nerdctl.lima": + return cluster.ProviderWithNerdctl(p), nil + default: + return cluster.DetectNodeProvider() + } +}