Skip to content

Commit bf1a738

Browse files
Kai Kummererdergeberl
andcommitted
Add SKE login command
Co-authored-by: Maximilian Geberl <[email protected]>
1 parent abbf55f commit bf1a738

File tree

8 files changed

+458
-12
lines changed

8 files changed

+458
-12
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ dist/
44

55
# IDE
66
.vscode
7+
.idea
78

89
# OS generated files
910
.DS_Store

go.mod

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,37 @@ require (
2424
golang.org/x/mod v0.16.0
2525
golang.org/x/oauth2 v0.18.0
2626
golang.org/x/text v0.14.0
27+
k8s.io/apimachinery v0.29.2
28+
k8s.io/client-go v0.29.2
29+
)
30+
31+
require (
32+
golang.org/x/term v0.18.0 // indirect
33+
golang.org/x/time v0.5.0 // indirect
2734
)
2835

2936
require (
3037
github.com/alessio/shellescape v1.4.2 // indirect
3138
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
3239
github.com/danieljoos/wincred v1.2.1 // indirect
40+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
3341
github.com/fsnotify/fsnotify v1.7.0 // indirect
42+
github.com/go-logr/logr v1.3.0 // indirect
3443
github.com/godbus/dbus/v5 v5.1.0 // indirect
44+
github.com/gogo/protobuf v1.3.2 // indirect
3545
github.com/golang/protobuf v1.5.3 // indirect
46+
github.com/google/gofuzz v1.2.0 // indirect
3647
github.com/hashicorp/hcl v1.0.0 // indirect
48+
github.com/imdario/mergo v0.3.6 // indirect
3749
github.com/inconshreveable/mousetrap v1.1.0 // indirect
50+
github.com/json-iterator/go v1.1.12 // indirect
3851
github.com/magiconair/properties v1.8.7 // indirect
3952
github.com/mattn/go-runewidth v0.0.15 // indirect
4053
github.com/mitchellh/mapstructure v1.5.0 // indirect
41-
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
54+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
55+
github.com/modern-go/reflect2 v1.0.2 // indirect
4256
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
4357
github.com/rivo/uniseg v0.4.4 // indirect
44-
github.com/rogpeppe/go-internal v1.10.0 // indirect
4558
github.com/russross/blackfriday/v2 v2.1.0 // indirect
4659
github.com/sagikazarmark/locafero v0.4.0 // indirect
4760
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
@@ -56,10 +69,18 @@ require (
5669
github.com/subosito/gotenv v1.6.0 // indirect
5770
go.uber.org/multierr v1.11.0 // indirect
5871
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
59-
golang.org/x/sys v0.16.0 // indirect
72+
golang.org/x/net v0.22.0 // indirect
73+
golang.org/x/sys v0.18.0 // indirect
6074
google.golang.org/appengine v1.6.8 // indirect
6175
google.golang.org/protobuf v1.32.0 // indirect
62-
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
76+
gopkg.in/inf.v0 v0.9.1 // indirect
6377
gopkg.in/ini.v1 v1.67.0 // indirect
78+
gopkg.in/yaml.v2 v2.4.0 // indirect
6479
gopkg.in/yaml.v3 v3.0.1 // indirect
80+
k8s.io/api v0.29.2 // indirect
81+
k8s.io/klog/v2 v2.110.1 // indirect
82+
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
83+
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
84+
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
85+
sigs.k8s.io/yaml v1.3.0 // indirect
6586
)

go.sum

Lines changed: 86 additions & 8 deletions
Large diffs are not rendered by default.

internal/cmd/ske/credentials/credentials.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package credentials
22

33
import (
44
"github.com/stackitcloud/stackit-cli/internal/cmd/ske/credentials/describe"
5+
"github.com/stackitcloud/stackit-cli/internal/cmd/ske/credentials/kubeconfig"
56
"github.com/stackitcloud/stackit-cli/internal/cmd/ske/credentials/rotate"
67
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
78
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
@@ -24,4 +25,5 @@ func NewCmd() *cobra.Command {
2425
func addSubcommands(cmd *cobra.Command) {
2526
cmd.AddCommand(describe.NewCmd())
2627
cmd.AddCommand(rotate.NewCmd())
28+
cmd.AddCommand(kubeconfig.NewCmd())
2729
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package kubeconfig
2+
3+
import (
4+
"github.com/stackitcloud/stackit-cli/internal/cmd/ske/credentials/kubeconfig/login"
5+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
6+
"github.com/stackitcloud/stackit-cli/internal/pkg/utils"
7+
8+
"github.com/spf13/cobra"
9+
)
10+
11+
func NewCmd() *cobra.Command {
12+
cmd := &cobra.Command{
13+
Use: "kubeconfig",
14+
Short: "Provides functionality for SKE kubeconfig",
15+
Long: "Provides functionality for STACKIT Kubernetes Engine (SKE) kubeconfig",
16+
Args: args.NoArgs,
17+
Run: utils.CmdHelp,
18+
}
19+
addSubcommands(cmd)
20+
return cmd
21+
}
22+
23+
func addSubcommands(cmd *cobra.Command) {
24+
cmd.AddCommand(login.NewCmd())
25+
}
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
package login
2+
3+
import (
4+
"context"
5+
"crypto/sha256"
6+
"crypto/x509"
7+
"encoding/json"
8+
"encoding/pem"
9+
"fmt"
10+
"time"
11+
12+
"github.com/stackitcloud/stackit-cli/internal/pkg/cache"
13+
"k8s.io/client-go/rest"
14+
15+
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
16+
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
17+
"github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/client"
18+
19+
"github.com/spf13/cobra"
20+
"github.com/stackitcloud/stackit-sdk-go/services/ske"
21+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22+
"k8s.io/client-go/kubernetes/scheme"
23+
clientauthenticationv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1"
24+
"k8s.io/client-go/tools/auth/exec"
25+
"k8s.io/client-go/tools/clientcmd"
26+
)
27+
28+
type inputModel struct {
29+
ProjectId string
30+
ClusterName string
31+
CacheKey string
32+
}
33+
34+
func NewCmd() *cobra.Command {
35+
cmd := &cobra.Command{
36+
Use: "login",
37+
Short: "login plugin for kubectl",
38+
Long: "login plugin for kubectl to create a short-lived kubeconfig to authenticate against a STACKIT Kubernetes Engine (SKE) cluster. To get a kubeconfig to use with the login command use the 'kubeconfig create' command",
39+
Args: args.NoArgs,
40+
Example: examples.Build(
41+
examples.NewExample(
42+
"login to a SKE cluster specified in the kubeconfig",
43+
"$ kubectl get pod"),
44+
),
45+
RunE: func(cmd *cobra.Command, args []string) error {
46+
ctx := context.Background()
47+
48+
model, err := parseInput()
49+
if err != nil {
50+
return fmt.Errorf("login SKE kubeconfig: parseInput: %w", err)
51+
}
52+
53+
// Configure API client
54+
apiClient, err := client.ConfigureClient(cmd)
55+
if err != nil {
56+
return err
57+
}
58+
59+
kubeconfig := getCachedKubeConfig(model.CacheKey)
60+
61+
if kubeconfig == nil {
62+
return getCacheAndOutputKubeconfig(ctx, cmd, apiClient, model, false, nil)
63+
}
64+
65+
certPem, _ := pem.Decode(kubeconfig.CertData)
66+
if certPem == nil {
67+
_ = cache.DeleteObject(model.CacheKey)
68+
return getCacheAndOutputKubeconfig(ctx, cmd, apiClient, model, false, nil)
69+
}
70+
71+
certificate, err := x509.ParseCertificate(certPem.Bytes)
72+
if err != nil {
73+
_ = cache.DeleteObject(model.CacheKey)
74+
return getCacheAndOutputKubeconfig(ctx, cmd, apiClient, model, false, nil)
75+
}
76+
77+
if time.Now().After(certificate.NotAfter.UTC()) {
78+
// cert expired, request new
79+
_ = cache.DeleteObject(model.CacheKey)
80+
return getCacheAndOutputKubeconfig(ctx, cmd, apiClient, model, false, nil)
81+
} else if time.Now().Add(time.Minute * 15).After(certificate.NotAfter.UTC()) {
82+
// cert expires in 15min, refresh
83+
return getCacheAndOutputKubeconfig(ctx, cmd, apiClient, model, true, kubeconfig)
84+
}
85+
86+
if err := output(cmd, model.CacheKey, kubeconfig); err != nil {
87+
return err
88+
}
89+
return nil
90+
},
91+
}
92+
return cmd
93+
}
94+
95+
func parseInput() (*inputModel, error) {
96+
obj, _, err := exec.LoadExecCredentialFromEnv()
97+
if err != nil {
98+
return nil, fmt.Errorf("LoadExecCredentialFromEnv: %w", err)
99+
}
100+
101+
if err := clientauthenticationv1.AddToScheme(scheme.Scheme); err != nil {
102+
return nil, err
103+
}
104+
105+
obj, err = scheme.Scheme.ConvertToVersion(obj, clientauthenticationv1.SchemeGroupVersion)
106+
if err != nil {
107+
return nil, fmt.Errorf("ConvertToVersion: %w", err)
108+
}
109+
110+
execCredential, ok := obj.(*clientauthenticationv1.ExecCredential)
111+
if !ok {
112+
return nil, fmt.Errorf("Conversion to ExecCredential failed")
113+
}
114+
if execCredential == nil || execCredential.Spec.Cluster == nil {
115+
return nil, fmt.Errorf("ExecCredential contains not all needed fields")
116+
}
117+
config := &SKEClusterConfig{}
118+
err = json.Unmarshal(execCredential.Spec.Cluster.Config.Raw, config)
119+
if err != nil {
120+
return nil, fmt.Errorf("unmarshal: %w", err)
121+
}
122+
123+
return &inputModel{
124+
ClusterName: config.ClusterName,
125+
ProjectId: config.STACKITProjectID,
126+
CacheKey: fmt.Sprintf("ske-login-%x", sha256.Sum256([]byte(execCredential.Spec.Cluster.Server))),
127+
}, nil
128+
}
129+
130+
func buildRequest(ctx context.Context, apiClient *ske.APIClient, model *inputModel) ske.ApiCreateKubeconfigRequest {
131+
req := apiClient.CreateKubeconfig(ctx, model.ProjectId, model.ClusterName)
132+
expirationSeconds := "1800" // 30 min
133+
134+
return req.CreateKubeconfigPayload(ske.CreateKubeconfigPayload{ExpirationSeconds: &expirationSeconds})
135+
}
136+
137+
func parseKubeConfigToExecCredential(kubeconfig *rest.Config) (*clientauthenticationv1.ExecCredential, error) {
138+
certPem, _ := pem.Decode(kubeconfig.CertData)
139+
if certPem == nil {
140+
return nil, fmt.Errorf("login SKE kubeconfig")
141+
}
142+
143+
certificate, err := x509.ParseCertificate(certPem.Bytes)
144+
if err != nil {
145+
return nil, fmt.Errorf("login SKE kubeconfig: %w", err)
146+
}
147+
148+
outputExecCredential := clientauthenticationv1.ExecCredential{
149+
TypeMeta: v1.TypeMeta{
150+
APIVersion: clientauthenticationv1.SchemeGroupVersion.String(),
151+
Kind: "ExecCredential",
152+
},
153+
Status: &clientauthenticationv1.ExecCredentialStatus{
154+
ExpirationTimestamp: &v1.Time{Time: certificate.NotAfter.Add(-time.Minute * 15)},
155+
ClientCertificateData: string(kubeconfig.CertData),
156+
ClientKeyData: string(kubeconfig.KeyData),
157+
},
158+
}
159+
return &outputExecCredential, nil
160+
}
161+
162+
func getCachedKubeConfig(key string) *rest.Config {
163+
cachedKubeconfig, err := cache.GetObject(key)
164+
if err != nil {
165+
return nil
166+
}
167+
168+
restConfig, err := clientcmd.RESTConfigFromKubeConfig(cachedKubeconfig)
169+
if err != nil {
170+
return nil
171+
}
172+
173+
return restConfig
174+
}
175+
176+
type SKEClusterConfig struct {
177+
STACKITProjectID string `json:"stackitProjectId"`
178+
ClusterName string `json:"clusterName"`
179+
}
180+
181+
func getCacheAndOutputKubeconfig(ctx context.Context, cmd *cobra.Command, apiClient *ske.APIClient, model *inputModel, refresh bool, oldKubeconfig *rest.Config) error {
182+
req := buildRequest(ctx, apiClient, model)
183+
kubeconfigResponse, err := req.Execute()
184+
if err != nil {
185+
if refresh {
186+
return output(cmd, model.CacheKey, oldKubeconfig)
187+
}
188+
return fmt.Errorf("login SKE kubeconfig: requesting kubeconfig: %w", err)
189+
}
190+
191+
kubeconfig, err := clientcmd.RESTConfigFromKubeConfig([]byte(*kubeconfigResponse.Kubeconfig))
192+
if err != nil {
193+
if refresh {
194+
return output(cmd, model.CacheKey, oldKubeconfig)
195+
}
196+
return fmt.Errorf("login SKE kubeconfig: parsing kubeconfig: %w", err)
197+
}
198+
if err = cache.PutObject(model.CacheKey, []byte(*kubeconfigResponse.Kubeconfig)); err != nil {
199+
if refresh {
200+
return output(cmd, model.CacheKey, oldKubeconfig)
201+
}
202+
return fmt.Errorf("login SKE kubeconfig: caching kubeconfig: %w", err)
203+
}
204+
205+
return output(cmd, model.CacheKey, kubeconfig)
206+
}
207+
208+
func output(cmd *cobra.Command, cacheKey string, kubeconfig *rest.Config) error {
209+
outputExecCredential, err := parseKubeConfigToExecCredential(kubeconfig)
210+
if err != nil {
211+
_ = cache.DeleteObject(cacheKey)
212+
return fmt.Errorf("login SKE kubeconfig: converting to ExecCredential: %w", err)
213+
}
214+
215+
output, err := json.Marshal(outputExecCredential)
216+
if err != nil {
217+
_ = cache.DeleteObject(cacheKey)
218+
return fmt.Errorf("login SKE kubeconfig: marshal ExecCredential: %w", err)
219+
}
220+
221+
cmd.Print(string(output))
222+
return nil
223+
}

0 commit comments

Comments
 (0)