Skip to content
Merged
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
37 changes: 29 additions & 8 deletions cmd/admin/v1/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package v1

import (
"fmt"
"strings"
"time"

"connectrpc.com/connect"
Expand Down Expand Up @@ -74,12 +75,14 @@ func newClusterCmd(c *config.Config) *cobra.Command {
kubeconfigCmd.Flags().DurationP("expiration", "", 8*time.Hour, "kubeconfig will expire after given time")
kubeconfigCmd.Flags().Bool("merge", true, "merges the kubeconfig into the current kubeconfig")
kubeconfigCmd.Flags().Bool("print-only", false, "only prints the kubeconfig to the console instead of writing it")
kubeconfigCmd.Flags().String("access-level", config.AccessLevelViewer, `access level for the kubeconfig. One of "admin" or "viewer"`)
kubeconfigCmd.Flags().String("auth-type", string(kubernetes.AuthTypeExec), `the way how the resulting kubeconfig authenticates at the api server. can be "exec" or "certs".
"exec" injects an exec config into the kubeconfig, which uses this CLI to automatically renew certificates when they expire.
"certs" simply adds the client certificates to the kubeconfig, there is no automatic renewal once the certificates have expired, the CLI is not called automatically.`)
kubeconfigCmd.Flags().String("kubeconfig", "", "specify an explicit path for the merged kubeconfig to be written, defaults to default kubeconfig paths if not provided")

genericcli.Must(kubeconfigCmd.RegisterFlagCompletionFunc("auth-type", c.Completion.ClusterKubeconfigAuthType))
genericcli.Must(kubeconfigCmd.RegisterFlagCompletionFunc("access-level", cobra.FixedCompletions(config.AccessLevels, cobra.ShellCompDirectiveNoFileComp)))

// metal admin cluster machine list

Expand Down Expand Up @@ -190,18 +193,36 @@ func (c *cluster) kubeconfig(args []string) error {
return err
}

expiration := viper.GetDuration("expiration")
req := &adminv1.ClusterServiceCredentialsRequest{
Uuid: id,
Expiration: durationpb.New(expiration),
accessLevel := viper.GetString("access-level")
if !config.IsValidAccessLevel(accessLevel) {
return fmt.Errorf("access-level must be one of: %s", strings.Join(config.AccessLevels, ", "))
}

resp, err := c.c.Client.Adminv1().Cluster().Credentials(ctx, connect.NewRequest(req))
if err != nil {
return fmt.Errorf("failed to get cluster credentials: %w", err)
expiration := durationpb.New(viper.GetDuration("expiration"))

var rawKubeconfig []byte
switch accessLevel {
case config.AccessLevelAdmin:
resp, err := c.c.Client.Adminv1().Cluster().GetAdminKubeconfig(ctx, connect.NewRequest(&adminv1.ClusterServiceGetAdminKubeconfigRequest{
Uuid: id,
Expiration: expiration,
}))
if err != nil {
return fmt.Errorf("failed to get admin kubeconfig: %w", err)
}
rawKubeconfig = []byte(resp.Msg.Kubeconfig)
case config.AccessLevelViewer:
resp, err := c.c.Client.Adminv1().Cluster().GetViewerKubeconfig(ctx, connect.NewRequest(&adminv1.ClusterServiceGetViewerKubeconfigRequest{
Uuid: id,
Expiration: expiration,
}))
if err != nil {
return fmt.Errorf("failed to get viewer kubeconfig: %w", err)
}
rawKubeconfig = []byte(resp.Msg.Kubeconfig)
}

kubeconfig, err := kubernetes.NewKubeconfigFromRaw(c.c.Fs, c.c.In, c.c.Out, []byte(resp.Msg.Kubeconfig), nil, c.c.GetProject(), id) // FIXME: reverse lookup project name
kubeconfig, err := kubernetes.NewKubeconfigFromRaw(c.c.Fs, c.c.In, c.c.Out, rawKubeconfig, nil, c.c.GetProject(), id) // FIXME: reverse lookup project name
if err != nil {
return err
}
Expand Down
47 changes: 31 additions & 16 deletions cmd/api/v1/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"slices"
"strings"
"time"

"connectrpc.com/connect"
Expand Down Expand Up @@ -118,13 +119,15 @@ func newClusterCmd(c *config.Config) *cobra.Command {
kubeconfigCmd.Flags().DurationP("expiration", "", 8*time.Hour, "kubeconfig will expire after given time")
kubeconfigCmd.Flags().Bool("merge", true, "merges the kubeconfig into the current kubeconfig")
kubeconfigCmd.Flags().Bool("print-only", false, "only prints the kubeconfig to the console instead of writing it")
kubeconfigCmd.Flags().String("access-level", config.AccessLevelViewer, `access level for the kubeconfig. One of "admin" or "viewer"`)
kubeconfigCmd.Flags().String("auth-type", string(kubernetes.AuthTypeExec), `the way how the resulting kubeconfig authenticates at the api server. can be "exec" or "certs".
"exec" injects an exec config into the kubeconfig, which uses this CLI to automatically renew certificates when they expire.
"certs" simply adds the client certificates to the kubeconfig, there is no automatic renewal once the certificates have expired, the CLI is not called automatically.`)
kubeconfigCmd.Flags().String("kubeconfig", "", "specify an explicit path for the merged kubeconfig to be written, defaults to default kubeconfig paths if not provided")

genericcli.Must(kubeconfigCmd.RegisterFlagCompletionFunc("project", c.Completion.ProjectListCompletion))
genericcli.Must(kubeconfigCmd.RegisterFlagCompletionFunc("auth-type", c.Completion.ClusterKubeconfigAuthType))
genericcli.Must(kubeconfigCmd.RegisterFlagCompletionFunc("access-level", cobra.FixedCompletions(config.AccessLevels, cobra.ShellCompDirectiveNoFileComp)))

execConfigCmd := &cobra.Command{
Use: "exec-config",
Expand Down Expand Up @@ -554,27 +557,39 @@ func (c *cluster) kubeconfig(args []string) error {
return err
}

req := &apiv1.ClusterServiceGetCredentialsRequest{
Uuid: id,
Project: c.c.GetProject(),
Expiration: durationpb.New(viper.GetDuration("expiration")),
accessLevel := viper.GetString("access-level")
if !config.IsValidAccessLevel(accessLevel) {
return fmt.Errorf("access-level must be one of: %s", strings.Join(config.AccessLevels, ", "))
}

resp, err := c.c.Client.Apiv1().Cluster().GetCredentials(ctx, connect.NewRequest(req))
if err != nil {
return fmt.Errorf("failed to get cluster credentials: %w", err)
}
expiration := durationpb.New(viper.GetDuration("expiration"))
project := c.c.GetProject()

projectResp, err := c.c.Client.Apiv1().Project().Get(ctx, connect.NewRequest(&apiv1.ProjectServiceGetRequest{Project: c.c.GetProject()}))
if err != nil {
return err
var rawKubeconfig []byte
switch accessLevel {
case config.AccessLevelAdmin:
resp, err := c.c.Client.Apiv1().Cluster().GetAdminKubeconfig(ctx, connect.NewRequest(&apiv1.ClusterServiceGetAdminKubeconfigRequest{
Uuid: id,
Project: project,
Expiration: expiration,
}))
if err != nil {
return fmt.Errorf("failed to get admin kubeconfig: %w", err)
}
rawKubeconfig = []byte(resp.Msg.Kubeconfig)
case config.AccessLevelViewer:
resp, err := c.c.Client.Apiv1().Cluster().GetViewerKubeconfig(ctx, connect.NewRequest(&apiv1.ClusterServiceGetViewerKubeconfigRequest{
Uuid: id,
Project: project,
Expiration: expiration,
}))
if err != nil {
return fmt.Errorf("failed to get viewer kubeconfig: %w", err)
}
rawKubeconfig = []byte(resp.Msg.Kubeconfig)
}

var (
projectName = helpers.TrimProvider(projectResp.Msg.Project.Name)
)

kubeconfig, err := kubernetes.NewKubeconfigFromRaw(c.c.Fs, c.c.In, c.c.Out, []byte(resp.Msg.Kubeconfig), &projectName, projectResp.Msg.Project.Uuid, id)
kubeconfig, err := kubernetes.NewKubeconfigFromRaw(c.c.Fs, c.c.In, c.c.Out, rawKubeconfig, &project, project, id)
if err != nil {
return err
}
Expand Down
13 changes: 13 additions & 0 deletions cmd/config/constants.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
package config

import "slices"

const (
DefaultApiURL = "https://api.metalstack.cloud"
DefaultAfterLoginPage = "https://metalstack.cloud"
DefaultConsoleURL = "https://console.metalstack.cloud"

// Access level admin used for shoot kubeconfig generation
AccessLevelAdmin = "admin"
// Access level viewer used for shoot kubeconfig generation
AccessLevelViewer = "viewer"
)

var AccessLevels = []string{AccessLevelAdmin, AccessLevelViewer}

func IsValidAccessLevel(level string) bool {
return slices.Contains(AccessLevels, level)
}
1 change: 1 addition & 0 deletions docs/metal_cluster_kubeconfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ metal cluster kubeconfig [flags]
### Options

```
--access-level string access level for the kubeconfig. One of "admin" or "viewer" (default "viewer")
--auth-type string the way how the resulting kubeconfig authenticates at the api server. can be "exec" or "certs".
"exec" injects an exec config into the kubeconfig, which uses this CLI to automatically renew certificates when they expire.
"certs" simply adds the client certificates to the kubeconfig, there is no automatic renewal once the certificates have expired, the CLI is not called automatically. (default "exec")
Expand Down