Adding a store
Adding a new kubeconfig store
Section titled “Adding a new kubeconfig store”This guide explains how to add support for a new backing store (a cloud provider, a managed Kubernetes platform, a secret store, …).
Thanks to the shared BaseStore and the store registry, adding a store does
not require touching the startup wiring (cmd/switcher/switcher.go). There
are only two places to edit plus one new file.
Overview
Section titled “Overview”A store implements the KubeconfigStore interface
(pkg/store/types/types.go):
type KubeconfigStore interface { GetID() string GetKind() types.StoreKind GetContextPrefix(path string) string VerifyKubeconfigPaths() error StartSearch(channel chan SearchResult) GetKubeconfigForPath(path string, tags map[string]string) ([]byte, error) GetLogger() *logrus.Entry GetStoreConfig() types.KubeconfigStore}Most of these are boilerplate. By embedding store.BaseStore
(pkg/store/base.go) you only have to implement what
is actually specific to your store:
StartSearch— discover the available kubeconfigs and push their paths (and optionalTags) into the channel.GetKubeconfigForPath— fetch the raw kubeconfig bytes for one path.GetContextPrefix— (usually) the prefix shown in the fuzzy-search list.
GetID, GetKind, GetStoreConfig, GetLogger and a no-op
VerifyKubeconfigPaths are provided by BaseStore. To change any of them,
just declare a method with the same name on your store: it shadows the
promoted one (see GardenerStore.GetID for a real example).
Step 1 — declare the store kind
Section titled “Step 1 — declare the store kind”In types/config.go:
- Add a
StoreKindconstant:// StoreKindFoo is an identifier for the Foo storeStoreKindFoo StoreKind = "foo" - Add it to
ValidStoreKindsso the config validator accepts it. - If your store needs configuration, add a typed config struct:
type StoreConfigFoo struct {APIToken string `yaml:"apiToken"`Region string `yaml:"region"`}
Step 2 — implement and register the store
Section titled “Step 2 — implement and register the store”Create pkg/store/kubeconfig_store_foo.go. The struct field for your store lives
in pkg/store/types.go (embed BaseStore):
type FooStore struct { BaseStore Client *foosdk.Client Config *types.StoreConfigFoo}Then the implementation file:
package store
import ( "fmt"
storetypes "github.com/MichaelSp/kswitch/pkg/store/types" "github.com/MichaelSp/kswitch/types")
// register the store so cmd/switcher can build it without a hardcoded switchfunc init() { Register(types.StoreKindFoo, func(s types.KubeconfigStore, deps Dependencies) (storetypes.KubeconfigStore, error) { return NewFooStore(s) })}
func NewFooStore(store types.KubeconfigStore) (*FooStore, error) { // ParseStoreConfig replaces the yaml.Marshal/yaml.Unmarshal boilerplate. // It returns a usable (non-nil) *StoreConfigFoo even with no config block. config, err := ParseStoreConfig[types.StoreConfigFoo](store) if err != nil { return nil, err }
if config.APIToken == "" { return nil, fmt.Errorf("the Foo store requires apiToken in the SwitchConfig file") }
return &FooStore{ BaseStore: NewBaseStore(types.StoreKindFoo, store), Config: config, // Client: ... }, nil}
func (s *FooStore) GetContextPrefix(path string) string { if s.GetStoreConfig().ShowPrefix != nil && !*s.GetStoreConfig().ShowPrefix { return "" } return string(types.StoreKindFoo)}
func (s *FooStore) StartSearch(channel chan storetypes.SearchResult) { // discover clusters and push their paths // channel <- storetypes.SearchResult{KubeconfigPath: name, Tags: map[string]string{"clusterID": id}}}
func (s *FooStore) GetKubeconfigForPath(path string, tags map[string]string) ([]byte, error) { // fetch and return the raw kubeconfig bytes return nil, fmt.Errorf("not implemented")}The Dependencies argument carries the process-wide inputs some stores need
(StateDirectory, KubeconfigName, VaultAPIAddressFromFlag,
VaultTokenFileName). Use only what you need.
Identifying clusters: prefer Tags
Section titled “Identifying clusters: prefer Tags”When the path you publish in StartSearch does not, on its own, uniquely
identify the cluster (duplicate names across projects/regions, opaque IDs),
attach a Tags map to the SearchResult and read it back in
GetKubeconfigForPath. Tags are persisted in the search index, so they also
work when discovery has not run in the current process. See the Scaleway and
Akamai stores for examples.
Optional interfaces
Section titled “Optional interfaces”Previewer(pkg/store/types/types.go): implementGetSearchPreview(path string, tags map[string]string) (string, error)to show a custom preview before the kubeconfig (see EKS / Azure).- Lazy initialization: if connecting to the backing store is slow, do it in
an
InitializeFooStore()method called fromStartSearch/GetKubeconfigForPathrather than in the constructor, so the fuzzy search can appear quickly (see GKE / Azure / EKS). - Store-specific validation: add a validator under
pkg/store/foo/and call it frompkg/config/validation/validation.go(see Gardener / GKE).
Step 3 — document and verify
Section titled “Step 3 — document and verify”- Add a
docs/stores/foo/foo.mdpage and link it fromdocs/kubeconfig_stores.md. go build ./...already verifies your store satisfies the interface (the registry factory returnsstoretypes.KubeconfigStore).- A factory may return
(nil, nil)to opt out for the current environment (e.g. the Digital Ocean store when nodoctlconfig exists); the startup loop skips it. Return an untyped nil in that case, never a typed nil pointer.