diff --git a/README.md b/README.md index 1f7eafda..a7095527 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Designed as a [drop-in replacement](#difference-to-kubectx) for [kubectx](https: - [Rancher](docs/stores/rancher/rancher.md) - Scaleway (documentation tbd) - [Akamai / Linode](docs/stores/akamai/akamai.md) + - [Cluster API (capi)](docs/stores/capi/capi.md) - Your favorite Cloud Provider or Managed Kubernetes Platform is not supported yet? Looking for contributions! - **Change the namespace** - **Change to any context and namespace from the history** diff --git a/cmd/switcher/switcher.go b/cmd/switcher/switcher.go index f083aa36..099e1d33 100644 --- a/cmd/switcher/switcher.go +++ b/cmd/switcher/switcher.go @@ -306,6 +306,15 @@ func initialize() ([]store.KubeconfigStore, *types.Config, error) { return nil, nil, err } s = akamaiStore + case types.StoreKindCapi: + capiStore, err := store.NewCapiStore(kubeconfigStoreFromConfig, stateDirectory) + if err != nil { + if kubeconfigStoreFromConfig.Required != nil && !*kubeconfigStoreFromConfig.Required { + continue + } + return nil, nil, err + } + s = capiStore default: return nil, nil, fmt.Errorf("unknown store %q", kubeconfigStoreFromConfig.Kind) } diff --git a/docs/stores/capi/capi.md b/docs/stores/capi/capi.md new file mode 100644 index 00000000..08c85c74 --- /dev/null +++ b/docs/stores/capi/capi.md @@ -0,0 +1,16 @@ +# Cluster API (capi) store + +To use the Cluster API (capi) store a kubeconfig file should be created for the management cluster. + +## Configuration + +The Cluster API store configuration is defined in the `kubeswitch` configuration file. + +```yaml +kind: SwitchConfig +version: v1alpha1 +kubeconfigStores: +- kind: capi + config: + kubeconfigPath: "/home/user/.kube/management.config" +``` diff --git a/pkg/store/kubeconfig_store_capi.go b/pkg/store/kubeconfig_store_capi.go new file mode 100644 index 00000000..2f42d8fd --- /dev/null +++ b/pkg/store/kubeconfig_store_capi.go @@ -0,0 +1,176 @@ +// Copyright 2024 The Kubeswitch authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package store + +import ( + "context" + "fmt" + "time" + + "github.com/danielfoehrkn/kubeswitch/types" + "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/tools/clientcmd" + clusterv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1" + utilkubeconfig "sigs.k8s.io/cluster-api/util/kubeconfig" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func NewCapiStore(store types.KubeconfigStore, stateDir string) (*CapiStore, error) { + storeConfig := &types.StoreConfigCapi{} + if store.Config != nil { + buf, err := yaml.Marshal(store.Config) + if err != nil { + return nil, err + } + + err = yaml.Unmarshal(buf, storeConfig) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal Azure config: %w", err) + } + } + + return &CapiStore{ + KubeconfigStore: store, + Logger: logrus.New().WithField("store", types.StoreKindCapi), + Config: storeConfig, + }, nil +} + +func (s *CapiStore) InitializeCapiStore() error { + s.Logger.Info("Initializing CAPI client") + k8sclient, err := s.getCapiClient() + if err != nil { + return err + } + s.Client = k8sclient + + return nil +} + +// GetID returns the unique store ID +func (s *CapiStore) GetID() string { + return fmt.Sprintf("%s.%s", types.StoreKindCapi, s.KubeconfigStore.ID) +} + +// GetKind returns the store kind +func (s *CapiStore) GetKind() types.StoreKind { + return types.StoreKindCapi +} + +// GetContextPrefix returns the context prefix +func (s *CapiStore) GetContextPrefix(path string) string { + return string(types.StoreKindCapi) +} + +// VerifyKubeconfigPaths verifies the kubeconfig paths +func (s *CapiStore) VerifyKubeconfigPaths() error { + return nil +} + +func (s *CapiStore) getCapiClient() (client.Client, error) { + scheme := runtime.NewScheme() + utilruntime.Must(corev1.AddToScheme(scheme)) + utilruntime.Must(clusterv1beta1.AddToScheme(scheme)) + + // client from s.Config.KubeconfigPath + clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + &clientcmd.ClientConfigLoadingRules{ExplicitPath: s.Config.KubeconfigPath}, + &clientcmd.ConfigOverrides{}) + + restConfig, err := clientConfig.ClientConfig() + if err != nil { + return nil, fmt.Errorf("unable to create rest config: %v", err) + } + + k8sclient, err := client.New(restConfig, client.Options{ + Scheme: scheme, + }) + if err != nil { + return nil, fmt.Errorf("unable to create client: %v", err) + } + return k8sclient, nil +} + +// StartSearch starts the search over the configured search paths +func (s *CapiStore) StartSearch(channel chan SearchResult) { + s.Logger.Debug("CAPI: start search") + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() + + // initialize CAPI client + if err := s.InitializeCapiStore(); err != nil { + channel <- SearchResult{ + KubeconfigPath: "", + Error: err, + } + return + } + + // list clusters + clusters := &clusterv1beta1.ClusterList{} + err := s.Client.List(ctx, clusters) + if err != nil { + channel <- SearchResult{ + KubeconfigPath: "", + Error: err, + } + return + } + + for _, cluster := range clusters.Items { + s.Logger.Debug("CAPI: found cluster", "name", cluster.Name, "namespace", cluster.Namespace) + + channel <- SearchResult{ + KubeconfigPath: fmt.Sprintf("%s-%s", cluster.Namespace, cluster.Name), + Error: nil, + Tags: map[string]string{ + "namespace": cluster.Namespace, + "name": cluster.Name, + }, + } + } +} + +// GetKubeconfigForPath returns the kubeconfig for the path +func (s *CapiStore) GetKubeconfigForPath(path string, tags map[string]string) ([]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() + + s.Logger.Debug("CAPI: GetKubeconfigForPath", "path", path) + + obj := client.ObjectKey{ + Namespace: tags["namespace"], + Name: tags["name"], + } + dataBytes, err := utilkubeconfig.FromSecret(ctx, s.Client, obj) + if err != nil { + s.Logger.Debug("CAPI: GetKubeconfigForPath", "error", err) + return nil, err + } + return dataBytes, nil +} + +func (s *CapiStore) GetLogger() *logrus.Entry { + return s.Logger +} + +func (s *CapiStore) GetStoreConfig() types.KubeconfigStore { + return s.KubeconfigStore +} diff --git a/pkg/store/types.go b/pkg/store/types.go index 464ae3ce..f44912b4 100644 --- a/pkg/store/types.go +++ b/pkg/store/types.go @@ -211,3 +211,10 @@ type AkamaiStore struct { Client *linodego.Client Config *types.StoreConfigAkamai } + +type CapiStore struct { + Logger *logrus.Entry + KubeconfigStore types.KubeconfigStore + Client client.Client + Config *types.StoreConfigCapi +} diff --git a/types/config.go b/types/config.go index 3be1a585..7485c29c 100644 --- a/types/config.go +++ b/types/config.go @@ -24,7 +24,7 @@ import ( type StoreKind string // ValidStoreKinds contains all valid store kinds -var ValidStoreKinds = sets.NewString(string(StoreKindVault), string(StoreKindFilesystem), string(StoreKindGardener), string(StoreKindGKE), string(StoreKindAzure), string(StoreKindEKS), string(StoreKindRancher), string(StoreKindOVH), string(StoreKindScaleway), string(StoreKindDigitalOcean), string(StoreKindAkamai)) +var ValidStoreKinds = sets.NewString(string(StoreKindVault), string(StoreKindFilesystem), string(StoreKindGardener), string(StoreKindGKE), string(StoreKindAzure), string(StoreKindEKS), string(StoreKindRancher), string(StoreKindOVH), string(StoreKindScaleway), string(StoreKindDigitalOcean), string(StoreKindAkamai), string(StoreKindCapi)) // ValidConfigVersions contains all valid config versions var ValidConfigVersions = sets.NewString("v1alpha1") @@ -52,6 +52,8 @@ const ( StoreKindDigitalOcean StoreKind = "digitalocean" // StoreKindAkamai is an identifier for the Akamai store StoreKindAkamai StoreKind = "akamai" + // StoreKindCapi is an identifier for the CAPI store + StoreKindCapi StoreKind = "capi" ) type Config struct { @@ -256,3 +258,9 @@ type StoreConfigScaleway struct { type StoreConfigAkamai struct { LinodeToken string `yaml:"linode_token"` } + +type StoreConfigCapi struct { + // KubeconfigPath is the path on the local filesystem pointing to the kubeconfig + // for the management cluster + KubeconfigPath string `yaml:"kubeconfigPath"` +}