diff --git a/cmd/kubernetes/kubernetes_app_add.go b/cmd/kubernetes/kubernetes_app_add.go index 48dba63d..14468c20 100644 --- a/cmd/kubernetes/kubernetes_app_add.go +++ b/cmd/kubernetes/kubernetes_app_add.go @@ -43,7 +43,7 @@ var kubernetesAppAddCmd = &cobra.Command{ os.Exit(1) } - result := utility.RequestedSplit(appList, args[0]) + result := utility.RequestedSplit(appList, args[0], kubernetesFindCluster.ClusterType) configKubernetes := &civogo.KubernetesClusterConfig{ Applications: result, } diff --git a/utility/kubernetes.go b/utility/kubernetes.go index 1ceb94db..29150f32 100644 --- a/utility/kubernetes.go +++ b/utility/kubernetes.go @@ -127,8 +127,11 @@ func writeConfig(path string, data []byte, suppressMessage bool, mergeConfigs bo // checkAppPlan is the function to verify if the application to be installed in the cluster // has a plan or not, in case it has a plan but does not specify it, -// we use the first one in the list -func checkAppPlan(appList []civogo.KubernetesMarketplaceApplication, requested string) (string, error) { +// we use the first one in the list. +// It returns the index of the matched application in appList so that callers can +// access the canonical application name (appList[idx].Name) without re-parsing +// the resolved string. +func checkAppPlan(appList []civogo.KubernetesMarketplaceApplication, requested string) (int, string, error) { foundIndex := -1 parts := strings.SplitN(requested, ":", 2) appName := parts[0] @@ -151,6 +154,9 @@ func checkAppPlan(appList []civogo.KubernetesMarketplaceApplication, requested s os.Exit(1) } + // NOTE: when no plan is specified, planName == "" which never matches + // any plan label, so checkAppPlan always falls back to the default plan. + // This is pre-existing behaviour inherited from master. if len(appList[foundIndex].Plans) > 0 { allPlan := []string{} @@ -161,10 +167,10 @@ func checkAppPlan(appList []civogo.KubernetesMarketplaceApplication, requested s _, found := find(allPlan, planName) if !found { YellowConfirm("the plan you gave doesn't exist for %s; we've picked a default one for you\n", appName) - return fmt.Sprintf("%s:%s", appName, appList[foundIndex].Plans[0].Label), nil + return foundIndex, fmt.Sprintf("%s:%s", appName, appList[foundIndex].Plans[0].Label), nil } - return requested, nil + return foundIndex, requested, nil } if planName != "" { @@ -172,20 +178,35 @@ func checkAppPlan(appList []civogo.KubernetesMarketplaceApplication, requested s os.Exit(1) } - return requested, nil + return foundIndex, requested, nil } // RequestedSplit is a function to split all app requested to be installed -func RequestedSplit(appList []civogo.KubernetesMarketplaceApplication, requested string) string { +func RequestedSplit(appList []civogo.KubernetesMarketplaceApplication, requested string, clusterType string) string { allsplit := strings.Split(requested, ",") allApp := []string{} for i := range allsplit { - checkApp, err := checkAppPlan(appList, allsplit[i]) + idx, checkApp, err := checkAppPlan(appList, allsplit[i]) + // NOTE: checkAppPlan either returns a nil error or calls os.Exit(1) + // internally — it never returns a non-nil error here. This branch + // is effectively dead code inherited from master. if err != nil { fmt.Print(err) } + // Block metrics-server on Talos clusters using the canonical app name + // from appList[idx].Name — this correctly handles partial-name inputs + // (e.g. "metrics" resolves to appList[idx].Name == "metrics-server"). + if strings.EqualFold(clusterType, "talos") && + strings.EqualFold(appList[idx].Name, "metrics-server") { + Error( + "The metrics-server marketplace application is not " + + "currently supported on Talos clusters.", + ) + os.Exit(1) + } + allApp = append(allApp, checkApp) } diff --git a/utility/kubernetes_test.go b/utility/kubernetes_test.go index 7e261b49..a0120eaa 100644 --- a/utility/kubernetes_test.go +++ b/utility/kubernetes_test.go @@ -6,6 +6,237 @@ import ( "github.com/civo/civogo" ) +// --- checkAppPlan tests --- + +func TestCheckAppPlanFindsAppByName(t *testing.T) { + appList := []civogo.KubernetesMarketplaceApplication{ + {Name: "redis"}, + {Name: "mysql"}, + {Name: "postgresql"}, + } + + idx, result, err := checkAppPlan(appList, "mysql") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result != "mysql" { + t.Errorf("expected 'mysql', got '%s'", result) + } + // Index must point to the matched entry + if appList[idx].Name != "mysql" { + t.Errorf("expected appList[idx].Name = 'mysql', got '%s'", appList[idx].Name) + } +} + +func TestCheckAppPlanFindsAppWithValidPlan(t *testing.T) { + appList := []civogo.KubernetesMarketplaceApplication{ + { + Name: "mysql", + Plans: []civogo.KubernetesMarketplacePlan{ + {Label: "5GB"}, + {Label: "10GB"}, + }, + }, + } + + _, result, err := checkAppPlan(appList, "mysql:10GB") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result != "mysql:10GB" { + t.Errorf("expected 'mysql:10GB', got '%s'", result) + } +} + +func TestCheckAppPlanReturnsDefaultForInvalidPlan(t *testing.T) { + appList := []civogo.KubernetesMarketplaceApplication{ + { + Name: "mysql", + Plans: []civogo.KubernetesMarketplacePlan{ + {Label: "5GB"}, + {Label: "10GB"}, + }, + }, + } + + _, result, err := checkAppPlan(appList, "mysql:999GB") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // Invalid plan falls back to default (first plan) + if result != "mysql:5GB" { + t.Errorf("expected 'mysql:5GB', got '%s'", result) + } +} + +func TestCheckAppPlanNoPlanSpecifiedWithPlansAvailable(t *testing.T) { + appList := []civogo.KubernetesMarketplaceApplication{ + { + Name: "mysql", + Plans: []civogo.KubernetesMarketplacePlan{ + {Label: "5GB"}, + }, + }, + } + + _, result, err := checkAppPlan(appList, "mysql") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // No plan specified and plan is empty string — doesn't match any plan, + // so it falls back to default + if result != "mysql:5GB" { + t.Errorf("expected 'mysql:5GB', got '%s'", result) + } +} + +func TestCheckAppPlanNoPlanSpecifiedNoPlansAvailable(t *testing.T) { + appList := []civogo.KubernetesMarketplaceApplication{ + {Name: "traefik"}, + } + + _, result, err := checkAppPlan(appList, "traefik") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result != "traefik" { + t.Errorf("expected 'traefik', got '%s'", result) + } +} + +func TestCheckAppPlanPartialNameMatch(t *testing.T) { + appList := []civogo.KubernetesMarketplaceApplication{ + {Name: "metrics-server"}, + {Name: "mysql"}, + } + + idx, result, err := checkAppPlan(appList, "metrics") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // String returned is verbatim user input (no plans on this app) + if result != "metrics" { + t.Errorf("expected 'metrics', got '%s'", result) + } + // But the index points to the CANONICAL app — metrics-server + if appList[idx].Name != "metrics-server" { + t.Errorf("expected appList[idx].Name = 'metrics-server', got '%s'", appList[idx].Name) + } +} + +// --- RequestedSplit tests --- + +func TestRequestedSplitSingleApp(t *testing.T) { + appList := []civogo.KubernetesMarketplaceApplication{ + {Name: "redis"}, + {Name: "mysql"}, + } + + result := RequestedSplit(appList, "mysql", "k3s") + if result != "mysql" { + t.Errorf("expected 'mysql', got '%s'", result) + } +} + +func TestRequestedSplitMultipleApps(t *testing.T) { + appList := []civogo.KubernetesMarketplaceApplication{ + {Name: "redis"}, + {Name: "mysql"}, + {Name: "postgresql"}, + } + + result := RequestedSplit(appList, "redis,mysql", "k3s") + if result != "redis,mysql" { + t.Errorf("expected 'redis,mysql', got '%s'", result) + } +} + +func TestRequestedSplitAppWithValidPlan(t *testing.T) { + appList := []civogo.KubernetesMarketplaceApplication{ + { + Name: "mysql", + Plans: []civogo.KubernetesMarketplacePlan{ + {Label: "5GB"}, + {Label: "10GB"}, + }, + }, + } + + result := RequestedSplit(appList, "mysql:10GB", "k3s") + if result != "mysql:10GB" { + t.Errorf("expected 'mysql:10GB', got '%s'", result) + } +} + +func TestRequestedSplitAppWithInvalidPlanFallsBackToDefault(t *testing.T) { + appList := []civogo.KubernetesMarketplaceApplication{ + { + Name: "mysql", + Plans: []civogo.KubernetesMarketplacePlan{ + {Label: "5GB"}, + {Label: "10GB"}, + }, + }, + } + + result := RequestedSplit(appList, "mysql:999GB", "k3s") + expected := "mysql:5GB" + if result != expected { + t.Errorf("expected '%s', got '%s'", expected, result) + } +} + +func TestRequestedSplitAppWithoutPlanAndPlansAvailable(t *testing.T) { + appList := []civogo.KubernetesMarketplaceApplication{ + { + Name: "mysql", + Plans: []civogo.KubernetesMarketplacePlan{ + {Label: "5GB"}, + }, + }, + } + + // When no plan is specified and app has plans, find("") returns false, + // so checkAppPlan falls back to the default (first) plan. + result := RequestedSplit(appList, "mysql", "k3s") + if result != "mysql:5GB" { + t.Errorf("expected 'mysql:5GB', got '%s'", result) + } +} + +func TestRequestedSplitMultipleAppsWithPlans(t *testing.T) { + appList := []civogo.KubernetesMarketplaceApplication{ + {Name: "redis"}, + { + Name: "mysql", + Plans: []civogo.KubernetesMarketplacePlan{ + {Label: "5GB"}, + {Label: "10GB"}, + }, + }, + } + + result := RequestedSplit(appList, "redis,mysql:10GB", "k3s") + if result != "redis,mysql:10GB" { + t.Errorf("expected 'redis,mysql:10GB', got '%s'", result) + } +} + +// NOTE: The Talos + metrics-server blocking path calls os.Exit(1) and +// cannot be unit tested directly. Manual testing is required for that path. +func TestRequestedSplitNonTalosAllowsMetricsServer(t *testing.T) { + appList := []civogo.KubernetesMarketplaceApplication{ + {Name: "metrics-server"}, + } + + result := RequestedSplit(appList, "metrics-server", "k3s") + if result != "metrics-server" { + t.Errorf("expected 'metrics-server', got '%s'", result) + } +} + +// --- RemoveApplicationFromInstalledList tests --- + func TestRemoveApplicationFromInstalledListSimpleName(t *testing.T) { current := []civogo.KubernetesInstalledApplication{ { @@ -56,18 +287,3 @@ func TestRemoveApplicationFromInstalledListWithMultiple(t *testing.T) { t.Errorf("expected 'redis', got '%s'", ret) } } - -// func TestRemoveApplicationFromInstalledListWithPlan(t *testing.T) { -// current := []civogo.KubernetesInstalledApplication{ -// { -// Name: "mysql", -// }, -// } -// uninstall := "mysql" - -// ret := RemoveApplicationFromInstalledList(current, uninstall) - -// if ret != "mysql" { -// t.Errorf("expected 'mysql', got '%s'", ret) -// } -// }