diff --git a/README.md b/README.md index 4028b396..1f7eafda 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Designed as a [drop-in replacement](#difference-to-kubectx) for [kubectx](https: - [OVH](docs/stores/ovh/ovh.md) - [Rancher](docs/stores/rancher/rancher.md) - Scaleway (documentation tbd) + - [Akamai / Linode](docs/stores/akamai/akamai.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 1bce6c59..f083aa36 100644 --- a/cmd/switcher/switcher.go +++ b/cmd/switcher/switcher.go @@ -297,6 +297,15 @@ func initialize() ([]store.KubeconfigStore, *types.Config, error) { } s = doStore digitalOceanStoreAddedViaConfig = true + case types.StoreKindAkamai: + akamaiStore, err := store.NewAkamaiStore(kubeconfigStoreFromConfig) + if err != nil { + if kubeconfigStoreFromConfig.Required != nil && !*kubeconfigStoreFromConfig.Required { + continue + } + return nil, nil, err + } + s = akamaiStore default: return nil, nil, fmt.Errorf("unknown store %q", kubeconfigStoreFromConfig.Kind) } diff --git a/docs/stores/akamai/akamai.md b/docs/stores/akamai/akamai.md new file mode 100644 index 00000000..9e8bbc5d --- /dev/null +++ b/docs/stores/akamai/akamai.md @@ -0,0 +1,23 @@ +# Akamai store + +To use the Akamai store a token should be created [on linode's website](https://cloud.linode.com/profile/tokens) + +In order to create this token you also need to specify the scope. +The required permissions for this plugin to work are the following: +- `read/write` for Kubernetes + +## Configuration + +The Akamai store configuration is defined in the `kubeswitch` configuration file. +An example configuration is shown below: + +```yaml +kind: SwitchConfig +version: v1alpha1 +kubeconfigStores: +- kind: akamai + config: + linode_token: "your-linode-token" +``` + +`linode_token` can be ignored if set with the environment variable `LINODE_TOKEN`. diff --git a/pkg/store/kubeconfig_store_akamai.go b/pkg/store/kubeconfig_store_akamai.go new file mode 100644 index 00000000..b0556a2b --- /dev/null +++ b/pkg/store/kubeconfig_store_akamai.go @@ -0,0 +1,172 @@ +// 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" + "encoding/base64" + "fmt" + "net/http" + "os" + "strconv" + "time" + + "golang.org/x/oauth2" + "gopkg.in/yaml.v3" + + "github.com/danielfoehrkn/kubeswitch/types" + "github.com/linode/linodego" + "github.com/sirupsen/logrus" +) + +func NewAkamaiStore(store types.KubeconfigStore) (*AkamaiStore, error) { + akamaiStoreConfig := &types.StoreConfigAkamai{} + if store.Config != nil { + buf, err := yaml.Marshal(store.Config) + if err != nil { + return nil, fmt.Errorf("failed to process akamai store config: %w", err) + } + + err = yaml.Unmarshal(buf, akamaiStoreConfig) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal akami config: %w", err) + } + } + + return &AkamaiStore{ + Logger: logrus.New().WithField("store", types.StoreKindAkamai), + KubeconfigStore: store, + Config: akamaiStoreConfig, + }, nil +} + +// InitializeAkamaiStore the Akamai client +func (s *AkamaiStore) InitializeAkamaiStore() error { + // use environment variables if token is not set + token := s.Config.LinodeToken + if token == "" { + envToken, ok := os.LookupEnv("LINODE_TOKEN") + if !ok { + return fmt.Errorf("linode token not set") + } + token = envToken + } + + tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + + oauth2Client := &http.Client{ + Transport: &oauth2.Transport{ + Source: tokenSource, + }, + } + + linodeClient := linodego.NewClient(oauth2Client) + + s.Client = &linodeClient + + return nil +} + +// GetID returns the unique store ID +func (s *AkamaiStore) GetID() string { + return fmt.Sprintf("%s.default", s.GetKind()) +} + +func (s *AkamaiStore) GetKind() types.StoreKind { + return types.StoreKindAkamai +} + +func (s *AkamaiStore) GetContextPrefix(path string) string { + return fmt.Sprintf("%s/%s", s.GetKind(), path) +} + +func (s *AkamaiStore) VerifyKubeconfigPaths() error { + // NOOP + return nil +} + +func (s *AkamaiStore) GetStoreConfig() types.KubeconfigStore { + return s.KubeconfigStore +} + +func (s *AkamaiStore) GetLogger() *logrus.Entry { + return s.Logger +} + +func (s *AkamaiStore) StartSearch(channel chan SearchResult) { + s.Logger.Debug("Akamai: start search") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + if err := s.InitializeAkamaiStore(); err != nil { + channel <- SearchResult{ + KubeconfigPath: "", + Error: err, + } + return + } + + // list linode instances + instances, err := s.Client.ListLKEClusters(ctx, nil) + if err != nil { + channel <- SearchResult{ + KubeconfigPath: "", + Error: err, + } + return + } + + for _, instance := range instances { + channel <- SearchResult{ + KubeconfigPath: instance.Label, + Tags: map[string]string{ + "clusterID": strconv.Itoa(instance.ID), + "region": instance.Region, + }, + } + } +} + +func (s *AkamaiStore) GetKubeconfigForPath(path string, tags map[string]string) ([]byte, error) { + s.Logger.Debugf("Akamai: get kubeconfig for path %s", path) + + // initialize client + if err := s.InitializeAkamaiStore(); err != nil { + return nil, err + } + + clusterID, err := strconv.Atoi(tags["clusterID"]) + if err != nil { + return nil, fmt.Errorf("failed to get clusterID: %w", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // get kubeconfig + LKEkubeconfig, err := s.Client.GetLKEClusterKubeconfig(ctx, clusterID) + if err != nil { + return nil, err + } + + // decode base64 kubeconfig + kubeconfig, err := base64.StdEncoding.DecodeString(LKEkubeconfig.KubeConfig) + if err != nil { + return nil, err + } + + return kubeconfig, nil +} diff --git a/pkg/store/types.go b/pkg/store/types.go index af9535e7..464ae3ce 100644 --- a/pkg/store/types.go +++ b/pkg/store/types.go @@ -28,6 +28,7 @@ import ( gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1" seedmanagementv1alpha1 "github.com/gardener/gardener/pkg/apis/seedmanagement/v1alpha1" vaultapi "github.com/hashicorp/vault/api" + "github.com/linode/linodego" "github.com/ovh/go-ovh/ovh" "github.com/rancher/norman/clientbase" managementClient "github.com/rancher/rancher/pkg/client/generated/management/v3" @@ -203,3 +204,10 @@ type DigitalOceanStore struct { ContextToKubernetesService map[string]do.KubernetesService Config doks.DoctlConfig } + +type AkamaiStore struct { + Logger *logrus.Entry + KubeconfigStore types.KubeconfigStore + Client *linodego.Client + Config *types.StoreConfigAkamai +} diff --git a/types/config.go b/types/config.go index 572dfb48..3be1a585 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)) +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)) // ValidConfigVersions contains all valid config versions var ValidConfigVersions = sets.NewString("v1alpha1") @@ -50,6 +50,8 @@ const ( StoreKindScaleway StoreKind = "scaleway" // StoreKindDigitalOcean is an identifier for the Azure store StoreKindDigitalOcean StoreKind = "digitalocean" + // StoreKindAkamai is an identifier for the Akamai store + StoreKindAkamai StoreKind = "akamai" ) type Config struct { @@ -250,3 +252,7 @@ type StoreConfigScaleway struct { ScalewaySecretKey string `yaml:"secret_key"` ScalewayRegion string `yaml:"region"` } + +type StoreConfigAkamai struct { + LinodeToken string `yaml:"linode_token"` +}