Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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 @@ -5,6 +5,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

### 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for your change :)

You can try how the API reacts with both fields, and we adapt then here but can make sense here.

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"`)
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ require (
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 +107,4 @@ 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
)
Loading