diff --git a/src/pkg/cluster/distro.go b/src/pkg/cluster/distro.go new file mode 100644 index 0000000000..33bd95db6e --- /dev/null +++ b/src/pkg/cluster/distro.go @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package cluster contains Zarf-specific cluster management functions. +package cluster + +import ( + "regexp" + + corev1 "k8s.io/api/core/v1" +) + +// List of supported distros via distro detection. +const ( + DistroIsUnknown = "unknown" + DistroIsK3s = "k3s" + DistroIsK3d = "k3d" + DistroIsKind = "kind" + DistroIsMicroK8s = "microk8s" + DistroIsEKS = "eks" + DistroIsEKSAnywhere = "eksanywhere" + DistroIsDockerDesktop = "dockerdesktop" + DistroIsGKE = "gke" + DistroIsAKS = "aks" + DistroIsRKE2 = "rke2" + DistroIsTKG = "tkg" +) + +// DetectDistro returns the matching distro or unknown if not found. +func detectDistro(node corev1.Node, namespaces []corev1.Namespace) string { + kindNodeRegex := regexp.MustCompile(`^kind://`) + k3dNodeRegex := regexp.MustCompile(`^k3s://k3d-`) + eksNodeRegex := regexp.MustCompile(`^aws:///`) + gkeNodeRegex := regexp.MustCompile(`^gce://`) + aksNodeRegex := regexp.MustCompile(`^azure:///subscriptions`) + rke2Regex := regexp.MustCompile(`^rancher/rancher-agent:v2`) + tkgRegex := regexp.MustCompile(`^projects\.registry\.vmware\.com/tkg/tanzu_core/`) + + // Regex explanation: https://regex101.com/r/TIUQVe/1 + // https://github.com/rancher/k3d/blob/v5.2.2/cmd/node/nodeCreate.go#L187 + if k3dNodeRegex.MatchString(node.Spec.ProviderID) { + return DistroIsK3d + } + + // Regex explanation: https://regex101.com/r/le7PRB/1 + // https://github.com/kubernetes-sigs/kind/pull/1805 + if kindNodeRegex.MatchString(node.Spec.ProviderID) { + return DistroIsKind + } + + // https://github.com/kubernetes/cloud-provider-aws/blob/454ed784c33b974c873c7d762f9d30e7c4caf935/pkg/providers/v2/instances.go#L234 + if eksNodeRegex.MatchString(node.Spec.ProviderID) { + return DistroIsEKS + } + + if gkeNodeRegex.MatchString(node.Spec.ProviderID) { + return DistroIsGKE + } + + // https://github.com/kubernetes/kubernetes/blob/v1.23.4/staging/src/k8s.io/legacy-cloud-providers/azure/azure_wrap.go#L46 + if aksNodeRegex.MatchString(node.Spec.ProviderID) { + return DistroIsAKS + } + + labels := node.GetLabels() + for _, label := range labels { + // kubectl get nodes --selector node.kubernetes.io/instance-type=k3s for K3s + if label == "node.kubernetes.io/instance-type=k3s" { + return DistroIsK3s + } + // kubectl get nodes --selector microk8s.io/cluster=true for MicroK8s + if label == "microk8s.io/cluster=true" { + return DistroIsMicroK8s + } + } + + if node.GetName() == "docker-desktop" { + return DistroIsDockerDesktop + } + + // TODO: Find a new detection method, by default the amount of images in the node status is limited. + for _, images := range node.Status.Images { + for _, image := range images.Names { + if rke2Regex.MatchString(image) { + return DistroIsRKE2 + } + if tkgRegex.MatchString(image) { + return DistroIsTKG + } + } + } + + // kubectl get ns eksa-system for EKS Anywhere + for _, namespace := range namespaces { + if namespace.Name == "eksa-system" { + return DistroIsEKSAnywhere + } + } + + return DistroIsUnknown +} diff --git a/src/pkg/cluster/distro_test.go b/src/pkg/cluster/distro_test.go new file mode 100644 index 0000000000..ec3a236c0e --- /dev/null +++ b/src/pkg/cluster/distro_test.go @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package cluster contains Zarf-specific cluster management functions. +package cluster + +import ( + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestDetectDistro(t *testing.T) { + t.Parallel() + + tests := []struct { + distro string + node corev1.Node + namespaces []corev1.Namespace + }{ + { + distro: DistroIsUnknown, + node: corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + Spec: corev1.NodeSpec{ + ProviderID: "hello world", + }, + }, + namespaces: []corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", + }, + }, + }, + }, + { + distro: DistroIsK3s, + node: corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "": "node.kubernetes.io/instance-type=k3s", + }, + }, + }, + }, + { + distro: DistroIsK3d, + node: corev1.Node{ + Spec: corev1.NodeSpec{ + ProviderID: "k3s://k3d-k3s-default-server-0", + }, + }, + }, + { + distro: DistroIsKind, + node: corev1.Node{ + Spec: corev1.NodeSpec{ + ProviderID: "kind://docker/kind/kind-control-plane", + }, + }, + }, + { + distro: DistroIsMicroK8s, + node: corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "": "microk8s.io/cluster=true", + }, + }, + }, + }, + { + distro: DistroIsEKS, + node: corev1.Node{ + Spec: corev1.NodeSpec{ + ProviderID: "aws:////i-112bac41a19da1819", + }, + }, + }, + { + distro: DistroIsEKSAnywhere, + namespaces: []corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "eksa-system", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "baz", + }, + }, + }, + }, + { + distro: DistroIsDockerDesktop, + node: corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "docker-desktop", + }, + }, + }, + { + distro: DistroIsGKE, + node: corev1.Node{ + Spec: corev1.NodeSpec{ + ProviderID: "gce://kthw-239419/us-central1-f/gk3-autopilot-cluster-1-pool-2-e87e560a-7gvw", + }, + }, + }, + { + distro: DistroIsAKS, + node: corev1.Node{ + Spec: corev1.NodeSpec{ + ProviderID: "azure:///subscriptions/9107f2fb-e486-a434-a948-52e2929b6f18/resourceGroups/MC_rg_capz-managed-aks_eastus/providers/Microsoft.Compute/virtualMachineScaleSets/aks-agentpool0-10226072-vmss/virtualMachines/0", + }, + }, + }, + { + distro: DistroIsRKE2, + node: corev1.Node{ + Status: corev1.NodeStatus{ + Images: []corev1.ContainerImage{ + { + Names: []string{"docker.io/library/ubuntu:latest"}, + }, + { + Names: []string{"rancher/rancher-agent:v2"}, + }, + }, + }, + }, + }, + { + distro: DistroIsTKG, + node: corev1.Node{ + Status: corev1.NodeStatus{ + Images: []corev1.ContainerImage{ + { + Names: []string{"docker.io/library/ubuntu:latest"}, + }, + { + Names: []string{"projects.registry.vmware.com/tkg/tanzu_core/"}, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.distro, func(t *testing.T) { + t.Parallel() + + distro := detectDistro(tt.node, tt.namespaces) + require.Equal(t, tt.distro, distro) + }) + } +} diff --git a/src/pkg/cluster/state.go b/src/pkg/cluster/state.go index a2585c4361..38e148d4e3 100644 --- a/src/pkg/cluster/state.go +++ b/src/pkg/cluster/state.go @@ -8,21 +8,20 @@ import ( "context" "encoding/json" "fmt" - "time" - "slices" + "time" - "github.com/defenseunicorns/zarf/src/config" - "github.com/defenseunicorns/zarf/src/config/lang" - "github.com/defenseunicorns/zarf/src/types" "github.com/fatih/color" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/defenseunicorns/pkg/helpers" - "github.com/defenseunicorns/zarf/src/pkg/k8s" + + "github.com/defenseunicorns/zarf/src/config" + "github.com/defenseunicorns/zarf/src/config/lang" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/pki" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/defenseunicorns/zarf/src/types" ) // Zarf Cluster Constants. @@ -54,20 +53,24 @@ func (c *Cluster) InitZarfState(ctx context.Context, initOptions types.ZarfInitO state = &types.ZarfState{} spinner.Updatef("New cluster, no prior Zarf deployments found") - // If the K3s component is being deployed, skip distro detection. if initOptions.ApplianceMode { - distro = k8s.DistroIsK3s + // If the K3s component is being deployed, skip distro detection. + distro = DistroIsK3s state.ZarfAppliance = true } else { // Otherwise, trying to detect the K8s distro type. - distro, err = c.DetectDistro(ctx) + nodeList, err := c.Clientset.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) + if err != nil { + return err + } + namespaceList, err := c.Clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) if err != nil { - // This is a basic failure right now but likely could be polished to provide user guidance to resolve. - return fmt.Errorf("unable to connect to the cluster to verify the distro: %w", err) + return err } + distro = detectDistro(nodeList.Items[0], namespaceList.Items) } - if distro != k8s.DistroIsUnknown { + if distro != DistroIsUnknown { spinner.Updatef("Detected K8s distro %s", distro) } @@ -144,13 +147,13 @@ func (c *Cluster) InitZarfState(ctx context.Context, initOptions types.ZarfInitO } switch state.Distro { - case k8s.DistroIsK3s, k8s.DistroIsK3d: + case DistroIsK3s, DistroIsK3d: state.StorageClass = "local-path" - case k8s.DistroIsKind, k8s.DistroIsGKE: + case DistroIsKind, DistroIsGKE: state.StorageClass = "standard" - case k8s.DistroIsDockerDesktop: + case DistroIsDockerDesktop: state.StorageClass = "hostpath" } diff --git a/src/pkg/k8s/info.go b/src/pkg/k8s/info.go index edb655b964..1a19a837e1 100644 --- a/src/pkg/k8s/info.go +++ b/src/pkg/k8s/info.go @@ -8,113 +8,9 @@ import ( "context" "errors" "fmt" - "regexp" - "strings" ) -// List of supported distros via distro detection. -const ( - DistroIsUnknown = "unknown" - DistroIsK3s = "k3s" - DistroIsK3d = "k3d" - DistroIsKind = "kind" - DistroIsMicroK8s = "microk8s" - DistroIsEKS = "eks" - DistroIsEKSAnywhere = "eksanywhere" - DistroIsDockerDesktop = "dockerdesktop" - DistroIsGKE = "gke" - DistroIsAKS = "aks" - DistroIsRKE2 = "rke2" - DistroIsTKG = "tkg" -) - -// DetectDistro returns the matching distro or unknown if not found. -func (k *K8s) DetectDistro(ctx context.Context) (string, error) { - kindNodeRegex := regexp.MustCompile(`^kind://`) - k3dNodeRegex := regexp.MustCompile(`^k3s://k3d-`) - eksNodeRegex := regexp.MustCompile(`^aws:///`) - gkeNodeRegex := regexp.MustCompile(`^gce://`) - aksNodeRegex := regexp.MustCompile(`^azure:///subscriptions`) - rke2Regex := regexp.MustCompile(`^rancher/rancher-agent:v2`) - tkgRegex := regexp.MustCompile(`^projects\.registry\.vmware\.com/tkg/tanzu_core/`) - - nodes, err := k.GetNodes(ctx) - if err != nil { - return DistroIsUnknown, errors.New("error getting cluster nodes") - } - - // All nodes should be the same for what we are looking for - node := nodes.Items[0] - - // Regex explanation: https://regex101.com/r/TIUQVe/1 - // https://github.com/rancher/k3d/blob/v5.2.2/cmd/node/nodeCreate.go#L187 - if k3dNodeRegex.MatchString(node.Spec.ProviderID) { - return DistroIsK3d, nil - } - - // Regex explanation: https://regex101.com/r/le7PRB/1 - // https://github.com/kubernetes-sigs/kind/pull/1805 - if kindNodeRegex.MatchString(node.Spec.ProviderID) { - return DistroIsKind, nil - } - - // https://github.com/kubernetes/cloud-provider-aws/blob/454ed784c33b974c873c7d762f9d30e7c4caf935/pkg/providers/v2/instances.go#L234 - if eksNodeRegex.MatchString(node.Spec.ProviderID) { - return DistroIsEKS, nil - } - - if gkeNodeRegex.MatchString(node.Spec.ProviderID) { - return DistroIsGKE, nil - } - - // https://github.com/kubernetes/kubernetes/blob/v1.23.4/staging/src/k8s.io/legacy-cloud-providers/azure/azure_wrap.go#L46 - if aksNodeRegex.MatchString(node.Spec.ProviderID) { - return DistroIsAKS, nil - } - - labels := node.GetLabels() - for _, label := range labels { - // kubectl get nodes --selector node.kubernetes.io/instance-type=k3s for K3s - if label == "node.kubernetes.io/instance-type=k3s" { - return DistroIsK3s, nil - } - // kubectl get nodes --selector microk8s.io/cluster=true for MicroK8s - if label == "microk8s.io/cluster=true" { - return DistroIsMicroK8s, nil - } - } - - if node.GetName() == "docker-desktop" { - return DistroIsDockerDesktop, nil - } - - for _, images := range node.Status.Images { - for _, image := range images.Names { - if rke2Regex.MatchString(image) { - return DistroIsRKE2, nil - } - if tkgRegex.MatchString(image) { - return DistroIsTKG, nil - } - } - } - - namespaces, err := k.GetNamespaces(ctx) - if err != nil { - return DistroIsUnknown, errors.New("error getting namespace list") - } - - // kubectl get ns eksa-system for EKS Anywhere - for _, namespace := range namespaces.Items { - if namespace.Name == "eksa-system" { - return DistroIsEKSAnywhere, nil - } - } - - return DistroIsUnknown, nil -} - // GetArchitectures returns the cluster system architectures if found. func (k *K8s) GetArchitectures(ctx context.Context) ([]string, error) { nodes, err := k.GetNodes(ctx)