Skip to content

Commit

Permalink
feat: kubectl plugin first iteration
Browse files Browse the repository at this point in the history
Signed-off-by: Dario Tranchitella <dario@tranchitella.eu>
  • Loading branch information
prometherion committed Dec 4, 2024
1 parent 9da801b commit 63dcacd
Show file tree
Hide file tree
Showing 14 changed files with 743 additions and 0 deletions.
16 changes: 16 additions & 0 deletions cmd/kubectl-kamaji/certificates/certificates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0

package certificates

import (
"github.com/spf13/cobra"
)

func NewCertificatesGroup() *cobra.Command {
return &cobra.Command{
Use: "certificates",
Short: "Certificate operations",
Long: "Performs operations on Tenant Control Plance related certificates.",
}
}
80 changes: 80 additions & 0 deletions cmd/kubectl-kamaji/certificates/rotate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0

package certificates

import (
"fmt"
"io"
"strings"

"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/cli-runtime/pkg/genericclioptions"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/clastix/kamaji/cmd/kubectl-kamaji/utils"
"github.com/clastix/kamaji/pkg/cli"
)

func NewRotateCertificates(writer io.Writer, flags *genericclioptions.ConfigFlags, k8sClient client.Client) *cobra.Command {
var all bool
var certificates []string

cmd := cobra.Command{
Use: "rotate {TCP_NAME} { --all | --certificates={CERTS_LIST} }",
Example: " kubectl [--namespace=$NAMESPACE] kamaji certificates rotate $TCP_NAME --all=true\n" +
" kubectl [--namespace=$NAMESPACE] kamaji certificates rotate $TCP_NAME --certificates=APIServer\n" +
" kubectl [--namespace=$NAMESPACE] kamaji certificates rotate $TCP_NAME --certificates=FrontProxyCA,FrontProxyClient",
Short: "Get kubeconfig",
Long: "Rotate one ore more Tenant Control Plane certificates.\n" +
"\n" +
"The CLI flag --certificates allows to specify which certificates kind should be rotated, such as: " +
strings.Join(cli.RotateCertificatesMap.Keys(), ", ") + ".\n" +
"At least one must be specified, or mutually exclusive with --all",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return utils.ValidArgsFunction(k8sClient, *flags.Namespace)(cmd, args, toComplete)
},
SilenceUsage: true,
PreRunE: func(cmd *cobra.Command, args []string) error {
switch {
case all:
return nil
case len(certificates) == 0:
return fmt.Errorf("at least one certificate must be specified")
default:
for _, arg := range certificates {
if _, ok := cli.RotateCertificatesMap[arg]; !ok {
return fmt.Errorf("unrecognized certificate, %q", arg)
}
}

return nil
}
},
RunE: func(cmd *cobra.Command, args []string) error {
certSet := sets.New[string](certificates...)

certsToDelete := make(cli.RotateCertOptions)
for k, v := range cli.RotateCertificatesMap {
if all || certSet.Has(k) {
certsToDelete[k] = v
}
}

if err := (&cli.Helper{Client: k8sClient}).RotateCertificate(cmd.Context(), *flags.Namespace, args[0], certsToDelete); err != nil {
return err
}

_, _ = writer.Write([]byte(fmt.Sprintf("The following certificates have been successfully rotated: %s", strings.Join(certsToDelete.Keys(), ","))))

return nil
},
}

cmd.Flags().StringSliceVar(&certificates, "certificates", []string{}, "Specify which certificates should be rotated, at least one should be provided.")
cmd.Flags().BoolVar(&all, "all", false, "When specified, rotate all available certificates related to this Tenant Control Plane.")

return &cmd
}
48 changes: 48 additions & 0 deletions cmd/kubectl-kamaji/kubeconfig/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0

package kubeconfig

import (
"io"

"github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/clastix/kamaji/cmd/kubectl-kamaji/utils"
"github.com/clastix/kamaji/pkg/cli"
)

func NewGetKubeconfig(writer io.Writer, flags *genericclioptions.ConfigFlags, k8sClient client.Client) *cobra.Command {
var secretKey string

cmd := cobra.Command{
Use: "get {TCP_NAME}",
Example: " kubectl [--namespace=$NAMESPACE] kamaji kubeconfig get $TCP_NAME [--secret-type=$KEY]",
Short: "Get kubeconfig",
Long: "Retrieve the decoded content of a Tenant Control Plane kubeconfig.\n" +
"\n" +
"The CLI flag --secret-type refers to the key name of the kubeconfig you want to extract. " +
"By default, the command will extract the `admin.conf` one, you can specify your preferred one until it exists.",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return utils.ValidArgsFunction(k8sClient, *flags.Namespace)(cmd, args, toComplete)
},
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
_, data, kcErr := (&cli.Helper{Client: k8sClient}).GetKubeconfig(cmd.Context(), *flags.Namespace, args[0], secretKey)
if kcErr != nil {
return kcErr
}

_, _ = writer.Write(data)

return nil
},
}

cmd.Flags().StringVar(&secretKey, "secret-key", "admin.conf", "The Secret key used to retrieve the kubeconfig.")

return &cmd
}
16 changes: 16 additions & 0 deletions cmd/kubectl-kamaji/kubeconfig/kubeconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0

package kubeconfig

import (
"github.com/spf13/cobra"
)

func NewKubeconfigGroup() *cobra.Command {
return &cobra.Command{
Use: "kubeconfig",
Short: "kubeconfig operations",
Long: "Performs operations on kubeconfig objects related to the given Tenant Control Plane.",
}
}
82 changes: 82 additions & 0 deletions cmd/kubectl-kamaji/kubeconfig/rotate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0

package kubeconfig

import (
"fmt"
"io"
"strings"

"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/cli-runtime/pkg/genericclioptions"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/clastix/kamaji/cmd/kubectl-kamaji/utils"
"github.com/clastix/kamaji/pkg/cli"
)

func NewRotateKubeconfig(writer io.Writer, flags *genericclioptions.ConfigFlags, k8sClient client.Client) *cobra.Command {
var components []string
var all bool

cmd := cobra.Command{
Use: "rotate {TCP_NAME} { --all=true | --components={COMPONENTS_LIST} }",
Example: " kubectl [--namespace=$NAMESPACE] kamaji kubeconfig rotate $TCP_NAME --all=true\n" +
" kubectl [--namespace=$NAMESPACE] kamaji kubeconfig rotate $TCP_NAME --components=Admin\n" +
" kubectl [--namespace=$NAMESPACE] kamaji kubeconfig rotate $TCP_NAME --certificates=Admin,ControllerManager",
Short: "Rotate a kubeconfig",
Long: "Kubeconfig generated by Kamaji are based on client certificate authentication. " +
"Despite Kamaji performs rotation thanks to the CertificateLifecycle following the defined deadline, " +
"you can trigger this action manually according to your needs.\n" +
"\n" +
"The CLI flag --components allows to specify which Control Plane components' kubeconfig objects should be rotated, " +
"possible values: " + strings.Join(cli.RotateKubeconfigMap.Keys(), ", ") + ".\n" +
"At least one must be specified, or mutually exclusive with --all.",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return utils.ValidArgsFunction(k8sClient, *flags.Namespace)(cmd, args, toComplete)
},
SilenceUsage: true,
PreRunE: func(cmd *cobra.Command, args []string) error {
switch {
case all:
return nil
case len(components) == 0:
return fmt.Errorf("at least one component must be specified")
default:
for _, arg := range components {
if _, ok := cli.RotateKubeconfigMap[arg]; !ok {
return fmt.Errorf("unrecognized component, %q", arg)
}
}

return nil
}
},
RunE: func(cmd *cobra.Command, args []string) error {
componentSet := sets.New[string](components...)

toDelete := make(cli.RotateKubeconfigOptions)
for k, v := range cli.RotateKubeconfigMap {
if all || componentSet.Has(k) {
toDelete[k] = v
}
}

if err := (&cli.Helper{Client: k8sClient}).RotateKubeconfig(cmd.Context(), *flags.Namespace, args[0], toDelete); err != nil {
return err
}

_, _ = writer.Write([]byte(fmt.Sprintf("The following kubeconfig resources have been successfully rotated: %s", strings.Join(toDelete.Keys(), ","))))

return nil
},
}

cmd.Flags().StringSliceVar(&components, "components", []string{}, "List of Control Plane components that should have kubeconfig rotated.")
cmd.Flags().BoolVar(&all, "all", false, "When specified, rotate all components' kubeconfig resources related to this Tenant Control Plane.")

return &cmd
}
88 changes: 88 additions & 0 deletions cmd/kubectl-kamaji/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0

package main

import (
"bufio"
"context"
"fmt"
"os"
"os/signal"
"syscall"

"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/cli-runtime/pkg/genericclioptions"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"

kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/cmd/kubectl-kamaji/certificates"
"github.com/clastix/kamaji/cmd/kubectl-kamaji/kubeconfig"
"github.com/clastix/kamaji/cmd/kubectl-kamaji/token"
)

func main() {
writer := bufio.NewWriter(os.Stdout)
scheme := runtime.NewScheme()

configFlags := genericclioptions.NewConfigFlags(true)

clientConfig := configFlags.ToRawKubeConfigLoader()
*configFlags.Namespace, _, _ = clientConfig.Namespace()

restConfig, restErr := clientConfig.ClientConfig()
if restErr != nil {
_, _ = writer.WriteString(fmt.Sprintf("cannot retrieve REST configuration: %s", restErr.Error()))
os.Exit(1)
}

k8sClient, clientErr := client.New(restConfig, client.Options{Scheme: scheme})
if clientErr != nil {
_, _ = writer.WriteString(fmt.Sprintf("cannot generate Kubernetes configuration: %s", clientErr))
os.Exit(1)
}

rootCmd := NewCmd(scheme, configFlags)

kubeconfigCmd := kubeconfig.NewKubeconfigGroup()
kubeconfigCmd.AddCommand(kubeconfig.NewGetKubeconfig(writer, configFlags, k8sClient))
kubeconfigCmd.AddCommand(kubeconfig.NewRotateKubeconfig(writer, configFlags, k8sClient))
rootCmd.AddCommand(kubeconfigCmd)

certificatesCmd := certificates.NewCertificatesGroup()
certificatesCmd.AddCommand(certificates.NewRotateCertificates(writer, configFlags, k8sClient))
rootCmd.AddCommand(certificatesCmd)

tokenCmd := token.NewTokenGroup()
tokenCmd.AddCommand(token.NewTokenJoin(writer, configFlags, k8sClient))
rootCmd.AddCommand(tokenCmd)

ctx, cancelFn := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancelFn()

if _, err := rootCmd.ExecuteContextC(ctx); err != nil {
cancelFn()
os.Exit(1) //nolint:gocritic
}

_ = writer.Flush()
}

func NewCmd(scheme *runtime.Scheme, flags *genericclioptions.ConfigFlags) *cobra.Command {
cmd := cobra.Command{
Use: "kubectl-kamaji",
Aliases: []string{"kubectl kamaji"},
Short: "A plugin to manage your Kamaji Tenant Control Planes with ease.",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(kamajiv1alpha1.AddToScheme(scheme))
},
}

flags.AddFlags(cmd.PersistentFlags())

return &cmd
}
51 changes: 51 additions & 0 deletions cmd/kubectl-kamaji/token/join.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0

package token

import (
"io"

"github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/clastix/kamaji/cmd/kubectl-kamaji/utils"
"github.com/clastix/kamaji/pkg/cli"
)

func NewTokenJoin(writer io.Writer, flags *genericclioptions.ConfigFlags, k8sClient client.Client) *cobra.Command {
var flavour string
var skipExpired bool

cmd := cobra.Command{
Use: "join {TCP_NAME}",
Example: " kubectl [--namespace=$NAMESPACE] kamaji token join $TCP_NAME [--flavour=$FLAVOUR]",
Short: "Print join command",
Long: "Prints the required command to launch on a worker node to let it join to a Tenant Control Plane.\n" +
"\n" +
"The CLI flag --flavour allows to generate the command according to the flavour, yaki, or standard kubeadm.\n" +
"When the yaki flavour is selected, the environment variable KUBERNETES_VERSION will reference the current Kubernetes version: " +
"you can customize it according to your needs, as well as expanding the available environment variables (reference: https://github.com/clastix/yaki)",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return utils.ValidArgsFunction(k8sClient, *flags.Namespace)(cmd, args, toComplete)
},
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
data, err := (&cli.Helper{Client: k8sClient}).JoinToken(cmd.Context(), *flags.Namespace, args[0], skipExpired, flavour)
if err != nil {
return err
}

_, _ = writer.Write([]byte(data))

return nil
},
}

cmd.Flags().StringVar(&flavour, "flavour", "yaki", "The flavour to use for the join command, supported values: yaki, kubeadm.")
cmd.Flags().BoolVar(&skipExpired, "skip-expired", true, "When enabled, expired bootstrap tokens will be ignored.")

return &cmd
}
Loading

0 comments on commit 63dcacd

Please sign in to comment.