Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
### Features

- storage: add `move` command for moving objects within and across buckets #838
- sks: add `name` flag to kubeconfig command to set custom cluster and context name
- Re-introduce ARM64 builds for Windows #844

### Improvements
Expand Down
106 changes: 105 additions & 1 deletion cmd/compute/sks/sks_kubeconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"fmt"

"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"

exocmd "github.com/exoscale/cli/cmd"
"github.com/exoscale/cli/pkg/globalstate"
Expand All @@ -23,6 +23,7 @@ type sksKubeconfigCmd struct {

ExecCredential bool `cli-short:"x" cli-usage:"output an ExecCredential object to use with a kubeconfig user.exec mode"`
Groups []string `cli-flag:"group" cli-short:"g" cli-usage:"client certificate group. Can be specified multiple times. Defaults to system:masters"`
Name string `cli-short:"n" cli-usage:"cluster name to use in the generated kubeconfig"`
TTL int64 `cli-short:"t" cli-usage:"client certificate validity duration in seconds"`
Zone v3.ZoneName `cli-short:"z" cli-usage:"SKS cluster zone"`
}
Expand Down Expand Up @@ -111,6 +112,10 @@ func (c *sksKubeconfigCmd) CmdPreRun(cmd *cobra.Command, args []string) error {
}

func (c *sksKubeconfigCmd) CmdRun(_ *cobra.Command, _ []string) error {
if c.ExecCredential && c.Name != "" {
return fmt.Errorf("--name cannot be used with --exec-credential")
}

Comment on lines +115 to +118

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Being a bit defensive here, could also just ignore --name when using --exec-credential. I leave that to your appreciation.

ctx := exocmd.GContext
client, err := exocmd.SwitchClientZoneV3(ctx, globalstate.EgoscaleV3Client, c.Zone)
if err != nil {
Expand Down Expand Up @@ -150,6 +155,13 @@ func (c *sksKubeconfigCmd) CmdRun(_ *cobra.Command, _ []string) error {
}

if !c.ExecCredential {
if c.Name != "" {
kubeconfig, err = setSKSKubeconfigClusterName(kubeconfig, c.Name)
if err != nil {
return fmt.Errorf("error rewriting kubeconfig content: %w", err)
}
}

fmt.Print(string(kubeconfig))
return nil
}
Expand Down Expand Up @@ -190,6 +202,98 @@ func (c *sksKubeconfigCmd) CmdRun(_ *cobra.Command, _ []string) error {
return nil
}

func setSKSKubeconfigClusterName(kubeconfig []byte, name string) ([]byte, error) {
if name == "" {
return kubeconfig, nil
}

var doc yaml.Node
if err := yaml.Unmarshal(kubeconfig, &doc); err != nil {
return nil, err
}

root := yamlDocumentRoot(&doc)
clusters := yamlMappingValue(root, "clusters")
if clusters == nil || clusters.Kind != yaml.SequenceNode || len(clusters.Content) == 0 {
return nil, fmt.Errorf("cluster name not found")
}

oldName := ""
for _, cluster := range clusters.Content {
clusterName := yamlMappingValue(cluster, "name")
if clusterName == nil {
continue
}

if oldName == "" {
oldName = clusterName.Value
}
if clusterName.Value == oldName {
setYAMLDoubleQuotedString(clusterName, name)
}
}
if oldName == "" {
return nil, fmt.Errorf("cluster name not found")
}

contexts := yamlMappingValue(root, "contexts")
if contexts != nil && contexts.Kind == yaml.SequenceNode {
for _, context := range contexts.Content {
contextName := yamlMappingValue(context, "name")
if contextName != nil && contextName.Value == oldName {
setYAMLDoubleQuotedString(contextName, name)
}

contextValue := yamlMappingValue(context, "context")
contextCluster := yamlMappingValue(contextValue, "cluster")
if contextCluster != nil && contextCluster.Value == oldName {
setYAMLDoubleQuotedString(contextCluster, name)
}
}
}

currentContext := yamlMappingValue(root, "current-context")
if currentContext != nil && currentContext.Value == oldName {
setYAMLDoubleQuotedString(currentContext, name)
}

out, err := yaml.Marshal(&doc)
if err != nil {
return nil, err
}

return out, nil
}

func yamlDocumentRoot(node *yaml.Node) *yaml.Node {
if node.Kind == yaml.DocumentNode && len(node.Content) > 0 {
return node.Content[0]
}

return node
}

func yamlMappingValue(node *yaml.Node, key string) *yaml.Node {
if node == nil || node.Kind != yaml.MappingNode {
return nil
}

for i := 0; i < len(node.Content)-1; i += 2 {
if node.Content[i].Value == key {
return node.Content[i+1]
}
}

return nil
}

func setYAMLDoubleQuotedString(node *yaml.Node, value string) {
node.Kind = yaml.ScalarNode
node.Tag = "!!str"
node.Value = value
node.Style = yaml.DoubleQuotedStyle
}

func init() {
cobra.CheckErr(exocmd.RegisterCLICommand(sksCmd, &sksKubeconfigCmd{
CliCommandSettings: exocmd.DefaultCLICmdSettings(),
Expand Down
104 changes: 104 additions & 0 deletions cmd/compute/sks/sks_kubeconfig_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package sks

import (
"testing"

"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)

const sksKubeconfigTestClusterID = "153fcc53-1197-46ae-a8e0-ccf6d09efcb0"

const sksKubeconfigTestContent = `apiVersion: v1
kind: Config
clusters:
- name: 153fcc53-1197-46ae-a8e0-ccf6d09efcb0
cluster:
certificate-authority-data: authority
server: https://153fcc53-1197-46ae-a8e0-ccf6d09efcb0.sks-ch-gva-2.exo.io:443
users:
- name: admin
user:
client-certificate-data: certificate
client-key-data: key
contexts:
- name: 153fcc53-1197-46ae-a8e0-ccf6d09efcb0
context:
cluster: 153fcc53-1197-46ae-a8e0-ccf6d09efcb0
user: admin
current-context: 153fcc53-1197-46ae-a8e0-ccf6d09efcb0
preferences: {}
`

func TestSetSKSKubeconfigClusterNameEmptyNamePreservesContent(t *testing.T) {
out, err := setSKSKubeconfigClusterName([]byte(sksKubeconfigTestContent), "")
require.NoError(t, err)
require.Equal(t, sksKubeconfigTestContent, string(out))
}

func TestSetSKSKubeconfigClusterNameRewritesClusterIdentity(t *testing.T) {
const name = "my cluster"

out, err := setSKSKubeconfigClusterName([]byte(sksKubeconfigTestContent), name)
require.NoError(t, err)

var kubeconfig struct {
Clusters []struct {
Name string `yaml:"name"`
Cluster struct {
Server string `yaml:"server"`
} `yaml:"cluster"`
} `yaml:"clusters"`
Users []struct {
Name string `yaml:"name"`
} `yaml:"users"`
Contexts []struct {
Name string `yaml:"name"`
Context struct {
Cluster string `yaml:"cluster"`
User string `yaml:"user"`
} `yaml:"context"`
} `yaml:"contexts"`
CurrentContext string `yaml:"current-context"`
}
require.NoError(t, yaml.Unmarshal(out, &kubeconfig))

require.Len(t, kubeconfig.Clusters, 1)
require.Equal(t, name, kubeconfig.Clusters[0].Name)
require.Equal(t, "https://"+sksKubeconfigTestClusterID+".sks-ch-gva-2.exo.io:443", kubeconfig.Clusters[0].Cluster.Server)
require.Len(t, kubeconfig.Users, 1)
require.Equal(t, "admin", kubeconfig.Users[0].Name)
require.Len(t, kubeconfig.Contexts, 1)
require.Equal(t, name, kubeconfig.Contexts[0].Name)
require.Equal(t, name, kubeconfig.Contexts[0].Context.Cluster)
require.Equal(t, "admin", kubeconfig.Contexts[0].Context.User)
require.Equal(t, name, kubeconfig.CurrentContext)

outString := string(out)
require.Contains(t, outString, `name: "my cluster"`)
require.Contains(t, outString, `cluster: "my cluster"`)
require.Contains(t, outString, `current-context: "my cluster"`)
require.Contains(t, outString, "server: https://"+sksKubeconfigTestClusterID+".sks-ch-gva-2.exo.io:443")
require.NotContains(t, outString, "server: https://my cluster")
}

func TestSKSKubeconfigCmdRejectsNameWithExecCredential(t *testing.T) {
cmd := &sksKubeconfigCmd{
ExecCredential: true,
Name: "my cluster",
}

err := cmd.CmdRun(nil, nil)
require.EqualError(t, err, "--name cannot be used with --exec-credential")
}

func TestSetSKSKubeconfigClusterNameEscapesDoubleQuotedName(t *testing.T) {
const name = `team "a" cluster`

out, err := setSKSKubeconfigClusterName([]byte(sksKubeconfigTestContent), name)
require.NoError(t, err)

outString := string(out)
require.Contains(t, outString, `name: "team \"a\" cluster"`)
require.Contains(t, outString, `current-context: "team \"a\" cluster"`)
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ require (
golang.org/x/term v0.37.0
golang.org/x/text v0.31.0
gopkg.in/h2non/gentleman.v2 v2.0.4
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added yaml.v3 as a direct dependency so we can cleanly generate double-quoted strings. Since it was already included transitively, I figured this was acceptable.

)

require (
Expand Down Expand Up @@ -106,5 +106,5 @@ require (
gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c // indirect
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
Loading