Skip to content

Commit

Permalink
feat: add akamai/linode provider
Browse files Browse the repository at this point in the history
  • Loading branch information
guilhem committed Nov 8, 2024
1 parent 8be9373 commit 9fb4590
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down
9 changes: 9 additions & 0 deletions cmd/switcher/switcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
23 changes: 23 additions & 0 deletions docs/stores/akamai/akamai.md
Original file line number Diff line number Diff line change
@@ -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`.
172 changes: 172 additions & 0 deletions pkg/store/kubeconfig_store_akamai.go
Original file line number Diff line number Diff line change
@@ -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
}
8 changes: 8 additions & 0 deletions pkg/store/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
8 changes: 7 additions & 1 deletion types/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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 {
Expand Down Expand Up @@ -250,3 +252,7 @@ type StoreConfigScaleway struct {
ScalewaySecretKey string `yaml:"secret_key"`
ScalewayRegion string `yaml:"region"`
}

type StoreConfigAkamai struct {
LinodeToken string `yaml:"linode_token"`
}

0 comments on commit 9fb4590

Please sign in to comment.