Skip to content
Closed
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
2 changes: 1 addition & 1 deletion cmd/kubernetes/kubernetes_app_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
35 changes: 28 additions & 7 deletions utility/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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{}

Expand All @@ -161,31 +167,46 @@ 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 != "" {
YellowConfirm("you are trying to install %s application with a plan but this application has no plans\n", appName)
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)
}

Expand Down
246 changes: 231 additions & 15 deletions utility/kubernetes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
{
Expand Down Expand Up @@ -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)
// }
// }