feat: Enhance PipeSpec and FromSpec with AuthorizedKeysSecret (#581)

* feat: Enhance PipeSpec and FromSpec with AuthorizedKeysSecret

- Added AuthorizedKeysSecret field to FromSpec for referencing Kubernetes secrets.
- Updated deepcopy functions to handle new AuthorizedKeysSecret field.
- Modified CRD definition to include the new private_key_secret field.
- Refactored fake client and lister implementations to support new fields.
- Introduced shared informers and listers for Pipe resources.
- Updated code generation scripts to reflect changes in API structure.

* refactor: Rename private_key_secret to authorized_keys_secret in CRD schema

* feat: Add support for authorized_keys from secret in skel and update k8sworkload.yaml

* feat: Enhance authorized keys handling to include secret name check

* Update plugin/kubernetes/skel.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Boshi Lian 2025-05-25 03:48:17 -07:00 committed by GitHub
parent ca311e15cd
commit 706a36b40e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 573 additions and 301 deletions

View file

@ -20,12 +20,13 @@ type PipeSpec struct {
}
type FromSpec struct {
Username string `json:"username"`
UsernameRegexMatch bool `json:"username_regex_match,omitempty"`
AuthorizedKeysData string `json:"authorized_keys_data,omitempty"`
HtpasswdData string `json:"htpasswd_data,omitempty"`
AuthorizedKeysFile string `json:"authorized_keys_file,omitempty"`
HtpasswdFile string `json:"htpasswd_file,omitempty"`
Username string `json:"username"`
UsernameRegexMatch bool `json:"username_regex_match,omitempty"`
AuthorizedKeysData string `json:"authorized_keys_data,omitempty"`
AuthorizedKeysFile string `json:"authorized_keys_file,omitempty"`
AuthorizedKeysSecret corev1.LocalObjectReference `json:"authorized_keys_secret,omitempty"`
HtpasswdData string `json:"htpasswd_data,omitempty"`
HtpasswdFile string `json:"htpasswd_file,omitempty"`
}
type ToSpec struct {

View file

@ -12,6 +12,7 @@ import (
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FromSpec) DeepCopyInto(out *FromSpec) {
*out = *in
out.AuthorizedKeysSecret = in.AuthorizedKeysSecret
return
}
@ -111,6 +112,7 @@ func (in *PipeSpec) DeepCopy() *PipeSpec {
func (in *ToSpec) DeepCopyInto(out *ToSpec) {
*out = *in
out.PrivateKeySecret = in.PrivateKeySecret
out.PasswordSecret = in.PasswordSecret
return
}

View file

@ -36,6 +36,15 @@ spec:
type: string
authorized_keys_file:
type: string
authorized_keys_secret:
description: LocalObjectReference contains enough information
to let you locate the referenced object inside the same namespace.
properties:
name:
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Add other useful fields. apiVersion, kind, uid?'
type: string
type: object
username:
type: string
htpasswd_data:

View file

@ -3,8 +3,8 @@
package versioned
import (
"fmt"
"net/http"
fmt "fmt"
http "net/http"
sshpiperv1beta1 "github.com/tg123/sshpiper/plugin/kubernetes/generated/clientset/versioned/typed/sshpiper/v1beta1"
discovery "k8s.io/client-go/discovery"

View file

@ -15,8 +15,12 @@ import (
// NewSimpleClientset returns a clientset that will respond with the provided objects.
// It's backed by a very simple object tracker that processes creates, updates and deletions as-is,
// without applying any validations and/or defaults. It shouldn't be considered a replacement
// without applying any field management, validations and/or defaults. It shouldn't be considered a replacement
// for a real clientset and is mostly useful in simple unit tests.
//
// DEPRECATED: NewClientset replaces this with support for field management, which significantly improves
// server side apply testing. NewClientset is only available when apply configurations are generated (e.g.
// via --with-applyconfig).
func NewSimpleClientset(objects ...runtime.Object) *Clientset {
o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder())
for _, obj := range objects {

View file

@ -3,111 +3,30 @@
package fake
import (
"context"
v1beta1 "github.com/tg123/sshpiper/plugin/kubernetes/apis/sshpiper/v1beta1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
labels "k8s.io/apimachinery/pkg/labels"
types "k8s.io/apimachinery/pkg/types"
watch "k8s.io/apimachinery/pkg/watch"
testing "k8s.io/client-go/testing"
sshpiperv1beta1 "github.com/tg123/sshpiper/plugin/kubernetes/generated/clientset/versioned/typed/sshpiper/v1beta1"
gentype "k8s.io/client-go/gentype"
)
// FakePipes implements PipeInterface
type FakePipes struct {
// fakePipes implements PipeInterface
type fakePipes struct {
*gentype.FakeClientWithList[*v1beta1.Pipe, *v1beta1.PipeList]
Fake *FakeSshpiperV1beta1
ns string
}
var pipesResource = v1beta1.SchemeGroupVersion.WithResource("pipes")
var pipesKind = v1beta1.SchemeGroupVersion.WithKind("Pipe")
// Get takes name of the pipe, and returns the corresponding pipe object, and an error if there is any.
func (c *FakePipes) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.Pipe, err error) {
obj, err := c.Fake.
Invokes(testing.NewGetAction(pipesResource, c.ns, name), &v1beta1.Pipe{})
if obj == nil {
return nil, err
func newFakePipes(fake *FakeSshpiperV1beta1, namespace string) sshpiperv1beta1.PipeInterface {
return &fakePipes{
gentype.NewFakeClientWithList[*v1beta1.Pipe, *v1beta1.PipeList](
fake.Fake,
namespace,
v1beta1.SchemeGroupVersion.WithResource("pipes"),
v1beta1.SchemeGroupVersion.WithKind("Pipe"),
func() *v1beta1.Pipe { return &v1beta1.Pipe{} },
func() *v1beta1.PipeList { return &v1beta1.PipeList{} },
func(dst, src *v1beta1.PipeList) { dst.ListMeta = src.ListMeta },
func(list *v1beta1.PipeList) []*v1beta1.Pipe { return gentype.ToPointerSlice(list.Items) },
func(list *v1beta1.PipeList, items []*v1beta1.Pipe) { list.Items = gentype.FromPointerSlice(items) },
),
fake,
}
return obj.(*v1beta1.Pipe), err
}
// List takes label and field selectors, and returns the list of Pipes that match those selectors.
func (c *FakePipes) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.PipeList, err error) {
obj, err := c.Fake.
Invokes(testing.NewListAction(pipesResource, pipesKind, c.ns, opts), &v1beta1.PipeList{})
if obj == nil {
return nil, err
}
label, _, _ := testing.ExtractFromListOptions(opts)
if label == nil {
label = labels.Everything()
}
list := &v1beta1.PipeList{ListMeta: obj.(*v1beta1.PipeList).ListMeta}
for _, item := range obj.(*v1beta1.PipeList).Items {
if label.Matches(labels.Set(item.Labels)) {
list.Items = append(list.Items, item)
}
}
return list, err
}
// Watch returns a watch.Interface that watches the requested pipes.
func (c *FakePipes) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
return c.Fake.
InvokesWatch(testing.NewWatchAction(pipesResource, c.ns, opts))
}
// Create takes the representation of a pipe and creates it. Returns the server's representation of the pipe, and an error, if there is any.
func (c *FakePipes) Create(ctx context.Context, pipe *v1beta1.Pipe, opts v1.CreateOptions) (result *v1beta1.Pipe, err error) {
obj, err := c.Fake.
Invokes(testing.NewCreateAction(pipesResource, c.ns, pipe), &v1beta1.Pipe{})
if obj == nil {
return nil, err
}
return obj.(*v1beta1.Pipe), err
}
// Update takes the representation of a pipe and updates it. Returns the server's representation of the pipe, and an error, if there is any.
func (c *FakePipes) Update(ctx context.Context, pipe *v1beta1.Pipe, opts v1.UpdateOptions) (result *v1beta1.Pipe, err error) {
obj, err := c.Fake.
Invokes(testing.NewUpdateAction(pipesResource, c.ns, pipe), &v1beta1.Pipe{})
if obj == nil {
return nil, err
}
return obj.(*v1beta1.Pipe), err
}
// Delete takes name of the pipe and deletes it. Returns an error if one occurs.
func (c *FakePipes) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
_, err := c.Fake.
Invokes(testing.NewDeleteActionWithOptions(pipesResource, c.ns, name, opts), &v1beta1.Pipe{})
return err
}
// DeleteCollection deletes a collection of objects.
func (c *FakePipes) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
action := testing.NewDeleteCollectionAction(pipesResource, c.ns, listOpts)
_, err := c.Fake.Invokes(action, &v1beta1.PipeList{})
return err
}
// Patch applies the patch and returns the patched pipe.
func (c *FakePipes) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.Pipe, err error) {
obj, err := c.Fake.
Invokes(testing.NewPatchSubresourceAction(pipesResource, c.ns, name, pt, data, subresources...), &v1beta1.Pipe{})
if obj == nil {
return nil, err
}
return obj.(*v1beta1.Pipe), err
}

View file

@ -13,7 +13,7 @@ type FakeSshpiperV1beta1 struct {
}
func (c *FakeSshpiperV1beta1) Pipes(namespace string) v1beta1.PipeInterface {
return &FakePipes{c, namespace}
return newFakePipes(c, namespace)
}
// RESTClient returns a RESTClient that is used to communicate

View file

@ -3,15 +3,14 @@
package v1beta1
import (
"context"
"time"
context "context"
v1beta1 "github.com/tg123/sshpiper/plugin/kubernetes/apis/sshpiper/v1beta1"
sshpiperv1beta1 "github.com/tg123/sshpiper/plugin/kubernetes/apis/sshpiper/v1beta1"
scheme "github.com/tg123/sshpiper/plugin/kubernetes/generated/clientset/versioned/scheme"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
types "k8s.io/apimachinery/pkg/types"
watch "k8s.io/apimachinery/pkg/watch"
rest "k8s.io/client-go/rest"
gentype "k8s.io/client-go/gentype"
)
// PipesGetter has a method to return a PipeInterface.
@ -22,141 +21,32 @@ type PipesGetter interface {
// PipeInterface has methods to work with Pipe resources.
type PipeInterface interface {
Create(ctx context.Context, pipe *v1beta1.Pipe, opts v1.CreateOptions) (*v1beta1.Pipe, error)
Update(ctx context.Context, pipe *v1beta1.Pipe, opts v1.UpdateOptions) (*v1beta1.Pipe, error)
Create(ctx context.Context, pipe *sshpiperv1beta1.Pipe, opts v1.CreateOptions) (*sshpiperv1beta1.Pipe, error)
Update(ctx context.Context, pipe *sshpiperv1beta1.Pipe, opts v1.UpdateOptions) (*sshpiperv1beta1.Pipe, error)
Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
Get(ctx context.Context, name string, opts v1.GetOptions) (*v1beta1.Pipe, error)
List(ctx context.Context, opts v1.ListOptions) (*v1beta1.PipeList, error)
Get(ctx context.Context, name string, opts v1.GetOptions) (*sshpiperv1beta1.Pipe, error)
List(ctx context.Context, opts v1.ListOptions) (*sshpiperv1beta1.PipeList, error)
Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.Pipe, err error)
Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *sshpiperv1beta1.Pipe, err error)
PipeExpansion
}
// pipes implements PipeInterface
type pipes struct {
client rest.Interface
ns string
*gentype.ClientWithList[*sshpiperv1beta1.Pipe, *sshpiperv1beta1.PipeList]
}
// newPipes returns a Pipes
func newPipes(c *SshpiperV1beta1Client, namespace string) *pipes {
return &pipes{
client: c.RESTClient(),
ns: namespace,
gentype.NewClientWithList[*sshpiperv1beta1.Pipe, *sshpiperv1beta1.PipeList](
"pipes",
c.RESTClient(),
scheme.ParameterCodec,
namespace,
func() *sshpiperv1beta1.Pipe { return &sshpiperv1beta1.Pipe{} },
func() *sshpiperv1beta1.PipeList { return &sshpiperv1beta1.PipeList{} },
),
}
}
// Get takes name of the pipe, and returns the corresponding pipe object, and an error if there is any.
func (c *pipes) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.Pipe, err error) {
result = &v1beta1.Pipe{}
err = c.client.Get().
Namespace(c.ns).
Resource("pipes").
Name(name).
VersionedParams(&options, scheme.ParameterCodec).
Do(ctx).
Into(result)
return
}
// List takes label and field selectors, and returns the list of Pipes that match those selectors.
func (c *pipes) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.PipeList, err error) {
var timeout time.Duration
if opts.TimeoutSeconds != nil {
timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
}
result = &v1beta1.PipeList{}
err = c.client.Get().
Namespace(c.ns).
Resource("pipes").
VersionedParams(&opts, scheme.ParameterCodec).
Timeout(timeout).
Do(ctx).
Into(result)
return
}
// Watch returns a watch.Interface that watches the requested pipes.
func (c *pipes) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
var timeout time.Duration
if opts.TimeoutSeconds != nil {
timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
}
opts.Watch = true
return c.client.Get().
Namespace(c.ns).
Resource("pipes").
VersionedParams(&opts, scheme.ParameterCodec).
Timeout(timeout).
Watch(ctx)
}
// Create takes the representation of a pipe and creates it. Returns the server's representation of the pipe, and an error, if there is any.
func (c *pipes) Create(ctx context.Context, pipe *v1beta1.Pipe, opts v1.CreateOptions) (result *v1beta1.Pipe, err error) {
result = &v1beta1.Pipe{}
err = c.client.Post().
Namespace(c.ns).
Resource("pipes").
VersionedParams(&opts, scheme.ParameterCodec).
Body(pipe).
Do(ctx).
Into(result)
return
}
// Update takes the representation of a pipe and updates it. Returns the server's representation of the pipe, and an error, if there is any.
func (c *pipes) Update(ctx context.Context, pipe *v1beta1.Pipe, opts v1.UpdateOptions) (result *v1beta1.Pipe, err error) {
result = &v1beta1.Pipe{}
err = c.client.Put().
Namespace(c.ns).
Resource("pipes").
Name(pipe.Name).
VersionedParams(&opts, scheme.ParameterCodec).
Body(pipe).
Do(ctx).
Into(result)
return
}
// Delete takes name of the pipe and deletes it. Returns an error if one occurs.
func (c *pipes) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
return c.client.Delete().
Namespace(c.ns).
Resource("pipes").
Name(name).
Body(&opts).
Do(ctx).
Error()
}
// DeleteCollection deletes a collection of objects.
func (c *pipes) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
var timeout time.Duration
if listOpts.TimeoutSeconds != nil {
timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second
}
return c.client.Delete().
Namespace(c.ns).
Resource("pipes").
VersionedParams(&listOpts, scheme.ParameterCodec).
Timeout(timeout).
Body(&opts).
Do(ctx).
Error()
}
// Patch applies the patch and returns the patched pipe.
func (c *pipes) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.Pipe, err error) {
result = &v1beta1.Pipe{}
err = c.client.Patch(pt).
Namespace(c.ns).
Resource("pipes").
Name(name).
SubResource(subresources...).
VersionedParams(&opts, scheme.ParameterCodec).
Body(data).
Do(ctx).
Into(result)
return
}

View file

@ -3,10 +3,10 @@
package v1beta1
import (
"net/http"
http "net/http"
v1beta1 "github.com/tg123/sshpiper/plugin/kubernetes/apis/sshpiper/v1beta1"
"github.com/tg123/sshpiper/plugin/kubernetes/generated/clientset/versioned/scheme"
sshpiperv1beta1 "github.com/tg123/sshpiper/plugin/kubernetes/apis/sshpiper/v1beta1"
scheme "github.com/tg123/sshpiper/plugin/kubernetes/generated/clientset/versioned/scheme"
rest "k8s.io/client-go/rest"
)
@ -69,10 +69,10 @@ func New(c rest.Interface) *SshpiperV1beta1Client {
}
func setConfigDefaults(config *rest.Config) error {
gv := v1beta1.SchemeGroupVersion
gv := sshpiperv1beta1.SchemeGroupVersion
config.GroupVersion = &gv
config.APIPath = "/apis"
config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
config.NegotiatedSerializer = rest.CodecFactoryForGeneratedClient(scheme.Scheme, scheme.Codecs).WithoutConversion()
if config.UserAgent == "" {
config.UserAgent = rest.DefaultKubernetesUserAgent()

View file

@ -0,0 +1,246 @@
// Code generated by informer-gen. DO NOT EDIT.
package externalversions
import (
reflect "reflect"
sync "sync"
time "time"
versioned "github.com/tg123/sshpiper/plugin/kubernetes/generated/clientset/versioned"
internalinterfaces "github.com/tg123/sshpiper/plugin/kubernetes/generated/informers/externalversions/internalinterfaces"
sshpiper "github.com/tg123/sshpiper/plugin/kubernetes/generated/informers/externalversions/sshpiper"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
cache "k8s.io/client-go/tools/cache"
)
// SharedInformerOption defines the functional option type for SharedInformerFactory.
type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory
type sharedInformerFactory struct {
client versioned.Interface
namespace string
tweakListOptions internalinterfaces.TweakListOptionsFunc
lock sync.Mutex
defaultResync time.Duration
customResync map[reflect.Type]time.Duration
transform cache.TransformFunc
informers map[reflect.Type]cache.SharedIndexInformer
// startedInformers is used for tracking which informers have been started.
// This allows Start() to be called multiple times safely.
startedInformers map[reflect.Type]bool
// wg tracks how many goroutines were started.
wg sync.WaitGroup
// shuttingDown is true when Shutdown has been called. It may still be running
// because it needs to wait for goroutines.
shuttingDown bool
}
// WithCustomResyncConfig sets a custom resync period for the specified informer types.
func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption {
return func(factory *sharedInformerFactory) *sharedInformerFactory {
for k, v := range resyncConfig {
factory.customResync[reflect.TypeOf(k)] = v
}
return factory
}
}
// WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory.
func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption {
return func(factory *sharedInformerFactory) *sharedInformerFactory {
factory.tweakListOptions = tweakListOptions
return factory
}
}
// WithNamespace limits the SharedInformerFactory to the specified namespace.
func WithNamespace(namespace string) SharedInformerOption {
return func(factory *sharedInformerFactory) *sharedInformerFactory {
factory.namespace = namespace
return factory
}
}
// WithTransform sets a transform on all informers.
func WithTransform(transform cache.TransformFunc) SharedInformerOption {
return func(factory *sharedInformerFactory) *sharedInformerFactory {
factory.transform = transform
return factory
}
}
// NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces.
func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory {
return NewSharedInformerFactoryWithOptions(client, defaultResync)
}
// NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory.
// Listers obtained via this SharedInformerFactory will be subject to the same filters
// as specified here.
// Deprecated: Please use NewSharedInformerFactoryWithOptions instead
func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory {
return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions))
}
// NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options.
func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory {
factory := &sharedInformerFactory{
client: client,
namespace: v1.NamespaceAll,
defaultResync: defaultResync,
informers: make(map[reflect.Type]cache.SharedIndexInformer),
startedInformers: make(map[reflect.Type]bool),
customResync: make(map[reflect.Type]time.Duration),
}
// Apply all options
for _, opt := range options {
factory = opt(factory)
}
return factory
}
func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) {
f.lock.Lock()
defer f.lock.Unlock()
if f.shuttingDown {
return
}
for informerType, informer := range f.informers {
if !f.startedInformers[informerType] {
f.wg.Add(1)
// We need a new variable in each loop iteration,
// otherwise the goroutine would use the loop variable
// and that keeps changing.
informer := informer
go func() {
defer f.wg.Done()
informer.Run(stopCh)
}()
f.startedInformers[informerType] = true
}
}
}
func (f *sharedInformerFactory) Shutdown() {
f.lock.Lock()
f.shuttingDown = true
f.lock.Unlock()
// Will return immediately if there is nothing to wait for.
f.wg.Wait()
}
func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool {
informers := func() map[reflect.Type]cache.SharedIndexInformer {
f.lock.Lock()
defer f.lock.Unlock()
informers := map[reflect.Type]cache.SharedIndexInformer{}
for informerType, informer := range f.informers {
if f.startedInformers[informerType] {
informers[informerType] = informer
}
}
return informers
}()
res := map[reflect.Type]bool{}
for informType, informer := range informers {
res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced)
}
return res
}
// InformerFor returns the SharedIndexInformer for obj using an internal
// client.
func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer {
f.lock.Lock()
defer f.lock.Unlock()
informerType := reflect.TypeOf(obj)
informer, exists := f.informers[informerType]
if exists {
return informer
}
resyncPeriod, exists := f.customResync[informerType]
if !exists {
resyncPeriod = f.defaultResync
}
informer = newFunc(f.client, resyncPeriod)
informer.SetTransform(f.transform)
f.informers[informerType] = informer
return informer
}
// SharedInformerFactory provides shared informers for resources in all known
// API group versions.
//
// It is typically used like this:
//
// ctx, cancel := context.Background()
// defer cancel()
// factory := NewSharedInformerFactory(client, resyncPeriod)
// defer factory.WaitForStop() // Returns immediately if nothing was started.
// genericInformer := factory.ForResource(resource)
// typedInformer := factory.SomeAPIGroup().V1().SomeType()
// factory.Start(ctx.Done()) // Start processing these informers.
// synced := factory.WaitForCacheSync(ctx.Done())
// for v, ok := range synced {
// if !ok {
// fmt.Fprintf(os.Stderr, "caches failed to sync: %v", v)
// return
// }
// }
//
// // Creating informers can also be created after Start, but then
// // Start must be called again:
// anotherGenericInformer := factory.ForResource(resource)
// factory.Start(ctx.Done())
type SharedInformerFactory interface {
internalinterfaces.SharedInformerFactory
// Start initializes all requested informers. They are handled in goroutines
// which run until the stop channel gets closed.
// Warning: Start does not block. When run in a go-routine, it will race with a later WaitForCacheSync.
Start(stopCh <-chan struct{})
// Shutdown marks a factory as shutting down. At that point no new
// informers can be started anymore and Start will return without
// doing anything.
//
// In addition, Shutdown blocks until all goroutines have terminated. For that
// to happen, the close channel(s) that they were started with must be closed,
// either before Shutdown gets called or while it is waiting.
//
// Shutdown may be called multiple times, even concurrently. All such calls will
// block until all goroutines have terminated.
Shutdown()
// WaitForCacheSync blocks until all started informers' caches were synced
// or the stop channel gets closed.
WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool
// ForResource gives generic access to a shared informer of the matching type.
ForResource(resource schema.GroupVersionResource) (GenericInformer, error)
// InformerFor returns the SharedIndexInformer for obj using an internal
// client.
InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer
Sshpiper() sshpiper.Interface
}
func (f *sharedInformerFactory) Sshpiper() sshpiper.Interface {
return sshpiper.New(f, f.namespace, f.tweakListOptions)
}

View file

@ -0,0 +1,46 @@
// Code generated by informer-gen. DO NOT EDIT.
package externalversions
import (
fmt "fmt"
v1beta1 "github.com/tg123/sshpiper/plugin/kubernetes/apis/sshpiper/v1beta1"
schema "k8s.io/apimachinery/pkg/runtime/schema"
cache "k8s.io/client-go/tools/cache"
)
// GenericInformer is type of SharedIndexInformer which will locate and delegate to other
// sharedInformers based on type
type GenericInformer interface {
Informer() cache.SharedIndexInformer
Lister() cache.GenericLister
}
type genericInformer struct {
informer cache.SharedIndexInformer
resource schema.GroupResource
}
// Informer returns the SharedIndexInformer.
func (f *genericInformer) Informer() cache.SharedIndexInformer {
return f.informer
}
// Lister returns the GenericLister.
func (f *genericInformer) Lister() cache.GenericLister {
return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource)
}
// ForResource gives generic access to a shared informer of the matching type
// TODO extend this to unknown resources with a client pool
func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) {
switch resource {
// Group=sshpiper, Version=v1beta1
case v1beta1.SchemeGroupVersion.WithResource("pipes"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Sshpiper().V1beta1().Pipes().Informer()}, nil
}
return nil, fmt.Errorf("no informer found for %v", resource)
}

View file

@ -0,0 +1,24 @@
// Code generated by informer-gen. DO NOT EDIT.
package internalinterfaces
import (
time "time"
versioned "github.com/tg123/sshpiper/plugin/kubernetes/generated/clientset/versioned"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
cache "k8s.io/client-go/tools/cache"
)
// NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer.
type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer
// SharedInformerFactory a small interface to allow for adding an informer without an import cycle
type SharedInformerFactory interface {
Start(stopCh <-chan struct{})
InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer
}
// TweakListOptionsFunc is a function that transforms a v1.ListOptions.
type TweakListOptionsFunc func(*v1.ListOptions)

View file

@ -0,0 +1,30 @@
// Code generated by informer-gen. DO NOT EDIT.
package sshpiper
import (
internalinterfaces "github.com/tg123/sshpiper/plugin/kubernetes/generated/informers/externalversions/internalinterfaces"
v1beta1 "github.com/tg123/sshpiper/plugin/kubernetes/generated/informers/externalversions/sshpiper/v1beta1"
)
// Interface provides access to each of this group's versions.
type Interface interface {
// V1beta1 provides access to shared informers for resources in V1beta1.
V1beta1() v1beta1.Interface
}
type group struct {
factory internalinterfaces.SharedInformerFactory
namespace string
tweakListOptions internalinterfaces.TweakListOptionsFunc
}
// New returns a new Interface.
func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
}
// V1beta1 returns a new v1beta1.Interface.
func (g *group) V1beta1() v1beta1.Interface {
return v1beta1.New(g.factory, g.namespace, g.tweakListOptions)
}

View file

@ -0,0 +1,29 @@
// Code generated by informer-gen. DO NOT EDIT.
package v1beta1
import (
internalinterfaces "github.com/tg123/sshpiper/plugin/kubernetes/generated/informers/externalversions/internalinterfaces"
)
// Interface provides access to all the informers in this group version.
type Interface interface {
// Pipes returns a PipeInformer.
Pipes() PipeInformer
}
type version struct {
factory internalinterfaces.SharedInformerFactory
namespace string
tweakListOptions internalinterfaces.TweakListOptionsFunc
}
// New returns a new Interface.
func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
}
// Pipes returns a PipeInformer.
func (v *version) Pipes() PipeInformer {
return &pipeInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
}

View file

@ -0,0 +1,74 @@
// Code generated by informer-gen. DO NOT EDIT.
package v1beta1
import (
context "context"
time "time"
apissshpiperv1beta1 "github.com/tg123/sshpiper/plugin/kubernetes/apis/sshpiper/v1beta1"
versioned "github.com/tg123/sshpiper/plugin/kubernetes/generated/clientset/versioned"
internalinterfaces "github.com/tg123/sshpiper/plugin/kubernetes/generated/informers/externalversions/internalinterfaces"
sshpiperv1beta1 "github.com/tg123/sshpiper/plugin/kubernetes/generated/listers/sshpiper/v1beta1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
watch "k8s.io/apimachinery/pkg/watch"
cache "k8s.io/client-go/tools/cache"
)
// PipeInformer provides access to a shared informer and lister for
// Pipes.
type PipeInformer interface {
Informer() cache.SharedIndexInformer
Lister() sshpiperv1beta1.PipeLister
}
type pipeInformer struct {
factory internalinterfaces.SharedInformerFactory
tweakListOptions internalinterfaces.TweakListOptionsFunc
namespace string
}
// NewPipeInformer constructs a new informer for Pipe type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewPipeInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
return NewFilteredPipeInformer(client, namespace, resyncPeriod, indexers, nil)
}
// NewFilteredPipeInformer constructs a new informer for Pipe type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewFilteredPipeInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
return cache.NewSharedIndexInformer(
&cache.ListWatch{
ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.SshpiperV1beta1().Pipes(namespace).List(context.TODO(), options)
},
WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.SshpiperV1beta1().Pipes(namespace).Watch(context.TODO(), options)
},
},
&apissshpiperv1beta1.Pipe{},
resyncPeriod,
indexers,
)
}
func (f *pipeInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return NewFilteredPipeInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
}
func (f *pipeInformer) Informer() cache.SharedIndexInformer {
return f.factory.InformerFor(&apissshpiperv1beta1.Pipe{}, f.defaultInformer)
}
func (f *pipeInformer) Lister() sshpiperv1beta1.PipeLister {
return sshpiperv1beta1.NewPipeLister(f.Informer().GetIndexer())
}

View file

@ -3,10 +3,10 @@
package v1beta1
import (
v1beta1 "github.com/tg123/sshpiper/plugin/kubernetes/apis/sshpiper/v1beta1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/tools/cache"
sshpiperv1beta1 "github.com/tg123/sshpiper/plugin/kubernetes/apis/sshpiper/v1beta1"
labels "k8s.io/apimachinery/pkg/labels"
listers "k8s.io/client-go/listers"
cache "k8s.io/client-go/tools/cache"
)
// PipeLister helps list Pipes.
@ -14,7 +14,7 @@ import (
type PipeLister interface {
// List lists all Pipes in the indexer.
// Objects returned here must be treated as read-only.
List(selector labels.Selector) (ret []*v1beta1.Pipe, err error)
List(selector labels.Selector) (ret []*sshpiperv1beta1.Pipe, err error)
// Pipes returns an object that can list and get Pipes.
Pipes(namespace string) PipeNamespaceLister
PipeListerExpansion
@ -22,25 +22,17 @@ type PipeLister interface {
// pipeLister implements the PipeLister interface.
type pipeLister struct {
indexer cache.Indexer
listers.ResourceIndexer[*sshpiperv1beta1.Pipe]
}
// NewPipeLister returns a new PipeLister.
func NewPipeLister(indexer cache.Indexer) PipeLister {
return &pipeLister{indexer: indexer}
}
// List lists all Pipes in the indexer.
func (s *pipeLister) List(selector labels.Selector) (ret []*v1beta1.Pipe, err error) {
err = cache.ListAll(s.indexer, selector, func(m interface{}) {
ret = append(ret, m.(*v1beta1.Pipe))
})
return ret, err
return &pipeLister{listers.New[*sshpiperv1beta1.Pipe](indexer, sshpiperv1beta1.Resource("pipe"))}
}
// Pipes returns an object that can list and get Pipes.
func (s *pipeLister) Pipes(namespace string) PipeNamespaceLister {
return pipeNamespaceLister{indexer: s.indexer, namespace: namespace}
return pipeNamespaceLister{listers.NewNamespaced[*sshpiperv1beta1.Pipe](s.ResourceIndexer, namespace)}
}
// PipeNamespaceLister helps list and get Pipes.
@ -48,36 +40,15 @@ func (s *pipeLister) Pipes(namespace string) PipeNamespaceLister {
type PipeNamespaceLister interface {
// List lists all Pipes in the indexer for a given namespace.
// Objects returned here must be treated as read-only.
List(selector labels.Selector) (ret []*v1beta1.Pipe, err error)
List(selector labels.Selector) (ret []*sshpiperv1beta1.Pipe, err error)
// Get retrieves the Pipe from the indexer for a given namespace and name.
// Objects returned here must be treated as read-only.
Get(name string) (*v1beta1.Pipe, error)
Get(name string) (*sshpiperv1beta1.Pipe, error)
PipeNamespaceListerExpansion
}
// pipeNamespaceLister implements the PipeNamespaceLister
// interface.
type pipeNamespaceLister struct {
indexer cache.Indexer
namespace string
}
// List lists all Pipes in the indexer for a given namespace.
func (s pipeNamespaceLister) List(selector labels.Selector) (ret []*v1beta1.Pipe, err error) {
err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
ret = append(ret, m.(*v1beta1.Pipe))
})
return ret, err
}
// Get retrieves the Pipe from the indexer for a given namespace and name.
func (s pipeNamespaceLister) Get(name string) (*v1beta1.Pipe, error) {
obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
if err != nil {
return nil, err
}
if !exists {
return nil, errors.NewNotFound(v1beta1.Resource("pipe"), name)
}
return obj.(*v1beta1.Pipe), nil
listers.ResourceIndexer[*sshpiperv1beta1.Pipe]
}

View file

@ -64,7 +64,7 @@ func (s *skelpipeWrapper) From() []skel.SkelPipeFrom {
to: &s.pipe.Spec.To,
}
if f.AuthorizedKeysData != "" || f.AuthorizedKeysFile != "" {
if f.AuthorizedKeysData != "" || f.AuthorizedKeysFile != "" || f.AuthorizedKeysSecret.Name != "" {
froms = append(froms, &skelpipePublicKeyWrapper{
skelpipeFromWrapper: *w,
})
@ -171,6 +171,26 @@ func (s *skelpipePublicKeyWrapper) AuthorizedKeys(conn libplugin.ConnMetadata) (
return nil, err
}
if s.from.AuthorizedKeysSecret.Name != "" {
log.Debugf("mapping to %v authorized keys using secret %v", s.pipe.Spec.To.Host, s.from.AuthorizedKeysSecret.Name)
anno := s.pipe.GetAnnotations()
secret, err := s.plugin.k8sclient.Secrets(s.pipe.Namespace).Get(context.Background(), s.from.AuthorizedKeysSecret.Name, metav1.GetOptions{})
if err != nil {
return nil, err
}
for _, k := range []string{anno["sshpiper.com/authorizedkeys_field_name"], anno["authorizedkeys_field_name"], "authorized_keys", "authorizedkeys", "ssh-authorizedkeys"} {
data := secret.Data[k]
if data != nil {
log.Debugf("found authorized keys in secret %v/%v", s.from.AuthorizedKeysSecret.Name, k)
byteSlices = append(byteSlices, data)
break
}
}
}
return bytes.Join(byteSlices, []byte("\n")), nil
}
@ -189,7 +209,7 @@ func (s *skelpipeToPrivateKeyWrapper) PrivateKey(conn libplugin.ConnMetadata) ([
var publicKey []byte
var privateKey []byte
for _, k := range []string{anno["privatekey_field_name"], "ssh-privatekey", "privatekey"} {
for _, k := range []string{anno["sshpiper.com/privatekey_field_name"], anno["privatekey_field_name"], "ssh-privatekey", "privatekey"} {
data := secret.Data[k]
if data != nil {
log.Debugf("found private key in secret %v/%v", s.to.PrivateKeySecret.Name, k)
@ -198,8 +218,8 @@ func (s *skelpipeToPrivateKeyWrapper) PrivateKey(conn libplugin.ConnMetadata) ([
}
}
if anno["no_ca_publickey"] != "true" {
for _, k := range []string{anno["publickey_field_name"], "ssh-publickey-cert", "publickey-cert", "ssh-publickey", "publickey"} {
if anno["no_ca_publickey"] != "true" && anno["sshpiper.com/no_ca_publickey"] != "true" {
for _, k := range []string{anno["sshpiper.com/publickey_field_name"], anno["publickey_field_name"], "ssh-publickey-cert", "publickey-cert", "ssh-publickey", "publickey"} {
data := secret.Data[k]
if data != nil {
log.Debugf("found publickey key cert in secret %v/%v", s.to.PrivateKeySecret.Name, k)

View file

@ -9,18 +9,22 @@ go mod vendor
SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")
REPO_ROOT=$(realpath "${SCRIPT_ROOT}/../../")
CODEGEN_PKG=${REPO_ROOT}/vendor/k8s.io/code-generator
THIS_PKG="github.com/tg123/sshpiper/plugin/kubernetes"
# generate the code with:
# --output-base because this script should also be able to run inside the vendor dir of
# k8s.io/kubernetes. The output-base is needed for the generators to output into the vendor dir
# instead of the $GOPATH directly. For normal projects this can be dropped.
# generators deepcopy,client,informer,lister
chmod +x "${CODEGEN_PKG}"/kube_codegen.sh
"${CODEGEN_PKG}"/kube_codegen.sh \
"deepcopy,client,lister" \
github.com/tg123/sshpiper/plugin/kubernetes/generated \
github.com/tg123/sshpiper/plugin/kubernetes/apis \
sshpiper:v1beta1 \
--go-header-file /dev/null \
--trim-path-prefix github.com/tg123/sshpiper/plugin/kubernetes/
source "${CODEGEN_PKG}/kube_codegen.sh"
kube::codegen::gen_helpers \
--boilerplate /dev/null \
"${SCRIPT_ROOT}"
kube::codegen::gen_register \
--boilerplate /dev/null \
"${SCRIPT_ROOT}"
kube::codegen::gen_client \
--with-watch \
--output-dir "${SCRIPT_ROOT}/generated" \
--output-pkg "${THIS_PKG}/generated" \
--boilerplate /dev/null \
"${SCRIPT_ROOT}/apis"