Skip to content

Commit

Permalink
feat: add a grpc plugin system
Browse files Browse the repository at this point in the history
  • Loading branch information
guilhem committed Nov 15, 2024
1 parent c68bd31 commit 35db70f
Show file tree
Hide file tree
Showing 16 changed files with 1,588 additions and 1 deletion.
9 changes: 9 additions & 0 deletions cmd/switcher/switcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,15 @@ func initialize() ([]storetypes.KubeconfigStore, *types.Config, error) {
return nil, nil, err
}
s = capiStore
case types.StoreKindPlugin:
pluginStore, err := store.NewPluginStore(kubeconfigStoreFromConfig)
if err != nil {
if kubeconfigStoreFromConfig.Required != nil && !*kubeconfigStoreFromConfig.Required {
continue
}
return nil, nil, err
}
s = pluginStore
default:
return nil, nil, fmt.Errorf("unknown store %q", kubeconfigStoreFromConfig.Kind)
}
Expand Down
153 changes: 153 additions & 0 deletions pkg/store/kubeconfig_store_plugins.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// 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"
"os/exec"

"github.com/hashicorp/go-plugin"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"

"github.com/danielfoehrkn/kubeswitch/pkg/store/plugins"
storetypes "github.com/danielfoehrkn/kubeswitch/pkg/store/types"
"github.com/danielfoehrkn/kubeswitch/types"
)

func NewPluginStore(store types.KubeconfigStore) (*PluginStore, error) {
storePlugin := &types.StoreConfigPlugin{}
if store.Config != nil {
buf, err := yaml.Marshal(store.Config)
if err != nil {
return nil, fmt.Errorf("failed to process plugin store config: %w", err)
}

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

return &PluginStore{
Logger: logrus.New().WithField("store", types.StoreKindPlugin),
KubeconfigStore: store,
Config: storePlugin,
}, nil
}

// InitializePluginStore initializes the plugin store
func (s *PluginStore) InitializePluginStore() error {

client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: plugins.Handshake,
Plugins: plugins.PluginMap,
Cmd: exec.Command(s.Config.CmdPath, s.Config.Args...),
AllowedProtocols: []plugin.Protocol{
plugin.ProtocolNetRPC, plugin.ProtocolGRPC},
})

// Connect via RPC
rpcClient, err := client.Client()
if err != nil {
return err
}

plugin, err := rpcClient.Dispense("store")
if err != nil {
return fmt.Errorf("failed to dispense plugin: %w", err)
}

c, ok := plugin.(plugins.Store)
if !ok {
return fmt.Errorf("plugin does not implement Store interface")
}

s.Client = c

return nil
}

// GetID returns the unique store ID
func (s *PluginStore) GetID() string {
ctx := context.Background()

id, err := s.Client.GetID(ctx)
if err != nil {
return fmt.Sprintf("%s.default", s.GetKind())
}
return id
}

func (s *PluginStore) GetKind() types.StoreKind {
return types.StoreKindPlugin
}

func (s *PluginStore) GetContextPrefix(path string) string {
ctx := context.Background()

prefix, err := s.Client.GetContextPrefix(ctx, path)
if err != nil {
return fmt.Sprintf("%s/%s", s.GetKind(), path)
}
return prefix
}

func (s *PluginStore) VerifyKubeconfigPaths() error {
ctx := context.Background()

if err := s.InitializePluginStore(); err != nil {
return err
}

return s.Client.VerifyKubeconfigPaths(ctx)
}

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

func (s *PluginStore) GetLogger() *logrus.Entry {
return s.Logger
}

func (s *PluginStore) StartSearch(channel chan storetypes.SearchResult) {
s.Logger.Debug("Plugin: start search")

ctx := context.Background()

if err := s.InitializePluginStore(); err != nil {
channel <- storetypes.SearchResult{
KubeconfigPath: "",
Error: err,
}
return
}

s.Client.StartSearch(ctx, channel)
}

func (s *PluginStore) GetKubeconfigForPath(path string, tags map[string]string) ([]byte, error) {
s.Logger.Debugf("Plugins: get kubeconfig for path %s", path)

ctx := context.Background()

if err := s.InitializePluginStore(); err != nil {
return nil, err
}

return s.Client.GetKubeconfigForPath(ctx, path, tags)
}
19 changes: 19 additions & 0 deletions pkg/store/plugins/buf.gen.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: v2
managed:
enabled: true
override:
- file_option: go_package_prefix
value: github.com/danielfoehrkn/kubeswitch/pkg/store/plugins
plugins:
- remote: buf.build/protocolbuffers/go:v1.35.1
out: .
opt:
- paths=source_relative
- remote: buf.build/grpc/go:v1.5.1
out: .
opt:
- paths=source_relative
- require_unimplemented_servers=false

inputs:
- directory: proto
10 changes: 10 additions & 0 deletions pkg/store/plugins/buf.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# For details on buf.yaml configuration, visit https://buf.build/docs/configuration/v2/buf-yaml
version: v2
modules:
- path: proto
lint:
use:
- STANDARD
breaking:
use:
- FILE
15 changes: 15 additions & 0 deletions pkg/store/plugins/example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Plugin example

This is an example plugin for the store.

## Usage

```yaml
kind: SwitchConfig
version: v1alpha1
kubeconfigStores:
- kind: plugin
config:
cmdPath: /path/to/plugin
# args:
```
28 changes: 28 additions & 0 deletions pkg/store/plugins/example/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module example.com/dumb-plugin

go 1.23.3

require (
github.com/danielfoehrkn/kubeswitch v0.0.0-00010101000000-000000000000
github.com/hashicorp/go-hclog v1.6.3
github.com/hashicorp/go-plugin v1.6.2
)

require (
github.com/fatih/color v1.17.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/oklog/run v1.0.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/grpc v1.68.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
k8s.io/apimachinery v0.31.2 // indirect
)

replace github.com/danielfoehrkn/kubeswitch => ../../../..
68 changes: 68 additions & 0 deletions pkg/store/plugins/example/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog=
github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q=
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw=
k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
83 changes: 83 additions & 0 deletions pkg/store/plugins/example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package main

import (
"context"

"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin"

"github.com/danielfoehrkn/kubeswitch/pkg/store/plugins"
storetypes "github.com/danielfoehrkn/kubeswitch/pkg/store/types"
)

// Store is the implementation of the store plugin
type Store struct {
Logger hclog.Logger
}

// GetID returns the ID of the store
func (s *Store) GetID(ctx context.Context) (string, error) {
return "example", nil
}

// GetContextPrefix returns the context prefix
func (s *Store) GetContextPrefix(ctx context.Context, path string) (string, error) {
return "example", nil
}

// VerifyKubeconfigPaths verifies the kubeconfig paths
func (s *Store) VerifyKubeconfigPaths(ctx context.Context) error {
return nil
}

// StartSearch starts the search
func (s *Store) StartSearch(ctx context.Context, channel chan storetypes.SearchResult) {
channel <- storetypes.SearchResult{
KubeconfigPath: "fake",
Error: nil,
}
}

// GetKubeconfigForPath gets the kubeconfig for the path
func (s *Store) GetKubeconfigForPath(ctx context.Context, path string, tags map[string]string) ([]byte, error) {
if path == "fake" {
// valid fake kubeconfig
file := `apiVersion: v1
kind: Config
preferences: {}
clusters:
- name: development
- name: test
users:
- name: developer
- name: experimenter
contexts:
- name: dev-frontend
- name: dev-storage
- name: exp-test`
return []byte(file), nil
}

s.Logger.Error("invalid path")
return nil, nil
}

func main() {
logger := hclog.Default()
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: plugins.Handshake,
Plugins: map[string]plugin.Plugin{
"store": &plugins.StorePlugin{Impl: &Store{Logger: logger}},
},
Logger: logger,

// A non-nil value here enables gRPC serving for this plugin...
GRPCServer: plugin.DefaultGRPCServer,
})
}
Loading

0 comments on commit 35db70f

Please sign in to comment.