Add --tls-sans flag to k3kcli (#925)

* Add --tls-sans flag to k3kcli cluster create

* Refine --tls-sans flag description in k3kcli documentation

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Fix TLS SANs handling in cluster creation by using hostname directly

* Implement extractHost function and update TLS SAN handling in cluster creation

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Enrico Candino
2026-07-02 13:23:13 +02:00
committed by GitHub
co-authored by Copilot Autofix powered by AI lnx01
parent 7879a7679d
commit 7f2cf6fe98
6 changed files with 126 additions and 6 deletions
+35 -6
View File
@@ -43,6 +43,7 @@ type CreateConfig struct {
agentEnvs []string
labels []string
annotations []string
tlsSANs []string
persistenceType string
storageClassName string
storageRequestSize string
@@ -130,17 +131,14 @@ func createAction(appCtx *AppContext, config *CreateConfig) func(cmd *cobra.Comm
}
// add Host IP address as an extra TLS-SAN to expose the k3k cluster
url, err := url.Parse(appCtx.RestConfig.Host)
host, err := extractHost(appCtx.RestConfig.Host, config.kubeconfigServerHost)
if err != nil {
return err
}
host := strings.Split(url.Host, ":")
if config.kubeconfigServerHost != "" {
host = []string{config.kubeconfigServerHost}
}
sans := append([]string{host}, config.tlsSANs...)
cluster.Spec.TLSSANs = []string{host[0]}
cluster.Spec.TLSSANs = uniqueStrings(sans)
if err := client.Create(ctx, cluster); err != nil {
if apierrors.IsAlreadyExists(err) {
@@ -482,3 +480,34 @@ func getClusterDetails(cluster *v1beta1.Cluster) (string, error) {
return buf.String(), nil
}
// extractHost returns the hostname from the given server URL, stripping any
// port and correctly handling IPv6 addresses (e.g. "[2001:db8::1]:6443").
// If override is non-empty it is returned as-is.
func extractHost(serverURL, override string) (string, error) {
if override != "" {
return override, nil
}
u, err := url.Parse(serverURL)
if err != nil {
return "", err
}
return u.Hostname(), nil
}
// uniqueStrings returns a new slice with duplicates removed, preserving the first occurrence order.
func uniqueStrings(ss []string) []string {
seen := make(map[string]struct{}, len(ss))
result := make([]string, 0, len(ss))
for _, s := range ss {
if _, ok := seen[s]; !ok {
seen[s] = struct{}{}
result = append(result, s)
}
}
return result
}
+1
View File
@@ -25,6 +25,7 @@ func createFlags(cmd *cobra.Command, cfg *CreateConfig) {
cmd.Flags().StringSliceVar(&cfg.agentArgs, "agent-args", []string{}, "agents extra arguments")
cmd.Flags().StringSliceVar(&cfg.serverEnvs, "server-envs", []string{}, "servers extra Envs")
cmd.Flags().StringSliceVar(&cfg.agentEnvs, "agent-envs", []string{}, "agents extra Envs")
cmd.Flags().StringSliceVar(&cfg.tlsSANs, "tls-sans", []string{}, "additional Subject Alternative Names for the cluster server TLS certificate")
cmd.Flags().StringArrayVar(&cfg.labels, "labels", []string{}, "Labels to add to the cluster object (e.g. key=value)")
cmd.Flags().StringArrayVar(&cfg.annotations, "annotations", []string{}, "Annotations to add to the cluster object (e.g. key=value)")
cmd.Flags().StringVar(&cfg.version, "version", "", "k3s version")
+49
View File
@@ -94,3 +94,52 @@ func Test_printClusterDetails(t *testing.T) {
})
}
}
func Test_extractHost(t *testing.T) {
tests := []struct {
name string
serverURL string
override string
want string
}{
{
name: "plain host",
serverURL: "https://example.com",
want: "example.com",
},
{
name: "host with port",
serverURL: "https://example.com:6443",
want: "example.com",
},
{
name: "ipv4 with port",
serverURL: "https://192.0.2.1:6443",
want: "192.0.2.1",
},
{
name: "ipv6 without port",
serverURL: "https://[2001:db8::1]",
want: "2001:db8::1",
},
{
name: "ipv6 with port",
serverURL: "https://[2001:db8::1]:6443",
want: "2001:db8::1",
},
{
name: "override wins",
serverURL: "https://example.com:6443",
override: "custom.host",
want: "custom.host",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
host, err := extractHost(tt.serverURL, tt.override)
assert.NoError(t, err)
assert.Equal(t, tt.want, host)
})
}
}
+1
View File
@@ -65,6 +65,7 @@ k3kcli cluster create [command options] NAME
--storage-class-name string storage class name for dynamic persistence type
--storage-request-size string storage size for dynamic persistence type
--timeout duration The timeout for waiting for the cluster to become ready (e.g., 10s, 5m, 1h). (default 3m0s)
--tls-sans strings additional Subject Alternative Names for the cluster server TLS certificate
--token string token of the cluster
--version string k3s version
----
+1
View File
@@ -36,6 +36,7 @@ k3kcli cluster create [command options] NAME
--storage-class-name string storage class name for dynamic persistence type
--storage-request-size string storage size for dynamic persistence type
--timeout duration The timeout for waiting for the cluster to become ready (e.g., 10s, 5m, 1h). (default 3m0s)
--tls-sans strings additional Subject Alternative Names for the cluster server TLS certificate
--token string token of the cluster
--version string k3s version
```
+39
View File
@@ -113,6 +113,45 @@ var _ = When("using the k3kcli", Label("cli"), func() {
Expect(err).To(Not(HaveOccurred()), string(stderr))
Expect(stderr).To(ContainSubstring("You can start using the cluster"))
})
It("can create a cluster with multiple --tls-sans values", func() {
var (
stderr string
err error
)
clusterName := "cluster-" + rand.String(5)
namespace := fwk3k.CreateNamespace(k8s)
clusterNamespace := namespace.Name
DeferCleanup(func() {
fwk3k.DeleteNamespaces(k8s, clusterNamespace)
})
_, stderr, err = K3kcli("cluster", "create",
"--namespace", clusterNamespace,
"--tls-sans", "extra.example.com",
"--tls-sans", "192.0.2.1",
"--tls-sans", "host.example.com:6443",
"--tls-sans", "2001:db8::1",
"--tls-sans", "[2001:db8::1]:6443",
clusterName,
)
Expect(err).To(Not(HaveOccurred()), string(stderr))
Expect(stderr).To(ContainSubstring("You can start using the cluster"))
var cluster v1beta1.Cluster
err = k8sClient.Get(context.Background(), types.NamespacedName{Name: clusterName, Namespace: clusterNamespace}, &cluster)
Expect(err).To(Not(HaveOccurred()))
Expect(cluster.Spec.TLSSANs).To(ContainElements(
"extra.example.com",
"192.0.2.1",
"host.example.com:6443",
"2001:db8::1",
"[2001:db8::1]:6443",
))
})
})
When("trying the policy commands", func() {