diff --git a/cmd/kubernetes/kubernetes.go b/cmd/kubernetes/kubernetes.go index 1da9d0d..48c880e 100644 --- a/cmd/kubernetes/kubernetes.go +++ b/cmd/kubernetes/kubernetes.go @@ -104,6 +104,7 @@ func init() { kubernetesCreateCmd.Flags().StringVarP(&cniPlugin, "cni-plugin", "p", "flannel", "optional, possible options: flannel,cilium.") kubernetesCreateCmd.Flags().StringVarP(&clusterType, "cluster-type", "", "k3s", "optional, possible options: k3s,talos.") kubernetesCreateCmd.Flags().StringVar(&volumeType, "volume-type", "", "optional, volume-type name as returned by 'civo volumetypes ls'") + kubernetesCreateCmd.Flags().BoolVar(&logsCollectorEnabled, "logs-collector-enabled", true, "optional, disabling this will prevent you from seeing logs in the Civo platform dashboard, but it will save node resources. Disabled by default on small and xsmall standard nodes") kubernetesRenameCmd.Flags().StringVarP(&kubernetesNewName, "name", "n", "", "the new name for the cluster.") diff --git a/cmd/kubernetes/kubernetes_create.go b/cmd/kubernetes/kubernetes_create.go index 53d3402..3ad1f02 100644 --- a/cmd/kubernetes/kubernetes_create.go +++ b/cmd/kubernetes/kubernetes_create.go @@ -18,6 +18,7 @@ var numTargetNodes int var rulesFirewall string var waitKubernetes, saveConfigKubernetes, mergeConfigKubernetes, switchConfigKubernetes, createFirewall bool var kubernetesVersion, targetNodesSize, clusterName, clusterType, applications, removeapplications, networkID, existingFirewall, cniPlugin, volumeType string +var logsCollectorEnabled bool var kubernetesCluster *civogo.KubernetesCluster var kubernetesCreateCmdExample = `civo kubernetes create CLUSTER_NAME [flags] @@ -140,6 +141,16 @@ var kubernetesCreateCmd = &cobra.Command{ CNIPlugin: cni, } + logsCollectorEnabledValue, logsCollectorMessage := utility.ResolveLogsCollectorEnabled( + targetNodesSize, + cmd.Flags().Changed("logs-collector-enabled"), + logsCollectorEnabled, + ) + configKubernetes.LogsCollectorEnabled = logsCollectorEnabledValue + if logsCollectorMessage != "" { + utility.Info(logsCollectorMessage) + } + if rulesFirewall != "default" && !createFirewall { utility.Error("You can't use --firewall-rules without --create-firewall flag") os.Exit(1) diff --git a/utility/kubernetes.go b/utility/kubernetes.go index 2378ec2..aa818c9 100644 --- a/utility/kubernetes.go +++ b/utility/kubernetes.go @@ -326,3 +326,56 @@ func SizeType(size string) string { return "Instance" } } + +// IsStandardSmallOrXSmallKubeSize returns true when the given node size is a +// "standard" tier (the "g4s" family) small or xsmall Kubernetes node, e.g. +// "g4s.kube.small" or "g4s.kube.xsmall". This works dynamically off the size +// name so it doesn't need updating if new standard node sizes are added - +// only small/xsmall standard nodes qualify, matching the resource-saving +// default requested for those specific sizes. +func IsStandardSmallOrXSmallKubeSize(size string) bool { + if !strings.HasPrefix(size, "g4s.") { + return false + } + + parts := strings.Split(size, ".") + if len(parts) == 0 { + return false + } + + switch parts[len(parts)-1] { + case "small", "xsmall": + return true + default: + return false + } +} + +// LogsCollectorDisabledMessage is shown to the user when the logs collector +// is automatically disabled on a standard small/xsmall node, so they know why +// and how to override it. +const LogsCollectorDisabledMessage = "Logs collector not installed by default to save resource on small and xsmall standard nodes. If you wish to enable logs collector create the cluster with LogsCollectorEnable set to true" + +// ResolveLogsCollectorEnabled works out the value that should be sent to the +// API for a cluster's LogsCollectorEnabled setting, and an informational +// message to display to the user (empty if there's nothing to say). +// +// - If the user explicitly passed --logs-collector-enabled, their choice is +// always respected, regardless of node size. +// - Otherwise, the logs collector is disabled by default on standard small +// and xsmall nodes to save node resources. +// - Otherwise, nil is returned so the API's own default applies (currently +// enabled). +func ResolveLogsCollectorEnabled(size string, explicitlySet bool, value bool) (enabled *bool, message string) { + if explicitlySet { + v := value + return &v, "" + } + + if IsStandardSmallOrXSmallKubeSize(size) { + v := false + return &v, LogsCollectorDisabledMessage + } + + return nil, "" +} diff --git a/utility/kubernetes_test.go b/utility/kubernetes_test.go index a7939ef..20037ec 100644 --- a/utility/kubernetes_test.go +++ b/utility/kubernetes_test.go @@ -119,3 +119,109 @@ func TestIsAppCompatibleWithClusterType(t *testing.T) { }) } } + +func TestIsStandardSmallOrXSmallKubeSize(t *testing.T) { + tests := []struct { + name string + size string + want bool + }{ + {name: "standard xsmall", size: "g4s.kube.xsmall", want: true}, + {name: "standard small", size: "g4s.kube.small", want: true}, + {name: "standard medium is not small/xsmall", size: "g4s.kube.medium", want: false}, + {name: "standard large is not small/xsmall", size: "g4s.kube.large", want: false}, + {name: "performance small is not standard tier", size: "g4p.kube.small", want: false}, + {name: "ram-optimized small is not standard tier", size: "g4m.kube.small", want: false}, + {name: "cpu-optimized small is not standard tier", size: "g4c.kube.small", want: false}, + {name: "legacy non-kube instance size", size: "g3.xsmall", want: false}, + {name: "empty size", size: "", want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsStandardSmallOrXSmallKubeSize(tt.size); got != tt.want { + t.Errorf("IsStandardSmallOrXSmallKubeSize(%q) = %v, want %v", tt.size, got, tt.want) + } + }) + } +} + +func TestResolveLogsCollectorEnabled(t *testing.T) { + tests := []struct { + name string + size string + explicitlySet bool + value bool + wantEnabled *bool + wantMessage string + }{ + { + name: "explicit true is respected even on xsmall", + size: "g4s.kube.xsmall", + explicitlySet: true, + value: true, + wantEnabled: boolPtr(true), + wantMessage: "", + }, + { + name: "explicit false is respected even on a large node", + size: "g4s.kube.large", + explicitlySet: true, + value: false, + wantEnabled: boolPtr(false), + wantMessage: "", + }, + { + name: "auto-disabled on standard small when not explicit", + size: "g4s.kube.small", + explicitlySet: false, + value: true, + wantEnabled: boolPtr(false), + wantMessage: LogsCollectorDisabledMessage, + }, + { + name: "auto-disabled on standard xsmall when not explicit", + size: "g4s.kube.xsmall", + explicitlySet: false, + value: true, + wantEnabled: boolPtr(false), + wantMessage: LogsCollectorDisabledMessage, + }, + { + name: "left to API default on standard medium when not explicit", + size: "g4s.kube.medium", + explicitlySet: false, + value: true, + wantEnabled: nil, + wantMessage: "", + }, + { + name: "performance-tier small is not auto-disabled", + size: "g4p.kube.small", + explicitlySet: false, + value: true, + wantEnabled: nil, + wantMessage: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotEnabled, gotMessage := ResolveLogsCollectorEnabled(tt.size, tt.explicitlySet, tt.value) + + if (gotEnabled == nil) != (tt.wantEnabled == nil) { + t.Fatalf("ResolveLogsCollectorEnabled(%q, %v, %v) enabled = %v, want %v", tt.size, tt.explicitlySet, tt.value, gotEnabled, tt.wantEnabled) + } + if gotEnabled != nil && *gotEnabled != *tt.wantEnabled { + t.Errorf("ResolveLogsCollectorEnabled(%q, %v, %v) enabled = %v, want %v", tt.size, tt.explicitlySet, tt.value, *gotEnabled, *tt.wantEnabled) + } + if gotMessage != tt.wantMessage { + t.Errorf("ResolveLogsCollectorEnabled(%q, %v, %v) message = %q, want %q", tt.size, tt.explicitlySet, tt.value, gotMessage, tt.wantMessage) + } + }) + } +} + +func boolPtr(b bool) *bool { + return &b +}