Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Designed as a [drop-in replacement](#difference-to-kubectx) for [kubectx](https:
- Scaleway (documentation tbd)
- [Akamai / Linode](docs/stores/akamai/akamai.md)
- [Cluster API (capi)](docs/stores/capi/capi.md)
- [OpenTelekom Cloud (OTC)](docs/stores/otc/otc.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 @@ -325,6 +325,15 @@ func initialize() ([]storetypes.KubeconfigStore, *types.Config, error) {
return nil, nil, err
}
s = capiStore
case types.StoreKindOTC:
capiStore, err := store.NewOTCStore(kubeconfigStoreFromConfig, stateDirectory)
if err != nil {
if kubeconfigStoreFromConfig.Required != nil && !*kubeconfigStoreFromConfig.Required {
continue
}
return nil, nil, err
}
s = capiStore
case types.StoreKindPlugin:
pluginStore, err := store.NewPluginStore(kubeconfigStoreFromConfig)
if err != nil {
Expand Down
69 changes: 69 additions & 0 deletions docs/stores/otc/otc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# OpenTelekom Cloud CCE store

Kubeswitch can discover CCE clusters from OTC.

Kubeswitch relies on the [Openstask configuration files](https://docs.openstack.org/python-openstackclient/latest/configuration/index.html), with additions as defined by the underlying [SDK](https://github.com/opentelekomcloud/gophertelekomcloud)

## Clouds configuration

First create a file `~/.config/openstack/clouds.yaml`

```
cat ~/.config/openstack/clouds.yaml

clouds:
my-cloud-with-permanent-aksk:
auth:
auth_url: https://iam.eu-de.otc.t-systems.com:443/v3
project_name: eu-de_xxx # also known as tenant name
ak: YOUR_AK
sk: YOUR_SK
interface: public
identity_api_version: "3"
auth_type: aksk
my-cloud-with-temporary-aksk:
auth:
auth_url: https://iam.eu-de.otc.t-systems.com/v3
project_name: eu-de_yyy # also known as tenant name
ak: YOUR_TEMPORARY_AK
sk: YOUR_TEMPORARY_SK
security_token: YOUR_TEMPORARY_STS
interface: public
identity_api_version: "3"
auth_type: aksk

```

Permanent and temporary AK/SK as well as temporary STS can be generated in OTC web console after you log in.

Other authentication methods supported by the format and SDK (password, federated, token, assume role, ...) may also work, but were not tested.


## Kubectx configuration

```
cat ~/.kube/switch-config.yaml

kind: SwitchConfig
version: "v1alpha1"
kubeconfigStores:
- kind: otc
id: otc1
config:
cloud: my-cloud-with-permanent-aksk

- kind: otc
id: otc2
config:
cloud: my-cloud-with-temporary-aksk
selectContext: externalTLSVerify
```

Retrieved kubeconfig for each CCE cluster contains 3 contexts (same ones as when it is downloaded from the web console):
- internal
- external
- externalTLSVerify

By default, all three are available for selection by `kubeswitch`.

The optinal parameter `selectContext` allows for selecting only one of these contexts for each cluster.
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ require (
github.com/onsi/ginkgo v1.16.5
github.com/onsi/ginkgo/v2 v2.19.1
github.com/onsi/gomega v1.34.0
github.com/opentelekomcloud/gophertelekomcloud v0.9.3
github.com/pkg/errors v0.9.1
github.com/rancher/norman v0.0.0-20240205154641-a6a6cf569608
github.com/rancher/rancher/pkg/client v0.0.0-20240416202124-a6da228939da
Expand All @@ -48,6 +49,7 @@ require (
require (
github.com/digitalocean/doctl v1.105.0
github.com/digitalocean/godo v1.113.0
github.com/exoscale/egoscale/v3 v3.1.9
github.com/hashicorp/go-plugin v1.6.2
github.com/linode/linodego v1.42.0
github.com/ovh/go-ovh v1.4.3
Expand Down Expand Up @@ -97,7 +99,6 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
github.com/exoscale/egoscale/v3 v3.1.9 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,8 @@ github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0
github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w=
github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA=
github.com/opentelekomcloud/gophertelekomcloud v0.9.3 h1:zdttgRAWc4uHgJ3PX5hP8ulhT1VYBh2JeRsItNPp8dg=
github.com/opentelekomcloud/gophertelekomcloud v0.9.3/go.mod h1:M1F6OfSRZRzAmAFKQqSLClX952at5hx5rHe4UTEykgg=
github.com/ovh/go-ovh v1.4.3 h1:Gs3V823zwTFpzgGLZNI6ILS4rmxZgJwJCz54Er9LwD0=
github.com/ovh/go-ovh v1.4.3/go.mod h1:AkPXVtgwB6xlKblMjRKJJmjRp+ogrE7fz2lVgcQY8SY=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
Expand Down
264 changes: 264 additions & 0 deletions pkg/store/kubeconfig_store_otc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
// Copyright 2021 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"
"github.com/disiqueira/gotree"
golangsdk "github.com/opentelekomcloud/gophertelekomcloud"
"github.com/opentelekomcloud/gophertelekomcloud/openstack/cce/v3/clusters"
"strings"
"time"

"github.com/opentelekomcloud/gophertelekomcloud/openstack"

storetypes "github.com/danielfoehrkn/kubeswitch/pkg/store/types"
"github.com/danielfoehrkn/kubeswitch/types"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
)

func NewOTCStore(store types.KubeconfigStore, _ string) (*OTCStore, error) {
otcStoreConfig := &types.StoreConfigOTC{}
if store.Config != nil {
buf, err := yaml.Marshal(store.Config)
if err != nil {
return nil, err
}

err = yaml.Unmarshal(buf, otcStoreConfig)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal OTC config: %w", err)
}
}

return &OTCStore{
KubeconfigStore: store,
Config: otcStoreConfig,
}, nil
}

func (s *OTCStore) InitializeOTCStore() error {
_, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

var names []string
if *s.Config.Cloud != "" {
names = []string{*s.Config.Cloud}
}
env := openstack.NewEnv("OTC_")
cloud, err := env.Cloud(names...)
if err != nil {
return fmt.Errorf("error getting cloud: %w", err)
}

// Cannot do this - there is a bug in gophertelekomcloud that STS key is not loaded from config
// client, err := openstack.AuthenticatedClientFromCloud(cloud)
// if err != nil {
// return fmt.Errorf("error getting client: %w", err)
// }

opts, err := openstack.AuthOptionsFromInfo(&cloud.AuthInfo, cloud.AuthType)
if err != nil {
return fmt.Errorf("failed to convert AuthInfo to AuthOptsBuilder with Env vars: %s", err)
}

// fix the issue from upstream
if aksk, ok := opts.(golangsdk.AKSKAuthOptions); ok {
if aksk.SecurityToken == "" && cloud.AuthInfo.SecurityToken != "" {
aksk.SecurityToken = cloud.AuthInfo.SecurityToken
opts = aksk
}
}

// finally get the client
client, err := openstack.AuthenticatedClient(opts)
if err != nil {
return fmt.Errorf("failed to authenticate client: %s", err)
}

cceClient, err := openstack.NewCCE(client, golangsdk.EndpointOpts{})
if err != nil {
return fmt.Errorf("Error getting cce: %+v\n", err)
}

s.Client = cceClient
return nil
}

func (s *OTCStore) IsInitialized() bool {
return s.Client != nil && s.Config != nil
}

func (s *OTCStore) GetID() string {
id := "default"

if s.KubeconfigStore.ID != nil {
id = *s.KubeconfigStore.ID
}

return fmt.Sprintf("%s.%s", types.StoreKindOTC, id)
}

func (s *OTCStore) GetKind() types.StoreKind {
return types.StoreKindOTC
}

func (s *OTCStore) GetStoreConfig() types.KubeconfigStore {
return s.KubeconfigStore
}

func (s *OTCStore) GetLogger() *logrus.Entry {
if s.Logger == nil {
s.Logger = logrus.WithField("store", s.GetID())
}
return s.Logger
}

func (s *OTCStore) GetContextPrefix(path string) string {
if s.GetStoreConfig().ShowPrefix != nil && !*s.GetStoreConfig().ShowPrefix {
return ""
}

return strings.ReplaceAll(path, "--", "-")
}

func (s *OTCStore) VerifyKubeconfigPaths() error {
// NOOP
return nil
}

func (s *OTCStore) StartSearch(channel chan storetypes.SearchResult) {
_, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

if err := s.InitializeOTCStore(); err != nil {
err := fmt.Errorf("failed to initialize store. This is most likely a problem with your provided OTC credentials: %v", err)
channel <- storetypes.SearchResult{
Error: err,
}
return
}

refinedClusters, err := clusters.List(s.Client, clusters.ListOpts{})
if err != nil {
channel <- storetypes.SearchResult{
Error: fmt.Errorf("Error getting clusters: %+v\n", err),
}
}

for _, cluster := range refinedClusters {
kubeconfigPath := fmt.Sprintf("otc_%s--%s", *s.Config.Cloud, cluster.Metadata.Name)
channel <- storetypes.SearchResult{
KubeconfigPath: kubeconfigPath,
Error: nil,
Tags: map[string]string{
"id": cluster.Metadata.Id,
"name": cluster.Metadata.Name,
"version": cluster.Spec.Version,
"type": cluster.Spec.Type,
"flavor": cluster.Spec.Flavor,
"status": cluster.Status.Phase,
},
}
}
s.GetLogger().Debugf("Search done for OTC")
}

func (s *OTCStore) GetKubeconfigForPath(_ string, tags map[string]string) ([]byte, error) {
_, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

if !s.IsInitialized() {
if err := s.InitializeOTCStore(); err != nil {
return nil, fmt.Errorf("failed to initialize OTC store: %w", err)
}
}

expiryOpts := clusters.ExpirationOpts{
Duration: -1,
}

kubeconfig, err := clusters.GetCertWithExpiration(s.Client, tags["id"], expiryOpts).ExtractMap()
if err != nil {
return nil, fmt.Errorf("unable to retrieve cluster kubeconfig: %w", err)
}

kubeconfig = s.convertKubeconfig(tags["name"], kubeconfig)

configYaml, err := yaml.Marshal(kubeconfig)
if err != nil {
return nil, fmt.Errorf("unable to marshal cluster kubeconfig: %w", err)
}
return configYaml, nil
}

func (s *OTCStore) convertKubeconfig(name string, kubeconfig map[string]interface{}) map[string]interface{} {
if s.Config.SelectContext == nil {
return kubeconfig
}

contexts, ok := kubeconfig["contexts"].([]interface{})
if !ok {
s.GetLogger().Warnf("contexts in kubeconfig is not an array, skipping context selection")
return kubeconfig
}

var selectedContext = make(map[string]interface{})
for _, ctx := range contexts {
ctxMap, ok := ctx.(map[string]interface{})
if !ok {
s.GetLogger().Warnf("context %v is not a map, skipping", ctx)
continue
}

if ctxMap["name"] == *s.Config.SelectContext {
selectedContext = ctxMap
break
}
}

if selectedContext == nil {
s.GetLogger().Warnf("selected context %q not found in kubeconfig", *s.Config.SelectContext)
return kubeconfig
}

selectedContext["name"] = name
kubeconfig["contexts"] = []interface{}{selectedContext}
return kubeconfig
}

func (s *OTCStore) GetSearchPreview(_ string, tags map[string]string) (string, error) {
_, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

if !s.IsInitialized() {
if err := s.InitializeOTCStore(); err != nil {
return "", fmt.Errorf("failed to initialize OTC store: %w", err)
}
}

asciTree := gotree.New(tags["name"])

asciTree.Add(fmt.Sprintf("ID: %s", tags["id"]))
asciTree.Add(fmt.Sprintf("Cloud: %s", *s.Config.Cloud))
asciTree.Add(fmt.Sprintf("Version: %s", tags["version"]))
asciTree.Add(fmt.Sprintf("Type: %s", tags["type"]))
asciTree.Add(fmt.Sprintf("Flavor: %s", tags["flavor"]))
asciTree.Add(fmt.Sprintf("Status: %s", tags["status"]))

return asciTree.Print(), nil
}
Loading