1
0
Fork 0
mirror of https://github.com/external-secrets/external-secrets.git synced 2024-12-14 11:57:59 +00:00

feat: implement validating webhook

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
This commit is contained in:
Moritz Johner 2022-02-17 23:14:39 +01:00
parent ab4a1f3d05
commit 8fc4484cc6
52 changed files with 1903 additions and 402 deletions

View file

@ -28,7 +28,7 @@ func (alpha *ExternalSecret) ConvertTo(betaRaw conversion.Hub) error {
v1beta1DataFrom := make([]esv1beta1.ExternalSecretDataFromRemoteRef, 0)
for _, v1alpha1RemoteRef := range alpha.Spec.DataFrom {
v1beta1RemoteRef := esv1beta1.ExternalSecretDataFromRemoteRef{
Extract: esv1beta1.ExternalSecretDataRemoteRef{
Extract: &esv1beta1.ExternalSecretDataRemoteRef{
Key: v1alpha1RemoteRef.Key,
Property: v1alpha1RemoteRef.Property,
Version: v1alpha1RemoteRef.Version,

View file

@ -183,7 +183,7 @@ func newExternalSecretV1Beta1() *esv1beta1.ExternalSecret {
},
DataFrom: []esv1beta1.ExternalSecretDataFromRemoteRef{
{
Extract: esv1beta1.ExternalSecretDataRemoteRef{
Extract: &esv1beta1.ExternalSecretDataRemoteRef{
Key: "key",
Property: "property",
Version: "version",

View file

@ -161,15 +161,19 @@ type ExternalSecretDataRemoteRef struct {
Property string `json:"property,omitempty"`
}
// +kubebuilder:validation:MinProperties=1
// +kubebuilder:validation:MaxProperties=1
type ExternalSecretDataFromRemoteRef struct {
// Used to extract multiple key/value pairs from one secret
// +optional
Extract ExternalSecretDataRemoteRef `json:"extract,omitempty"`
Extract *ExternalSecretDataRemoteRef `json:"extract,omitempty"`
// Used to find secrets based on tags or regular expressions
// +optional
Find ExternalSecretFind `json:"find,omitempty"`
Find *ExternalSecretFind `json:"find,omitempty"`
}
// +kubebuilder:validation:MinProperties=1
// +kubebuilder:validation:MaxProperties=1
type ExternalSecretFind struct {
// Finds secrets based on the name.
// +optional

View file

@ -12,36 +12,47 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package provider
package v1beta1
import (
"context"
"sigs.k8s.io/controller-runtime/pkg/client"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
)
// +kubebuilder:object:root=false
// +kubebuilder:object:generate:false
// +k8s:deepcopy-gen:interfaces=nil
// +k8s:deepcopy-gen=nil
// Provider is a common interface for interacting with secret backends.
type Provider interface {
// NewClient constructs a SecretsManager Provider
NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (SecretsClient, error)
NewClient(ctx context.Context, store GenericStore, kube client.Client, namespace string) (SecretsClient, error)
// ValidateStore checks if the provided store is valid
ValidateStore(store GenericStore) error
}
// +kubebuilder:object:root=false
// +kubebuilder:object:generate:false
// +k8s:deepcopy-gen:interfaces=nil
// +k8s:deepcopy-gen=nil
// SecretsClient provides access to secrets.
type SecretsClient interface {
// GetSecret returns a single secret from the provider
GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error)
GetSecret(ctx context.Context, ref ExternalSecretDataRemoteRef) ([]byte, error)
// Validate checks if the client is configured correctly
// and is able to retrieve secrets from the provider
Validate() error
// GetSecretMap returns multiple k/v pairs from the provider
GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error)
GetSecretMap(ctx context.Context, ref ExternalSecretDataRemoteRef) (map[string][]byte, error)
// GetAllSecrets returns multiple k/v pairs from the provider
GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error)
GetAllSecrets(ctx context.Context, ref ExternalSecretFind) (map[string][]byte, error)
Close(ctx context.Context) error
}

View file

@ -12,27 +12,24 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package schema
package v1beta1
import (
"encoding/json"
"fmt"
"sync"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/provider"
)
var builder map[string]provider.Provider
var builder map[string]Provider
var buildlock sync.RWMutex
func init() {
builder = make(map[string]provider.Provider)
builder = make(map[string]Provider)
}
// Register a store backend type. Register panics if a
// backend with the same store is already registered.
func Register(s provider.Provider, storeSpec *esv1beta1.SecretStoreProvider) {
func Register(s Provider, storeSpec *SecretStoreProvider) {
storeName, err := getProviderName(storeSpec)
if err != nil {
panic(fmt.Sprintf("store error registering schema: %s", err.Error()))
@ -50,7 +47,7 @@ func Register(s provider.Provider, storeSpec *esv1beta1.SecretStoreProvider) {
// ForceRegister adds to store schema, overwriting a store if
// already registered. Should only be used for testing.
func ForceRegister(s provider.Provider, storeSpec *esv1beta1.SecretStoreProvider) {
func ForceRegister(s Provider, storeSpec *SecretStoreProvider) {
storeName, err := getProviderName(storeSpec)
if err != nil {
panic(fmt.Sprintf("store error registering schema: %s", err.Error()))
@ -62,7 +59,7 @@ func ForceRegister(s provider.Provider, storeSpec *esv1beta1.SecretStoreProvider
}
// GetProviderByName returns the provider implementation by name.
func GetProviderByName(name string) (provider.Provider, bool) {
func GetProviderByName(name string) (Provider, bool) {
buildlock.RLock()
f, ok := builder[name]
buildlock.RUnlock()
@ -70,8 +67,11 @@ func GetProviderByName(name string) (provider.Provider, bool) {
}
// GetProvider returns the provider from the generic store.
func GetProvider(s esv1beta1.GenericStore) (provider.Provider, error) {
func GetProvider(s GenericStore) (Provider, error) {
spec := s.GetSpec()
if spec == nil {
return nil, fmt.Errorf("no spec found in %#v", s)
}
storeName, err := getProviderName(spec.Provider)
if err != nil {
return nil, fmt.Errorf("store error for %s: %w", s.GetName(), err)
@ -90,7 +90,7 @@ func GetProvider(s esv1beta1.GenericStore) (provider.Provider, error) {
// getProviderName returns the name of the configured provider
// or an error if the provider is not configured.
func getProviderName(storeSpec *esv1beta1.SecretStoreProvider) (string, error) {
func getProviderName(storeSpec *SecretStoreProvider) (string, error) {
storeBytes, err := json.Marshal(storeSpec)
if err != nil || storeBytes == nil {
return "", fmt.Errorf("failed to marshal store spec: %w", err)

View file

@ -11,7 +11,7 @@ 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 schema
package v1beta1
import (
"context"
@ -19,9 +19,6 @@ import (
"github.com/stretchr/testify/assert"
"sigs.k8s.io/controller-runtime/pkg/client"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/provider"
)
type PP struct{}
@ -29,22 +26,22 @@ type PP struct{}
const shouldBeRegistered = "provider should be registered"
// New constructs a SecretsManager Provider.
func (p *PP) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
func (p *PP) NewClient(ctx context.Context, store GenericStore, kube client.Client, namespace string) (SecretsClient, error) {
return p, nil
}
// GetSecret returns a single secret from the provider.
func (p *PP) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
func (p *PP) GetSecret(ctx context.Context, ref ExternalSecretDataRemoteRef) ([]byte, error) {
return []byte("NOOP"), nil
}
// GetSecretMap returns multiple k/v pairs from the provider.
func (p *PP) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
func (p *PP) GetSecretMap(ctx context.Context, ref ExternalSecretDataRemoteRef) (map[string][]byte, error) {
return map[string][]byte{}, nil
}
// Empty GetAllSecrets.
func (p *PP) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
func (p *PP) GetAllSecrets(ctx context.Context, ref ExternalSecretFind) (map[string][]byte, error) {
// TO be implemented
return map[string][]byte{}, nil
}
@ -57,6 +54,10 @@ func (p *PP) Validate() error {
return nil
}
func (p *PP) ValidateStore(store GenericStore) error {
return nil
}
// TestRegister tests if the Register function
// (1) panics if it tries to register something invalid
// (2) stores the correct provider.
@ -66,22 +67,22 @@ func TestRegister(t *testing.T) {
name string
expPanic bool
expExists bool
provider *esv1beta1.SecretStoreProvider
provider *SecretStoreProvider
}{
{
test: "should panic when given an invalid provider",
name: "aws",
expPanic: true,
expExists: false,
provider: &esv1beta1.SecretStoreProvider{},
provider: &SecretStoreProvider{},
},
{
test: "should register an correct provider",
name: "aws",
expExists: false,
provider: &esv1beta1.SecretStoreProvider{
AWS: &esv1beta1.AWSProvider{
Service: esv1beta1.AWSServiceSecretsManager,
provider: &SecretStoreProvider{
AWS: &AWSProvider{
Service: AWSServiceSecretsManager,
},
},
},
@ -90,9 +91,9 @@ func TestRegister(t *testing.T) {
name: "aws",
expPanic: true,
expExists: true,
provider: &esv1beta1.SecretStoreProvider{
AWS: &esv1beta1.AWSProvider{
Service: esv1beta1.AWSServiceSecretsManager,
provider: &SecretStoreProvider{
AWS: &AWSProvider{
Service: AWSServiceSecretsManager,
},
},
},
@ -109,10 +110,10 @@ func TestRegister(t *testing.T) {
}
}
func runTest(t *testing.T, name string, provider *esv1beta1.SecretStoreProvider, expPanic bool) {
func runTest(t *testing.T, name string, provider *SecretStoreProvider, expPanic bool) {
testProvider := &PP{}
secretStore := &esv1beta1.SecretStore{
Spec: esv1beta1.SecretStoreSpec{
secretStore := &SecretStore{
Spec: SecretStoreSpec{
Provider: provider,
},
}
@ -135,19 +136,19 @@ func runTest(t *testing.T, name string, provider *esv1beta1.SecretStoreProvider,
// ForceRegister is used by other tests, we should ensure it works as expected.
func TestForceRegister(t *testing.T) {
testProvider := &PP{}
provider := &esv1beta1.SecretStoreProvider{
AWS: &esv1beta1.AWSProvider{
Service: esv1beta1.AWSServiceParameterStore,
provider := &SecretStoreProvider{
AWS: &AWSProvider{
Service: AWSServiceParameterStore,
},
}
secretStore := &esv1beta1.SecretStore{
Spec: esv1beta1.SecretStoreSpec{
secretStore := &SecretStore{
Spec: SecretStoreSpec{
Provider: provider,
},
}
ForceRegister(testProvider, &esv1beta1.SecretStoreProvider{
AWS: &esv1beta1.AWSProvider{
Service: esv1beta1.AWSServiceParameterStore,
ForceRegister(testProvider, &SecretStoreProvider{
AWS: &AWSProvider{
Service: AWSServiceParameterStore,
},
})
p1, ok := GetProviderByName("aws")
@ -164,10 +165,10 @@ func TestRegisterGCP(t *testing.T) {
assert.False(t, ok, "provider should not be registered")
testProvider := &PP{}
secretStore := &esv1beta1.SecretStore{
Spec: esv1beta1.SecretStoreSpec{
Provider: &esv1beta1.SecretStoreProvider{
GCPSM: &esv1beta1.GCPSMProvider{},
secretStore := &SecretStore{
Spec: SecretStoreSpec{
Provider: &SecretStoreProvider{
GCPSM: &GCPSMProvider{},
},
},
}

View file

@ -0,0 +1,62 @@
/*
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 v1beta1
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)
var _ admission.CustomValidator = &GenericStoreValidator{}
const (
errInvalidStore = "invalid store"
)
type GenericStoreValidator struct{}
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
func (r *GenericStoreValidator) ValidateCreate(ctx context.Context, obj runtime.Object) error {
st, ok := obj.(GenericStore)
if !ok {
return fmt.Errorf(errInvalidStore)
}
return validateStore(st)
}
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
func (r *GenericStoreValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error {
st, ok := newObj.(GenericStore)
if !ok {
return fmt.Errorf(errInvalidStore)
}
return validateStore(st)
}
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
func (r *GenericStoreValidator) ValidateDelete(ctx context.Context, obj runtime.Object) error {
return nil
}
func validateStore(store GenericStore) error {
provider, err := GetProvider(store)
if err != nil {
return err
}
return provider.ValidateStore(store)
}

View file

@ -21,11 +21,13 @@ import (
func (c *SecretStore) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(c).
WithValidator(&GenericStoreValidator{}).
Complete()
}
func (c *ClusterSecretStore) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(c).
WithValidator(&GenericStoreValidator{}).
Complete()
}

View file

@ -422,8 +422,16 @@ func (in *ExternalSecretData) DeepCopy() *ExternalSecretData {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ExternalSecretDataFromRemoteRef) DeepCopyInto(out *ExternalSecretDataFromRemoteRef) {
*out = *in
out.Extract = in.Extract
in.Find.DeepCopyInto(&out.Find)
if in.Extract != nil {
in, out := &in.Extract, &out.Extract
*out = new(ExternalSecretDataRemoteRef)
**out = **in
}
if in.Find != nil {
in, out := &in.Find, &out.Find
*out = new(ExternalSecretFind)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretDataFromRemoteRef.
@ -794,6 +802,21 @@ func (in *GCPWorkloadIdentity) DeepCopy() *GCPWorkloadIdentity {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GenericStoreValidator) DeepCopyInto(out *GenericStoreValidator) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericStoreValidator.
func (in *GenericStoreValidator) DeepCopy() *GenericStoreValidator {
if in == nil {
return nil
}
out := new(GenericStoreValidator)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GitlabAuth) DeepCopyInto(out *GitlabAuth) {
*out = *in

View file

@ -28,12 +28,13 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"github.com/external-secrets/external-secrets/pkg/controllers/crds"
"github.com/external-secrets/external-secrets/pkg/controllers/webhookconfig"
)
var certcontrollerCmd = &cobra.Command{
Use: "certcontroller",
Short: "Controller to manage certificates for external secrets CRDs",
Long: `Controller to manage certificates for external secrets CRDs.
Short: "Controller to manage certificates for external secrets CRDs and ValidatingWebhookConfigs",
Long: `Controller to manage certificates for external secrets CRDs and ValidatingWebhookConfigs.
For more information visit https://external-secrets.io`,
Run: func(cmd *cobra.Command, args []string) {
var lvl zapcore.Level
@ -46,11 +47,12 @@ var certcontrollerCmd = &cobra.Command{
ctrl.SetLogger(logger)
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
MetricsBindAddress: metricsAddr,
Port: 9443,
LeaderElection: enableLeaderElection,
LeaderElectionID: "crd-certs-controller",
Scheme: scheme,
MetricsBindAddress: metricsAddr,
HealthProbeBindAddress: healthzAddr,
Port: 9443,
LeaderElection: enableLeaderElection,
LeaderElectionID: "crd-certs-controller",
ClientDisableCacheFor: []client.Object{
// the client creates a ListWatch for all resource kinds that
// are requested with .Get().
@ -59,31 +61,48 @@ var certcontrollerCmd = &cobra.Command{
// that he owns.
// see #721
&v1.Secret{},
}})
},
})
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}
crds := &crds.Reconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("webhook-certs-updater"),
Scheme: mgr.GetScheme(),
SvcName: serviceName,
SvcNamespace: serviceNamespace,
SecretName: secretName,
SecretNamespace: secretNamespace,
RequeueInterval: crdRequeueInterval,
CrdResources: []string{"externalsecrets.external-secrets.io", "clustersecretstores.external-secrets.io", "secretstores.external-secrets.io"},
CAName: "external-secrets",
CAOrganization: "external-secrets",
RestartOnSecretRefresh: false,
}
if err := crds.SetupWithManager(mgr, controller.Options{
crdctrl := crds.New(mgr.GetClient(), mgr.GetScheme(),
ctrl.Log.WithName("controllers").WithName("webhook-certs-updater"),
crdRequeueInterval, serviceName, serviceNamespace, secretName, secretNamespace, []string{
"externalsecrets.external-secrets.io",
"clustersecretstores.external-secrets.io",
"secretstores.external-secrets.io",
})
if err := crdctrl.SetupWithManager(mgr, controller.Options{
MaxConcurrentReconciles: concurrent,
}); err != nil {
setupLog.Error(err, errCreateController, "controller", "CustomResourceDefinition")
os.Exit(1)
}
whc := webhookconfig.New(mgr.GetClient(), mgr.GetScheme(),
ctrl.Log.WithName("controllers").WithName("webhook-certs-updater"),
serviceName, serviceNamespace,
secretName, secretNamespace, crdRequeueInterval)
if err := whc.SetupWithManager(mgr, controller.Options{
MaxConcurrentReconciles: concurrent,
}); err != nil {
setupLog.Error(err, errCreateController, "controller", "WebhookConfig")
os.Exit(1)
}
err = mgr.AddReadyzCheck("crd-inject", crdctrl.ReadyCheck)
if err != nil {
setupLog.Error(err, "unable to add crd readyz check")
os.Exit(1)
}
err = mgr.AddReadyzCheck("validation-webhook-inject", whc.ReadyCheck)
if err != nil {
setupLog.Error(err, "unable to add webhook readyz check")
os.Exit(1)
}
setupLog.Info("starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "problem running manager")
@ -96,6 +115,7 @@ func init() {
rootCmd.AddCommand(certcontrollerCmd)
certcontrollerCmd.Flags().StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
certcontrollerCmd.Flags().StringVar(&healthzAddr, "healthz-addr", ":8081", "The address the health endpoint binds to.")
certcontrollerCmd.Flags().StringVar(&serviceName, "service-name", "external-secrets-webhook", "Webhook service name")
certcontrollerCmd.Flags().StringVar(&serviceNamespace, "service-namespace", "default", "Webhook service namespace")
certcontrollerCmd.Flags().StringVar(&secretName, "secret-name", "external-secrets-webhook", "Secret to store certs for webhook")

View file

@ -22,11 +22,12 @@ import (
"github.com/spf13/cobra"
"go.uber.org/zap/zapcore"
v1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
// To allow using gcp auth.
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
_ "k8s.io/client-go/plugin/pkg/client/auth"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
@ -44,6 +45,7 @@ var (
dnsName string
certDir string
metricsAddr string
healthzAddr string
controllerClass string
enableLeaderElection bool
concurrent int
@ -64,6 +66,7 @@ func init() {
_ = clientgoscheme.AddToScheme(scheme)
_ = esv1beta1.AddToScheme(scheme)
_ = esv1alpha1.AddToScheme(scheme)
_ = apiextensionsv1.AddToScheme(scheme)
}
var rootCmd = &cobra.Command{

View file

@ -17,6 +17,7 @@ package cmd
import (
"context"
"net/http"
"os"
"os/signal"
"syscall"
@ -65,12 +66,12 @@ var webhookCmd = &cobra.Command{
logger := zap.New(zap.Level(lvl))
ctrl.SetLogger(logger)
setupLog.Info("validating certs")
err = crds.CheckCerts(c, dnsName, time.Now().Add(time.Hour))
err = waitForCerts(c, time.Minute*2)
if err != nil {
setupLog.Error(err, "error checking certs")
setupLog.Error(err, "unable to validate certificates")
os.Exit(1)
}
ctx, cancel := context.WithCancel(context.Background())
go func(c crds.CertInfo, dnsName string, every time.Duration) {
sigs := make(chan os.Signal, 1)
@ -92,10 +93,11 @@ var webhookCmd = &cobra.Command{
}(c, dnsName, certCheckInterval)
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
MetricsBindAddress: metricsAddr,
Port: 9443,
CertDir: certDir,
Scheme: scheme,
MetricsBindAddress: metricsAddr,
HealthProbeBindAddress: healthzAddr,
Port: 9443,
CertDir: certDir,
})
if err != nil {
setupLog.Error(err, "unable to start manager")
@ -125,6 +127,15 @@ var webhookCmd = &cobra.Command{
setupLog.Error(err, errCreateWebhook, "webhook", "ClusterSecretStore-v1alpha1")
os.Exit(1)
}
err = mgr.AddReadyzCheck("certs", func(_ *http.Request) error {
return crds.CheckCerts(c, dnsName, time.Now().Add(time.Hour))
})
if err != nil {
setupLog.Error(err, "unable to add certs readyz check")
os.Exit(1)
}
setupLog.Info("starting manager")
if err := mgr.Start(ctx); err != nil {
setupLog.Error(err, "problem running manager")
@ -133,9 +144,33 @@ var webhookCmd = &cobra.Command{
},
}
// waitForCerts waits until the certificates become ready.
// If they don't become ready within a given time duration
// this function returns an error.
// certs are generated by the certcontroller.
func waitForCerts(c crds.CertInfo, timeout time.Duration) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
for {
setupLog.Info("validating certs")
err := crds.CheckCerts(c, dnsName, time.Now().Add(time.Hour))
if err == nil {
return nil
}
if err != nil {
setupLog.Error(err, "invalid certs. retrying...")
<-time.After(time.Second * 10)
}
if ctx.Err() != nil {
return ctx.Err()
}
}
}
func init() {
rootCmd.AddCommand(webhookCmd)
webhookCmd.Flags().StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
webhookCmd.Flags().StringVar(&healthzAddr, "healthz-addr", ":8081", "The address the health endpoint binds to.")
webhookCmd.Flags().StringVar(&dnsName, "dns-name", "localhost", "DNS name to validate certificates with")
webhookCmd.Flags().StringVar(&certDir, "cert-dir", "/tmp/k8s-webhook-server/serving-certs", "path to check for certs")
webhookCmd.Flags().StringVar(&loglevel, "loglevel", "info", "loglevel to use, one of: debug, info, warn, error, dpanic, panic, fatal")

View file

@ -320,6 +320,8 @@ spec:
Provider data If multiple entries are specified, the Secret keys
are merged in the specified order
items:
maxProperties: 1
minProperties: 1
properties:
extract:
description: Used to extract multiple key/value pairs from one
@ -341,6 +343,8 @@ spec:
type: object
find:
description: Used to find secrets based on tags or regular expressions
maxProperties: 1
minProperties: 1
properties:
name:
description: Finds secrets based on the name.

View file

@ -61,6 +61,12 @@ spec:
- containerPort: {{ .Values.certController.prometheus.service.port }}
protocol: TCP
name: metrics
readinessProbe:
httpGet:
port: 8081
path: /readyz
initialDelaySeconds: 20
periodSeconds: 5
{{- with .Values.certController.extraEnv }}
env:
{{- toYaml . | nindent 12 }}

View file

@ -16,6 +16,31 @@ rules:
- "watch"
- "update"
- "patch"
- apiGroups:
- "admissionregistration.k8s.io"
resources:
- "validatingwebhookconfigurations"
verbs:
- "get"
- "list"
- "watch"
- "update"
- "patch"
- apiGroups:
- ""
resources:
- "endpoints"
verbs:
- "list"
- "get"
- "watch"
- apiGroups:
- ""
resources:
- "events"
verbs:
- "create"
- "patch"
- apiGroups:
- ""
resources:

View file

@ -0,0 +1,41 @@
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: secretstore-validate
labels:
external-secrets.io/component: webhook
webhooks:
- name: "validate.secretstore.external-secrets.io"
rules:
- apiGroups: ["external-secrets.io"]
apiVersions: ["v1beta1"]
operations: ["CREATE", "UPDATE", "DELETE"]
resources: ["secretstores"]
scope: "Namespaced"
clientConfig:
service:
namespace: {{ .Release.Namespace | quote }}
name: {{ include "external-secrets.fullname" . }}-webhook
path: /validate-external-secrets-io-v1beta1-secretstore
# will be set by controller
caBundle: Cg==
admissionReviewVersions: ["v1", "v1beta1"]
sideEffects: None
timeoutSeconds: 5
- name: "validate.clustersecretstore.external-secrets.io"
rules:
- apiGroups: ["external-secrets.io"]
apiVersions: ["v1beta1"]
operations: ["CREATE", "UPDATE", "DELETE"]
resources: ["clustersecretstores"]
scope: "Cluster"
clientConfig:
service:
namespace: {{ .Release.Namespace | quote }}
name: {{ include "external-secrets.fullname" . }}-webhook
path: /validate-external-secrets-io-v1beta1-clustersecretstore
caBundle: Cg== # will be set by controller
admissionReviewVersions: ["v1", "v1beta1"]
sideEffects: None
timeoutSeconds: 5

View file

@ -63,8 +63,9 @@ spec:
protocol: TCP
name: webhook
readinessProbe:
tcpSocket:
port: 9443
httpGet:
port: 8081
path: /readyz
initialDelaySeconds: 20
periodSeconds: 5
{{- with .Values.webhook.extraEnv }}

View file

@ -2,6 +2,7 @@ apiVersion: v1
kind: Secret
metadata:
name: {{ include "external-secrets.fullname" . }}-webhook
namespace: {{ .Release.Namespace | quote }}
labels:
{{- include "external-secrets-webhook.labels" . | nindent 4 }}
external-secrets.io/component : webhook

View file

@ -2,6 +2,7 @@ apiVersion: v1
kind: Service
metadata:
name: {{ include "external-secrets.fullname" . }}-webhook
namespace: {{ .Release.Namespace | quote }}
labels:
{{- include "external-secrets-webhook.labels" . | nindent 4 }}
external-secrets.io/component : webhook

View file

@ -2244,6 +2244,8 @@ spec:
dataFrom:
description: DataFrom is used to fetch all properties from a specific Provider data If multiple entries are specified, the Secret keys are merged in the specified order
items:
maxProperties: 1
minProperties: 1
properties:
extract:
description: Used to extract multiple key/value pairs from one secret
@ -2262,6 +2264,8 @@ spec:
type: object
find:
description: Used to find secrets based on tags or regular expressions
maxProperties: 1
minProperties: 1
properties:
name:
description: Finds secrets based on the name.

View file

@ -15,28 +15,22 @@ package crds
import (
"context"
"encoding/json"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
)
const (
crdGroup = "apiextensions.k8s.io"
crdKind = "CustomResourceDefinition"
crdVersion = "v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
type testCase struct {
crd unstructured.Unstructured
crd2 unstructured.Unstructured
crd *apiextensions.CustomResourceDefinition
crd2 *apiextensions.CustomResourceDefinition
service corev1.Service
secret corev1.Secret
assert func()
@ -50,31 +44,27 @@ var _ = Describe("CRD reconcile", func() {
})
AfterEach(func() {
// To improve later on with proper clean up.
ctx := context.Background()
k8sClient.Delete(ctx, &test.secret)
k8sClient.Delete(ctx, &test.service)
deleteCRD(test.crd)
deleteCRD(test.crd2)
})
// a invalid provider config should be reflected
// in the store status condition
PatchesCRD := func(tc *testCase) {
tc.assert = func() {
Consistently(func() bool {
ss := unstructured.Unstructured{}
ss.SetGroupVersionKind(schema.GroupVersionKind{Kind: crdKind, Version: crdVersion, Group: crdGroup})
Eventually(func() bool {
crd := &apiextensions.CustomResourceDefinition{}
err := k8sClient.Get(context.Background(), types.NamespacedName{
Name: "secretstores.test.io",
}, &ss)
}, crd)
if err != nil {
return false
}
val, ok, err := unstructured.NestedString(ss.Object, "spec", "conversion", "webhook", "clientConfig", "service", "name")
if err != nil || !ok {
return false
}
want, ok, err := unstructured.NestedString(tc.crd.Object, "spec", "conversion", "webhook", "clientConfig", "service", "name")
if err != nil || !ok {
return false
}
return want != val
return crd.Spec.Conversion.Webhook.ClientConfig.Service.Name !=
tc.crd.Spec.Conversion.Webhook.ClientConfig.Service.Name
}).
WithTimeout(time.Second * 10).
WithPolling(time.Second).
@ -87,23 +77,15 @@ var _ = Describe("CRD reconcile", func() {
ignoreNonTargetCRDs := func(tc *testCase) {
tc.assert = func() {
Consistently(func() bool {
ss := unstructured.Unstructured{}
ss.SetGroupVersionKind(schema.GroupVersionKind{Kind: crdKind, Version: crdVersion, Group: crdGroup})
crd := &apiextensions.CustomResourceDefinition{}
err := k8sClient.Get(context.Background(), types.NamespacedName{
Name: "some-other.test.io",
}, &ss)
}, crd)
if err != nil {
return false
}
got, ok, err := unstructured.NestedString(ss.Object, "spec", "conversion", "webhook", "clientConfig", "service", "name")
if !ok || err != nil {
return false
}
want, ok, err := unstructured.NestedString(tc.crd2.Object, "spec", "conversion", "webhook", "clientConfig", "service", "name")
if !ok || err != nil {
return false
}
return got == want
return crd.Spec.Conversion.Webhook.ClientConfig.Service.Name ==
tc.crd2.Spec.Conversion.Webhook.ClientConfig.Service.Name
}).
WithTimeout(time.Second * 3).
WithPolling(time.Millisecond * 500).
@ -116,21 +98,47 @@ var _ = Describe("CRD reconcile", func() {
mut(test)
}
ctx := context.Background()
k8sClient.Create(ctx, &test.secret)
k8sClient.Create(ctx, &test.service)
k8sClient.Create(ctx, &test.crd)
k8sClient.Create(ctx, &test.crd2)
err := k8sClient.Create(ctx, &test.secret)
Expect(err).ToNot(HaveOccurred())
err = k8sClient.Create(ctx, &test.service)
Expect(err).ToNot(HaveOccurred())
err = k8sClient.Create(ctx, test.crd)
Expect(err).ToNot(HaveOccurred())
err = k8sClient.Create(ctx, test.crd2)
Expect(err).ToNot(HaveOccurred())
test.assert()
},
Entry("[namespace] Ignore non Target CRDs", ignoreNonTargetCRDs),
Entry("[namespace] Patch target CRDs", PatchesCRD),
)
})
func makeUnstructuredCRD(plural, group string) unstructured.Unstructured {
crd := apiextensions.CustomResourceDefinition{
func deleteCRD(crd *apiextensions.CustomResourceDefinition) {
err := k8sClient.Delete(context.Background(), crd, client.GracePeriodSeconds(0))
if err != nil && !apierrors.IsNotFound(err) {
Fail("unable to delete crd " + crd.Name)
return
}
Eventually(func() bool {
err := k8sClient.Get(context.Background(), types.NamespacedName{
Name: crd.Name,
}, crd)
if err == nil {
// force delete by removing finalizers
// note: we can not delete a CRD with an invalid caBundle field
cpy := crd.DeepCopy()
controllerutil.RemoveFinalizer(cpy, "customresourcecleanup.apiextensions.k8s.io")
p := client.MergeFrom(crd)
k8sClient.Patch(context.Background(), cpy, p)
return false
}
return apierrors.IsNotFound(err)
}).WithTimeout(time.Second * 5).WithPolling(time.Second).Should(BeTrue())
}
func makeCRD(plural, group string) *apiextensions.CustomResourceDefinition {
return &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: plural + "." + group,
},
@ -160,7 +168,7 @@ func makeUnstructuredCRD(plural, group string) unstructured.Unstructured {
Webhook: &apiextensions.WebhookConversion{
ConversionReviewVersions: []string{"v1"},
ClientConfig: &apiextensions.WebhookClientConfig{
CABundle: []byte("foobar"),
CABundle: []byte(`Cg==`),
Service: &apiextensions.ServiceReference{
Name: "webhook",
Namespace: "default",
@ -170,14 +178,6 @@ func makeUnstructuredCRD(plural, group string) unstructured.Unstructured {
},
},
}
marshal, _ := json.Marshal(crd)
unmarshal := make(map[string]interface{})
json.Unmarshal(marshal, &unmarshal)
u := unstructured.Unstructured{
Object: unmarshal,
}
u.SetGroupVersionKind(schema.GroupVersionKind{Kind: crdKind, Version: "v1", Group: crdGroup})
return u
}
func makeSecret() corev1.Secret {
@ -217,8 +217,8 @@ func makeDefaultTestcase() *testCase {
assert: func() {
// this is a noop by default
},
crd: makeUnstructuredCRD("secretstores", "test.io"),
crd2: makeUnstructuredCRD("some-other", "test.io"),
crd: makeCRD("secretstores", "test.io"),
crd2: makeCRD("some-other", "test.io"),
secret: makeSecret(),
service: makeService(),
}

View file

@ -22,19 +22,20 @@ import (
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"math/big"
"net/http"
"os"
"path/filepath"
"sync"
"time"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
@ -49,31 +50,49 @@ const (
caKeyName = "ca.key"
certValidityDuration = 10 * 365 * 24 * time.Hour
LookaheadInterval = 90 * 24 * time.Hour
)
type WebhookType int
const (
Validating WebhookType = iota
Mutating
CRDConversion
errResNotReady = "resource not ready: %s"
errSubsetsNotReady = "subsets not ready"
errAddressesNotReady = "addresses not ready"
)
type Reconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
recorder record.EventRecorder
SvcName string
SvcNamespace string
SecretName string
SecretNamespace string
CrdResources []string
dnsName string
CAName string
CAOrganization string
RestartOnSecretRefresh bool
RequeueInterval time.Duration
Log logr.Logger
Scheme *runtime.Scheme
recorder record.EventRecorder
SvcName string
SvcNamespace string
SecretName string
SecretNamespace string
CrdResources []string
dnsName string
CAName string
CAOrganization string
RequeueInterval time.Duration
// the controller is ready when all crds are injected
rdyMu *sync.Mutex
readyStatusMap map[string]bool
}
func New(k8sClient client.Client, scheme *runtime.Scheme, logger logr.Logger,
interval time.Duration, svcName, svcNamespace, secretName, secretNamespace string, resources []string) *Reconciler {
return &Reconciler{
Client: k8sClient,
Log: logger,
Scheme: scheme,
SvcName: svcName,
SvcNamespace: svcNamespace,
SecretName: secretName,
SecretNamespace: secretNamespace,
RequeueInterval: interval,
CrdResources: resources,
CAName: "external-secrets",
CAOrganization: "external-secrets",
rdyMu: &sync.Mutex{},
readyStatusMap: map[string]bool{},
}
}
type CertInfo struct {
@ -82,10 +101,6 @@ type CertInfo struct {
KeyName string
CAName string
}
type WebhookInfo struct {
Name string
Type WebhookType
}
func contains(s []string, e string) bool {
for _, a := range s {
@ -95,44 +110,62 @@ func contains(s []string, e string) bool {
}
return false
}
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := r.Log.WithValues("CustomResourceDefinition", req.NamespacedName)
if contains(r.CrdResources, req.NamespacedName.Name) {
err := r.updateCRD(ctx, req)
if err != nil {
log.Error(err, "failed to inject conversion webhook")
r.rdyMu.Lock()
r.readyStatusMap[req.NamespacedName.Name] = false
r.rdyMu.Unlock()
return ctrl.Result{}, err
}
r.rdyMu.Lock()
r.readyStatusMap[req.NamespacedName.Name] = true
r.rdyMu.Unlock()
}
return ctrl.Result{RequeueAfter: r.RequeueInterval}, nil
}
func (r *Reconciler) ConvertToWebhookInfo() []WebhookInfo {
info := make([]WebhookInfo, len(r.CrdResources))
for p, v := range r.CrdResources {
r := WebhookInfo{
Name: v,
Type: CRDConversion,
// ReadyCheck reviews if all webhook configs have been injected into the CRDs
// and if the referenced webhook service is ready.
func (r *Reconciler) ReadyCheck(_ *http.Request) error {
for _, res := range r.CrdResources {
r.rdyMu.Lock()
rdy := r.readyStatusMap[res]
r.rdyMu.Unlock()
if !rdy {
return fmt.Errorf(errResNotReady, res)
}
info[p] = r
}
return info
var eps corev1.Endpoints
err := r.Get(context.TODO(), types.NamespacedName{
Name: r.SvcName,
Namespace: r.SvcNamespace,
}, &eps)
if err != nil {
return err
}
if len(eps.Subsets) == 0 {
return fmt.Errorf(errSubsetsNotReady)
}
if len(eps.Subsets[0].Addresses) == 0 {
return fmt.Errorf(errAddressesNotReady)
}
return nil
}
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
crdGVK := schema.GroupVersionKind{Group: "apiextensions.k8s.io", Version: "v1", Kind: "CustomResourceDefinition"}
res := &unstructured.Unstructured{}
res.SetGroupVersionKind(crdGVK)
r.recorder = mgr.GetEventRecorderFor("custom-resource-definition")
return ctrl.NewControllerManagedBy(mgr).
WithOptions(opts).
For(res).
For(&apiext.CustomResourceDefinition{}).
Complete(r)
}
func (r *Reconciler) updateCRD(ctx context.Context, req ctrl.Request) error {
crdGVK := schema.GroupVersionKind{Group: "apiextensions.k8s.io", Version: "v1", Kind: "CustomResourceDefinition"}
secret := corev1.Secret{}
secretName := types.NamespacedName{
Name: r.SecretName,
@ -142,16 +175,15 @@ func (r *Reconciler) updateCRD(ctx context.Context, req ctrl.Request) error {
if err != nil {
return err
}
updatedResource := &unstructured.Unstructured{}
updatedResource.SetGroupVersionKind(crdGVK)
if err := r.Get(ctx, req.NamespacedName, updatedResource); err != nil {
var updatedResource apiext.CustomResourceDefinition
if err := r.Get(ctx, req.NamespacedName, &updatedResource); err != nil {
return err
}
svc := types.NamespacedName{
Name: r.SvcName,
Namespace: r.SvcNamespace,
}
if err := injectSvcToConversionWebhook(updatedResource, svc); err != nil {
if err := injectService(&updatedResource, svc); err != nil {
return err
}
r.dnsName = fmt.Sprintf("%v.%v.svc", r.SvcName, r.SvcNamespace)
@ -164,45 +196,35 @@ func (r *Reconciler) updateCRD(ctx context.Context, req ctrl.Request) error {
if err != nil {
return err
}
if err := injectCertToConversionWebhook(updatedResource, artifacts.CertPEM); err != nil {
if err := injectCert(&updatedResource, artifacts.CertPEM); err != nil {
return err
}
}
if err := r.Update(ctx, updatedResource); err != nil {
if err := r.Update(ctx, &updatedResource); err != nil {
return err
}
return nil
}
func injectSvcToConversionWebhook(crd *unstructured.Unstructured, svc types.NamespacedName) error {
_, found, err := unstructured.NestedMap(crd.Object, "spec", "conversion", "webhook", "clientConfig")
if err != nil {
return err
}
if !found {
return errors.New("`conversion.webhook.clientConfig` field not found in CustomResourceDefinition")
}
if err := unstructured.SetNestedField(crd.Object, svc.Name, "spec", "conversion", "webhook", "clientConfig", "service", "name"); err != nil {
return err
}
if err := unstructured.SetNestedField(crd.Object, svc.Namespace, "spec", "conversion", "webhook", "clientConfig", "service", "namespace"); err != nil {
return err
func injectService(crd *apiext.CustomResourceDefinition, svc types.NamespacedName) error {
if crd.Spec.Conversion == nil ||
crd.Spec.Conversion.Webhook == nil ||
crd.Spec.Conversion.Webhook.ClientConfig == nil ||
crd.Spec.Conversion.Webhook.ClientConfig.Service == nil {
return fmt.Errorf("unexpected crd conversion webhook config")
}
crd.Spec.Conversion.Webhook.ClientConfig.Service.Namespace = svc.Namespace
crd.Spec.Conversion.Webhook.ClientConfig.Service.Name = svc.Name
return nil
}
func injectCertToConversionWebhook(crd *unstructured.Unstructured, certPem []byte) error {
_, found, err := unstructured.NestedMap(crd.Object, "spec", "conversion", "webhook", "clientConfig")
if err != nil {
return err
func injectCert(crd *apiext.CustomResourceDefinition, certPem []byte) error {
if crd.Spec.Conversion == nil ||
crd.Spec.Conversion.Webhook == nil ||
crd.Spec.Conversion.Webhook.ClientConfig == nil {
return fmt.Errorf("unexpected crd conversion webhook config")
}
if !found {
return errors.New("`conversion.webhook.clientConfig` field not found in CustomResourceDefinition")
}
if err := unstructured.SetNestedField(crd.Object, base64.StdEncoding.EncodeToString(certPem), "spec", "conversion", "webhook", "clientConfig", "caBundle"); err != nil {
return err
}
crd.Spec.Conversion.Webhook.ClientConfig.CABundle = certPem
return nil
}
@ -289,18 +311,12 @@ func (r *Reconciler) refreshCertIfNeeded(secret *corev1.Secret) (bool, error) {
if err := r.refreshCerts(true, secret); err != nil {
return false, err
}
if r.RestartOnSecretRefresh {
os.Exit(0)
}
return true, nil
}
if !r.validServerCert(secret.Data[caCertName], secret.Data[certName], secret.Data[keyName]) {
if err := r.refreshCerts(false, secret); err != nil {
return false, err
}
if r.RestartOnSecretRefresh {
os.Exit(0)
}
return true, nil
}
return true, nil
@ -450,21 +466,23 @@ func (r *Reconciler) writeSecret(cert, key []byte, caArtifacts *KeyPairArtifacts
return r.Update(context.Background(), secret)
}
// CheckCerts verifies that certificates exist in a given fs location
// and if they're valid.
func CheckCerts(c CertInfo, dnsName string, at time.Time) error {
certFile := c.CertDir + "/" + c.CertName
certFile := filepath.Join(c.CertDir, c.CertName)
_, err := os.Stat(certFile)
if err != nil {
return err
}
ca, err := os.ReadFile(c.CertDir + "/" + c.CAName)
ca, err := os.ReadFile(filepath.Join(c.CertDir, c.CAName))
if err != nil {
return err
}
cert, err := os.ReadFile(c.CertDir + "/" + c.CertName)
cert, err := os.ReadFile(filepath.Join(c.CertDir, c.CertName))
if err != nil {
return err
}
key, err := os.ReadFile(c.CertDir + "/" + c.KeyName)
key, err := os.ReadFile(filepath.Join(c.CertDir, c.KeyName))
if err != nil {
return err
}

View file

@ -18,7 +18,6 @@ import (
"context"
"crypto/rsa"
"crypto/x509"
"encoding/json"
"os"
"testing"
"time"
@ -26,15 +25,12 @@ import (
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
client "sigs.k8s.io/controller-runtime/pkg/client/fake"
)
const (
setupError = "Could not setup test"
errorSearchingField = "Error when searching for field"
failedCreateCaCerts = "could not create ca certificates:%v"
failedCreateServerCerts = "could not create server certificates:%v"
invalidCerts = "generated certificates are invalid:%v,%v"
@ -92,21 +88,6 @@ func newCRD() apiextensionsv1.CustomResourceDefinition {
},
}
}
func TestConvertToWebhookInfo(t *testing.T) {
rec := newReconciler()
info := rec.ConvertToWebhookInfo()
if len(info) != 3 {
t.Errorf("Convert to WebhookInfo failed. Total resources:%d", len(info))
}
for _, v := range info {
if v.Type != CRDConversion {
t.Errorf("Convert to WebhookInfo failed. wrong type:%v", v.Type)
}
if v.Name != "one" && v.Name != "two" && v.Name != "three" {
t.Errorf("Convert to WebhookInfo failed. wrong name:%v", v.Name)
}
}
}
func TestUpdateCRD(t *testing.T) {
rec := newReconciler()
@ -123,51 +104,26 @@ func TestUpdateCRD(t *testing.T) {
}
err := rec.updateCRD(ctx, req)
if err != nil {
t.Errorf("Failed updating CRD:%v", err)
t.Errorf("Failed updating CRD: %v", err)
}
}
func TestInjectSvcToConversionWebhook(t *testing.T) {
svc := newService()
crd := newCRD()
crdunmarshalled := make(map[string]interface{})
crdJSON, err := json.Marshal(crd)
if err != nil {
t.Fatal(setupError)
}
err = json.Unmarshal(crdJSON, &crdunmarshalled)
if err != nil {
t.Fatal(setupError)
}
u := unstructured.Unstructured{
Object: crdunmarshalled,
}
name := types.NamespacedName{
Name: svc.Name,
Namespace: svc.Namespace,
}
err = injectSvcToConversionWebhook(&u, name)
err := injectService(&crd, name)
if err != nil {
t.Errorf("Failed: error when injecting: %v", err)
}
val, found, err := unstructured.NestedString(u.Object, "spec", "conversion", "webhook", "clientConfig", "service", "name")
if err != nil {
t.Error(errorSearchingField)
}
if !found {
t.Error("fieldNotFound")
}
val := crd.Spec.Conversion.Webhook.ClientConfig.Service.Name
if val != "foo" {
t.Errorf("Wrong service name injected: %v", val)
}
val, found, err = unstructured.NestedString(u.Object, "spec", "conversion", "webhook", "clientConfig", "service", "namespace")
if err != nil {
t.Error(errorSearchingField)
}
if !found {
t.Error("fieldNotFound")
}
val = crd.Spec.Conversion.Webhook.ClientConfig.Service.Namespace
if val != "default" {
t.Errorf("Wrong service namespace injected: %v", val)
}
@ -176,31 +132,12 @@ func TestInjectSvcToConversionWebhook(t *testing.T) {
func TestInjectCertToConversionWebhook(t *testing.T) {
certPEM := []byte("foobar")
crd := newCRD()
crdunmarshalled := make(map[string]interface{})
crdJSON, err := json.Marshal(crd)
if err != nil {
t.Fatal(setupError)
}
err = json.Unmarshal(crdJSON, &crdunmarshalled)
if err != nil {
t.Fatal(setupError)
}
u := unstructured.Unstructured{
Object: crdunmarshalled,
}
err = injectCertToConversionWebhook(&u, certPEM)
err := injectCert(&crd, certPEM)
if err != nil {
t.Errorf("Failed: error when injecting: %v", err)
}
val, found, err := unstructured.NestedString(u.Object, "spec", "conversion", "webhook", "clientConfig", "caBundle")
if err != nil {
t.Error(errorSearchingField)
}
if !found {
t.Error("fieldNotFound")
}
if val != "Zm9vYmFy" {
t.Errorf("Wrong certificate name injected: %v", val)
if string(crd.Spec.Conversion.Webhook.ClientConfig.CABundle) != "foobar" {
t.Errorf("Wrong certificate name injected: %v", string(crd.Spec.Conversion.Webhook.ClientConfig.CABundle))
}
}
func TestPopulateSecret(t *testing.T) {

View file

@ -18,6 +18,7 @@ import (
"context"
"path/filepath"
"testing"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
@ -73,19 +74,11 @@ var _ = BeforeSuite(func() {
Expect(err).ToNot(HaveOccurred())
Expect(k8sClient).ToNot(BeNil())
err = (&Reconciler{
Client: k8sClient,
Scheme: k8sManager.GetScheme(),
Log: ctrl.Log.WithName("controllers").WithName("CustomResourceDefinition"),
SvcName: "foo",
SvcNamespace: "default",
SecretName: "foo",
SecretNamespace: "default",
CrdResources: []string{"externalsecrets.test.io", "secretstores.test.io", "clustersecretstores.test.io"},
CAName: "external-secrets",
CAOrganization: "external-secrets",
RestartOnSecretRefresh: false,
}).SetupWithManager(k8sManager, controller.Options{})
rec := New(k8sClient, k8sManager.GetScheme(), log, time.Second*1,
"foo", "default", "foo", "default", []string{
"secretstores.test.io",
})
rec.SetupWithManager(k8sManager, controller.Options{})
Expect(err).ToNot(HaveOccurred())
go func() {

View file

@ -36,11 +36,9 @@ import (
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/controllers/secretstore"
"github.com/external-secrets/external-secrets/pkg/provider"
// Loading registered providers.
_ "github.com/external-secrets/external-secrets/pkg/provider/register"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
"github.com/external-secrets/external-secrets/pkg/utils"
)
@ -135,7 +133,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
return ctrl.Result{}, nil
}
storeProvider, err := schema.GetProvider(store)
storeProvider, err := esv1beta1.GetProvider(store)
if err != nil {
log.Error(err, errStoreProvider)
syncCallsError.With(syncCallsMetricLabels).Inc()
@ -393,19 +391,19 @@ func (r *Reconciler) getStore(ctx context.Context, externalSecret *esv1beta1.Ext
}
// getProviderSecretData returns the provider's secret data with the provided ExternalSecret.
func (r *Reconciler) getProviderSecretData(ctx context.Context, providerClient provider.SecretsClient, externalSecret *esv1beta1.ExternalSecret) (map[string][]byte, error) {
func (r *Reconciler) getProviderSecretData(ctx context.Context, providerClient esv1beta1.SecretsClient, externalSecret *esv1beta1.ExternalSecret) (map[string][]byte, error) {
providerData := make(map[string][]byte)
for _, remoteRef := range externalSecret.Spec.DataFrom {
var secretMap map[string][]byte
var err error
if len(remoteRef.Find.Tags) > 0 || remoteRef.Find.Name != nil {
secretMap, err = providerClient.GetAllSecrets(ctx, remoteRef.Find)
if remoteRef.Find != nil {
secretMap, err = providerClient.GetAllSecrets(ctx, *remoteRef.Find)
if err != nil {
return nil, fmt.Errorf(errGetSecretKey, remoteRef.Extract.Key, externalSecret.Name, err)
}
} else if remoteRef.Extract.Key != "" {
secretMap, err = providerClient.GetSecretMap(ctx, remoteRef.Extract)
} else if remoteRef.Extract != nil {
secretMap, err = providerClient.GetSecretMap(ctx, *remoteRef.Extract)
if err != nil {
return nil, fmt.Errorf(errGetSecretKey, remoteRef.Extract.Key, externalSecret.Name, err)
}

View file

@ -30,8 +30,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/provider"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
"github.com/external-secrets/external-secrets/pkg/provider/testing/fake"
)
@ -187,7 +185,9 @@ var _ = Describe("ExternalSecret controller", func() {
}
return true
},
checkExternalSecret: func(es *esv1beta1.ExternalSecret) {},
checkExternalSecret: func(es *esv1beta1.ExternalSecret) {
// noop by default
},
secretStore: &esv1beta1.SecretStore{
ObjectMeta: metav1.ObjectMeta{
Name: ExternalSecretStore,
@ -540,7 +540,7 @@ var _ = Describe("ExternalSecret controller", func() {
}
tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
{
Extract: esv1beta1.ExternalSecretDataRemoteRef{
Extract: &esv1beta1.ExternalSecretDataRemoteRef{
Key: "datamap",
},
},
@ -684,7 +684,7 @@ var _ = Describe("ExternalSecret controller", func() {
tc.externalSecret.Spec.Data = []esv1beta1.ExternalSecretData{}
tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
{
Extract: esv1beta1.ExternalSecretDataRemoteRef{
Extract: &esv1beta1.ExternalSecretDataRemoteRef{
Key: remoteKey,
},
},
@ -726,7 +726,7 @@ var _ = Describe("ExternalSecret controller", func() {
tc.externalSecret.Spec.Data = []esv1beta1.ExternalSecretData{}
tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
{
Extract: esv1beta1.ExternalSecretDataRemoteRef{
Extract: &esv1beta1.ExternalSecretDataRemoteRef{
Key: remoteKey,
},
},
@ -791,7 +791,7 @@ var _ = Describe("ExternalSecret controller", func() {
tc.externalSecret.Spec.Data = nil
tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
{
Extract: esv1beta1.ExternalSecretDataRemoteRef{
Extract: &esv1beta1.ExternalSecretDataRemoteRef{
Key: remoteKey,
},
},
@ -813,7 +813,7 @@ var _ = Describe("ExternalSecret controller", func() {
tc.externalSecret.Spec.Data = nil
tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
{
Find: esv1beta1.ExternalSecretFind{
Find: &esv1beta1.ExternalSecretFind{
Name: &esv1beta1.FindName{
RegExp: "foobar",
},
@ -844,7 +844,7 @@ var _ = Describe("ExternalSecret controller", func() {
tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
{
Extract: esv1beta1.ExternalSecretDataRemoteRef{
Extract: &esv1beta1.ExternalSecretDataRemoteRef{
Key: remoteKey,
},
},
@ -925,7 +925,7 @@ var _ = Describe("ExternalSecret controller", func() {
// a SecretSyncedError status condition must be set
storeConstructErrCondition := func(tc *testCase) {
fakeProvider.WithNew(func(context.Context, esv1beta1.GenericStore, client.Client,
string) (provider.SecretsClient, error) {
string) (esv1beta1.SecretsClient, error) {
return nil, fmt.Errorf("artificial constructor error")
})
tc.checkCondition = func(es *esv1beta1.ExternalSecret) bool {
@ -1392,7 +1392,7 @@ func externalSecretConditionShouldBe(name, ns string, ct esv1beta1.ExternalSecre
func init() {
fakeProvider = fake.New()
schema.ForceRegister(fakeProvider, &esv1beta1.SecretStoreProvider{
esv1beta1.ForceRegister(fakeProvider, &esv1beta1.SecretStoreProvider{
AWS: &esv1beta1.AWSProvider{
Service: esv1beta1.AWSServiceSecretsManager,
},

View file

@ -25,7 +25,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
)
const (
@ -77,7 +76,7 @@ func reconcile(ctx context.Context, req ctrl.Request, ss esapi.GenericStore, cl
// if it fails sets a condition and writes events.
func validateStore(ctx context.Context, namespace string, store esapi.GenericStore,
client client.Client, recorder record.EventRecorder) error {
storeProvider, err := schema.GetProvider(store)
storeProvider, err := esapi.GetProvider(store)
if err != nil {
cond := NewSecretStoreCondition(esapi.SecretStoreReady, v1.ConditionFalse, esapi.ReasonInvalidStore, errUnableGetProvider)
SetExternalSecretCondition(store, *cond)

View file

@ -0,0 +1,100 @@
/*
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 webhookconfig
import (
"context"
"path/filepath"
"testing"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
)
var cfg *rest.Config
var k8sClient client.Client
var testEnv *envtest.Environment
var cancel context.CancelFunc
var reconciler *Reconciler
const (
ctrlSvcName = "foo"
ctrlSvcNamespace = "default"
ctrlSecretName = "foo"
ctrlSecretNamespace = "default"
)
func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Controller Suite")
}
var _ = BeforeSuite(func() {
log := zap.New(zap.WriteTo(GinkgoWriter))
logf.SetLogger(log)
By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "deploy", "crds")},
}
var ctx context.Context
ctx, cancel = context.WithCancel(context.Background())
var err error
cfg, err = testEnv.Start()
Expect(err).ToNot(HaveOccurred())
Expect(cfg).ToNot(BeNil())
err = esapi.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
Scheme: scheme.Scheme,
MetricsBindAddress: "0", // avoid port collision when testing
})
Expect(err).ToNot(HaveOccurred())
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).ToNot(HaveOccurred())
Expect(k8sClient).ToNot(BeNil())
reconciler = New(k8sClient, k8sManager.GetScheme(), ctrl.Log, ctrlSvcName, ctrlSvcNamespace, ctrlSecretName, ctrlSecretNamespace, time.Second)
reconciler.SetupWithManager(k8sManager, controller.Options{})
Expect(err).ToNot(HaveOccurred())
go func() {
defer GinkgoRecover()
Expect(k8sManager.Start(ctx)).ToNot(HaveOccurred())
}()
})
var _ = AfterSuite(func() {
By("tearing down the test environment")
cancel() // stop manager
err := testEnv.Stop()
Expect(err).ToNot(HaveOccurred())
})

View file

@ -0,0 +1,186 @@
/*
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 webhookconfig
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/go-logr/logr"
admissionregistration "k8s.io/api/admissionregistration/v1"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
)
type Reconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
recorder record.EventRecorder
RequeueDuration time.Duration
SvcName string
SvcNamespace string
SecretName string
SecretNamespace string
rdyMu *sync.Mutex
ready bool
}
func New(k8sClient client.Client, scheme *runtime.Scheme,
log logr.Logger, svcName, svcNamespace, secretName, secretNamespace string,
requeueInterval time.Duration) *Reconciler {
return &Reconciler{
Client: k8sClient,
Scheme: scheme,
Log: log,
RequeueDuration: requeueInterval,
SvcName: svcName,
SvcNamespace: svcNamespace,
SecretName: secretName,
SecretNamespace: secretNamespace,
rdyMu: &sync.Mutex{},
ready: false,
}
}
const (
wellKnownLabelKey = "external-secrets.io/component"
wellKnownLabelValue = "webhook"
ReasonUpdateFailed = "UpdateFailed"
errWebhookNotReady = "webhook not ready"
errSubsetsNotReady = "subsets not ready"
errAddressesNotReady = "addresses not ready"
errCACertNotReady = "ca cert not yet ready"
caCertName = "ca.crt"
)
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := r.Log.WithValues("Webhookconfig", req.NamespacedName)
var cfg admissionregistration.ValidatingWebhookConfiguration
err := r.Get(ctx, req.NamespacedName, &cfg)
if apierrors.IsNotFound(err) {
return ctrl.Result{}, nil
} else if err != nil {
log.Error(err, "unable to get Webhookconfig")
return ctrl.Result{}, err
}
if cfg.Labels[wellKnownLabelKey] != wellKnownLabelValue {
log.Info("ignoring webhook due to missing labels", wellKnownLabelKey, wellKnownLabelValue)
return ctrl.Result{}, nil
}
log.Info("updating webhook config")
err = r.updateConfig(ctx, &cfg)
if err != nil {
log.Error(err, "could not update webhook config")
r.recorder.Eventf(&cfg, v1.EventTypeWarning, ReasonUpdateFailed, err.Error())
return ctrl.Result{
RequeueAfter: time.Minute,
}, err
}
log.Info("updated webhook config")
// right now we only have one single
// webhook config we care about
r.rdyMu.Lock()
defer r.rdyMu.Unlock()
r.ready = true
return ctrl.Result{
RequeueAfter: r.RequeueDuration,
}, nil
}
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
r.recorder = mgr.GetEventRecorderFor("validating-webhook-configuration")
return ctrl.NewControllerManagedBy(mgr).
WithOptions(opts).
For(&admissionregistration.ValidatingWebhookConfiguration{}).
Complete(r)
}
func (r *Reconciler) ReadyCheck(_ *http.Request) error {
r.rdyMu.Lock()
defer r.rdyMu.Unlock()
if !r.ready {
return fmt.Errorf(errWebhookNotReady)
}
var eps v1.Endpoints
err := r.Get(context.TODO(), types.NamespacedName{
Name: r.SvcName,
Namespace: r.SvcNamespace,
}, &eps)
if err != nil {
return err
}
if len(eps.Subsets) == 0 {
return fmt.Errorf(errSubsetsNotReady)
}
if len(eps.Subsets[0].Addresses) == 0 {
return fmt.Errorf(errAddressesNotReady)
}
return nil
}
// reads the ca cert and updates the webhook config.
func (r *Reconciler) updateConfig(ctx context.Context, cfg *admissionregistration.ValidatingWebhookConfiguration) error {
secret := v1.Secret{}
secretName := types.NamespacedName{
Name: r.SecretName,
Namespace: r.SecretNamespace,
}
err := r.Get(context.Background(), secretName, &secret)
if err != nil {
return err
}
crt, ok := secret.Data[caCertName]
if !ok {
return fmt.Errorf(errCACertNotReady)
}
if err := r.inject(cfg, r.SvcName, r.SvcNamespace, crt); err != nil {
return err
}
return r.Update(ctx, cfg)
}
func (r *Reconciler) inject(cfg *admissionregistration.ValidatingWebhookConfiguration, svcName, svcNamespace string, certData []byte) error {
r.Log.Info("injecting ca certificate and service names", "cacrt", base64.StdEncoding.EncodeToString(certData), "name", cfg.Name)
for idx, w := range cfg.Webhooks {
if !strings.HasSuffix(w.Name, "external-secrets.io") {
r.Log.Info("skipping webhook", "name", cfg.Name, "webhook-name", w.Name)
continue
}
// we just patch the relevant fields
cfg.Webhooks[idx].ClientConfig.Service.Name = svcName
cfg.Webhooks[idx].ClientConfig.Service.Namespace = svcNamespace
cfg.Webhooks[idx].ClientConfig.CABundle = certData
}
return nil
}

View file

@ -0,0 +1,324 @@
/*
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 webhookconfig
import (
"bytes"
"context"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
admissionregistration "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/pointer"
)
const defaultCACert = `-----BEGIN CERTIFICATE-----
MIIDRjCCAi6gAwIBAgIBADANBgkqhkiG9w0BAQsFADA2MRkwFwYDVQQKExBleHRl
cm5hbC1zZWNyZXRzMRkwFwYDVQQDExBleHRlcm5hbC1zZWNyZXRzMB4XDTIyMDIx
NzEwMDYxMFoXDTMyMDIxNTExMDYxMFowNjEZMBcGA1UEChMQZXh0ZXJuYWwtc2Vj
cmV0czEZMBcGA1UEAxMQZXh0ZXJuYWwtc2VjcmV0czCCASIwDQYJKoZIhvcNAQEB
BQADggEPADCCAQoCggEBAKSINgqU2dBdX8JpPjRHWSdpxuoltGl6xXmQHOhbTXAt
/STDu7oi6eiFgepQ2QHuWLGwZgbbYnEhtLvw4dUwPcLyv6WIdeiUSA4pdFxL7asc
WV4tjiRkRTJVrixJTxXpry/CsPqXBlvnu1YGESkrLOYCmA2xnDH8voEBbwYvXXB9
3g5rOJncSh/7g+H55ZFFyWrIPyDUnfwE3CREjZXpsagFhRYpkuRlXTnU6t0OTEEh
qLHlZ+ebUzL8NaegEgEHD32PrQPXpls1yurIrsA+I6McWkXGykykYHVK+1a1pL1g
e+PBkegtwtX+EmB2ux7PVVeB4TTYqzCKbnObW4mJLZkCAwEAAaNfMF0wDgYDVR0P
AQH/BAQDAgKkMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHgSu/Im2gyu4TU0
AWrMSFbtoVokMBsGA1UdEQQUMBKCEGV4dGVybmFsLXNlY3JldHMwDQYJKoZIhvcN
AQELBQADggEBAJU88jCcPsAHN8DKLu+QMCoKYbeftX4gXxyoijGSde2w2O8NPtMP
awu4Y5x3LNTwyIIxXi78UD0RI53GbUgHvS+X9v6CC2IZMS65xqKR+EsjzEh7Ldbm
vZoF4ZDnfb2s5SK6MeYf67BE7XWpGfbHmjt6h80xsYjL6ovcik+dlu/AixMyLslS
tDbMybAR8kR0zdQLYcZq7XEX5QsOO8qBn5rTfD6MiYik8ZrP7FqUMHyVpHiBuNio
krnSOvynvuA9mlf2F2727dMt2Ij9uER+9QnhWBQex1h8CwALmm2k9G5Gt+RjB8oe
lNjvmHAXUfOE/cbD7EP++X17kWt41FjmePc=
-----END CERTIFICATE-----
`
type testCase struct {
vwc *admissionregistration.ValidatingWebhookConfiguration
service *corev1.Service
endpoints *corev1.Endpoints
secret *corev1.Secret
assert func()
}
var _ = Describe("ValidatingWebhookConfig reconcile", Ordered, func() {
var test *testCase
BeforeEach(func() {
test = makeDefaultTestcase()
})
AfterEach(func() {
ctx := context.Background()
k8sClient.Delete(ctx, test.vwc)
k8sClient.Delete(ctx, test.secret)
k8sClient.Delete(ctx, test.service)
k8sClient.Delete(ctx, test.endpoints)
})
// Should patch VWC
PatchAndReady := func(tc *testCase) {
tc.endpoints.Subsets = nil
// endpoints become ready in a moment
go func() {
<-time.After(time.Second * 4)
eps := makeEndpoints()
err := k8sClient.Update(context.Background(), eps)
Expect(err).ToNot(HaveOccurred())
}()
tc.assert = func() {
Eventually(func() bool {
// the controller should become ready at some point!
err := reconciler.ReadyCheck(nil)
return err == nil
}).
WithTimeout(time.Second * 10).
WithPolling(time.Second).
Should(BeTrue())
Eventually(func() bool {
var vwc admissionregistration.ValidatingWebhookConfiguration
err := k8sClient.Get(context.Background(), types.NamespacedName{
Name: tc.vwc.Name,
}, &vwc)
if err != nil {
return false
}
for _, wc := range vwc.Webhooks {
if !bytes.Equal(wc.ClientConfig.CABundle, []byte(defaultCACert)) {
return false
}
if wc.ClientConfig.Service == nil {
return false
}
if wc.ClientConfig.Service.Name != ctrlSvcName {
return false
}
if wc.ClientConfig.Service.Namespace != ctrlSvcNamespace {
return false
}
}
return true
}).
WithTimeout(time.Second * 10).
WithPolling(time.Second).
Should(BeTrue())
}
}
IgnoreNoMatch := func(tc *testCase) {
delete(tc.vwc.ObjectMeta.Labels, wellKnownLabelKey)
tc.assert = func() {
Consistently(func() bool {
var vwc admissionregistration.ValidatingWebhookConfiguration
err := k8sClient.Get(context.Background(), types.NamespacedName{
Name: tc.vwc.Name,
}, &vwc)
if err != nil {
return false
}
for _, wc := range vwc.Webhooks {
if bytes.Equal(wc.ClientConfig.CABundle, []byte(defaultCACert)) {
return false
}
if wc.ClientConfig.Service.Name == ctrlSvcName {
return false
}
if wc.ClientConfig.Service.Namespace == ctrlSvcNamespace {
return false
}
}
return true
}).
WithTimeout(time.Second * 10).
WithPolling(time.Second).
Should(BeTrue())
}
}
// Should patch and update VWC after requeue duration has passed
PatchAndUpdate := func(tc *testCase) {
foobar := "new value"
// ca cert will change after some time
go func() {
<-time.After(time.Second * 4)
sec := makeSecret()
sec.Data[caCertName] = []byte(foobar)
err := k8sClient.Update(context.Background(), sec)
Expect(err).ToNot(HaveOccurred())
}()
tc.assert = func() {
Eventually(func() bool {
var vwc admissionregistration.ValidatingWebhookConfiguration
err := k8sClient.Get(context.Background(), types.NamespacedName{
Name: tc.vwc.Name,
}, &vwc)
if err != nil {
return false
}
for _, wc := range vwc.Webhooks {
if !bytes.Equal(wc.ClientConfig.CABundle, []byte(foobar)) {
return false
}
if wc.ClientConfig.Service == nil {
return false
}
if wc.ClientConfig.Service.Name != ctrlSvcName {
return false
}
if wc.ClientConfig.Service.Namespace != ctrlSvcNamespace {
return false
}
}
return true
}).
WithTimeout(time.Second * 10).
WithPolling(time.Second).
Should(BeTrue())
}
}
DescribeTable("Controller Reconcile logic", func(muts ...func(tc *testCase)) {
for _, mut := range muts {
mut(test)
}
ctx := context.Background()
err := k8sClient.Create(ctx, test.vwc)
Expect(err).ToNot(HaveOccurred())
err = k8sClient.Create(ctx, test.secret)
Expect(err).ToNot(HaveOccurred())
err = k8sClient.Create(ctx, test.service)
Expect(err).ToNot(HaveOccurred())
err = k8sClient.Create(ctx, test.endpoints)
Expect(err).ToNot(HaveOccurred())
test.assert()
},
Entry("should patch matching webhook configs", PatchAndReady),
Entry("should update vwc with new ca cert after requeue duration", PatchAndUpdate),
Entry("should ignore when vwc labels are missing", IgnoreNoMatch),
)
})
func makeValidatingWebhookConfig() *admissionregistration.ValidatingWebhookConfiguration {
return &admissionregistration.ValidatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: "name-shouldnt-matter",
Labels: map[string]string{
wellKnownLabelKey: wellKnownLabelValue,
},
},
Webhooks: []admissionregistration.ValidatingWebhook{
{
Name: "secretstores.external-secrets.io",
SideEffects: (*admissionregistration.SideEffectClass)(pointer.StringPtr(string(admissionregistration.SideEffectClassNone))),
AdmissionReviewVersions: []string{"v1"},
ClientConfig: admissionregistration.WebhookClientConfig{
CABundle: []byte("Cg=="),
Service: &admissionregistration.ServiceReference{
Name: "noop",
Namespace: "noop",
Path: pointer.StringPtr("/validate-secretstore"),
},
},
},
{
Name: "clustersecretstores.external-secrets.io",
SideEffects: (*admissionregistration.SideEffectClass)(pointer.StringPtr(string(admissionregistration.SideEffectClassNone))),
AdmissionReviewVersions: []string{"v1"},
ClientConfig: admissionregistration.WebhookClientConfig{
CABundle: []byte("Cg=="),
Service: &admissionregistration.ServiceReference{
Name: "noop",
Namespace: "noop",
Path: pointer.StringPtr("/validate-clustersecretstore"),
},
},
},
},
}
}
func makeSecret() *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: ctrlSecretName,
Namespace: ctrlSecretNamespace,
},
Data: map[string][]byte{
caCertName: []byte(defaultCACert),
},
}
}
func makeService() *corev1.Service {
return &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: ctrlSvcName,
Namespace: ctrlSvcNamespace,
},
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: "http",
Port: 80,
},
},
},
}
}
func makeEndpoints() *corev1.Endpoints {
return &corev1.Endpoints{
ObjectMeta: metav1.ObjectMeta{
Name: ctrlSvcName,
Namespace: ctrlSvcNamespace,
},
Subsets: []corev1.EndpointSubset{
{
Addresses: []corev1.EndpointAddress{
{
IP: "1.2.3.4",
},
},
},
},
}
}
func makeDefaultTestcase() *testCase {
return &testCase{
assert: func() {
// this is a noop by default
},
vwc: makeValidatingWebhookConfig(),
secret: makeSecret(),
service: makeService(),
endpoints: makeEndpoints(),
}
}

View file

@ -24,8 +24,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/provider"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
"github.com/external-secrets/external-secrets/pkg/utils"
)
@ -56,17 +54,21 @@ type akeylessVaultInterface interface {
}
func init() {
schema.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
Akeyless: &esv1beta1.AkeylessProvider{},
})
}
// NewClient constructs a new secrets client based on the provided store.
func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
return newClient(ctx, store, kube, namespace)
}
func newClient(_ context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
func (p *Provider) ValidateStore(store esv1beta1.GenericStore) error {
return nil
}
func newClient(_ context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
akl := &akeylessBase{
kube: kube,
store: store,

View file

@ -26,9 +26,7 @@ import (
kclient "sigs.k8s.io/controller-runtime/pkg/client"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/provider"
"github.com/external-secrets/external-secrets/pkg/provider/aws/util"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
"github.com/external-secrets/external-secrets/pkg/utils"
)
@ -165,7 +163,7 @@ func (kms *KeyManagementService) GetSecretMap(ctx context.Context, ref esv1beta1
}
// NewClient constructs a new secrets client based on the provided store.
func (kms *KeyManagementService) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
func (kms *KeyManagementService) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
storeSpec := store.GetSpec()
alibabaSpec := storeSpec.Provider.Alibaba
iStore := &Client{
@ -196,8 +194,12 @@ func (kms *KeyManagementService) Validate() error {
return nil
}
func (kms *KeyManagementService) ValidateStore(store esv1beta1.GenericStore) error {
return nil
}
func init() {
schema.Register(&KeyManagementService{}, &esv1beta1.SecretStoreProvider{
esv1beta1.Register(&KeyManagementService{}, &esv1beta1.SecretStoreProvider{
Alibaba: &esv1beta1.AlibabaProvider{},
})
}

View file

@ -18,15 +18,15 @@ import (
"context"
"fmt"
"github.com/aws/aws-sdk-go/aws/endpoints"
"sigs.k8s.io/controller-runtime/pkg/client"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/provider"
awsauth "github.com/external-secrets/external-secrets/pkg/provider/aws/auth"
"github.com/external-secrets/external-secrets/pkg/provider/aws/parameterstore"
"github.com/external-secrets/external-secrets/pkg/provider/aws/secretsmanager"
"github.com/external-secrets/external-secrets/pkg/provider/aws/util"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
"github.com/external-secrets/external-secrets/pkg/utils"
)
// Provider satisfies the provider interface.
@ -35,14 +35,62 @@ type Provider struct{}
const (
errUnableCreateSession = "unable to create session: %w"
errUnknownProviderService = "unknown AWS Provider Service: %s"
errRegionNotFound = "region not found: %s"
)
// NewClient constructs a new secrets client based on the provided store.
func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
return newClient(ctx, store, kube, namespace, awsauth.DefaultSTSProvider)
}
func newClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string, assumeRoler awsauth.STSProvider) (provider.SecretsClient, error) {
func (p *Provider) ValidateStore(store esv1beta1.GenericStore) error {
prov, err := util.GetAWSProvider(store)
if err != nil {
return err
}
err = validateRegion(prov)
if err != nil {
return err
}
// case: static credentials
if prov.Auth.SecretRef != nil {
if err := utils.ValidateSecretSelector(store, prov.Auth.SecretRef.AccessKeyID); err != nil {
return fmt.Errorf("invalid Auth.SecretRef.AccessKeyID: %w", err)
}
if err := utils.ValidateSecretSelector(store, prov.Auth.SecretRef.SecretAccessKey); err != nil {
return fmt.Errorf("invalid Auth.SecretRef.SecretAccessKey: %w", err)
}
}
// case: jwt credentials
if prov.Auth.JWTAuth != nil && prov.Auth.JWTAuth.ServiceAccountRef != nil {
if err := utils.ValidateServiceAccountSelector(store, *prov.Auth.JWTAuth.ServiceAccountRef); err != nil {
return fmt.Errorf("invalid Auth.JWT.ServiceAccountRef: %w", err)
}
}
return nil
}
func validateRegion(prov *esv1beta1.AWSProvider) error {
resolver := endpoints.DefaultResolver()
partitions := resolver.(endpoints.EnumPartitions).Partitions()
found := false
for _, p := range partitions {
for id := range p.Regions() {
if id == prov.Region {
found = true
}
}
}
if !found {
return fmt.Errorf(errRegionNotFound, prov.Region)
}
return nil
}
func newClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string, assumeRoler awsauth.STSProvider) (esv1beta1.SecretsClient, error) {
prov, err := util.GetAWSProvider(store)
if err != nil {
return nil, err
@ -63,7 +111,7 @@ func newClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Cl
}
func init() {
schema.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
AWS: &esv1beta1.AWSProvider{},
})
}

View file

@ -21,6 +21,8 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/stretchr/testify/assert"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
@ -146,3 +148,198 @@ func TestProvider(t *testing.T) {
})
}
}
const validRegion = "eu-central-1"
func TestValidateStore(t *testing.T) {
type args struct {
store esv1beta1.GenericStore
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "invalid region",
wantErr: true,
args: args{
store: &esv1beta1.SecretStore{
Spec: esv1beta1.SecretStoreSpec{
Provider: &esv1beta1.SecretStoreProvider{
AWS: &esv1beta1.AWSProvider{
Region: "noop.",
},
},
},
},
},
},
{
name: "valid region",
args: args{
store: &esv1beta1.SecretStore{
Spec: esv1beta1.SecretStoreSpec{
Provider: &esv1beta1.SecretStoreProvider{
AWS: &esv1beta1.AWSProvider{
Region: validRegion,
},
},
},
},
},
},
{
name: "invalid static creds auth / AccessKeyID",
wantErr: true,
args: args{
store: &esv1beta1.SecretStore{
Spec: esv1beta1.SecretStoreSpec{
Provider: &esv1beta1.SecretStoreProvider{
AWS: &esv1beta1.AWSProvider{
Region: validRegion,
Auth: esv1beta1.AWSAuth{
SecretRef: &esv1beta1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "foobar",
Namespace: pointer.StringPtr("unacceptable"),
},
},
},
},
},
},
},
},
},
{
name: "invalid static creds auth / SecretAccessKey",
wantErr: true,
args: args{
store: &esv1beta1.SecretStore{
Spec: esv1beta1.SecretStoreSpec{
Provider: &esv1beta1.SecretStoreProvider{
AWS: &esv1beta1.AWSProvider{
Region: validRegion,
Auth: esv1beta1.AWSAuth{
SecretRef: &esv1beta1.AWSAuthSecretRef{
SecretAccessKey: esmeta.SecretKeySelector{
Name: "foobar",
Namespace: pointer.StringPtr("unacceptable"),
},
},
},
},
},
},
},
},
},
{
name: "invalid static creds auth / SecretAccessKey missing namespace",
wantErr: true,
args: args{
store: &esv1beta1.ClusterSecretStore{
TypeMeta: v1.TypeMeta{
Kind: esv1beta1.ClusterSecretStoreKind,
},
Spec: esv1beta1.SecretStoreSpec{
Provider: &esv1beta1.SecretStoreProvider{
AWS: &esv1beta1.AWSProvider{
Region: validRegion,
Auth: esv1beta1.AWSAuth{
SecretRef: &esv1beta1.AWSAuthSecretRef{
SecretAccessKey: esmeta.SecretKeySelector{
Name: "foobar",
},
},
},
},
},
},
},
},
},
{
name: "invalid static creds auth / AccessKeyID missing namespace",
wantErr: true,
args: args{
store: &esv1beta1.ClusterSecretStore{
TypeMeta: v1.TypeMeta{
Kind: esv1beta1.ClusterSecretStoreKind,
},
Spec: esv1beta1.SecretStoreSpec{
Provider: &esv1beta1.SecretStoreProvider{
AWS: &esv1beta1.AWSProvider{
Region: validRegion,
Auth: esv1beta1.AWSAuth{
SecretRef: &esv1beta1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "foobar",
},
},
},
},
},
},
},
},
},
{
name: "invalid jwt auth: missing sa selector namespace",
wantErr: true,
args: args{
store: &esv1beta1.ClusterSecretStore{
TypeMeta: v1.TypeMeta{
Kind: esv1beta1.ClusterSecretStoreKind,
},
Spec: esv1beta1.SecretStoreSpec{
Provider: &esv1beta1.SecretStoreProvider{
AWS: &esv1beta1.AWSProvider{
Region: validRegion,
Auth: esv1beta1.AWSAuth{
JWTAuth: &esv1beta1.AWSJWTAuth{
ServiceAccountRef: &esmeta.ServiceAccountSelector{
Name: "foobar",
},
},
},
},
},
},
},
},
},
{
name: "invalid jwt auth: not allowed sa selector namespace",
wantErr: true,
args: args{
store: &esv1beta1.SecretStore{
Spec: esv1beta1.SecretStoreSpec{
Provider: &esv1beta1.SecretStoreProvider{
AWS: &esv1beta1.AWSProvider{
Region: validRegion,
Auth: esv1beta1.AWSAuth{
JWTAuth: &esv1beta1.AWSJWTAuth{
ServiceAccountRef: &esmeta.ServiceAccountSelector{
Name: "foobar",
Namespace: pointer.StringPtr("unacceptable"),
},
},
},
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &Provider{}
if err := p.ValidateStore(tt.args.store); (err != nil) != tt.wantErr {
t.Errorf("Provider.ValidateStore() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View file

@ -42,6 +42,10 @@ func (sm *Client) GetSecretValue(in *awssm.GetSecretValueInput) (*awssm.GetSecre
return nil, fmt.Errorf("test case not found")
}
func (sm *Client) ListSecrets(*awssm.ListSecretsInput) (*awssm.ListSecretsOutput, error) {
return nil, nil
}
func (sm *Client) cacheKeyForInput(in *awssm.GetSecretValueInput) string {
var secretID, versionID string
if in.SecretId != nil {

View file

@ -30,8 +30,7 @@ import (
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
smmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
"github.com/external-secrets/external-secrets/pkg/provider"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
"github.com/external-secrets/external-secrets/pkg/utils"
)
const (
@ -52,6 +51,13 @@ const (
errMissingClientIDSecret = "missing accessKeyID/secretAccessKey in store config"
errFindSecret = "could not find secret %s/%s: %w"
errFindDataKey = "no data for %q in secret '%s/%s'"
errInvalidStore = "invalid store"
errInvalidStoreSpec = "invalid store spec"
errInvalidStoreProv = "invalid store provider"
errInvalidAzureProv = "invalid azure keyvault provider"
errInvalidSecRefClientID = "invalid AuthSecretRef.ClientID: %w"
errInvalidSecRefClientSecret = "invalid AuthSecretRef.ClientSecret: %w"
)
// interface to keyvault.BaseClient.
@ -71,17 +77,17 @@ type Azure struct {
}
func init() {
schema.Register(&Azure{}, &esv1beta1.SecretStoreProvider{
esv1beta1.Register(&Azure{}, &esv1beta1.SecretStoreProvider{
AzureKV: &esv1beta1.AzureKVProvider{},
})
}
// NewClient constructs a new secrets client based on the provided store.
func (a *Azure) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
func (a *Azure) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
return newClient(ctx, store, kube, namespace)
}
func newClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
func newClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
provider, err := getProvider(store)
if err != nil {
return nil, err
@ -115,6 +121,36 @@ func getProvider(store esv1beta1.GenericStore) (*esv1beta1.AzureKVProvider, erro
return spc.Provider.AzureKV, nil
}
func (a *Azure) ValidateStore(store esv1beta1.GenericStore) error {
if store == nil {
return fmt.Errorf(errInvalidStore)
}
spc := store.GetSpec()
if spc == nil {
return fmt.Errorf(errInvalidStoreSpec)
}
if spc.Provider == nil {
return fmt.Errorf(errInvalidStoreProv)
}
p := spc.Provider.AzureKV
if p == nil {
return fmt.Errorf(errInvalidAzureProv)
}
if p.AuthSecretRef != nil {
if p.AuthSecretRef.ClientID != nil {
if err := utils.ValidateSecretSelector(store, *p.AuthSecretRef.ClientID); err != nil {
return fmt.Errorf(errInvalidSecRefClientID, err)
}
}
if p.AuthSecretRef.ClientSecret != nil {
if err := utils.ValidateSecretSelector(store, *p.AuthSecretRef.ClientSecret); err != nil {
return fmt.Errorf(errInvalidSecRefClientSecret, err)
}
}
}
return nil
}
// Empty GetAllSecrets.
func (a *Azure) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
// TO be implemented

View file

@ -30,7 +30,6 @@ import (
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
fake "github.com/external-secrets/external-secrets/pkg/provider/azure/keyvault/fake"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
utils "github.com/external-secrets/external-secrets/pkg/utils"
)
@ -99,7 +98,7 @@ func TestNewClientManagedIdentityNoNeedForCredentials(t *testing.T) {
}}},
}
provider, err := schema.GetProvider(&store)
provider, err := esv1beta1.GetProvider(&store)
tassert.Nil(t, err, "the return err should be nil")
k8sClient := clientfake.NewClientBuilder().Build()
secretClient, err := provider.NewClient(context.Background(), &store, k8sClient, namespace)
@ -127,7 +126,7 @@ func TestNewClientNoCreds(t *testing.T) {
TenantID: &tenantID,
}}},
}
provider, err := schema.GetProvider(&store)
provider, err := esv1beta1.GetProvider(&store)
tassert.Nil(t, err, "the return err should be nil")
k8sClient := clientfake.NewClientBuilder().Build()
_, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
@ -381,3 +380,65 @@ func makeValidRef() *esv1beta1.ExternalSecretDataRemoteRef {
Version: "default",
}
}
func TestValidateStore(t *testing.T) {
type args struct {
auth esv1beta1.AzureKVAuth
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "empty auth",
wantErr: false,
},
{
name: "empty client id",
wantErr: false,
args: args{
auth: esv1beta1.AzureKVAuth{},
},
},
{
name: "invalid client id",
wantErr: true,
args: args{
auth: esv1beta1.AzureKVAuth{
ClientID: &v1.SecretKeySelector{
Namespace: pointer.StringPtr("invalid"),
},
},
},
},
{
name: "invalid client secret",
wantErr: true,
args: args{
auth: esv1beta1.AzureKVAuth{
ClientSecret: &v1.SecretKeySelector{
Namespace: pointer.StringPtr("invalid"),
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &Azure{}
store := &esv1beta1.SecretStore{
Spec: esv1beta1.SecretStoreSpec{
Provider: &esv1beta1.SecretStoreProvider{
AzureKV: &esv1beta1.AzureKVProvider{
AuthSecretRef: &tt.args.auth,
},
},
},
}
if err := a.ValidateStore(store); (err != nil) != tt.wantErr {
t.Errorf("Azure.ValidateStore() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View file

@ -21,8 +21,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/provider"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
)
var (
@ -35,7 +33,7 @@ type Provider struct {
config *esv1beta1.FakeProvider
}
func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
cfg, err := getProvider(store)
if err != nil {
return nil, err
@ -99,8 +97,12 @@ func (p *Provider) Validate() error {
return nil
}
func (p *Provider) ValidateStore(store esv1beta1.GenericStore) error {
return nil
}
func init() {
schema.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
Fake: &esv1beta1.FakeProvider{},
})
}

View file

@ -30,8 +30,6 @@ import (
kclient "sigs.k8s.io/controller-runtime/pkg/client"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/provider"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
"github.com/external-secrets/external-secrets/pkg/utils"
)
@ -52,6 +50,13 @@ const (
errUninitalizedGCPProvider = "provider GCP is not initialized"
errClientGetSecretAccess = "unable to access Secret from SecretManager Client: %w"
errJSONSecretUnmarshal = "unable to unmarshal secret: %w"
errInvalidStore = "invalid store"
errInvalidStoreSpec = "invalid store spec"
errInvalidStoreProv = "invalid store provider"
errInvalidGCPProv = "invalid gcp secrets manager provider"
errInvalidAuthSecretRef = "invalid auth secret ref: %w"
errInvalidWISARef = "invalid workload identity service account reference: %w"
)
type GoogleSecretManagerClient interface {
@ -132,7 +137,7 @@ func serviceAccountTokenSource(ctx context.Context, store esv1beta1.GenericStore
}
// NewClient constructs a GCP Provider.
func (sm *ProviderGCP) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
func (sm *ProviderGCP) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
storeSpec := store.GetSpec()
if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.GCPSM == nil {
return nil, fmt.Errorf(errGCPSMStore)
@ -270,8 +275,36 @@ func (sm *ProviderGCP) Validate() error {
return nil
}
func (sm *ProviderGCP) ValidateStore(store esv1beta1.GenericStore) error {
if store == nil {
return fmt.Errorf(errInvalidStore)
}
spc := store.GetSpec()
if spc == nil {
return fmt.Errorf(errInvalidStoreSpec)
}
if spc.Provider == nil {
return fmt.Errorf(errInvalidStoreProv)
}
p := spc.Provider.GCPSM
if p == nil {
return fmt.Errorf(errInvalidGCPProv)
}
if p.Auth.SecretRef != nil {
if err := utils.ValidateSecretSelector(store, p.Auth.SecretRef.SecretAccessKey); err != nil {
return fmt.Errorf(errInvalidAuthSecretRef, err)
}
}
if p.Auth.WorkloadIdentity != nil {
if err := utils.ValidateServiceAccountSelector(store, p.Auth.WorkloadIdentity.ServiceAccountRef); err != nil {
return fmt.Errorf(errInvalidWISARef, err)
}
}
return nil
}
func init() {
schema.Register(&ProviderGCP{}, &esv1beta1.SecretStoreProvider{
esv1beta1.Register(&ProviderGCP{}, &esv1beta1.SecretStoreProvider{
GCPSM: &esv1beta1.GCPSMProvider{},
})
}

View file

@ -21,8 +21,10 @@ import (
"testing"
secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1"
"k8s.io/utils/pointer"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
fakesm "github.com/external-secrets/external-secrets/pkg/provider/gcp/secretmanager/fake"
)
@ -210,3 +212,65 @@ func ErrorContains(out error, want string) bool {
}
return strings.Contains(out.Error(), want)
}
func TestValidateStore(t *testing.T) {
type args struct {
auth esv1beta1.GCPSMAuth
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "empty auth",
wantErr: false,
},
{
name: "invalid secret ref",
wantErr: true,
args: args{
auth: esv1beta1.GCPSMAuth{
SecretRef: &esv1beta1.GCPSMAuthSecretRef{
SecretAccessKey: v1.SecretKeySelector{
Name: "foo",
Namespace: pointer.StringPtr("invalid"),
},
},
},
},
},
{
name: "invalid wi sa ref",
wantErr: true,
args: args{
auth: esv1beta1.GCPSMAuth{
WorkloadIdentity: &esv1beta1.GCPWorkloadIdentity{
ServiceAccountRef: v1.ServiceAccountSelector{
Name: "foo",
Namespace: pointer.StringPtr("invalid"),
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sm := &ProviderGCP{}
store := &esv1beta1.SecretStore{
Spec: esv1beta1.SecretStoreSpec{
Provider: &esv1beta1.SecretStoreProvider{
GCPSM: &esv1beta1.GCPSMProvider{
Auth: tt.args.auth,
},
},
},
}
if err := sm.ValidateStore(store); (err != nil) != tt.wantErr {
t.Errorf("ProviderGCP.ValidateStore() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View file

@ -27,8 +27,6 @@ import (
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/e2e/framework/log"
"github.com/external-secrets/external-secrets/pkg/provider"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
"github.com/external-secrets/external-secrets/pkg/utils"
)
@ -63,7 +61,7 @@ type gClient struct {
}
func init() {
schema.Register(&Gitlab{}, &esv1beta1.SecretStoreProvider{
esv1beta1.Register(&Gitlab{}, &esv1beta1.SecretStoreProvider{
Gitlab: &esv1beta1.GitlabProvider{},
})
}
@ -108,7 +106,7 @@ func NewGitlabProvider() *Gitlab {
}
// Method on Gitlab Provider to set up client with credentials and populate projectID.
func (g *Gitlab) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
func (g *Gitlab) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
storeSpec := store.GetSpec()
if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Gitlab == nil {
return nil, fmt.Errorf("no store type or wrong store type")
@ -220,3 +218,7 @@ func (g *Gitlab) Close(ctx context.Context) error {
func (g *Gitlab) Validate() error {
return nil
}
func (g *Gitlab) ValidateStore(store esv1beta1.GenericStore) error {
return nil
}

View file

@ -27,8 +27,6 @@ import (
kclient "sigs.k8s.io/controller-runtime/pkg/client"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/provider"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
"github.com/external-secrets/external-secrets/pkg/utils"
)
@ -323,7 +321,11 @@ func (ibm *providerIBM) Validate() error {
return nil
}
func (ibm *providerIBM) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
func (ibm *providerIBM) ValidateStore(store esv1beta1.GenericStore) error {
return nil
}
func (ibm *providerIBM) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
storeSpec := store.GetSpec()
ibmSpec := storeSpec.Provider.IBM
@ -376,7 +378,7 @@ func (ibm *providerIBM) NewClient(ctx context.Context, store esv1beta1.GenericSt
}
func init() {
schema.Register(&providerIBM{}, &esv1beta1.SecretStoreProvider{
esv1beta1.Register(&providerIBM{}, &esv1beta1.SecretStoreProvider{
IBM: &esv1beta1.IBMProvider{},
})
}

View file

@ -27,8 +27,6 @@ import (
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
"github.com/external-secrets/external-secrets/pkg/provider"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
"github.com/external-secrets/external-secrets/pkg/utils"
)
@ -51,7 +49,7 @@ type ProviderKubernetes struct {
Client KClient
}
var _ provider.SecretsClient = &ProviderKubernetes{}
var _ esv1beta1.SecretsClient = &ProviderKubernetes{}
type BaseClient struct {
kube kclient.Client
@ -65,13 +63,13 @@ type BaseClient struct {
}
func init() {
schema.Register(&ProviderKubernetes{}, &esv1beta1.SecretStoreProvider{
esv1beta1.Register(&ProviderKubernetes{}, &esv1beta1.SecretStoreProvider{
Kubernetes: &esv1beta1.KubernetesProvider{},
})
}
// NewClient constructs a Kubernetes Provider.
func (k *ProviderKubernetes) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
func (k *ProviderKubernetes) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
storeSpec := store.GetSpec()
if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Kubernetes == nil {
return nil, fmt.Errorf("no store type or wrong store type")
@ -233,3 +231,7 @@ func (k *BaseClient) fetchSecretKey(ctx context.Context, key esmeta.SecretKeySel
func (k *ProviderKubernetes) Validate() error {
return nil
}
func (k *ProviderKubernetes) ValidateStore(store esv1beta1.GenericStore) error {
return nil
}

View file

@ -27,9 +27,7 @@ import (
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
"github.com/external-secrets/external-secrets/pkg/provider"
"github.com/external-secrets/external-secrets/pkg/provider/aws/util"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
"github.com/external-secrets/external-secrets/pkg/utils"
)
@ -124,7 +122,7 @@ func (vms *VaultManagementService) GetSecretMap(ctx context.Context, ref esv1bet
}
// NewClient constructs a new secrets client based on the provided store.
func (vms *VaultManagementService) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
func (vms *VaultManagementService) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
storeSpec := store.GetSpec()
oracleSpec := storeSpec.Provider.Oracle
@ -225,8 +223,12 @@ func (vms *VaultManagementService) Validate() error {
return nil
}
func (vms *VaultManagementService) ValidateStore(store esv1beta1.GenericStore) error {
return nil
}
func init() {
schema.Register(&VaultManagementService{}, &esv1beta1.SecretStoreProvider{
esv1beta1.Register(&VaultManagementService{}, &esv1beta1.SecretStoreProvider{
Oracle: &esv1beta1.OracleProvider{},
})
}

View file

@ -20,16 +20,14 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/provider"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
)
var _ provider.Provider = &Client{}
var _ esv1beta1.Provider = &Client{}
// Client is a fake client for testing.
type Client struct {
NewFn func(context.Context, esv1beta1.GenericStore, client.Client,
string) (provider.SecretsClient, error)
string) (esv1beta1.SecretsClient, error)
GetSecretFn func(context.Context, esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error)
GetSecretMapFn func(context.Context, esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error)
GetAllSecretsFn func(context.Context, esv1beta1.ExternalSecretFind) (map[string][]byte, error)
@ -49,7 +47,7 @@ func New() *Client {
},
}
v.NewFn = func(context.Context, esv1beta1.GenericStore, client.Client, string) (provider.SecretsClient, error) {
v.NewFn = func(context.Context, esv1beta1.GenericStore, client.Client, string) (esv1beta1.SecretsClient, error) {
return v, nil
}
@ -58,7 +56,7 @@ func New() *Client {
// RegisterAs registers the fake client in the schema.
func (v *Client) RegisterAs(provider *esv1beta1.SecretStoreProvider) {
schema.ForceRegister(v, provider)
esv1beta1.ForceRegister(v, provider)
}
// GetSecret implements the provider.Provider interface.
@ -92,6 +90,10 @@ func (v *Client) Validate() error {
return nil
}
func (v *Client) ValidateStore(store esv1beta1.GenericStore) error {
return nil
}
// WithGetSecretMap wraps the secret data map returned by this fake provider.
func (v *Client) WithGetSecretMap(secData map[string][]byte, err error) *Client {
v.GetSecretMapFn = func(context.Context, esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
@ -110,13 +112,13 @@ func (v *Client) WithGetAllSecrets(secData map[string][]byte, err error) *Client
// WithNew wraps the fake provider factory function.
func (v *Client) WithNew(f func(context.Context, esv1beta1.GenericStore, client.Client,
string) (provider.SecretsClient, error)) *Client {
string) (esv1beta1.SecretsClient, error)) *Client {
v.NewFn = f
return v
}
// NewClient returns a new fake provider.
func (v *Client) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
func (v *Client) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
c, err := v.NewFn(ctx, store, kube, namespace)
if err != nil {
return nil, err

View file

@ -37,13 +37,12 @@ import (
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
"github.com/external-secrets/external-secrets/pkg/provider"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
"github.com/external-secrets/external-secrets/pkg/utils"
)
var (
_ provider.Provider = &connector{}
_ provider.SecretsClient = &client{}
_ esv1beta1.Provider = &connector{}
_ esv1beta1.SecretsClient = &client{}
)
const (
@ -79,6 +78,19 @@ const (
errUnknownCAProvider = "unknown caProvider type given"
errCANamespace = "cannot read secret for CAProvider due to missing namespace on kind ClusterSecretStore"
errInvalidStore = "invalid store"
errInvalidStoreSpec = "invalid store spec"
errInvalidStoreProv = "invalid store provider"
errInvalidVaultProv = "invalid vault provider"
errInvalidAppRoleSec = "invalid Auth.AppRole.SecretRef: %w"
errInvalidClientCert = "invalid Auth.Cert.ClientCert: %w"
errInvalidCertSec = "invalid Auth.Cert.SecretRef: %w"
errInvalidJwtSec = "invalid Auth.Jwt.SecretRef: %w"
errInvalidKubeSA = "invalid Auth.Kubernetes.ServiceAccountRef: %w"
errInvalidKubeSec = "invalid Auth.Kubernetes.SecretRef: %w"
errInvalidLdapSec = "invalid Auth.Ldap.SecretRef: %w"
errInvalidTokenRef = "invalid Auth.TokenSecretRef: %w"
)
type Client interface {
@ -101,7 +113,7 @@ type client struct {
}
func init() {
schema.Register(&connector{
esv1beta1.Register(&connector{
newVaultClient: newVaultClient,
}, &esv1beta1.SecretStoreProvider{
Vault: &esv1beta1.VaultProvider{},
@ -116,7 +128,7 @@ type connector struct {
newVaultClient func(c *vault.Config) (Client, error)
}
func (c *connector) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
func (c *connector) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
storeSpec := store.GetSpec()
if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Vault == nil {
return nil, errors.New(errVaultStore)
@ -158,6 +170,64 @@ func (c *connector) NewClient(ctx context.Context, store esv1beta1.GenericStore,
return vStore, nil
}
func (c *connector) ValidateStore(store esv1beta1.GenericStore) error {
if store == nil {
return fmt.Errorf(errInvalidStore)
}
spc := store.GetSpec()
if spc == nil {
return fmt.Errorf(errInvalidStoreSpec)
}
if spc.Provider == nil {
return fmt.Errorf(errInvalidStoreProv)
}
p := spc.Provider.Vault
if p == nil {
return fmt.Errorf(errInvalidVaultProv)
}
if p.Auth.AppRole != nil {
if err := utils.ValidateSecretSelector(store, p.Auth.AppRole.SecretRef); err != nil {
return fmt.Errorf(errInvalidAppRoleSec, err)
}
}
if p.Auth.Cert != nil {
if err := utils.ValidateSecretSelector(store, p.Auth.Cert.ClientCert); err != nil {
return fmt.Errorf(errInvalidClientCert, err)
}
if err := utils.ValidateSecretSelector(store, p.Auth.Cert.SecretRef); err != nil {
return fmt.Errorf(errInvalidCertSec, err)
}
}
if p.Auth.Jwt != nil {
if err := utils.ValidateSecretSelector(store, p.Auth.Jwt.SecretRef); err != nil {
return fmt.Errorf(errInvalidJwtSec, err)
}
}
if p.Auth.Kubernetes != nil {
if p.Auth.Kubernetes.ServiceAccountRef != nil {
if err := utils.ValidateServiceAccountSelector(store, *p.Auth.Kubernetes.ServiceAccountRef); err != nil {
return fmt.Errorf(errInvalidKubeSA, err)
}
}
if p.Auth.Kubernetes.SecretRef != nil {
if err := utils.ValidateSecretSelector(store, *p.Auth.Kubernetes.SecretRef); err != nil {
return fmt.Errorf(errInvalidKubeSec, err)
}
}
}
if p.Auth.Ldap != nil {
if err := utils.ValidateSecretSelector(store, p.Auth.Ldap.SecretRef); err != nil {
return fmt.Errorf(errInvalidLdapSec, err)
}
}
if p.Auth.TokenSecretRef != nil {
if err := utils.ValidateSecretSelector(store, *p.Auth.TokenSecretRef); err != nil {
return fmt.Errorf(errInvalidTokenRef, err)
}
}
return nil
}
// Empty GetAllSecrets.
func (v *client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
// TO be implemented

View file

@ -29,6 +29,7 @@ import (
vault "github.com/hashicorp/vault/api"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
kclient "sigs.k8s.io/controller-runtime/pkg/client"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
@ -1070,3 +1071,142 @@ func TestGetSecretPath(t *testing.T) {
})
}
}
func TestValidateStore(t *testing.T) {
type args struct {
auth esv1beta1.VaultAuth
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "empty auth",
args: args{},
},
{
name: "invalid approle with namespace",
args: args{
auth: esv1beta1.VaultAuth{
AppRole: &esv1beta1.VaultAppRole{
SecretRef: esmeta.SecretKeySelector{
Namespace: pointer.StringPtr("invalid"),
},
},
},
},
wantErr: true,
},
{
name: "invalid clientcert",
args: args{
auth: esv1beta1.VaultAuth{
Cert: &esv1beta1.VaultCertAuth{
ClientCert: esmeta.SecretKeySelector{
Namespace: pointer.StringPtr("invalid"),
},
},
},
},
wantErr: true,
},
{
name: "invalid cert secret",
args: args{
auth: esv1beta1.VaultAuth{
Cert: &esv1beta1.VaultCertAuth{
SecretRef: esmeta.SecretKeySelector{
Namespace: pointer.StringPtr("invalid"),
},
},
},
},
wantErr: true,
},
{
name: "invalid jwt secret",
args: args{
auth: esv1beta1.VaultAuth{
Jwt: &esv1beta1.VaultJwtAuth{
SecretRef: esmeta.SecretKeySelector{
Namespace: pointer.StringPtr("invalid"),
},
},
},
},
wantErr: true,
},
{
name: "invalid kubernetes sa",
args: args{
auth: esv1beta1.VaultAuth{
Kubernetes: &esv1beta1.VaultKubernetesAuth{
ServiceAccountRef: &esmeta.ServiceAccountSelector{
Namespace: pointer.StringPtr("invalid"),
},
},
},
},
wantErr: true,
},
{
name: "invalid kubernetes secret",
args: args{
auth: esv1beta1.VaultAuth{
Kubernetes: &esv1beta1.VaultKubernetesAuth{
SecretRef: &esmeta.SecretKeySelector{
Namespace: pointer.StringPtr("invalid"),
},
},
},
},
wantErr: true,
},
{
name: "invalid ldap secret",
args: args{
auth: esv1beta1.VaultAuth{
Ldap: &esv1beta1.VaultLdapAuth{
SecretRef: esmeta.SecretKeySelector{
Namespace: pointer.StringPtr("invalid"),
},
},
},
},
wantErr: true,
},
{
name: "invalid token secret",
args: args{
auth: esv1beta1.VaultAuth{
TokenSecretRef: &esmeta.SecretKeySelector{
Namespace: pointer.StringPtr("invalid"),
},
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &connector{
newVaultClient: nil,
}
store := &esv1beta1.SecretStore{
Spec: esv1beta1.SecretStoreSpec{
Provider: &esv1beta1.SecretStoreProvider{
Vault: &esv1beta1.VaultProvider{
Auth: tt.args.auth,
},
},
},
}
if err := c.ValidateStore(store); (err != nil) != tt.wantErr {
t.Errorf("connector.ValidateStore() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View file

@ -34,8 +34,6 @@ import (
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
"github.com/external-secrets/external-secrets/pkg/provider"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
"github.com/external-secrets/external-secrets/pkg/template/v2"
)
@ -51,12 +49,12 @@ type WebHook struct {
}
func init() {
schema.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
Webhook: &esv1beta1.WebhookProvider{},
})
}
func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
whClient := &WebHook{
kube: kube,
store: store,
@ -74,6 +72,10 @@ func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore,
return whClient, nil
}
func (p *Provider) ValidateStore(store esv1beta1.GenericStore) error {
return nil
}
func getProvider(store esv1beta1.GenericStore) (*esv1beta1.WebhookProvider, error) {
spc := store.GetSpec()
if spc == nil || spc.Provider == nil || spc.Provider.Webhook == nil {

View file

@ -28,7 +28,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/provider"
)
type testCase struct {
@ -268,7 +267,7 @@ func runTestCase(tc testCase, t *testing.T) {
}
}
func testGetSecretMap(tc testCase, t *testing.T, client provider.SecretsClient) {
func testGetSecretMap(tc testCase, t *testing.T, client esv1beta1.SecretsClient) {
testRef := esv1beta1.ExternalSecretDataRemoteRef{
Key: tc.Args.Key,
Version: tc.Args.Version,
@ -293,7 +292,7 @@ func testGetSecretMap(tc testCase, t *testing.T, client provider.SecretsClient)
}
}
func testGetSecret(tc testCase, t *testing.T, client provider.SecretsClient) {
func testGetSecret(tc testCase, t *testing.T, client esv1beta1.SecretsClient) {
testRef := esv1beta1.ExternalSecretDataRemoteRef{
Key: tc.Args.Key,
Version: tc.Args.Version,

View file

@ -30,8 +30,6 @@ import (
kclient "sigs.k8s.io/controller-runtime/pkg/client"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/provider"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client/grpc"
)
@ -66,7 +64,7 @@ func newLockboxProvider(yandexCloudCreator client.YandexCloudCreator) *lockboxPr
}
// NewClient constructs a Yandex Lockbox Provider.
func (p *lockboxProvider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
func (p *lockboxProvider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
storeSpec := store.GetSpec()
if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.YandexLockbox == nil {
return nil, fmt.Errorf("received invalid Yandex Lockbox SecretStore resource")
@ -218,6 +216,10 @@ func (p *lockboxProvider) cleanUpIamTokenMap() {
}
}
func (p *lockboxProvider) ValidateStore(store esv1beta1.GenericStore) error {
return nil
}
// lockboxSecretsClient is a secrets client for Yandex Lockbox.
type lockboxSecretsClient struct {
lockboxClient client.LockboxClient
@ -327,7 +329,7 @@ func init() {
}
}()
schema.Register(
esv1beta1.Register(
lockboxProvider,
&esv1beta1.SecretStoreProvider{
YandexLockbox: &esv1beta1.YandexLockboxProvider{},

View file

@ -34,7 +34,6 @@ import (
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client/fake"
)
@ -58,7 +57,7 @@ func TestNewClient(t *testing.T) {
},
},
}
provider, err := schema.GetProvider(store)
provider, err := esv1beta1.GetProvider(store)
tassert.Nil(t, err)
k8sClient := clientfake.NewClientBuilder().Build()

View file

@ -21,6 +21,9 @@ import (
"fmt"
"reflect"
"strings"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
)
// MergeByteMap merges map of byte slices.
@ -66,3 +69,31 @@ func ErrorContains(out error, want string) bool {
}
return strings.Contains(out.Error(), want)
}
// ValidateSecretSelector just checks if the namespace field is present/absent
// depending on the secret store type.
// We MUST NOT check the name or key property here. It MAY be defaulted by the provider.
func ValidateSecretSelector(store esv1beta1.GenericStore, ref esmeta.SecretKeySelector) error {
clusterScope := store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind
if clusterScope && ref.Namespace == nil {
return fmt.Errorf("cluster scope requires namespace")
}
if !clusterScope && ref.Namespace != nil {
return fmt.Errorf("namespace not allowed with namespaced SecretStore")
}
return nil
}
// ValidateServiceAccountSelector just checks if the namespace field is present/absent
// depending on the secret store type.
// We MUST NOT check the name or key property here. It MAY be defaulted by the provider.
func ValidateServiceAccountSelector(store esv1beta1.GenericStore, ref esmeta.ServiceAccountSelector) error {
clusterScope := store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind
if clusterScope && ref.Namespace == nil {
return fmt.Errorf("cluster scope requires namespace")
}
if !clusterScope && ref.Namespace != nil {
return fmt.Errorf("namespace not allowed with namespaced SecretStore")
}
return nil
}