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

Feature/push secret (#1315)

Introduces Push Secret feature with implementations for the following providers:

* GCP Secret Manager
* AWS Secrets Manager
* AWS Parameter Store
* Hashicorp Vault KV

Signed-off-by: Dominic Meddick <dominic.meddick@engineerbetter.com>
Signed-off-by: Amr Fawzy <amr.fawzy@container-solutions.com>
Signed-off-by: William Young <will.young@engineerbetter.com>
Signed-off-by: James Cleveland <james.cleveland@engineerbetter.com>
Signed-off-by: Lilly Daniell <lilly.daniell@engineerbetter.com>
Signed-off-by: Adrienne Galloway <adrienne.galloway@engineerbetter.com>
Signed-off-by: Marcus Dantas <marcus.dantas@engineerbetter.com>
Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>
Signed-off-by: Nick Ruffles <nick.ruffles@engineerbetter.com>
This commit is contained in:
Gustavo Fernandes de Carvalho 2022-11-29 16:04:46 -03:00 committed by GitHub
parent d71e905a47
commit 0cb799b5cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 5016 additions and 172 deletions

View file

@ -0,0 +1,157 @@
/*
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 v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
ReasonSynced = "Synced"
ReasonErrored = "Errored"
)
type PushSecretStoreRef struct {
// Optionally, sync to the SecretStore of the given name
// +optional
Name string `json:"name"`
// Optionally, sync to secret stores with label selector
// +optional
LabelSelector *metav1.LabelSelector `json:"labelSelector"`
// Kind of the SecretStore resource (SecretStore or ClusterSecretStore)
// Defaults to `SecretStore`
// +kubebuilder:default="SecretStore"
// +optional
Kind string `json:"kind,omitempty"`
}
type PushSecretDeletionPolicy string
const (
PushSecretDeletionPolicyDelete PushSecretDeletionPolicy = "Delete"
PushSecretDeletionPolicyNone PushSecretDeletionPolicy = "None"
)
// PushSecretSpec configures the behavior of the PushSecret.
type PushSecretSpec struct {
// The Interval to which External Secrets will try to push a secret definition
RefreshInterval *metav1.Duration `json:"refreshInterval,omitempty"`
SecretStoreRefs []PushSecretStoreRef `json:"secretStoreRefs"`
// Deletion Policy to handle Secrets in the provider. Possible Values: "Delete/None". Defaults to "None".
// +kubebuilder:default="None"
// +optional
DeletionPolicy PushSecretDeletionPolicy `json:"deletionPolicy,omitempty"`
// The Secret Selector (k8s source) for the Push Secret
Selector PushSecretSelector `json:"selector"`
// Secret Data that should be pushed to providers
Data []PushSecretData `json:"data,omitempty"`
}
type PushSecretSecret struct {
// Name of the Secret. The Secret must exist in the same namespace as the PushSecret manifest.
Name string `json:"name"`
}
type PushSecretSelector struct {
// Select a Secret to Push.
Secret PushSecretSecret `json:"secret"`
}
type PushSecretRemoteRef struct {
// Name of the resulting provider secret.
RemoteKey string `json:"remoteKey"`
}
func (r PushSecretRemoteRef) GetRemoteKey() string {
return r.RemoteKey
}
type PushSecretMatch struct {
// Secret Key to be pushed
SecretKey string `json:"secretKey"`
// Remote Refs to push to providers.
RemoteRef PushSecretRemoteRef `json:"remoteRef"`
}
type PushSecretData struct {
// Match a given Secret Key to be pushed to the provider.
Match PushSecretMatch `json:"match"`
}
// PushSecretConditionType indicates the condition of the PushSecret.
type PushSecretConditionType string
const (
PushSecretReady PushSecretConditionType = "Ready"
)
// PushSecretStatusCondition indicates the status of the PushSecret.
type PushSecretStatusCondition struct {
Type PushSecretConditionType `json:"type"`
Status corev1.ConditionStatus `json:"status"`
// +optional
Reason string `json:"reason,omitempty"`
// +optional
Message string `json:"message,omitempty"`
// +optional
LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"`
}
type SyncedPushSecretsMap map[string]map[string]PushSecretData
// PushSecretStatus indicates the history of the status of PushSecret.
type PushSecretStatus struct {
// +nullable
// refreshTime is the time and date the external secret was fetched and
// the target secret updated
RefreshTime metav1.Time `json:"refreshTime,omitempty"`
// SyncedResourceVersion keeps track of the last synced version.
SyncedResourceVersion string `json:"syncedResourceVersion,omitempty"`
// Synced Push Secrets for later deletion. Matches Secret Stores to PushSecretData that was stored to that secretStore.
// +optional
SyncedPushSecrets SyncedPushSecretsMap `json:"syncedPushSecrets,omitempty"`
// +optional
Conditions []PushSecretStatusCondition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:storageversion
// PushSecrets is the Schema for the PushSecrets API.
// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp"
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].reason`
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Namespaced,categories={pushsecrets}
type PushSecret struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec PushSecretSpec `json:"spec,omitempty"`
Status PushSecretStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp"
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].reason`
// PushSecretList contains a list of PushSecret resources.
type PushSecretList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []PushSecret `json:"items"`
}

View file

@ -60,8 +60,16 @@ var (
ClusterSecretStoreGroupVersionKind = SchemeGroupVersion.WithKind(ClusterSecretStoreKind) ClusterSecretStoreGroupVersionKind = SchemeGroupVersion.WithKind(ClusterSecretStoreKind)
) )
var (
PushSecretKind = reflect.TypeOf(PushSecret{}).Name()
PushSecretGroupKind = schema.GroupKind{Group: Group, Kind: PushSecretKind}.String()
PushSecretKindAPIVersion = PushSecretKind + "." + SchemeGroupVersion.String()
PushSecretGroupVersionKind = SchemeGroupVersion.WithKind(PushSecretKind)
)
func init() { func init() {
SchemeBuilder.Register(&ExternalSecret{}, &ExternalSecretList{}) SchemeBuilder.Register(&ExternalSecret{}, &ExternalSecretList{})
SchemeBuilder.Register(&SecretStore{}, &SecretStoreList{}) SchemeBuilder.Register(&SecretStore{}, &SecretStoreList{})
SchemeBuilder.Register(&ClusterSecretStore{}, &ClusterSecretStoreList{}) SchemeBuilder.Register(&ClusterSecretStore{}, &ClusterSecretStoreList{})
SchemeBuilder.Register(&PushSecret{}, &PushSecretList{})
} }

View file

@ -994,6 +994,252 @@ func (in *OracleSecretRef) DeepCopy() *OracleSecretRef {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecret) DeepCopyInto(out *PushSecret) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecret.
func (in *PushSecret) DeepCopy() *PushSecret {
if in == nil {
return nil
}
out := new(PushSecret)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PushSecret) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretData) DeepCopyInto(out *PushSecretData) {
*out = *in
out.Match = in.Match
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretData.
func (in *PushSecretData) DeepCopy() *PushSecretData {
if in == nil {
return nil
}
out := new(PushSecretData)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretList) DeepCopyInto(out *PushSecretList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]PushSecret, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretList.
func (in *PushSecretList) DeepCopy() *PushSecretList {
if in == nil {
return nil
}
out := new(PushSecretList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PushSecretList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretMatch) DeepCopyInto(out *PushSecretMatch) {
*out = *in
out.RemoteRef = in.RemoteRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretMatch.
func (in *PushSecretMatch) DeepCopy() *PushSecretMatch {
if in == nil {
return nil
}
out := new(PushSecretMatch)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretRemoteRef) DeepCopyInto(out *PushSecretRemoteRef) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretRemoteRef.
func (in *PushSecretRemoteRef) DeepCopy() *PushSecretRemoteRef {
if in == nil {
return nil
}
out := new(PushSecretRemoteRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretSecret) DeepCopyInto(out *PushSecretSecret) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretSecret.
func (in *PushSecretSecret) DeepCopy() *PushSecretSecret {
if in == nil {
return nil
}
out := new(PushSecretSecret)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretSelector) DeepCopyInto(out *PushSecretSelector) {
*out = *in
out.Secret = in.Secret
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretSelector.
func (in *PushSecretSelector) DeepCopy() *PushSecretSelector {
if in == nil {
return nil
}
out := new(PushSecretSelector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretSpec) DeepCopyInto(out *PushSecretSpec) {
*out = *in
if in.RefreshInterval != nil {
in, out := &in.RefreshInterval, &out.RefreshInterval
*out = new(v1.Duration)
**out = **in
}
if in.SecretStoreRefs != nil {
in, out := &in.SecretStoreRefs, &out.SecretStoreRefs
*out = make([]PushSecretStoreRef, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
out.Selector = in.Selector
if in.Data != nil {
in, out := &in.Data, &out.Data
*out = make([]PushSecretData, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretSpec.
func (in *PushSecretSpec) DeepCopy() *PushSecretSpec {
if in == nil {
return nil
}
out := new(PushSecretSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretStatus) DeepCopyInto(out *PushSecretStatus) {
*out = *in
in.RefreshTime.DeepCopyInto(&out.RefreshTime)
if in.SyncedPushSecrets != nil {
in, out := &in.SyncedPushSecrets, &out.SyncedPushSecrets
*out = make(SyncedPushSecretsMap, len(*in))
for key, val := range *in {
var outVal map[string]PushSecretData
if val == nil {
(*out)[key] = nil
} else {
in, out := &val, &outVal
*out = make(map[string]PushSecretData, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
(*out)[key] = outVal
}
}
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]PushSecretStatusCondition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretStatus.
func (in *PushSecretStatus) DeepCopy() *PushSecretStatus {
if in == nil {
return nil
}
out := new(PushSecretStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretStatusCondition) DeepCopyInto(out *PushSecretStatusCondition) {
*out = *in
in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretStatusCondition.
func (in *PushSecretStatusCondition) DeepCopy() *PushSecretStatusCondition {
if in == nil {
return nil
}
out := new(PushSecretStatusCondition)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretStoreRef) DeepCopyInto(out *PushSecretStoreRef) {
*out = *in
if in.LabelSelector != nil {
in, out := &in.LabelSelector, &out.LabelSelector
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretStoreRef.
func (in *PushSecretStoreRef) DeepCopy() *PushSecretStoreRef {
if in == nil {
return nil
}
out := new(PushSecretStoreRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretStore) DeepCopyInto(out *SecretStore) { func (in *SecretStore) DeepCopyInto(out *SecretStore) {
*out = *in *out = *in
@ -1252,6 +1498,37 @@ func (in *ServiceAccountAuth) DeepCopy() *ServiceAccountAuth {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in SyncedPushSecretsMap) DeepCopyInto(out *SyncedPushSecretsMap) {
{
in := &in
*out = make(SyncedPushSecretsMap, len(*in))
for key, val := range *in {
var outVal map[string]PushSecretData
if val == nil {
(*out)[key] = nil
} else {
in, out := &val, &outVal
*out = make(map[string]PushSecretData, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
(*out)[key] = outVal
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SyncedPushSecretsMap.
func (in SyncedPushSecretsMap) DeepCopy() SyncedPushSecretsMap {
if in == nil {
return nil
}
out := new(SyncedPushSecretsMap)
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TemplateFrom) DeepCopyInto(out *TemplateFrom) { func (in *TemplateFrom) DeepCopyInto(out *TemplateFrom) {
*out = *in *out = *in

View file

@ -0,0 +1,102 @@
// Code generated by counterfeiter. DO NOT EDIT.
package fakes
import (
"sync"
"github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
)
type PushRemoteRef struct {
GetRemoteKeyStub func() string
getRemoteKeyMutex sync.RWMutex
getRemoteKeyArgsForCall []struct {
}
getRemoteKeyReturns struct {
result1 string
}
getRemoteKeyReturnsOnCall map[int]struct {
result1 string
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *PushRemoteRef) GetRemoteKey() string {
fake.getRemoteKeyMutex.Lock()
ret, specificReturn := fake.getRemoteKeyReturnsOnCall[len(fake.getRemoteKeyArgsForCall)]
fake.getRemoteKeyArgsForCall = append(fake.getRemoteKeyArgsForCall, struct {
}{})
stub := fake.GetRemoteKeyStub
fakeReturns := fake.getRemoteKeyReturns
fake.recordInvocation("GetRemoteKey", []interface{}{})
fake.getRemoteKeyMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *PushRemoteRef) GetRemoteKeyCallCount() int {
fake.getRemoteKeyMutex.RLock()
defer fake.getRemoteKeyMutex.RUnlock()
return len(fake.getRemoteKeyArgsForCall)
}
func (fake *PushRemoteRef) GetRemoteKeyCalls(stub func() string) {
fake.getRemoteKeyMutex.Lock()
defer fake.getRemoteKeyMutex.Unlock()
fake.GetRemoteKeyStub = stub
}
func (fake *PushRemoteRef) GetRemoteKeyReturns(result1 string) {
fake.getRemoteKeyMutex.Lock()
defer fake.getRemoteKeyMutex.Unlock()
fake.GetRemoteKeyStub = nil
fake.getRemoteKeyReturns = struct {
result1 string
}{result1}
}
func (fake *PushRemoteRef) GetRemoteKeyReturnsOnCall(i int, result1 string) {
fake.getRemoteKeyMutex.Lock()
defer fake.getRemoteKeyMutex.Unlock()
fake.GetRemoteKeyStub = nil
if fake.getRemoteKeyReturnsOnCall == nil {
fake.getRemoteKeyReturnsOnCall = make(map[int]struct {
result1 string
})
}
fake.getRemoteKeyReturnsOnCall[i] = struct {
result1 string
}{result1}
}
func (fake *PushRemoteRef) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.getRemoteKeyMutex.RLock()
defer fake.getRemoteKeyMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *PushRemoteRef) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ v1beta1.PushRemoteRef = new(PushRemoteRef)

View file

@ -51,6 +51,8 @@ type Provider interface {
// ValidateStore checks if the provided store is valid // ValidateStore checks if the provided store is valid
ValidateStore(store GenericStore) error ValidateStore(store GenericStore) error
// Capabilities returns the provider Capabilities (Read, Write, ReadWrite)
Capabilities() SecretStoreCapabilities
} }
// +kubebuilder:object:root=false // +kubebuilder:object:root=false
@ -65,6 +67,12 @@ type SecretsClient interface {
// then the secret entry will be deleted depending on the deletionPolicy. // then the secret entry will be deleted depending on the deletionPolicy.
GetSecret(ctx context.Context, ref ExternalSecretDataRemoteRef) ([]byte, error) GetSecret(ctx context.Context, ref ExternalSecretDataRemoteRef) ([]byte, error)
// PushSecret will write a single secret into the provider
PushSecret(ctx context.Context, value []byte, remoteRef PushRemoteRef) error
// DeleteSecret will delete the secret from a provider
DeleteSecret(ctx context.Context, remoteRef PushRemoteRef) error
// Validate checks if the client is configured correctly // Validate checks if the client is configured correctly
// and is able to retrieve secrets from the provider. // and is able to retrieve secrets from the provider.
// If the validation result is unknown it will be ignored. // If the validation result is unknown it will be ignored.

View file

@ -25,11 +25,25 @@ type PP struct{}
const shouldBeRegistered = "provider should be registered" const shouldBeRegistered = "provider should be registered"
func (p *PP) Capabilities() SecretStoreCapabilities {
return SecretStoreReadOnly
}
// New constructs a SecretsManager Provider. // New constructs a SecretsManager Provider.
func (p *PP) NewClient(ctx context.Context, store GenericStore, kube client.Client, namespace string) (SecretsClient, error) { func (p *PP) NewClient(ctx context.Context, store GenericStore, kube client.Client, namespace string) (SecretsClient, error) {
return p, nil return p, nil
} }
// PushSecret writes a single secret into a provider.
func (p *PP) PushSecret(ctx context.Context, value []byte, remoteRef PushRemoteRef) error {
return nil
}
// DeleteSecret deletes a single secret from a provider.
func (p *PP) DeleteSecret(ctx context.Context, remoteRef PushRemoteRef) error {
return nil
}
// GetSecret returns a single secret from the provider. // GetSecret returns a single secret from the provider.
func (p *PP) GetSecret(ctx context.Context, ref ExternalSecretDataRemoteRef) ([]byte, error) { func (p *PP) GetSecret(ctx context.Context, ref ExternalSecretDataRemoteRef) ([]byte, error) {
return []byte("NOOP"), nil return []byte("NOOP"), nil

View file

@ -0,0 +1,24 @@
/*
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
// +kubebuilder:object:root=false
// +kubebuilder:object:generate:false
// +k8s:deepcopy-gen:interfaces=nil
// +k8s:deepcopy-gen=nil
// This interface is to allow using v1alpha1 content in Provider registered in v1beta1.
type PushRemoteRef interface {
GetRemoteKey() string
}

View file

@ -184,10 +184,21 @@ type SecretStoreStatusCondition struct {
LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"`
} }
// SecretStoreCapabilities defines the possible operations a SecretStore can do.
type SecretStoreCapabilities string
const (
SecretStoreReadOnly SecretStoreCapabilities = "ReadOnly"
SecretStoreWriteOnly SecretStoreCapabilities = "WriteOnly"
SecretStoreReadWrite SecretStoreCapabilities = "ReadWrite"
)
// SecretStoreStatus defines the observed state of the SecretStore. // SecretStoreStatus defines the observed state of the SecretStore.
type SecretStoreStatus struct { type SecretStoreStatus struct {
// +optional // +optional
Conditions []SecretStoreStatusCondition `json:"conditions"` Conditions []SecretStoreStatusCondition `json:"conditions"`
// +optional
Capabilities SecretStoreCapabilities `json:"capabilities"`
} }
// +kubebuilder:object:root=true // +kubebuilder:object:root=true
@ -196,6 +207,7 @@ type SecretStoreStatus struct {
// SecretStore represents a secure external location for storing secrets, which can be referenced as part of `storeRef` fields. // SecretStore represents a secure external location for storing secrets, which can be referenced as part of `storeRef` fields.
// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp"
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].reason` // +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].reason`
// +kubebuilder:printcolumn:name="Capabilities",type=string,JSONPath=`.status.capabilities`
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status` // +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
// +kubebuilder:subresource:status // +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Namespaced,categories={externalsecrets},shortName=ss // +kubebuilder:resource:scope=Namespaced,categories={externalsecrets},shortName=ss
@ -222,6 +234,7 @@ type SecretStoreList struct {
// ClusterSecretStore represents a secure external location for storing secrets, which can be referenced as part of `storeRef` fields. // ClusterSecretStore represents a secure external location for storing secrets, which can be referenced as part of `storeRef` fields.
// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp"
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].reason` // +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].reason`
// +kubebuilder:printcolumn:name="Capabilities",type=string,JSONPath=`.status.capabilities`
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status` // +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
// +kubebuilder:subresource:status // +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster,categories={externalsecrets},shortName=css // +kubebuilder:resource:scope=Cluster,categories={externalsecrets},shortName=css

View file

@ -37,6 +37,7 @@ import (
genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1" genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
"github.com/external-secrets/external-secrets/pkg/controllers/clusterexternalsecret" "github.com/external-secrets/external-secrets/pkg/controllers/clusterexternalsecret"
"github.com/external-secrets/external-secrets/pkg/controllers/externalsecret" "github.com/external-secrets/external-secrets/pkg/controllers/externalsecret"
"github.com/external-secrets/external-secrets/pkg/controllers/pushsecret"
"github.com/external-secrets/external-secrets/pkg/controllers/secretstore" "github.com/external-secrets/external-secrets/pkg/controllers/secretstore"
awsauth "github.com/external-secrets/external-secrets/pkg/provider/aws/auth" awsauth "github.com/external-secrets/external-secrets/pkg/provider/aws/auth"
"github.com/external-secrets/external-secrets/pkg/provider/vault" "github.com/external-secrets/external-secrets/pkg/provider/vault"
@ -61,6 +62,7 @@ var (
namespace string namespace string
enableClusterStoreReconciler bool enableClusterStoreReconciler bool
enableClusterExternalSecretReconciler bool enableClusterExternalSecretReconciler bool
enablePushSecretReconciler bool
enableFloodGate bool enableFloodGate bool
storeRequeueInterval time.Duration storeRequeueInterval time.Duration
serviceName, serviceNamespace string serviceName, serviceNamespace string
@ -166,6 +168,18 @@ var rootCmd = &cobra.Command{
setupLog.Error(err, errCreateController, "controller", "ExternalSecret") setupLog.Error(err, errCreateController, "controller", "ExternalSecret")
os.Exit(1) os.Exit(1)
} }
if enablePushSecretReconciler {
if err = (&pushsecret.Reconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("PushSecret"),
Scheme: mgr.GetScheme(),
ControllerClass: controllerClass,
RequeueInterval: time.Hour,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, errCreateController, "controller", "PushSecret")
os.Exit(1)
}
}
if enableClusterExternalSecretReconciler { if enableClusterExternalSecretReconciler {
if err = (&clusterexternalsecret.Reconciler{ if err = (&clusterexternalsecret.Reconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
@ -210,10 +224,11 @@ func init() {
rootCmd.Flags().IntVar(&clientBurst, "client-burst", 0, "Maximum Burst allowed to be passed to rest.Client") rootCmd.Flags().IntVar(&clientBurst, "client-burst", 0, "Maximum Burst allowed to be passed to rest.Client")
rootCmd.Flags().StringVar(&loglevel, "loglevel", "info", "loglevel to use, one of: debug, info, warn, error, dpanic, panic, fatal") rootCmd.Flags().StringVar(&loglevel, "loglevel", "info", "loglevel to use, one of: debug, info, warn, error, dpanic, panic, fatal")
rootCmd.Flags().StringVar(&namespace, "namespace", "", "watch external secrets scoped in the provided namespace only. ClusterSecretStore can be used but only work if it doesn't reference resources from other namespaces") rootCmd.Flags().StringVar(&namespace, "namespace", "", "watch external secrets scoped in the provided namespace only. ClusterSecretStore can be used but only work if it doesn't reference resources from other namespaces")
rootCmd.Flags().BoolVar(&enableClusterStoreReconciler, "enable-cluster-store-reconciler", true, "Enables the cluster store reconciler.") rootCmd.Flags().BoolVar(&enableClusterStoreReconciler, "enable-cluster-store-reconciler", true, "Enable cluster store reconciler.")
rootCmd.Flags().BoolVar(&enableClusterExternalSecretReconciler, "enable-cluster-external-secret-reconciler", true, "Enables the cluster external secret reconciler.") rootCmd.Flags().BoolVar(&enableClusterExternalSecretReconciler, "enable-cluster-external-secret-reconciler", true, "Enable cluster external secret reconciler.")
rootCmd.Flags().BoolVar(&enableSecretsCache, "enable-secrets-caching", false, "Enables the secrets caching for external-secrets pod.") rootCmd.Flags().BoolVar(&enablePushSecretReconciler, "enable-push-secret-reconciler", true, "Enable push secret reconciler.")
rootCmd.Flags().BoolVar(&enableConfigMapsCache, "enable-configmaps-caching", false, "Enables the ConfigMap caching for external-secrets pod.") rootCmd.Flags().BoolVar(&enableSecretsCache, "enable-secrets-caching", false, "Enable secrets caching for external-secrets pod.")
rootCmd.Flags().BoolVar(&enableConfigMapsCache, "enable-configmaps-caching", false, "Enable secrets caching for external-secrets pod.")
rootCmd.Flags().DurationVar(&storeRequeueInterval, "store-requeue-interval", time.Minute*5, "Default Time duration between reconciling (Cluster)SecretStores") rootCmd.Flags().DurationVar(&storeRequeueInterval, "store-requeue-interval", time.Minute*5, "Default Time duration between reconciling (Cluster)SecretStores")
rootCmd.Flags().BoolVar(&enableFloodGate, "enable-flood-gate", true, "Enable flood gate. External secret will be reconciled only if the ClusterStore or Store have an healthy or unknown state.") rootCmd.Flags().BoolVar(&enableFloodGate, "enable-flood-gate", true, "Enable flood gate. External secret will be reconciled only if the ClusterStore or Store have an healthy or unknown state.")
rootCmd.Flags().BoolVar(&enableAWSSession, "experimental-enable-aws-session-cache", false, "Enable experimental AWS session cache. External secret will reuse the AWS session without creating a new one on each request.") rootCmd.Flags().BoolVar(&enableAWSSession, "experimental-enable-aws-session-cache", false, "Enable experimental AWS session cache. External secret will reuse the AWS session without creating a new one on each request.")

View file

@ -1512,6 +1512,9 @@ spec:
- jsonPath: .status.conditions[?(@.type=="Ready")].reason - jsonPath: .status.conditions[?(@.type=="Ready")].reason
name: Status name: Status
type: string type: string
- jsonPath: .status.capabilities
name: Capabilities
type: string
- jsonPath: .status.conditions[?(@.type=="Ready")].status - jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready name: Ready
type: string type: string
@ -3331,6 +3334,10 @@ spec:
status: status:
description: SecretStoreStatus defines the observed state of the SecretStore. description: SecretStoreStatus defines the observed state of the SecretStore.
properties: properties:
capabilities:
description: SecretStoreCapabilities defines the possible operations
a SecretStore can do.
type: string
conditions: conditions:
items: items:
properties: properties:

View file

@ -0,0 +1,232 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.10.0
creationTimestamp: null
name: pushsecrets.external-secrets.io
spec:
group: external-secrets.io
names:
categories:
- pushsecrets
kind: PushSecret
listKind: PushSecretList
plural: pushsecrets
singular: pushsecret
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .metadata.creationTimestamp
name: AGE
type: date
- jsonPath: .status.conditions[?(@.type=="Ready")].reason
name: Status
type: string
name: v1alpha1
schema:
openAPIV3Schema:
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: PushSecretSpec configures the behavior of the PushSecret.
properties:
data:
description: Secret Data that should be pushed to providers
items:
properties:
match:
description: Match a given Secret Key to be pushed to the provider.
properties:
remoteRef:
description: Remote Refs to push to providers.
properties:
remoteKey:
description: Name of the resulting provider secret.
type: string
required:
- remoteKey
type: object
secretKey:
description: Secret Key to be pushed
type: string
required:
- remoteRef
- secretKey
type: object
required:
- match
type: object
type: array
deletionPolicy:
default: None
description: 'Deletion Policy to handle Secrets in the provider. Possible
Values: "Delete/None". Defaults to "None".'
type: string
refreshInterval:
description: The Interval to which External Secrets will try to push
a secret definition
type: string
secretStoreRefs:
items:
properties:
kind:
default: SecretStore
description: Kind of the SecretStore resource (SecretStore or
ClusterSecretStore) Defaults to `SecretStore`
type: string
labelSelector:
description: Optionally, sync to secret stores with label selector
properties:
matchExpressions:
description: matchExpressions is a list of label selector
requirements. The requirements are ANDed.
items:
description: A label selector requirement is a selector
that contains values, a key, and an operator that relates
the key and values.
properties:
key:
description: key is the label key that the selector
applies to.
type: string
operator:
description: operator represents a key's relationship
to a set of values. Valid operators are In, NotIn,
Exists and DoesNotExist.
type: string
values:
description: values is an array of string values.
If the operator is In or NotIn, the values array
must be non-empty. If the operator is Exists or
DoesNotExist, the values array must be empty. This
array is replaced during a strategic merge patch.
items:
type: string
type: array
required:
- key
- operator
type: object
type: array
matchLabels:
additionalProperties:
type: string
description: matchLabels is a map of {key,value} pairs.
A single {key,value} in the matchLabels map is equivalent
to an element of matchExpressions, whose key field is
"key", the operator is "In", and the values array contains
only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
name:
description: Optionally, sync to the SecretStore of the given
name
type: string
type: object
type: array
selector:
description: The Secret Selector (k8s source) for the Push Secret
properties:
secret:
description: Select a Secret to Push.
properties:
name:
description: Name of the Secret. The Secret must exist in
the same namespace as the PushSecret manifest.
type: string
required:
- name
type: object
required:
- secret
type: object
required:
- secretStoreRefs
- selector
type: object
status:
description: PushSecretStatus indicates the history of the status of PushSecret.
properties:
conditions:
items:
description: PushSecretStatusCondition indicates the status of the
PushSecret.
properties:
lastTransitionTime:
format: date-time
type: string
message:
type: string
reason:
type: string
status:
type: string
type:
description: PushSecretConditionType indicates the condition
of the PushSecret.
type: string
required:
- status
- type
type: object
type: array
refreshTime:
description: refreshTime is the time and date the external secret
was fetched and the target secret updated
format: date-time
nullable: true
type: string
syncedPushSecrets:
additionalProperties:
additionalProperties:
properties:
match:
description: Match a given Secret Key to be pushed to the
provider.
properties:
remoteRef:
description: Remote Refs to push to providers.
properties:
remoteKey:
description: Name of the resulting provider secret.
type: string
required:
- remoteKey
type: object
secretKey:
description: Secret Key to be pushed
type: string
required:
- remoteRef
- secretKey
type: object
required:
- match
type: object
type: object
description: Synced Push Secrets for later deletion. Matches Secret
Stores to PushSecretData that was stored to that secretStore.
type: object
syncedResourceVersion:
description: SyncedResourceVersion keeps track of the last synced
version.
type: string
type: object
type: object
served: true
storage: true
subresources:
status: {}

View file

@ -1512,6 +1512,9 @@ spec:
- jsonPath: .status.conditions[?(@.type=="Ready")].reason - jsonPath: .status.conditions[?(@.type=="Ready")].reason
name: Status name: Status
type: string type: string
- jsonPath: .status.capabilities
name: Capabilities
type: string
- jsonPath: .status.conditions[?(@.type=="Ready")].status - jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready name: Ready
type: string type: string
@ -3331,6 +3334,10 @@ spec:
status: status:
description: SecretStoreStatus defines the observed state of the SecretStore. description: SecretStoreStatus defines the observed state of the SecretStore.
properties: properties:
capabilities:
description: SecretStoreCapabilities defines the possible operations
a SecretStore can do.
type: string
conditions: conditions:
items: items:
properties: properties:

View file

@ -77,6 +77,7 @@ The command removes all the Kubernetes components associated with the chart and
| controllerClass | string | `""` | If set external secrets will filter matching Secret Stores with the appropriate controller values. | | controllerClass | string | `""` | If set external secrets will filter matching Secret Stores with the appropriate controller values. |
| crds.createClusterExternalSecret | bool | `true` | If true, create CRDs for Cluster External Secret. | | crds.createClusterExternalSecret | bool | `true` | If true, create CRDs for Cluster External Secret. |
| crds.createClusterSecretStore | bool | `true` | If true, create CRDs for Cluster Secret Store. | | crds.createClusterSecretStore | bool | `true` | If true, create CRDs for Cluster Secret Store. |
| crds.createPushSecret | bool | `true` | If true, create CRDs for Push Secret. |
| createOperator | bool | `true` | Specifies whether an external secret operator deployment be created. | | createOperator | bool | `true` | Specifies whether an external secret operator deployment be created. |
| deploymentAnnotations | object | `{}` | Annotations to add to Deployment | | deploymentAnnotations | object | `{}` | Annotations to add to Deployment |
| dnsConfig | object | `{}` | Specifies `dnsOptions` to deployment | | dnsConfig | object | `{}` | Specifies `dnsOptions` to deployment |

View file

@ -20,6 +20,7 @@ rules:
- "clustersecretstores" - "clustersecretstores"
- "externalsecrets" - "externalsecrets"
- "clusterexternalsecrets" - "clusterexternalsecrets"
- "pushsecrets"
verbs: verbs:
- "get" - "get"
- "list" - "list"
@ -39,6 +40,9 @@ rules:
- "clusterexternalsecrets" - "clusterexternalsecrets"
- "clusterexternalsecrets/status" - "clusterexternalsecrets/status"
- "clusterexternalsecrets/finalizers" - "clusterexternalsecrets/finalizers"
- "pushsecrets"
- "pushsecrets/status"
- "pushsecrets/finalizers"
verbs: verbs:
- "update" - "update"
- "patch" - "patch"
@ -128,6 +132,7 @@ rules:
- "externalsecrets" - "externalsecrets"
- "secretstores" - "secretstores"
- "clustersecretstores" - "clustersecretstores"
- "pushsecrets"
verbs: verbs:
- "get" - "get"
- "watch" - "watch"
@ -155,6 +160,7 @@ rules:
- "externalsecrets" - "externalsecrets"
- "secretstores" - "secretstores"
- "clustersecretstores" - "clustersecretstores"
- "pushsecrets"
verbs: verbs:
- "create" - "create"
- "delete" - "delete"

View file

@ -17,6 +17,8 @@ crds:
createClusterExternalSecret: true createClusterExternalSecret: true
# -- If true, create CRDs for Cluster Secret Store. # -- If true, create CRDs for Cluster Secret Store.
createClusterSecretStore: true createClusterSecretStore: true
# -- If true, create CRDs for Push Secret.
createPushSecret: true
imagePullSecrets: [] imagePullSecrets: []
nameOverride: "" nameOverride: ""

View file

@ -1545,6 +1545,9 @@ spec:
- jsonPath: .status.conditions[?(@.type=="Ready")].reason - jsonPath: .status.conditions[?(@.type=="Ready")].reason
name: Status name: Status
type: string type: string
- jsonPath: .status.capabilities
name: Capabilities
type: string
- jsonPath: .status.conditions[?(@.type=="Ready")].status - jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready name: Ready
type: string type: string
@ -2857,6 +2860,9 @@ spec:
status: status:
description: SecretStoreStatus defines the observed state of the SecretStore. description: SecretStoreStatus defines the observed state of the SecretStore.
properties: properties:
capabilities:
description: SecretStoreCapabilities defines the possible operations a SecretStore can do.
type: string
conditions: conditions:
items: items:
properties: properties:
@ -3504,6 +3510,220 @@ spec:
--- ---
apiVersion: apiextensions.k8s.io/v1 apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.10.0
creationTimestamp: null
name: pushsecrets.external-secrets.io
spec:
group: external-secrets.io
names:
categories:
- pushsecrets
kind: PushSecret
listKind: PushSecretList
plural: pushsecrets
singular: pushsecret
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .metadata.creationTimestamp
name: AGE
type: date
- jsonPath: .status.conditions[?(@.type=="Ready")].reason
name: Status
type: string
name: v1alpha1
schema:
openAPIV3Schema:
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: PushSecretSpec configures the behavior of the PushSecret.
properties:
data:
description: Secret Data that should be pushed to providers
items:
properties:
match:
description: Match a given Secret Key to be pushed to the provider.
properties:
remoteRef:
description: Remote Refs to push to providers.
properties:
remoteKey:
description: Name of the resulting provider secret.
type: string
required:
- remoteKey
type: object
secretKey:
description: Secret Key to be pushed
type: string
required:
- remoteRef
- secretKey
type: object
required:
- match
type: object
type: array
deletionPolicy:
default: None
description: 'Deletion Policy to handle Secrets in the provider. Possible Values: "Delete/None". Defaults to "None".'
type: string
refreshInterval:
description: The Interval to which External Secrets will try to push a secret definition
type: string
secretStoreRefs:
items:
properties:
kind:
default: SecretStore
description: Kind of the SecretStore resource (SecretStore or ClusterSecretStore) Defaults to `SecretStore`
type: string
labelSelector:
description: Optionally, sync to secret stores with label selector
properties:
matchExpressions:
description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
items:
description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.
properties:
key:
description: key is the label key that the selector applies to.
type: string
operator:
description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.
items:
type: string
type: array
required:
- key
- operator
type: object
type: array
matchLabels:
additionalProperties:
type: string
description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
name:
description: Optionally, sync to the SecretStore of the given name
type: string
type: object
type: array
selector:
description: The Secret Selector (k8s source) for the Push Secret
properties:
secret:
description: Select a Secret to Push.
properties:
name:
description: Name of the Secret. The Secret must exist in the same namespace as the PushSecret manifest.
type: string
required:
- name
type: object
required:
- secret
type: object
required:
- secretStoreRefs
- selector
type: object
status:
description: PushSecretStatus indicates the history of the status of PushSecret.
properties:
conditions:
items:
description: PushSecretStatusCondition indicates the status of the PushSecret.
properties:
lastTransitionTime:
format: date-time
type: string
message:
type: string
reason:
type: string
status:
type: string
type:
description: PushSecretConditionType indicates the condition of the PushSecret.
type: string
required:
- status
- type
type: object
type: array
refreshTime:
description: refreshTime is the time and date the external secret was fetched and the target secret updated
format: date-time
nullable: true
type: string
syncedPushSecrets:
additionalProperties:
additionalProperties:
properties:
match:
description: Match a given Secret Key to be pushed to the provider.
properties:
remoteRef:
description: Remote Refs to push to providers.
properties:
remoteKey:
description: Name of the resulting provider secret.
type: string
required:
- remoteKey
type: object
secretKey:
description: Secret Key to be pushed
type: string
required:
- remoteRef
- secretKey
type: object
required:
- match
type: object
type: object
description: Synced Push Secrets for later deletion. Matches Secret Stores to PushSecretData that was stored to that secretStore.
type: object
syncedResourceVersion:
description: SyncedResourceVersion keeps track of the last synced version.
type: string
type: object
type: object
served: true
storage: true
subresources:
status: {}
conversion:
strategy: Webhook
webhook:
conversionReviewVersions:
- v1
clientConfig:
service:
name: kubernetes
namespace: default
path: /convert
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata: metadata:
annotations: annotations:
controller-gen.kubebuilder.io/version: v0.10.0 controller-gen.kubebuilder.io/version: v0.10.0
@ -4604,6 +4824,9 @@ spec:
- jsonPath: .status.conditions[?(@.type=="Ready")].reason - jsonPath: .status.conditions[?(@.type=="Ready")].reason
name: Status name: Status
type: string type: string
- jsonPath: .status.capabilities
name: Capabilities
type: string
- jsonPath: .status.conditions[?(@.type=="Ready")].status - jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready name: Ready
type: string type: string
@ -5916,6 +6139,9 @@ spec:
status: status:
description: SecretStoreStatus defines the observed state of the SecretStore. description: SecretStoreStatus defines the observed state of the SecretStore.
properties: properties:
capabilities:
description: SecretStoreCapabilities defines the possible operations a SecretStore can do.
type: string
conditions: conditions:
items: items:
properties: properties:

View file

@ -1,8 +1,8 @@
```yaml ```yaml
--- ---
title: SecretSink title: PushSecret
version: v1alpha1 version: v1alpha1
authors: authors:
creation-date: 2022-01-25 creation-date: 2022-01-25
status: draft status: draft
--- ---
@ -18,7 +18,7 @@ status: draft
## Summary ## Summary
The Secret Sink is a feature to allow Secrets from Kubernetes to be saved back into some providers. Where ExternalSecret is responsible to download a Secret from a Provider into Kubernetes (as a K8s Secret), SecretSink will upload a Kubernetes Secret to a Provider. The Secret Sink is a feature to allow Secrets from Kubernetes to be saved back into some providers. Where ExternalSecret is responsible to download a Secret from a Provider into Kubernetes (as a K8s Secret), PushSecret will upload a Kubernetes Secret to a Provider.
## Motivation ## Motivation
Secret Sink allows some inCluster generated secrets to also be available on a given secret provider. It also allows multiple Providers having the same secret (which means a way to perform failover in case a given secret provider is on downtime or compromised for whatever the reason). Secret Sink allows some inCluster generated secrets to also be available on a given secret provider. It also allows multiple Providers having the same secret (which means a way to perform failover in case a given secret provider is on downtime or compromised for whatever the reason).
@ -26,7 +26,7 @@ Secret Sink allows some inCluster generated secrets to also be available on a gi
### Goals ### Goals
- CRD Design for the SecretSink - CRD Design for the SecretSink
- Define the need for a SinkStore - Define the need for a SinkStore
- -
### Non-Goals ### Non-Goals
Do not implement full compatibility mechanisms with each provider (we are not Terraform neither Crossplane) Do not implement full compatibility mechanisms with each provider (we are not Terraform neither Crossplane)
@ -113,7 +113,7 @@ spec:
```yaml ```yaml
apiVersion: external-secrets.io/v1alpha1 apiVersion: external-secrets.io/v1alpha1
kind: SecretSink kind: PushSecret
metadata: metadata:
name: "hello-world" name: "hello-world"
namespace: my-ns # Same of the SecretStores namespace: my-ns # Same of the SecretStores
@ -130,17 +130,17 @@ spec:
secret: secret:
name: foobar name: foobar
data: data:
match: - match:
- secretKey: foobar secretKey: foobar
remoteRefs: remoteRefs:
- remoteKey: my/path/foobar - remoteKey: my/path/foobar
property: my-property #optional. To allow coming back from a 'dataFrom' property: my-property #optional. To allow coming back from a 'dataFrom'
- remoteKey: secret/my-path-foobar - remoteKey: secret/my-path-foobar
property: another-property property: another-property
rewrite: rewrite:
- secretKey: game-(.+).(.+) secretKey: game-(.+).(.+)
remoteRefs: remoteRefs:
- remoteKey: my/path/($1) - remoteKey: my/path/($1)
property: prop-($2) property: prop-($2)
- remoteKey: my-path-($1)-($2) #Applies this way to all other secretStores - remoteKey: my-path-($1)-($2) #Applies this way to all other secretStores
@ -148,7 +148,7 @@ status:
refreshTime: "2019-08-12T12:33:02Z" refreshTime: "2019-08-12T12:33:02Z"
conditions: conditions:
- type: Ready - type: Ready
status: "True" status: "True"
reason: "SecretSynced" reason: "SecretSynced"
message: "Secret was synced" #Fully synced message: "Secret was synced" #Fully synced
lastTransitionTime: "2019-08-12T12:33:02Z" lastTransitionTime: "2019-08-12T12:33:02Z"
@ -165,7 +165,7 @@ status:
``` ```
### Behavior ### Behavior
When checking SecretSink for the Source Secret, check existing labels for SecretStore reference of that particular Secret. If this SecretStore reference is an object in SecretSink SecretStore lists, a SecretSyncError should be emited as we cannot sync the secret to the same SecretStore. When checking PushSecret for the Source Secret, check existing labels for SecretStore reference of that particular Secret. If this SecretStore reference is an object in PushSecret SecretStore lists, a SecretSyncError should be emited as we cannot sync the secret to the same SecretStore.
If the SecretStores are all fine or if the Secret has no labels (secret created by user / another tool), for Each SecretStore, get the SyncState of this store (New, SecretSynced, SecretSyncedErr). If the SecretStores are all fine or if the Secret has no labels (secret created by user / another tool), for Each SecretStore, get the SyncState of this store (New, SecretSynced, SecretSyncedErr).
@ -177,9 +177,9 @@ We had several discussions on how to implement this feature, and it turns out ju
### Acceptance Criteria ### Acceptance Criteria
+ ExternalSecrets create appropriate labels on generated Secrets + ExternalSecrets create appropriate labels on generated Secrets
+ SecretSinks can read labels on source Secrets + PushSecrets can read labels on source Secrets
+ SecretSinks cannot have same references to SecretStores + PushSecrets cannot have same references to SecretStores
+ SecretSinks respect refreshInterval + PushSecrets respect refreshInterval
## Alternatives ## Alternatives
Using some integration with Crossplane can allow to sync the secrets. Cons is this must be either manual or through some integration that would be an independent project on its own. Using some integration with Crossplane can allow to sync the secrets. Cons is this must be either manual or through some integration that would be an independent project on its own.

5
docs/api/pushsecret.md Normal file
View file

@ -0,0 +1,5 @@
The `PushSecret` is namespaced and specifies how to push secrets to secret stores.
``` yaml
{% include 'full-pushsecret.yaml' %}
```

View file

@ -3621,6 +3621,11 @@ github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
<p> <p>
<p>Provider is a common interface for interacting with secret backends.</p> <p>Provider is a common interface for interacting with secret backends.</p>
</p> </p>
<h3 id="external-secrets.io/v1beta1.PushRemoteRef">PushRemoteRef
</h3>
<p>
<p>This interface is to allow using v1alpha1 content in Provider registered in v1beta1.</p>
</p>
<h3 id="external-secrets.io/v1beta1.SecretStore">SecretStore <h3 id="external-secrets.io/v1beta1.SecretStore">SecretStore
</h3> </h3>
<p> <p>
@ -3730,6 +3735,30 @@ SecretStoreStatus
</tr> </tr>
</tbody> </tbody>
</table> </table>
<h3 id="external-secrets.io/v1beta1.SecretStoreCapabilities">SecretStoreCapabilities
(<code>string</code> alias)</p></h3>
<p>
(<em>Appears on:</em>
<a href="#external-secrets.io/v1beta1.SecretStoreStatus">SecretStoreStatus</a>)
</p>
<p>
<p>SecretStoreCapabilities defines the possible operations a SecretStore can do.</p>
</p>
<table>
<thead>
<tr>
<th>Value</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr><td><p>&#34;ReadOnly&#34;</p></td>
<td></td>
</tr><tr><td><p>&#34;ReadWrite&#34;</p></td>
<td></td>
</tr><tr><td><p>&#34;WriteOnly&#34;</p></td>
<td></td>
</tr></tbody>
</table>
<h3 id="external-secrets.io/v1beta1.SecretStoreConditionType">SecretStoreConditionType <h3 id="external-secrets.io/v1beta1.SecretStoreConditionType">SecretStoreConditionType
(<code>string</code> alias)</p></h3> (<code>string</code> alias)</p></h3>
<p> <p>
@ -4190,6 +4219,19 @@ int
<em>(Optional)</em> <em>(Optional)</em>
</td> </td>
</tr> </tr>
<tr>
<td>
<code>capabilities</code></br>
<em>
<a href="#external-secrets.io/v1beta1.SecretStoreCapabilities">
SecretStoreCapabilities
</a>
</em>
</td>
<td>
<em>(Optional)</em>
</td>
</tr>
</tbody> </tbody>
</table> </table>
<h3 id="external-secrets.io/v1beta1.SecretStoreStatusCondition">SecretStoreStatusCondition <h3 id="external-secrets.io/v1beta1.SecretStoreStatusCondition">SecretStoreStatusCondition

View file

@ -29,9 +29,11 @@ Create a IAM Policy to pin down access to secrets matching `dev-*`, for further
{ {
"Effect": "Allow", "Effect": "Allow",
"Action": [ "Action": [
"ssm:GetParameter*" "ssm:GetParameterWithContext",
"ssm:ListTagsForResourceWithContext",
"ssm:DescribeParametersWithContext",
], ],
"Resource": "arn:aws:ssm:us-east-2:123456789012:parameter/dev-*" "Resource": "arn:aws:ssm:us-east-2:1234567889911:parameter/dev-*"
} }
] ]
} }
@ -71,5 +73,54 @@ spec:
property: friends.1.first # Roger property: friends.1.first # Roger
``` ```
### Parameter Versions
ParameterStore creates a new version of a parameter every time it is updated with a new value. The parameter can be referenced via the `version` property
## SetSecret
The SetSecret method for the Parameter Store allows the user to set the value stored within the Kubernetes cluster to the remote AWS Parameter Store.
### Creating a Push Secret
```yaml
{% include "full-pushsecret.yaml" %}
```
#### Check successful secret sync
To be able to check that the secret has been succesfully synced you can run the following command:
```bash
kubectl get pushsecret pushsecret-example
```
If the secret has synced successfully it will show the status as "Synced".
#### Test new secret using AWS CLI
To View your parameter on AWS Parameter Store using the AWS CLI, install and login to the AWS CLI using the following guide: [AWS CLI](https://aws.amazon.com/cli/).
Run the following commands to get your synchronized parameter from AWS Parameter Store:
```bash
aws ssm get-parameter --name=my-first-parameter --region=us-east-1
```
You should see something similar to the following output:
```json
{
"Parameter": {
"Name": "my-first-parameter",
"Type": "String",
"Value": "charmander",
"Version": 4,
"LastModifiedDate": "2022-09-15T13:04:31.098000-03:00",
"ARN": "arn:aws:ssm:us-east-1:1234567890123:parameter/my-first-parameter",
"DataType": "text"
}
}
```
--8<-- "snippets/provider-aws-access.md" --8<-- "snippets/provider-aws-access.md"

View file

@ -0,0 +1,6 @@
## Push Secret
### IAM Policy

View file

@ -0,0 +1,18 @@
apiVersion: external-secrets.io/v1alpha1
kind: PushSecret
metadata:
name: pushsecret-example # Customisable
namespace: default # Same of the SecretStores
spec:
refreshInterval: 10s # Refresh interval for which push secret will reconcile
secretStoreRefs: # A list of secret stores to push secrets to
- name: aws-parameterstore
kind: SecretStore
selector:
secret:
name: pokedex-credentials # Source Kubernetes secret to be pushed
data:
- match:
secretKey: best-pokemon # Source Kubernetes secret key to be pushed
remoteRefs:
- remoteKey: my-first-parameter # Remote reference (where the secret is going to be pushed)

View file

@ -32,7 +32,7 @@ import (
vault "github.com/hashicorp/vault/api" vault "github.com/hashicorp/vault/api"
// nolint // nolint
. "github.com/onsi/ginkgo/v2" ginkgo "github.com/onsi/ginkgo/v2"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -93,7 +93,7 @@ type OperatorInitResponse struct {
} }
func (l *Vault) Install() error { func (l *Vault) Install() error {
By("Installing vault in " + l.Namespace) ginkgo.By("Installing vault in " + l.Namespace)
err := l.chart.Install() err := l.chart.Install()
if err != nil { if err != nil {
return err return err
@ -168,13 +168,13 @@ func (l *Vault) initVault() error {
l.KubernetesAuthPath = "mykubernetes" // see configure-vault.sh l.KubernetesAuthPath = "mykubernetes" // see configure-vault.sh
l.KubernetesAuthRole = "external-secrets-operator" // see configure-vault.sh l.KubernetesAuthRole = "external-secrets-operator" // see configure-vault.sh
By("Creating vault TLS secret") ginkgo.By("Creating vault TLS secret")
err = l.chart.config.CRClient.Create(context.Background(), sec) err = l.chart.config.CRClient.Create(context.Background(), sec)
if err != nil { if err != nil {
return err return err
} }
By("Waiting for vault pods to be running") ginkgo.By("Waiting for vault pods to be running")
pl, err := util.WaitForPodsRunning(l.chart.config.KubeClientSet, 1, l.Namespace, metav1.ListOptions{ pl, err := util.WaitForPodsRunning(l.chart.config.KubeClientSet, 1, l.Namespace, metav1.ListOptions{
LabelSelector: "app.kubernetes.io/name=vault", LabelSelector: "app.kubernetes.io/name=vault",
}) })
@ -183,7 +183,7 @@ func (l *Vault) initVault() error {
} }
l.PodName = pl.Items[0].Name l.PodName = pl.Items[0].Name
By("Initializing vault") ginkgo.By("Initializing vault")
out, err := util.ExecCmd( out, err := util.ExecCmd(
l.chart.config.KubeClientSet, l.chart.config.KubeClientSet,
l.chart.config.KubeConfig, l.chart.config.KubeConfig,
@ -192,7 +192,7 @@ func (l *Vault) initVault() error {
return fmt.Errorf("error initializing vault: %w", err) return fmt.Errorf("error initializing vault: %w", err)
} }
By("Parsing init response") ginkgo.By("Parsing init response")
var res OperatorInitResponse var res OperatorInitResponse
err = json.Unmarshal([]byte(out), &res) err = json.Unmarshal([]byte(out), &res)
if err != nil { if err != nil {
@ -200,7 +200,7 @@ func (l *Vault) initVault() error {
} }
l.RootToken = res.RootToken l.RootToken = res.RootToken
By("Unsealing vault") ginkgo.By("Unsealing vault")
for _, k := range res.UnsealKeysB64 { for _, k := range res.UnsealKeysB64 {
_, err = util.ExecCmd( _, err = util.ExecCmd(
l.chart.config.KubeClientSet, l.chart.config.KubeClientSet,
@ -238,7 +238,7 @@ func (l *Vault) initVault() error {
} }
func (l *Vault) configureVault() error { func (l *Vault) configureVault() error {
By("configuring vault") ginkgo.By("configuring vault")
cmd := `sh /etc/vault-config/configure-vault.sh %s` cmd := `sh /etc/vault-config/configure-vault.sh %s`
_, err := util.ExecCmd( _, err := util.ExecCmd(
l.chart.config.KubeClientSet, l.chart.config.KubeClientSet,

1
go.mod
View file

@ -96,6 +96,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0
github.com/hashicorp/golang-lru v0.5.4 github.com/hashicorp/golang-lru v0.5.4
github.com/maxbrunsfeld/counterfeiter/v6 v6.5.0
github.com/sethvargo/go-password v0.2.0 github.com/sethvargo/go-password v0.2.0
sigs.k8s.io/yaml v1.3.0 sigs.k8s.io/yaml v1.3.0
) )

3
go.sum
View file

@ -596,6 +596,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/maxbrunsfeld/counterfeiter/v6 v6.5.0 h1:rBhB9Rls+yb8kA4x5a/cWxOufWfXt24E+kq4YlbGj3g=
github.com/maxbrunsfeld/counterfeiter/v6 v6.5.0/go.mod h1:fJ0UAZc1fx3xZhU4eSHQDJ1ApFmTVhp5VTpV9tm2ogg=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
@ -732,6 +734,7 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI= github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI=
github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE= github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE=

View file

@ -49,6 +49,7 @@ nav:
- SecretStore: api/secretstore.md - SecretStore: api/secretstore.md
- ClusterSecretStore: api/clustersecretstore.md - ClusterSecretStore: api/clustersecretstore.md
- ClusterExternalSecret: api/clusterexternalsecret.md - ClusterExternalSecret: api/clusterexternalsecret.md
- PushSecret: api/pushsecret.md
- Generators: - Generators:
- "api/generator/index.md" - "api/generator/index.md"
- Azure Container Registry: api/generator/acr.md - Azure Container Registry: api/generator/acr.md

View file

@ -21,6 +21,8 @@ for i in "${HELM_DIR}"/templates/crds/*.yml; do
cp "$i" "$i.bkp" cp "$i" "$i.bkp"
if [[ "$CRDS_FLAG_NAME" == *"Cluster"* ]]; then if [[ "$CRDS_FLAG_NAME" == *"Cluster"* ]]; then
echo "{{- if and (.Values.installCRDs) (.Values.crds.$CRDS_FLAG_NAME) }}" > "$i" echo "{{- if and (.Values.installCRDs) (.Values.crds.$CRDS_FLAG_NAME) }}" > "$i"
elif [[ "$$CRDS_FLAG_NAME" == *"PushSecret"* ]]; then
echo "{{- if and (.Values.installCRDs) (.Values.crds.$$CRDS_FLAG_NAME) }}" > "$$i"
else else
echo "{{- if .Values.installCRDs }}" > "$i" echo "{{- if .Values.installCRDs }}" > "$i"
fi fi

View file

@ -29,10 +29,10 @@ import (
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1" genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
"github.com/external-secrets/external-secrets/pkg/controllers/externalsecret/clientmanager" // Loading registered providers.
"github.com/external-secrets/external-secrets/pkg/controllers/secretstore"
// Loading registered generators. // Loading registered generators.
_ "github.com/external-secrets/external-secrets/pkg/generator/register" _ "github.com/external-secrets/external-secrets/pkg/generator/register"
// Loading registered providers.
_ "github.com/external-secrets/external-secrets/pkg/provider/register" _ "github.com/external-secrets/external-secrets/pkg/provider/register"
"github.com/external-secrets/external-secrets/pkg/utils" "github.com/external-secrets/external-secrets/pkg/utils"
) )
@ -43,7 +43,7 @@ func (r *Reconciler) getProviderSecretData(ctx context.Context, externalSecret *
// Clientmanager keeps track of the client instances // Clientmanager keeps track of the client instances
// that are created during the fetching process and closes clients // that are created during the fetching process and closes clients
// if needed. // if needed.
mgr := clientmanager.New(r.Client, r.ControllerClass, r.EnableFloodGate) mgr := secretstore.NewManager(r.Client, r.ControllerClass, r.EnableFloodGate)
defer mgr.Close(ctx) defer mgr.Close(ctx)
providerData := make(map[string][]byte) providerData := make(map[string][]byte)
@ -87,7 +87,7 @@ func (r *Reconciler) getProviderSecretData(ctx context.Context, externalSecret *
return providerData, nil return providerData, nil
} }
func (r *Reconciler) handleSecretData(ctx context.Context, i int, externalSecret esv1beta1.ExternalSecret, secretRef esv1beta1.ExternalSecretData, providerData map[string][]byte, cmgr *clientmanager.Manager) error { func (r *Reconciler) handleSecretData(ctx context.Context, i int, externalSecret esv1beta1.ExternalSecret, secretRef esv1beta1.ExternalSecretData, providerData map[string][]byte, cmgr *secretstore.Manager) error {
client, err := cmgr.Get(ctx, externalSecret.Spec.SecretStoreRef, externalSecret.Namespace, secretRef.SourceRef) client, err := cmgr.Get(ctx, externalSecret.Spec.SecretStoreRef, externalSecret.Namespace, secretRef.SourceRef)
if err != nil { if err != nil {
return err return err
@ -170,7 +170,7 @@ func (r *Reconciler) getGeneratorDefinition(ctx context.Context, namespace strin
return &apiextensions.JSON{Raw: jsonRes}, nil return &apiextensions.JSON{Raw: jsonRes}, nil
} }
func (r *Reconciler) handleExtractSecrets(ctx context.Context, externalSecret *esv1beta1.ExternalSecret, remoteRef esv1beta1.ExternalSecretDataFromRemoteRef, cmgr *clientmanager.Manager, i int) (map[string][]byte, error) { func (r *Reconciler) handleExtractSecrets(ctx context.Context, externalSecret *esv1beta1.ExternalSecret, remoteRef esv1beta1.ExternalSecretDataFromRemoteRef, cmgr *secretstore.Manager, i int) (map[string][]byte, error) {
client, err := cmgr.Get(ctx, externalSecret.Spec.SecretStoreRef, externalSecret.Namespace, remoteRef.SourceRef) client, err := cmgr.Get(ctx, externalSecret.Spec.SecretStoreRef, externalSecret.Namespace, remoteRef.SourceRef)
if err != nil { if err != nil {
return nil, err return nil, err
@ -199,7 +199,7 @@ func (r *Reconciler) handleExtractSecrets(ctx context.Context, externalSecret *e
return secretMap, err return secretMap, err
} }
func (r *Reconciler) handleFindAllSecrets(ctx context.Context, externalSecret *esv1beta1.ExternalSecret, remoteRef esv1beta1.ExternalSecretDataFromRemoteRef, cmgr *clientmanager.Manager, i int) (map[string][]byte, error) { func (r *Reconciler) handleFindAllSecrets(ctx context.Context, externalSecret *esv1beta1.ExternalSecret, remoteRef esv1beta1.ExternalSecretDataFromRemoteRef, cmgr *secretstore.Manager, i int) (map[string][]byte, error) {
client, err := cmgr.Get(ctx, externalSecret.Spec.SecretStoreRef, externalSecret.Namespace, remoteRef.SourceRef) client, err := cmgr.Get(ctx, externalSecret.Spec.SecretStoreRef, externalSecret.Namespace, remoteRef.SourceRef)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -0,0 +1,398 @@
/*
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 pushsecret
import (
"context"
"fmt"
"strings"
"time"
"github.com/go-logr/logr"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"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/controllerutil"
esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
v1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/controllers/secretstore"
)
const (
errFailedGetSecret = "could not get source secret"
errPatchStatus = "error merging"
errGetSecretStore = "could not get SecretStore %q, %w"
errGetClusterSecretStore = "could not get ClusterSecretStore %q, %w"
errGetProviderFailed = "could not start provider"
errGetSecretsClientFailed = "could not start secrets client"
errCloseStoreClient = "error when calling provider close method"
errSetSecretFailed = "could not write remote ref %v to target secretstore %v: %v"
errFailedSetSecret = "set secret failed: %v"
pushSecretFinalizer = "pushsecret.externalsecrets.io/finalizer"
)
type Reconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
recorder record.EventRecorder
RequeueInterval time.Duration
ControllerClass string
}
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := r.Log.WithValues("pushsecret", req.NamespacedName)
var ps esapi.PushSecret
err := r.Get(ctx, req.NamespacedName, &ps)
mgr := secretstore.NewManager(r.Client, r.ControllerClass, false)
defer mgr.Close(ctx)
if apierrors.IsNotFound(err) {
return ctrl.Result{}, nil
} else if err != nil {
msg := "unable to get PushSecret"
r.recorder.Event(&ps, v1.EventTypeWarning, esapi.ReasonErrored, msg)
log.Error(err, msg)
return ctrl.Result{}, fmt.Errorf("get resource: %w", err)
}
refreshInt := r.RequeueInterval
if ps.Spec.RefreshInterval != nil {
refreshInt = ps.Spec.RefreshInterval.Duration
}
p := client.MergeFrom(ps.DeepCopy())
defer func() {
err := r.Client.Status().Patch(ctx, &ps, p)
if err != nil {
log.Error(err, errPatchStatus)
}
}()
switch ps.Spec.DeletionPolicy {
case esapi.PushSecretDeletionPolicyDelete:
// finalizer logic. Only added if we should delete the secrets
if ps.ObjectMeta.DeletionTimestamp.IsZero() {
if !controllerutil.ContainsFinalizer(&ps, pushSecretFinalizer) {
controllerutil.AddFinalizer(&ps, pushSecretFinalizer)
err := r.Client.Update(ctx, &ps, &client.UpdateOptions{})
if err != nil {
return ctrl.Result{}, fmt.Errorf("could not update finalizers: %w", err)
}
return ctrl.Result{}, nil
}
} else {
if controllerutil.ContainsFinalizer(&ps, pushSecretFinalizer) {
// trigger a cleanup with no Synced Map
badState, err := r.DeleteSecretFromProviders(ctx, &ps, esapi.SyncedPushSecretsMap{}, mgr)
if err != nil {
msg := fmt.Sprintf("Failed to Delete Secrets from Provider: %v", err)
cond := NewPushSecretCondition(esapi.PushSecretReady, v1.ConditionFalse, esapi.ReasonErrored, msg)
ps = SetPushSecretCondition(ps, *cond)
r.SetSyncedSecrets(&ps, badState)
r.recorder.Event(&ps, v1.EventTypeWarning, esapi.ReasonErrored, msg)
return ctrl.Result{}, err
}
controllerutil.RemoveFinalizer(&ps, pushSecretFinalizer)
err = r.Client.Update(ctx, &ps, &client.UpdateOptions{})
if err != nil {
return ctrl.Result{}, fmt.Errorf("could not update finalizers: %w", err)
}
return ctrl.Result{}, nil
}
}
case esapi.PushSecretDeletionPolicyNone:
default:
}
secret, err := r.GetSecret(ctx, ps)
if err != nil {
cond := NewPushSecretCondition(esapi.PushSecretReady, v1.ConditionFalse, esapi.ReasonErrored, errFailedGetSecret)
ps = SetPushSecretCondition(ps, *cond)
r.recorder.Event(&ps, v1.EventTypeWarning, esapi.ReasonErrored, errFailedGetSecret)
return ctrl.Result{}, err
}
secretStores, err := r.GetSecretStores(ctx, ps)
if err != nil {
cond := NewPushSecretCondition(esapi.PushSecretReady, v1.ConditionFalse, esapi.ReasonErrored, err.Error())
ps = SetPushSecretCondition(ps, *cond)
r.recorder.Event(&ps, v1.EventTypeWarning, esapi.ReasonErrored, err.Error())
return ctrl.Result{}, err
}
syncedSecrets, err := r.PushSecretToProviders(ctx, secretStores, ps, secret, mgr)
if err != nil {
msg := fmt.Sprintf(errFailedSetSecret, err)
cond := NewPushSecretCondition(esapi.PushSecretReady, v1.ConditionFalse, esapi.ReasonErrored, msg)
ps = SetPushSecretCondition(ps, *cond)
totalSecrets := mergeSecretState(syncedSecrets, ps.Status.SyncedPushSecrets)
r.SetSyncedSecrets(&ps, totalSecrets)
r.recorder.Event(&ps, v1.EventTypeWarning, esapi.ReasonErrored, msg)
return ctrl.Result{}, err
}
switch ps.Spec.DeletionPolicy {
case esapi.PushSecretDeletionPolicyDelete:
badSyncState, err := r.DeleteSecretFromProviders(ctx, &ps, syncedSecrets, mgr)
if err != nil {
msg := fmt.Sprintf("Failed to Delete Secrets from Provider: %v", err)
cond := NewPushSecretCondition(esapi.PushSecretReady, v1.ConditionFalse, esapi.ReasonErrored, msg)
ps = SetPushSecretCondition(ps, *cond)
r.SetSyncedSecrets(&ps, badSyncState)
r.recorder.Event(&ps, v1.EventTypeWarning, esapi.ReasonErrored, msg)
return ctrl.Result{}, err
}
case esapi.PushSecretDeletionPolicyNone:
default:
}
msg := "PushSecret synced successfully"
cond := NewPushSecretCondition(esapi.PushSecretReady, v1.ConditionTrue, esapi.ReasonSynced, msg)
ps = SetPushSecretCondition(ps, *cond)
r.SetSyncedSecrets(&ps, syncedSecrets)
r.recorder.Event(&ps, v1.EventTypeNormal, esapi.ReasonSynced, msg)
return ctrl.Result{RequeueAfter: refreshInt}, nil
}
func (r *Reconciler) SetSyncedSecrets(ps *esapi.PushSecret, status esapi.SyncedPushSecretsMap) {
ps.Status.SyncedPushSecrets = status
}
func mergeSecretState(newMap, old esapi.SyncedPushSecretsMap) esapi.SyncedPushSecretsMap {
out := newMap.DeepCopy()
for k, v := range old {
_, ok := out[k]
if !ok {
out[k] = make(map[string]esapi.PushSecretData)
}
for kk, vv := range v {
out[k][kk] = vv
}
}
return out
}
func (r *Reconciler) DeleteSecretFromProviders(ctx context.Context, ps *esapi.PushSecret, newMap esapi.SyncedPushSecretsMap, mgr *secretstore.Manager) (esapi.SyncedPushSecretsMap, error) {
out := mergeSecretState(newMap, ps.Status.SyncedPushSecrets)
for storeName, oldData := range ps.Status.SyncedPushSecrets {
storeRef := v1beta1.SecretStoreRef{
Name: strings.Split(storeName, "/")[1],
Kind: strings.Split(storeName, "/")[0],
}
client, err := mgr.Get(ctx, storeRef, ps.Namespace, nil)
if err != nil {
return out, fmt.Errorf("could not get secrets client for store %v: %w", storeName, err)
}
newData, ok := newMap[storeName]
if !ok {
err = r.DeleteAllSecretsFromStore(ctx, client, oldData)
if err != nil {
return out, err
}
delete(out, storeName)
continue
}
for oldEntry, oldRef := range oldData {
_, ok := newData[oldEntry]
if !ok {
err = r.DeleteSecretFromStore(ctx, client, oldRef)
if err != nil {
return out, err
}
delete(out[storeName], oldRef.Match.RemoteRef.RemoteKey)
}
}
}
return out, nil
}
func (r *Reconciler) DeleteAllSecretsFromStore(ctx context.Context, client v1beta1.SecretsClient, data map[string]esapi.PushSecretData) error {
for _, v := range data {
err := r.DeleteSecretFromStore(ctx, client, v)
if err != nil {
return err
}
}
return nil
}
func (r *Reconciler) DeleteSecretFromStore(ctx context.Context, client v1beta1.SecretsClient, data esapi.PushSecretData) error {
return client.DeleteSecret(ctx, data.Match.RemoteRef)
}
func (r *Reconciler) PushSecretToProviders(ctx context.Context, stores map[esapi.PushSecretStoreRef]v1beta1.GenericStore, ps esapi.PushSecret, secret *v1.Secret, mgr *secretstore.Manager) (esapi.SyncedPushSecretsMap, error) {
out := esapi.SyncedPushSecretsMap{}
for ref, store := range stores {
storeKey := fmt.Sprintf("%v/%v", ref.Kind, store.GetName())
out[storeKey] = make(map[string]esapi.PushSecretData)
storeRef := v1beta1.SecretStoreRef{
Name: store.GetName(),
Kind: ref.Kind,
}
client, err := mgr.Get(ctx, storeRef, ps.GetNamespace(), nil)
if err != nil {
return out, fmt.Errorf("could not get secrets client for store %v: %w", store.GetName(), err)
}
for _, ref := range ps.Spec.Data {
secretValue, ok := secret.Data[ref.Match.SecretKey]
if !ok {
return out, fmt.Errorf("secret key %v does not exist", ref.Match.SecretKey)
}
err := client.PushSecret(ctx, secretValue, ref.Match.RemoteRef)
if err != nil {
return out, fmt.Errorf(errSetSecretFailed, ref.Match.SecretKey, store.GetName(), err)
}
out[storeKey][ref.Match.RemoteRef.RemoteKey] = ref
}
}
return out, nil
}
func (r *Reconciler) GetSecret(ctx context.Context, ps esapi.PushSecret) (*v1.Secret, error) {
secretName := types.NamespacedName{Name: ps.Spec.Selector.Secret.Name, Namespace: ps.Namespace}
secret := &v1.Secret{}
err := r.Client.Get(ctx, secretName, secret)
if err != nil {
return nil, err
}
return secret, nil
}
func (r *Reconciler) GetSecretStores(ctx context.Context, ps esapi.PushSecret) (map[esapi.PushSecretStoreRef]v1beta1.GenericStore, error) {
stores := make(map[esapi.PushSecretStoreRef]v1beta1.GenericStore)
for _, refStore := range ps.Spec.SecretStoreRefs {
if refStore.LabelSelector != nil {
labelSelector, err := metav1.LabelSelectorAsSelector(refStore.LabelSelector)
if err != nil {
return nil, fmt.Errorf("could not convert labels: %w", err)
}
if refStore.Kind == v1beta1.ClusterSecretStoreKind {
clusterSecretStoreList := v1beta1.ClusterSecretStoreList{}
err = r.List(ctx, &clusterSecretStoreList, &client.ListOptions{LabelSelector: labelSelector})
if err != nil {
return nil, fmt.Errorf("could not list cluster Secret Stores: %w", err)
}
for k, v := range clusterSecretStoreList.Items {
key := esapi.PushSecretStoreRef{
Name: v.Name,
Kind: v1beta1.ClusterSecretStoreKind,
}
stores[key] = &clusterSecretStoreList.Items[k]
}
} else {
secretStoreList := v1beta1.SecretStoreList{}
err = r.List(ctx, &secretStoreList, &client.ListOptions{LabelSelector: labelSelector})
if err != nil {
return nil, fmt.Errorf("could not list Secret Stores: %w", err)
}
for k, v := range secretStoreList.Items {
key := esapi.PushSecretStoreRef{
Name: v.Name,
Kind: v1beta1.SecretStoreKind,
}
stores[key] = &secretStoreList.Items[k]
}
}
} else {
store, err := r.getSecretStoreFromName(ctx, refStore, ps.Namespace)
if err != nil {
return nil, err
}
stores[refStore] = store
}
}
return stores, nil
}
func (r *Reconciler) getSecretStoreFromName(ctx context.Context, refStore esapi.PushSecretStoreRef, ns string) (v1beta1.GenericStore, error) {
if refStore.Name == "" {
return nil, fmt.Errorf("refStore Name must be provided")
}
ref := types.NamespacedName{
Name: refStore.Name,
}
if refStore.Kind == v1beta1.ClusterSecretStoreKind {
var store v1beta1.ClusterSecretStore
err := r.Get(ctx, ref, &store)
if err != nil {
return nil, fmt.Errorf(errGetClusterSecretStore, ref.Name, err)
}
return &store, nil
}
ref.Namespace = ns
var store v1beta1.SecretStore
err := r.Get(ctx, ref, &store)
if err != nil {
return nil, fmt.Errorf(errGetSecretStore, ref.Name, err)
}
return &store, nil
}
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
r.recorder = mgr.GetEventRecorderFor("pushsecret")
return ctrl.NewControllerManagedBy(mgr).
For(&esapi.PushSecret{}).
Complete(r)
}
func NewPushSecretCondition(condType esapi.PushSecretConditionType, status v1.ConditionStatus, reason, message string) *esapi.PushSecretStatusCondition {
return &esapi.PushSecretStatusCondition{
Type: condType,
Status: status,
LastTransitionTime: metav1.Now(),
Reason: reason,
Message: message,
}
}
func SetPushSecretCondition(gs esapi.PushSecret, condition esapi.PushSecretStatusCondition) esapi.PushSecret {
status := gs.Status
currentCond := GetPushSecretCondition(status, condition.Type)
if currentCond != nil && currentCond.Status == condition.Status &&
currentCond.Reason == condition.Reason && currentCond.Message == condition.Message {
return gs
}
// Do not update lastTransitionTime if the status of the condition doesn't change.
if currentCond != nil && currentCond.Status == condition.Status {
condition.LastTransitionTime = currentCond.LastTransitionTime
}
status.Conditions = append(filterOutCondition(status.Conditions, condition.Type), condition)
gs.Status = status
return gs
}
// filterOutCondition returns an empty set of conditions with the provided type.
func filterOutCondition(conditions []esapi.PushSecretStatusCondition, condType esapi.PushSecretConditionType) []esapi.PushSecretStatusCondition {
newConditions := make([]esapi.PushSecretStatusCondition, 0, len(conditions))
for _, c := range conditions {
if c.Type == condType {
continue
}
newConditions = append(newConditions, c)
}
return newConditions
}
// GetSecretStoreCondition returns the condition with the provided type.
func GetPushSecretCondition(status esapi.PushSecretStatus, condType esapi.PushSecretConditionType) *esapi.PushSecretStatusCondition {
for i := range status.Conditions {
c := status.Conditions[i]
if c.Type == condType {
return &c
}
}
return nil
}

View file

@ -0,0 +1,727 @@
/*
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 pushsecret
import (
"bytes"
"context"
"fmt"
"os"
"strconv"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
v1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
v1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
ctest "github.com/external-secrets/external-secrets/pkg/controllers/commontest"
"github.com/external-secrets/external-secrets/pkg/provider/testing/fake"
)
var (
fakeProvider *fake.Client
timeout = time.Second * 10
interval = time.Millisecond * 250
)
type testCase struct {
store v1beta1.GenericStore
pushsecret *v1alpha1.PushSecret
secret *v1.Secret
assert func(pushsecret *v1alpha1.PushSecret, secret *v1.Secret) bool
}
func init() {
fakeProvider = fake.New()
v1beta1.ForceRegister(fakeProvider, &v1beta1.SecretStoreProvider{
Fake: &v1beta1.FakeProvider{},
})
}
func checkCondition(status v1alpha1.PushSecretStatus, cond v1alpha1.PushSecretStatusCondition) bool {
for _, condition := range status.Conditions {
if condition.Message == cond.Message &&
condition.Reason == cond.Reason &&
condition.Status == cond.Status &&
condition.Type == cond.Type {
return true
}
}
return false
}
type testTweaks func(*testCase)
var _ = Describe("ExternalSecret controller", func() {
const (
PushSecretName = "test-es"
PushSecretFQDN = "externalsecrets.external-secrets.io/test-es"
PushSecretStore = "test-store"
SecretName = "test-secret"
PushSecretTargetSecretName = "test-secret"
FakeManager = "fake.manager"
expectedSecretVal = "SOMEVALUE was templated"
targetPropObj = "{{ .targetProperty | toString | upper }} was templated"
FooValue = "map-foo-value"
BarValue = "map-bar-value"
)
var PushSecretNamespace string
// if we are in debug and need to increase the timeout for testing, we can do so by using an env var
if customTimeout := os.Getenv("TEST_CUSTOM_TIMEOUT_SEC"); customTimeout != "" {
if t, err := strconv.Atoi(customTimeout); err == nil {
timeout = time.Second * time.Duration(t)
}
}
BeforeEach(func() {
var err error
PushSecretNamespace, err = ctest.CreateNamespace("test-ns", k8sClient)
Expect(err).ToNot(HaveOccurred())
fakeProvider.Reset()
})
AfterEach(func() {
k8sClient.Delete(context.Background(), &v1alpha1.PushSecret{
ObjectMeta: metav1.ObjectMeta{
Name: PushSecretName,
Namespace: PushSecretNamespace,
},
})
// give a time for reconciler to remove finalizers before removing SecretStores
// TODO: Secret Stores should have finalizers bound to PushSecrets if DeletionPolicy == Delete
time.Sleep(2 * time.Second)
k8sClient.Delete(context.Background(), &v1beta1.SecretStore{
ObjectMeta: metav1.ObjectMeta{
Name: PushSecretStore,
Namespace: PushSecretNamespace,
},
})
k8sClient.Delete(context.Background(), &v1beta1.ClusterSecretStore{
ObjectMeta: metav1.ObjectMeta{
Name: PushSecretStore,
},
})
k8sClient.Delete(context.Background(), &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: SecretName,
Namespace: PushSecretNamespace,
},
})
Expect(k8sClient.Delete(context.Background(), &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: PushSecretNamespace,
},
})).To(Succeed())
})
makeDefaultTestcase := func() *testCase {
return &testCase{
pushsecret: &v1alpha1.PushSecret{
ObjectMeta: metav1.ObjectMeta{
Name: PushSecretName,
Namespace: PushSecretNamespace,
},
Spec: v1alpha1.PushSecretSpec{
SecretStoreRefs: []v1alpha1.PushSecretStoreRef{
{
Name: PushSecretStore,
Kind: "SecretStore",
},
},
Selector: v1alpha1.PushSecretSelector{
Secret: v1alpha1.PushSecretSecret{
Name: SecretName,
},
},
Data: []v1alpha1.PushSecretData{
{
Match: v1alpha1.PushSecretMatch{
SecretKey: "key",
RemoteRef: v1alpha1.PushSecretRemoteRef{
RemoteKey: "path/to/key",
},
},
},
},
},
},
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: SecretName,
Namespace: PushSecretNamespace,
},
Data: map[string][]byte{
"key": []byte("value"),
},
},
store: &v1beta1.SecretStore{
ObjectMeta: metav1.ObjectMeta{
Name: PushSecretStore,
Namespace: PushSecretNamespace,
},
TypeMeta: metav1.TypeMeta{
Kind: "SecretStore",
},
Spec: v1beta1.SecretStoreSpec{
Provider: &v1beta1.SecretStoreProvider{
Fake: &v1beta1.FakeProvider{
Data: []v1beta1.FakeProviderData{},
},
},
},
},
}
}
// if target Secret name is not specified it should use the ExternalSecret name.
syncSuccessfully := func(tc *testCase) {
fakeProvider.SetSecretFn = func() error {
return nil
}
tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
Eventually(func() bool {
By("checking if Provider value got updated")
secretValue := secret.Data["key"]
providerValue, ok := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey]
if !ok {
return false
}
got := providerValue.Value
return bytes.Equal(got, secretValue)
}, time.Second*10, time.Second).Should(BeTrue())
return true
}
}
// if target Secret name is not specified it should use the ExternalSecret name.
syncAndDeleteSuccessfully := func(tc *testCase) {
fakeProvider.SetSecretFn = func() error {
return nil
}
tc.pushsecret = &v1alpha1.PushSecret{
ObjectMeta: metav1.ObjectMeta{
Name: PushSecretName,
Namespace: PushSecretNamespace,
},
Spec: v1alpha1.PushSecretSpec{
DeletionPolicy: v1alpha1.PushSecretDeletionPolicyDelete,
SecretStoreRefs: []v1alpha1.PushSecretStoreRef{
{
Name: PushSecretStore,
Kind: "SecretStore",
},
},
Selector: v1alpha1.PushSecretSelector{
Secret: v1alpha1.PushSecretSecret{
Name: SecretName,
},
},
Data: []v1alpha1.PushSecretData{
{
Match: v1alpha1.PushSecretMatch{
SecretKey: "key",
RemoteRef: v1alpha1.PushSecretRemoteRef{
RemoteKey: "path/to/key",
},
},
},
},
},
}
tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
ps.Spec.Data[0].Match.RemoteRef.RemoteKey = "different-key"
updatedPS := &v1alpha1.PushSecret{}
Expect(k8sClient.Update(context.Background(), ps, &client.UpdateOptions{})).Should(Succeed())
Eventually(func() bool {
psKey := types.NamespacedName{Name: PushSecretName, Namespace: PushSecretNamespace}
By("checking if Provider value got updated")
err := k8sClient.Get(context.Background(), psKey, updatedPS)
if err != nil {
return false
}
key, ok := updatedPS.Status.SyncedPushSecrets[fmt.Sprintf("SecretStore/%v", PushSecretStore)]["different-key"]
if !ok {
return false
}
return key.Match.SecretKey == "key"
}, time.Second*10, time.Second).Should(BeTrue())
return true
}
}
failDelete := func(tc *testCase) {
fakeProvider.SetSecretFn = func() error {
return nil
}
fakeProvider.DeleteSecretFn = func() error {
return fmt.Errorf("Nope")
}
tc.pushsecret = &v1alpha1.PushSecret{
ObjectMeta: metav1.ObjectMeta{
Name: PushSecretName,
Namespace: PushSecretNamespace,
},
Spec: v1alpha1.PushSecretSpec{
DeletionPolicy: v1alpha1.PushSecretDeletionPolicyDelete,
SecretStoreRefs: []v1alpha1.PushSecretStoreRef{
{
Name: PushSecretStore,
Kind: "SecretStore",
},
},
Selector: v1alpha1.PushSecretSelector{
Secret: v1alpha1.PushSecretSecret{
Name: SecretName,
},
},
Data: []v1alpha1.PushSecretData{
{
Match: v1alpha1.PushSecretMatch{
SecretKey: "key",
RemoteRef: v1alpha1.PushSecretRemoteRef{
RemoteKey: "path/to/key",
},
},
},
},
},
}
tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
ps.Spec.Data[0].Match.RemoteRef.RemoteKey = "different-key"
updatedPS := &v1alpha1.PushSecret{}
Expect(k8sClient.Update(context.Background(), ps, &client.UpdateOptions{})).Should(Succeed())
Eventually(func() bool {
psKey := types.NamespacedName{Name: PushSecretName, Namespace: PushSecretNamespace}
By("checking if synced secrets correspond to both keys")
err := k8sClient.Get(context.Background(), psKey, updatedPS)
if err != nil {
return false
}
_, ok := updatedPS.Status.SyncedPushSecrets[fmt.Sprintf("SecretStore/%v", PushSecretStore)]["different-key"]
if !ok {
return false
}
_, ok = updatedPS.Status.SyncedPushSecrets[fmt.Sprintf("SecretStore/%v", PushSecretStore)]["path/to/key"]
return ok
}, time.Second*10, time.Second).Should(BeTrue())
return true
}
}
failDeleteStore := func(tc *testCase) {
fakeProvider.SetSecretFn = func() error {
return nil
}
fakeProvider.DeleteSecretFn = func() error {
return fmt.Errorf("boom")
}
tc.pushsecret.Spec.DeletionPolicy = v1alpha1.PushSecretDeletionPolicyDelete
tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
secondStore := &v1beta1.SecretStore{
ObjectMeta: metav1.ObjectMeta{
Name: "new-store",
Namespace: PushSecretNamespace,
},
TypeMeta: metav1.TypeMeta{
Kind: "SecretStore",
},
Spec: v1beta1.SecretStoreSpec{
Provider: &v1beta1.SecretStoreProvider{
Fake: &v1beta1.FakeProvider{
Data: []v1beta1.FakeProviderData{},
},
},
},
}
Expect(k8sClient.Create(context.Background(), secondStore, &client.CreateOptions{})).Should(Succeed())
ps.Spec.SecretStoreRefs[0].Name = "new-store"
updatedPS := &v1alpha1.PushSecret{}
Expect(k8sClient.Update(context.Background(), ps, &client.UpdateOptions{})).Should(Succeed())
Eventually(func() bool {
psKey := types.NamespacedName{Name: PushSecretName, Namespace: PushSecretNamespace}
By("checking if Provider value got updated")
err := k8sClient.Get(context.Background(), psKey, updatedPS)
if err != nil {
return false
}
syncedLen := len(updatedPS.Status.SyncedPushSecrets)
return syncedLen == 2
}, time.Second*10, time.Second).Should(BeTrue())
return true
}
}
deleteWholeStore := func(tc *testCase) {
fakeProvider.SetSecretFn = func() error {
return nil
}
fakeProvider.DeleteSecretFn = func() error {
return nil
}
tc.pushsecret.Spec.DeletionPolicy = v1alpha1.PushSecretDeletionPolicyDelete
tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
secondStore := &v1beta1.SecretStore{
ObjectMeta: metav1.ObjectMeta{
Name: "new-store",
Namespace: PushSecretNamespace,
},
TypeMeta: metav1.TypeMeta{
Kind: "SecretStore",
},
Spec: v1beta1.SecretStoreSpec{
Provider: &v1beta1.SecretStoreProvider{
Fake: &v1beta1.FakeProvider{
Data: []v1beta1.FakeProviderData{},
},
},
},
}
Expect(k8sClient.Create(context.Background(), secondStore, &client.CreateOptions{})).Should(Succeed())
ps.Spec.SecretStoreRefs[0].Name = "new-store"
updatedPS := &v1alpha1.PushSecret{}
Expect(k8sClient.Update(context.Background(), ps, &client.UpdateOptions{})).Should(Succeed())
Eventually(func() bool {
psKey := types.NamespacedName{Name: PushSecretName, Namespace: PushSecretNamespace}
By("checking if Provider value got updated")
err := k8sClient.Get(context.Background(), psKey, updatedPS)
if err != nil {
return false
}
key, ok := updatedPS.Status.SyncedPushSecrets["SecretStore/new-store"]["path/to/key"]
if !ok {
return false
}
syncedLen := len(updatedPS.Status.SyncedPushSecrets)
if syncedLen != 1 {
return false
}
return key.Match.SecretKey == "key"
}, time.Second*10, time.Second).Should(BeTrue())
return true
}
}
// if target Secret name is not specified it should use the ExternalSecret name.
syncMatchingLabels := func(tc *testCase) {
fakeProvider.SetSecretFn = func() error {
return nil
}
fakeProvider.DeleteSecretFn = func() error {
return nil
}
tc.pushsecret = &v1alpha1.PushSecret{
ObjectMeta: metav1.ObjectMeta{
Name: PushSecretName,
Namespace: PushSecretNamespace,
},
Spec: v1alpha1.PushSecretSpec{
SecretStoreRefs: []v1alpha1.PushSecretStoreRef{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"foo": "bar",
},
},
Kind: "SecretStore",
},
},
Selector: v1alpha1.PushSecretSelector{
Secret: v1alpha1.PushSecretSecret{
Name: SecretName,
},
},
Data: []v1alpha1.PushSecretData{
{
Match: v1alpha1.PushSecretMatch{
SecretKey: "key",
RemoteRef: v1alpha1.PushSecretRemoteRef{
RemoteKey: "path/to/key",
},
},
},
},
},
}
tc.store = &v1beta1.SecretStore{
TypeMeta: metav1.TypeMeta{
Kind: "SecretStore",
},
ObjectMeta: metav1.ObjectMeta{
Name: PushSecretStore,
Namespace: PushSecretNamespace,
Labels: map[string]string{
"foo": "bar",
},
},
Spec: v1beta1.SecretStoreSpec{
Provider: &v1beta1.SecretStoreProvider{
Fake: &v1beta1.FakeProvider{
Data: []v1beta1.FakeProviderData{},
},
},
},
}
tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
secretValue := secret.Data["key"]
providerValue := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey].Value
expected := v1alpha1.PushSecretStatusCondition{
Type: v1alpha1.PushSecretReady,
Status: v1.ConditionTrue,
Reason: v1alpha1.ReasonSynced,
Message: "PushSecret synced successfully",
}
return bytes.Equal(secretValue, providerValue) && checkCondition(ps.Status, expected)
}
}
syncWithClusterStore := func(tc *testCase) {
fakeProvider.SetSecretFn = func() error {
return nil
}
tc.store = &v1beta1.ClusterSecretStore{
TypeMeta: metav1.TypeMeta{
Kind: "ClusterSecretStore",
},
ObjectMeta: metav1.ObjectMeta{
Name: PushSecretStore,
},
Spec: v1beta1.SecretStoreSpec{
Provider: &v1beta1.SecretStoreProvider{
Fake: &v1beta1.FakeProvider{
Data: []v1beta1.FakeProviderData{},
},
},
},
}
tc.pushsecret.Spec.SecretStoreRefs[0].Kind = "ClusterSecretStore"
tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
secretValue := secret.Data["key"]
providerValue := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey].Value
expected := v1alpha1.PushSecretStatusCondition{
Type: v1alpha1.PushSecretReady,
Status: v1.ConditionTrue,
Reason: v1alpha1.ReasonSynced,
Message: "PushSecret synced successfully",
}
return bytes.Equal(secretValue, providerValue) && checkCondition(ps.Status, expected)
}
}
// if target Secret name is not specified it should use the ExternalSecret name.
syncWithClusterStoreMatchingLabels := func(tc *testCase) {
fakeProvider.SetSecretFn = func() error {
return nil
}
tc.pushsecret = &v1alpha1.PushSecret{
ObjectMeta: metav1.ObjectMeta{
Name: PushSecretName,
Namespace: PushSecretNamespace,
},
Spec: v1alpha1.PushSecretSpec{
SecretStoreRefs: []v1alpha1.PushSecretStoreRef{
{
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"foo": "bar",
},
},
Kind: "ClusterSecretStore",
},
},
Selector: v1alpha1.PushSecretSelector{
Secret: v1alpha1.PushSecretSecret{
Name: SecretName,
},
},
Data: []v1alpha1.PushSecretData{
{
Match: v1alpha1.PushSecretMatch{
SecretKey: "key",
RemoteRef: v1alpha1.PushSecretRemoteRef{
RemoteKey: "path/to/key",
},
},
},
},
},
}
tc.store = &v1beta1.ClusterSecretStore{
ObjectMeta: metav1.ObjectMeta{
Name: PushSecretStore,
Labels: map[string]string{
"foo": "bar",
},
},
Spec: v1beta1.SecretStoreSpec{
Provider: &v1beta1.SecretStoreProvider{
Fake: &v1beta1.FakeProvider{
Data: []v1beta1.FakeProviderData{},
},
},
},
}
tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
secretValue := secret.Data["key"]
providerValue := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey].Value
expected := v1alpha1.PushSecretStatusCondition{
Type: v1alpha1.PushSecretReady,
Status: v1.ConditionTrue,
Reason: v1alpha1.ReasonSynced,
Message: "PushSecret synced successfully",
}
return bytes.Equal(secretValue, providerValue) && checkCondition(ps.Status, expected)
}
}
// if target Secret name is not specified it should use the ExternalSecret name.
failNoSecret := func(tc *testCase) {
fakeProvider.SetSecretFn = func() error {
return nil
}
tc.secret = nil
tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
expected := v1alpha1.PushSecretStatusCondition{
Type: v1alpha1.PushSecretReady,
Status: v1.ConditionFalse,
Reason: v1alpha1.ReasonErrored,
Message: "could not get source secret",
}
return checkCondition(ps.Status, expected)
}
}
// if target Secret name is not specified it should use the ExternalSecret name.
failNoSecretKey := func(tc *testCase) {
fakeProvider.SetSecretFn = func() error {
return nil
}
tc.pushsecret.Spec.Data[0].Match.SecretKey = "unexisting"
tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
expected := v1alpha1.PushSecretStatusCondition{
Type: v1alpha1.PushSecretReady,
Status: v1.ConditionFalse,
Reason: v1alpha1.ReasonErrored,
Message: "set secret failed: secret key unexisting does not exist",
}
return checkCondition(ps.Status, expected)
}
}
// if target Secret name is not specified it should use the ExternalSecret name.
failNoSecretStore := func(tc *testCase) {
fakeProvider.SetSecretFn = func() error {
return nil
}
tc.store = nil
tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
expected := v1alpha1.PushSecretStatusCondition{
Type: v1alpha1.PushSecretReady,
Status: v1.ConditionFalse,
Reason: v1alpha1.ReasonErrored,
Message: "could not get SecretStore \"test-store\", secretstores.external-secrets.io \"test-store\" not found",
}
return checkCondition(ps.Status, expected)
}
}
// if target Secret name is not specified it should use the ExternalSecret name.
failNoClusterStore := func(tc *testCase) {
fakeProvider.SetSecretFn = func() error {
return nil
}
tc.store = nil
tc.pushsecret.Spec.SecretStoreRefs[0].Kind = "ClusterSecretStore"
tc.pushsecret.Spec.SecretStoreRefs[0].Name = "unexisting"
tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
expected := v1alpha1.PushSecretStatusCondition{
Type: v1alpha1.PushSecretReady,
Status: v1.ConditionFalse,
Reason: v1alpha1.ReasonErrored,
Message: "could not get ClusterSecretStore \"unexisting\", clustersecretstores.external-secrets.io \"unexisting\" not found",
}
return checkCondition(ps.Status, expected)
}
} // if target Secret name is not specified it should use the ExternalSecret name.
setSecretFail := func(tc *testCase) {
fakeProvider.SetSecretFn = func() error {
return fmt.Errorf("boom")
}
tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
expected := v1alpha1.PushSecretStatusCondition{
Type: v1alpha1.PushSecretReady,
Status: v1.ConditionFalse,
Reason: v1alpha1.ReasonErrored,
Message: "set secret failed: could not write remote ref key to target secretstore test-store: boom",
}
return checkCondition(ps.Status, expected)
}
}
// if target Secret name is not specified it should use the ExternalSecret name.
newClientFail := func(tc *testCase) {
fakeProvider.NewFn = func(context.Context, v1beta1.GenericStore, client.Client, string) (v1beta1.SecretsClient, error) {
return nil, fmt.Errorf("boom")
}
tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
expected := v1alpha1.PushSecretStatusCondition{
Type: v1alpha1.PushSecretReady,
Status: v1.ConditionFalse,
Reason: v1alpha1.ReasonErrored,
Message: "set secret failed: could not get secrets client for store test-store: boom",
}
return checkCondition(ps.Status, expected)
}
}
DescribeTable("When reconciling a PushSecret",
func(tweaks ...testTweaks) {
tc := makeDefaultTestcase()
for _, tweak := range tweaks {
tweak(tc)
}
ctx := context.Background()
By("creating a secret store, secret and pushsecret")
if tc.store != nil {
Expect(k8sClient.Create(ctx, tc.store)).To(Succeed())
}
if tc.secret != nil {
Expect(k8sClient.Create(ctx, tc.secret)).To(Succeed())
}
if tc.pushsecret != nil {
Expect(k8sClient.Create(ctx, tc.pushsecret)).Should(Succeed())
}
time.Sleep(2 * time.Second)
psKey := types.NamespacedName{Name: PushSecretName, Namespace: PushSecretNamespace}
createdPS := &v1alpha1.PushSecret{}
By("checking the pushSecret condition")
Eventually(func() bool {
err := k8sClient.Get(ctx, psKey, createdPS)
if err != nil {
return false
}
return tc.assert(createdPS, tc.secret)
}, timeout, interval).Should(BeTrue())
// this must be optional so we can test faulty es configuration
},
Entry("should sync", syncSuccessfully),
Entry("should delete if DeletionPolicy=Delete", syncAndDeleteSuccessfully),
Entry("should track deletion tasks if Delete fails", failDelete),
Entry("should track deleted stores if Delete fails", failDeleteStore),
Entry("should delete all secrets if SecretStore changes", deleteWholeStore),
Entry("should sync to stores matching labels", syncMatchingLabels),
Entry("should sync with ClusterStore", syncWithClusterStore),
Entry("should sync with ClusterStore matching labels", syncWithClusterStoreMatchingLabels),
Entry("should fail if Secret is not created", failNoSecret),
Entry("should fail if Secret Key does not exist", failNoSecretKey),
Entry("should fail if SetSecret fails", setSecretFail),
Entry("should fail if no valid SecretStore", failNoSecretStore),
Entry("should fail if no valid ClusterSecretStore", failNoClusterStore),
Entry("should fail if NewClient fails", newClientFail),
)
})

View file

@ -0,0 +1,105 @@
/*
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 pushsecret
import (
"context"
"path/filepath"
"testing"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"go.uber.org/zap/zapcore"
"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/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
)
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
var cfg *rest.Config
var k8sClient client.Client
var testEnv *envtest.Environment
var cancel context.CancelFunc
func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Controller Suite")
}
var _ = BeforeSuite(func() {
log := zap.New(zap.WriteTo(GinkgoWriter), zap.Level(zapcore.DebugLevel))
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 = esv1beta1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
err = esv1alpha1.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())
// do not use k8sManager.GetClient()
// see https://github.com/kubernetes-sigs/controller-runtime/issues/343#issuecomment-469435686
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(k8sClient).ToNot(BeNil())
Expect(err).ToNot(HaveOccurred())
err = (&Reconciler{
Client: k8sClient,
Scheme: k8sManager.GetScheme(),
Log: ctrl.Log.WithName("controllers").WithName("ExternalSecrets"),
RequeueInterval: time.Second,
}).SetupWithManager(k8sManager)
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

@ -12,7 +12,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package clientmanager package secretstore
import ( import (
"context" "context"
@ -28,7 +28,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/controllers/secretstore"
) )
const ( const (
@ -63,7 +62,7 @@ type clientVal struct {
} }
// New constructs a new manager with defaults. // New constructs a new manager with defaults.
func New(ctrlClient client.Client, controllerClass string, enableFloodgate bool) *Manager { func NewManager(ctrlClient client.Client, controllerClass string, enableFloodgate bool) *Manager {
log := ctrl.Log.WithName("clientmanager") log := ctrl.Log.WithName("clientmanager")
return &Manager{ return &Manager{
log: log, log: log,
@ -74,36 +73,7 @@ func New(ctrlClient client.Client, controllerClass string, enableFloodgate bool)
} }
} }
// Get returns a provider client from the given storeRef or sourceRef.secretStoreRef func (m *Manager) GetFromStore(ctx context.Context, store esv1beta1.GenericStore, namespace string) (esv1beta1.SecretsClient, error) {
// while sourceRef.SecretStoreRef takes precedence over storeRef.
// Do not close the client returned from this func, instead close
// the manager once you're done with recinciling the external secret.
func (m *Manager) Get(ctx context.Context, storeRef esv1beta1.SecretStoreRef, namespace string, sourceRef *esv1beta1.SourceRef) (esv1beta1.SecretsClient, error) {
if sourceRef != nil && sourceRef.SecretStoreRef != nil {
storeRef = *sourceRef.SecretStoreRef
}
store, err := m.getStore(ctx, &storeRef, namespace)
if err != nil {
return nil, err
}
// check if store should be handled by this controller instance
if !secretstore.ShouldProcessStore(store, m.controllerClass) {
return nil, fmt.Errorf("can not reference unmanaged store")
}
// when using ClusterSecretStore, validate the ClusterSecretStore namespace conditions
shouldProcess, err := m.shouldProcessSecret(store, namespace)
if err != nil || !shouldProcess {
if err == nil && !shouldProcess {
err = fmt.Errorf(errClusterStoreMismatch, store.GetName(), namespace)
}
return nil, err
}
if m.enableFloodgate {
err := assertStoreIsUsable(store)
if err != nil {
return nil, err
}
}
storeProvider, err := esv1beta1.GetProvider(store) storeProvider, err := esv1beta1.GetProvider(store)
if err != nil { if err != nil {
return nil, err return nil, err
@ -129,6 +99,39 @@ func (m *Manager) Get(ctx context.Context, storeRef esv1beta1.SecretStoreRef, na
return secretClient, nil return secretClient, nil
} }
// Get returns a provider client from the given storeRef or sourceRef.secretStoreRef
// while sourceRef.SecretStoreRef takes precedence over storeRef.
// Do not close the client returned from this func, instead close
// the manager once you're done with recinciling the external secret.
func (m *Manager) Get(ctx context.Context, storeRef esv1beta1.SecretStoreRef, namespace string, sourceRef *esv1beta1.SourceRef) (esv1beta1.SecretsClient, error) {
if sourceRef != nil && sourceRef.SecretStoreRef != nil {
storeRef = *sourceRef.SecretStoreRef
}
store, err := m.getStore(ctx, &storeRef, namespace)
if err != nil {
return nil, err
}
// check if store should be handled by this controller instance
if !ShouldProcessStore(store, m.controllerClass) {
return nil, fmt.Errorf("can not reference unmanaged store")
}
// when using ClusterSecretStore, validate the ClusterSecretStore namespace conditions
shouldProcess, err := m.shouldProcessSecret(store, namespace)
if err != nil || !shouldProcess {
if err == nil && !shouldProcess {
err = fmt.Errorf(errClusterStoreMismatch, store.GetName(), namespace)
}
return nil, err
}
if m.enableFloodgate {
err := assertStoreIsUsable(store)
if err != nil {
return nil, err
}
}
return m.GetFromStore(ctx, store, namespace)
}
// returns a previously stored client from the cache if store and store-version match // returns a previously stored client from the cache if store and store-version match
// if a client exists for the same provider which points to a different store or store version // if a client exists for the same provider which points to a different store or store version
// it will be cleaned up. // it will be cleaned up.
@ -248,7 +251,7 @@ func assertStoreIsUsable(store esv1beta1.GenericStore) error {
if store == nil { if store == nil {
return nil return nil
} }
condition := secretstore.GetSecretStoreCondition(store.GetStatus(), esv1beta1.SecretStoreReady) condition := GetSecretStoreCondition(store.GetStatus(), esv1beta1.SecretStoreReady)
if condition == nil || condition.Status != v1.ConditionTrue { if condition == nil || condition.Status != v1.ConditionTrue {
return fmt.Errorf(errSecretStoreNotReady, store.GetName()) return fmt.Errorf(errSecretStoreNotReady, store.GetName())
} }

View file

@ -12,7 +12,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package clientmanager package secretstore
import ( import (
"context" "context"
@ -20,6 +20,7 @@ import (
"github.com/go-logr/logr" "github.com/go-logr/logr"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -41,13 +42,13 @@ func TestManagerGet(t *testing.T) {
// the behavior of the NewClient func. // the behavior of the NewClient func.
fakeProvider := &WrapProvider{} fakeProvider := &WrapProvider{}
esv1beta1.ForceRegister(fakeProvider, &esv1beta1.SecretStoreProvider{ esv1beta1.ForceRegister(fakeProvider, &esv1beta1.SecretStoreProvider{
Fake: &esv1beta1.FakeProvider{}, AWS: &esv1beta1.AWSProvider{},
}) })
// fake clients are re-used to compare the // fake clients are re-used to compare the
// in-memory reference // in-memory reference
clientA := &FakeClient{id: "1"} clientA := &MockFakeClient{id: "1"}
clientB := &FakeClient{id: "2"} clientB := &MockFakeClient{id: "2"}
const testNamespace = "foo" const testNamespace = "foo"
@ -62,14 +63,7 @@ func TestManagerGet(t *testing.T) {
fakeSpec := esv1beta1.SecretStoreSpec{ fakeSpec := esv1beta1.SecretStoreSpec{
Provider: &esv1beta1.SecretStoreProvider{ Provider: &esv1beta1.SecretStoreProvider{
Fake: &esv1beta1.FakeProvider{ AWS: &esv1beta1.AWSProvider{},
Data: []esv1beta1.FakeProviderData{
{
Key: "foo",
Value: "bar",
},
},
},
}, },
} }
@ -96,7 +90,7 @@ func TestManagerGet(t *testing.T) {
var mgr *Manager var mgr *Manager
provKey := clientKey{ provKey := clientKey{
providerType: "*clientmanager.WrapProvider", providerType: "*secretstore.WrapProvider",
} }
type fields struct { type fields struct {
@ -148,7 +142,7 @@ func TestManagerGet(t *testing.T) {
// and it mustbe the client defined in clientConstructor // and it mustbe the client defined in clientConstructor
assert.NotNil(t, sc) assert.NotNil(t, sc)
c, ok := mgr.clientMap[provKey] c, ok := mgr.clientMap[provKey]
assert.True(t, ok) require.True(t, ok)
assert.Same(t, c.client, clientA) assert.Same(t, c.client, clientA)
}, },
@ -332,35 +326,47 @@ func (f *WrapProvider) NewClient(
return f.newClientFunc(ctx, store, kube, namespace) return f.newClientFunc(ctx, store, kube, namespace)
} }
func (f *WrapProvider) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadOnly
}
// ValidateStore checks if the provided store is valid. // ValidateStore checks if the provided store is valid.
func (f *WrapProvider) ValidateStore(store esv1beta1.GenericStore) error { func (f *WrapProvider) ValidateStore(store esv1beta1.GenericStore) error {
return nil return nil
} }
type FakeClient struct { type MockFakeClient struct {
id string id string
closeCalled bool closeCalled bool
} }
func (c *FakeClient) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) { func (c *MockFakeClient) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
return nil
}
func (c *MockFakeClient) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
return nil
}
func (c *MockFakeClient) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
return nil, nil return nil, nil
} }
func (c *FakeClient) Validate() (esv1beta1.ValidationResult, error) { func (c *MockFakeClient) Validate() (esv1beta1.ValidationResult, error) {
return esv1beta1.ValidationResultReady, nil return esv1beta1.ValidationResultReady, nil
} }
// GetSecretMap returns multiple k/v pairs from the provider. // GetSecretMap returns multiple k/v pairs from the provider.
func (c *FakeClient) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) { func (c *MockFakeClient) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
return nil, nil return nil, nil
} }
// GetAllSecrets returns multiple k/v pairs from the provider. // GetAllSecrets returns multiple k/v pairs from the provider.
func (c *FakeClient) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) { func (c *MockFakeClient) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
return nil, nil return nil, nil
} }
func (c *FakeClient) Close(ctx context.Context) error { func (c *MockFakeClient) Close(ctx context.Context) error {
c.closeCalled = true c.closeCalled = true
return nil return nil
} }

View file

@ -62,11 +62,20 @@ func reconcile(ctx context.Context, req ctrl.Request, ss esapi.GenericStore, cl
// validateStore modifies the store conditions // validateStore modifies the store conditions
// we have to patch the status // we have to patch the status
log.V(1).Info("validating") log.V(1).Info("validating")
err := validateStore(ctx, req.Namespace, ss, cl, recorder) err := validateStore(ctx, req.Namespace, controllerClass, ss, cl, recorder)
if err != nil { if err != nil {
log.Error(err, "unable to validate store") log.Error(err, "unable to validate store")
return ctrl.Result{}, err return ctrl.Result{}, err
} }
storeProvider, err := esapi.GetProvider(ss)
if err != nil {
return ctrl.Result{}, err
}
capStatus := esapi.SecretStoreStatus{
Capabilities: storeProvider.Capabilities(),
Conditions: ss.GetStatus().Conditions,
}
ss.SetStatus(capStatus)
recorder.Event(ss, v1.EventTypeNormal, esapi.ReasonStoreValid, msgStoreValidated) recorder.Event(ss, v1.EventTypeNormal, esapi.ReasonStoreValid, msgStoreValidated)
cond := NewSecretStoreCondition(esapi.SecretStoreReady, v1.ConditionTrue, esapi.ReasonStoreValid, msgStoreValidated) cond := NewSecretStoreCondition(esapi.SecretStoreReady, v1.ConditionTrue, esapi.ReasonStoreValid, msgStoreValidated)
@ -79,24 +88,17 @@ func reconcile(ctx context.Context, req ctrl.Request, ss esapi.GenericStore, cl
// validateStore tries to construct a new client // validateStore tries to construct a new client
// if it fails sets a condition and writes events. // if it fails sets a condition and writes events.
func validateStore(ctx context.Context, namespace string, store esapi.GenericStore, func validateStore(ctx context.Context, namespace, controllerClass string, store esapi.GenericStore,
client client.Client, recorder record.EventRecorder) error { client client.Client, recorder record.EventRecorder) error {
storeProvider, err := esapi.GetProvider(store) mgr := NewManager(client, controllerClass, false)
if err != nil { defer mgr.Close(ctx)
cond := NewSecretStoreCondition(esapi.SecretStoreReady, v1.ConditionFalse, esapi.ReasonInvalidStore, errUnableGetProvider) cl, err := mgr.GetFromStore(ctx, store, namespace)
SetExternalSecretCondition(store, *cond)
recorder.Event(store, v1.EventTypeWarning, esapi.ReasonInvalidStore, err.Error())
return fmt.Errorf(errStoreProvider, err)
}
cl, err := storeProvider.NewClient(ctx, store, client, namespace)
if err != nil { if err != nil {
cond := NewSecretStoreCondition(esapi.SecretStoreReady, v1.ConditionFalse, esapi.ReasonInvalidProviderConfig, errUnableCreateClient) cond := NewSecretStoreCondition(esapi.SecretStoreReady, v1.ConditionFalse, esapi.ReasonInvalidProviderConfig, errUnableCreateClient)
SetExternalSecretCondition(store, *cond) SetExternalSecretCondition(store, *cond)
recorder.Event(store, v1.EventTypeWarning, esapi.ReasonInvalidProviderConfig, err.Error()) recorder.Event(store, v1.EventTypeWarning, esapi.ReasonInvalidProviderConfig, err.Error())
return fmt.Errorf(errStoreClient, err) return fmt.Errorf(errStoreClient, err)
} }
defer cl.Close(ctx)
validationResult, err := cl.Validate() validationResult, err := cl.Validate()
if err != nil && validationResult != esapi.ValidationResultUnknown { if err != nil && validationResult != esapi.ValidationResultUnknown {

View file

@ -125,6 +125,37 @@ var _ = Describe("SecretStore reconcile", func() {
} }
readWrite := func(tc *testCase) {
spc := tc.store.GetSpec()
spc.Provider.Vault = nil
spc.Provider.Fake = &esapi.FakeProvider{
Data: []esapi.FakeProviderData{},
}
tc.assert = func() {
Eventually(func() bool {
ss := tc.store.Copy()
err := k8sClient.Get(context.Background(), types.NamespacedName{
Name: defaultStoreName,
Namespace: ss.GetNamespace(),
}, ss)
if err != nil {
return false
}
if ss.GetStatus().Capabilities != esapi.SecretStoreReadWrite {
return false
}
return true
}).
WithTimeout(time.Second * 10).
WithPolling(time.Second).
Should(BeTrue())
}
}
DescribeTable("Controller Reconcile logic", func(muts ...func(tc *testCase)) { DescribeTable("Controller Reconcile logic", func(muts ...func(tc *testCase)) {
for _, mut := range muts { for _, mut := range muts {
mut(test) mut(test)
@ -137,11 +168,13 @@ var _ = Describe("SecretStore reconcile", func() {
Entry("[namespace] invalid provider with secretStore should set InvalidStore condition", invalidProvider), Entry("[namespace] invalid provider with secretStore should set InvalidStore condition", invalidProvider),
Entry("[namespace] ignore stores with non-matching class", ignoreControllerClass), Entry("[namespace] ignore stores with non-matching class", ignoreControllerClass),
Entry("[namespace] valid provider has status=ready", validProvider), Entry("[namespace] valid provider has status=ready", validProvider),
Entry("[namespace] valid provider has capabilities=ReadWrite", readWrite),
// cluster store // cluster store
Entry("[cluster] invalid provider with secretStore should set InvalidStore condition", invalidProvider, useClusterStore), Entry("[cluster] invalid provider with secretStore should set InvalidStore condition", invalidProvider, useClusterStore),
Entry("[cluster] ignore stores with non-matching class", ignoreControllerClass, useClusterStore), Entry("[cluster] ignore stores with non-matching class", ignoreControllerClass, useClusterStore),
Entry("[cluster] valid provider has status=ready", validProvider, useClusterStore), Entry("[cluster] valid provider has status=ready", validProvider, useClusterStore),
Entry("[cluster] valid provider has capabilities=ReadWrite", readWrite, useClusterStore),
) )
}) })

View file

@ -70,6 +70,11 @@ func init() {
}) })
} }
// Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadOnly
}
// NewClient constructs a new secrets client based on the provided store. // 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) (esv1beta1.SecretsClient, error) { func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
// controller-runtime/client does not support TokenRequest or other subresource APIs // controller-runtime/client does not support TokenRequest or other subresource APIs
@ -204,6 +209,14 @@ func (a *Akeyless) Validate() (esv1beta1.ValidationResult, error) {
return esv1beta1.ValidationResultReady, nil return esv1beta1.ValidationResultReady, nil
} }
func (a *Akeyless) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
func (a *Akeyless) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
// Implements store.Client.GetSecret Interface. // Implements store.Client.GetSecret Interface.
// Retrieves a secret with the secret name defined in ref.Name. // Retrieves a secret with the secret name defined in ref.Name.
func (a *Akeyless) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) { func (a *Akeyless) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {

View file

@ -114,6 +114,14 @@ func (c *Client) setAuth(ctx context.Context) error {
return nil return nil
} }
func (kms *KeyManagementService) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
func (kms *KeyManagementService) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
// Empty GetAllSecrets. // Empty GetAllSecrets.
func (kms *KeyManagementService) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) { func (kms *KeyManagementService) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
// TO be implemented // TO be implemented
@ -168,6 +176,11 @@ func (kms *KeyManagementService) GetSecretMap(ctx context.Context, ref esv1beta1
return secretData, nil return secretData, nil
} }
// Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
func (kms *KeyManagementService) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadOnly
}
// NewClient constructs a new secrets client based on the provided store. // 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) (esv1beta1.SecretsClient, error) { func (kms *KeyManagementService) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
storeSpec := store.GetSpec() storeSpec := store.GetSpec()

View file

@ -14,27 +14,82 @@ limitations under the License.
package fake package fake
import ( import (
"context"
"fmt" "fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/ssm" "github.com/aws/aws-sdk-go/service/ssm"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
) )
// Client implements the aws parameterstore interface. // Client implements the aws parameterstore interface.
type Client struct { type Client struct {
valFn func(*ssm.GetParameterInput) (*ssm.GetParameterOutput, error) GetParameterWithContextFn GetParameterWithContextFn
PutParameterWithContextFn PutParameterWithContextFn
DeleteParameterWithContextFn DeleteParameterWithContextFn
DescribeParametersWithContextFn DescribeParametersWithContextFn
ListTagsForResourceWithContextFn ListTagsForResourceWithContextFn
} }
func (sm *Client) GetParameter(in *ssm.GetParameterInput) (*ssm.GetParameterOutput, error) { type GetParameterWithContextFn func(aws.Context, *ssm.GetParameterInput, ...request.Option) (*ssm.GetParameterOutput, error)
return sm.valFn(in) type PutParameterWithContextFn func(aws.Context, *ssm.PutParameterInput, ...request.Option) (*ssm.PutParameterOutput, error)
type DescribeParametersWithContextFn func(aws.Context, *ssm.DescribeParametersInput, ...request.Option) (*ssm.DescribeParametersOutput, error)
type ListTagsForResourceWithContextFn func(aws.Context, *ssm.ListTagsForResourceInput, ...request.Option) (*ssm.ListTagsForResourceOutput, error)
type DeleteParameterWithContextFn func(ctx aws.Context, input *ssm.DeleteParameterInput, opts ...request.Option) (*ssm.DeleteParameterOutput, error)
func (sm *Client) ListTagsForResourceWithContext(ctx aws.Context, input *ssm.ListTagsForResourceInput, options ...request.Option) (*ssm.ListTagsForResourceOutput, error) {
return sm.ListTagsForResourceWithContextFn(ctx, input, options...)
} }
func (sm *Client) DescribeParameters(*ssm.DescribeParametersInput) (*ssm.DescribeParametersOutput, error) { func NewListTagsForResourceWithContextFn(output *ssm.ListTagsForResourceOutput, err error) ListTagsForResourceWithContextFn {
return nil, nil return func(aws.Context, *ssm.ListTagsForResourceInput, ...request.Option) (*ssm.ListTagsForResourceOutput, error) {
return output, err
}
}
func (sm *Client) DeleteParameterWithContext(ctx aws.Context, input *ssm.DeleteParameterInput, opts ...request.Option) (*ssm.DeleteParameterOutput, error) {
return sm.DeleteParameterWithContextFn(ctx, input, opts...)
}
func NewDeleteParameterWithContextFn(output *ssm.DeleteParameterOutput, err error) DeleteParameterWithContextFn {
return func(aws.Context, *ssm.DeleteParameterInput, ...request.Option) (*ssm.DeleteParameterOutput, error) {
return output, err
}
}
func (sm *Client) GetParameterWithContext(ctx aws.Context, input *ssm.GetParameterInput, options ...request.Option) (*ssm.GetParameterOutput, error) {
return sm.GetParameterWithContextFn(ctx, input, options...)
}
func NewGetParameterWithContextFn(output *ssm.GetParameterOutput, err error) GetParameterWithContextFn {
return func(aws.Context, *ssm.GetParameterInput, ...request.Option) (*ssm.GetParameterOutput, error) {
return output, err
}
}
func (sm *Client) DescribeParametersWithContext(ctx context.Context, input *ssm.DescribeParametersInput, options ...request.Option) (*ssm.DescribeParametersOutput, error) {
return sm.DescribeParametersWithContextFn(ctx, input, options...)
}
func NewDescribeParametersWithContextFn(output *ssm.DescribeParametersOutput, err error) DescribeParametersWithContextFn {
return func(aws.Context, *ssm.DescribeParametersInput, ...request.Option) (*ssm.DescribeParametersOutput, error) {
return output, err
}
}
func (sm *Client) PutParameterWithContext(ctx aws.Context, input *ssm.PutParameterInput, options ...request.Option) (*ssm.PutParameterOutput, error) {
return sm.PutParameterWithContextFn(ctx, input, options...)
}
func NewPutParameterWithContextFn(output *ssm.PutParameterOutput, err error) PutParameterWithContextFn {
return func(aws.Context, *ssm.PutParameterInput, ...request.Option) (*ssm.PutParameterOutput, error) {
return output, err
}
} }
func (sm *Client) WithValue(in *ssm.GetParameterInput, val *ssm.GetParameterOutput, err error) { func (sm *Client) WithValue(in *ssm.GetParameterInput, val *ssm.GetParameterOutput, err error) {
sm.valFn = func(paramIn *ssm.GetParameterInput) (*ssm.GetParameterOutput, error) { sm.GetParameterWithContextFn = func(ctx aws.Context, paramIn *ssm.GetParameterInput, options ...request.Option) (*ssm.GetParameterOutput, error) {
if !cmp.Equal(paramIn, in) { if !cmp.Equal(paramIn, in) {
return nil, fmt.Errorf("unexpected test argument") return nil, fmt.Errorf("unexpected test argument")
} }

View file

@ -21,6 +21,8 @@ import (
"strings" "strings"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ssm" "github.com/aws/aws-sdk-go/service/ssm"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
@ -32,7 +34,11 @@ import (
) )
// https://github.com/external-secrets/external-secrets/issues/644 // https://github.com/external-secrets/external-secrets/issues/644
var _ esv1beta1.SecretsClient = &ParameterStore{} var (
_ esv1beta1.SecretsClient = &ParameterStore{}
managedBy = "managed-by"
externalSecrets = "external-secrets"
)
// ParameterStore is a provider for AWS ParameterStore. // ParameterStore is a provider for AWS ParameterStore.
type ParameterStore struct { type ParameterStore struct {
@ -43,8 +49,11 @@ type ParameterStore struct {
// PMInterface is a subset of the parameterstore api. // PMInterface is a subset of the parameterstore api.
// see: https://docs.aws.amazon.com/sdk-for-go/api/service/ssm/ssmiface/ // see: https://docs.aws.amazon.com/sdk-for-go/api/service/ssm/ssmiface/
type PMInterface interface { type PMInterface interface {
GetParameter(*ssm.GetParameterInput) (*ssm.GetParameterOutput, error) GetParameterWithContext(aws.Context, *ssm.GetParameterInput, ...request.Option) (*ssm.GetParameterOutput, error)
DescribeParameters(*ssm.DescribeParametersInput) (*ssm.DescribeParametersOutput, error) PutParameterWithContext(aws.Context, *ssm.PutParameterInput, ...request.Option) (*ssm.PutParameterOutput, error)
DescribeParametersWithContext(aws.Context, *ssm.DescribeParametersInput, ...request.Option) (*ssm.DescribeParametersOutput, error)
ListTagsForResourceWithContext(aws.Context, *ssm.ListTagsForResourceInput, ...request.Option) (*ssm.ListTagsForResourceOutput, error)
DeleteParameterWithContext(ctx aws.Context, input *ssm.DeleteParameterInput, opts ...request.Option) (*ssm.DeleteParameterOutput, error)
} }
const ( const (
@ -59,18 +68,150 @@ func New(sess *session.Session, cfg *aws.Config) (*ParameterStore, error) {
}, nil }, nil
} }
// Empty GetAllSecrets. func (pm *ParameterStore) getTagsByName(ctx aws.Context, ref *ssm.GetParameterOutput) ([]*ssm.Tag, error) {
parameterType := "Parameter"
parameterTags := ssm.ListTagsForResourceInput{
ResourceId: ref.Parameter.Name,
ResourceType: &parameterType,
}
data, err := pm.client.ListTagsForResourceWithContext(ctx, &parameterTags)
if err != nil {
return nil, fmt.Errorf("error listing tags %w", err)
}
return data.TagList, nil
}
func (pm *ParameterStore) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
secretName := remoteRef.GetRemoteKey()
secretValue := ssm.GetParameterInput{
Name: &secretName,
}
existing, err := pm.client.GetParameterWithContext(ctx, &secretValue)
var awsError awserr.Error
ok := errors.As(err, &awsError)
if err != nil && (!ok || awsError.Code() != ssm.ErrCodeParameterNotFound) {
return fmt.Errorf("unexpected error getting parameter %v: %w", secretName, err)
}
if existing != nil && existing.Parameter != nil {
fmt.Println("The existing value contains data:", existing.String())
tags, err := pm.getTagsByName(ctx, existing)
if err != nil {
return fmt.Errorf("error getting the existing tags for the parameter %v: %w", secretName, err)
}
isManaged := isManagedByESO(tags)
if !isManaged {
// If the secret is not managed by external-secrets, it is "deleted" effectively by all means
return nil
}
deleteInput := &ssm.DeleteParameterInput{
Name: &secretName,
}
_, err = pm.client.DeleteParameterWithContext(ctx, deleteInput)
if err != nil {
return fmt.Errorf("could not delete parameter %v: %w", secretName, err)
}
}
return nil
}
func (pm *ParameterStore) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
parameterType := "String"
overwrite := true
stringValue := string(value)
secretName := remoteRef.GetRemoteKey()
secretRequest := ssm.PutParameterInput{
Name: &secretName,
Value: &stringValue,
Type: &parameterType,
Overwrite: &overwrite,
}
secretValue := ssm.GetParameterInput{
Name: &secretName,
}
existing, err := pm.client.GetParameterWithContext(ctx, &secretValue)
var awsError awserr.Error
ok := errors.As(err, &awsError)
if err != nil && (!ok || awsError.Code() != ssm.ErrCodeParameterNotFound) {
return fmt.Errorf("unexpected error getting parameter %v: %w", secretName, err)
}
// If we have a valid parameter returned to us, check its tags
if existing != nil && existing.Parameter != nil {
fmt.Println("The existing value contains data:", existing.String())
tags, err := pm.getTagsByName(ctx, existing)
if err != nil {
return fmt.Errorf("error getting the existing tags for the parameter %v: %w", secretName, err)
}
isManaged := isManagedByESO(tags)
if !isManaged {
return fmt.Errorf("secret not managed by external-secrets")
}
if existing.Parameter.Value != nil && *existing.Parameter.Value == string(value) {
return nil
}
return pm.setManagedRemoteParameter(ctx, secretRequest, false)
}
// let's set the secret
// Do we need to delete the existing parameter on the remote?
return pm.setManagedRemoteParameter(ctx, secretRequest, true)
}
func isManagedByESO(tags []*ssm.Tag) bool {
for _, tag := range tags {
if *tag.Key == managedBy && *tag.Value == externalSecrets {
return true
}
}
return false
}
func (pm *ParameterStore) setManagedRemoteParameter(ctx context.Context, secretRequest ssm.PutParameterInput, createManagedByTags bool) error {
externalSecretsTag := ssm.Tag{
Key: &managedBy,
Value: &externalSecrets,
}
overwrite := true
secretRequest.Overwrite = &overwrite
if createManagedByTags {
secretRequest.Tags = append(secretRequest.Tags, &externalSecretsTag)
overwrite = false
}
_, err := pm.client.PutParameterWithContext(ctx, &secretRequest)
if err != nil {
return fmt.Errorf("unexpected error pushing parameter %v: %w", secretRequest.Name, err)
}
return nil
}
// GetAllSecrets fetches information from multiple secrets into a single kubernetes secret.
func (pm *ParameterStore) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) { func (pm *ParameterStore) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
if ref.Name != nil { if ref.Name != nil {
return pm.findByName(ref) return pm.findByName(ctx, ref)
} }
if ref.Tags != nil { if ref.Tags != nil {
return pm.findByTags(ref) return pm.findByTags(ctx, ref)
} }
return nil, errors.New(errUnexpectedFindOperator) return nil, errors.New(errUnexpectedFindOperator)
} }
func (pm *ParameterStore) findByName(ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) { func (pm *ParameterStore) findByName(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
matcher, err := find.New(*ref.Name) matcher, err := find.New(*ref.Name)
if err != nil { if err != nil {
return nil, err return nil, err
@ -86,10 +227,12 @@ func (pm *ParameterStore) findByName(ref esv1beta1.ExternalSecretFind) (map[stri
data := make(map[string][]byte) data := make(map[string][]byte)
var nextToken *string var nextToken *string
for { for {
it, err := pm.client.DescribeParameters(&ssm.DescribeParametersInput{ it, err := pm.client.DescribeParametersWithContext(
NextToken: nextToken, ctx,
ParameterFilters: pathFilter, &ssm.DescribeParametersInput{
}) NextToken: nextToken,
ParameterFilters: pathFilter,
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -97,7 +240,7 @@ func (pm *ParameterStore) findByName(ref esv1beta1.ExternalSecretFind) (map[stri
if !matcher.MatchName(*param.Name) { if !matcher.MatchName(*param.Name) {
continue continue
} }
err = pm.fetchAndSet(data, *param.Name) err = pm.fetchAndSet(ctx, data, *param.Name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -111,7 +254,7 @@ func (pm *ParameterStore) findByName(ref esv1beta1.ExternalSecretFind) (map[stri
return data, nil return data, nil
} }
func (pm *ParameterStore) findByTags(ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) { func (pm *ParameterStore) findByTags(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
filters := make([]*ssm.ParameterStringFilter, 0) filters := make([]*ssm.ParameterStringFilter, 0)
for k, v := range ref.Tags { for k, v := range ref.Tags {
filters = append(filters, &ssm.ParameterStringFilter{ filters = append(filters, &ssm.ParameterStringFilter{
@ -132,15 +275,17 @@ func (pm *ParameterStore) findByTags(ref esv1beta1.ExternalSecretFind) (map[stri
data := make(map[string][]byte) data := make(map[string][]byte)
var nextToken *string var nextToken *string
for { for {
it, err := pm.client.DescribeParameters(&ssm.DescribeParametersInput{ it, err := pm.client.DescribeParametersWithContext(
ParameterFilters: filters, ctx,
NextToken: nextToken, &ssm.DescribeParametersInput{
}) ParameterFilters: filters,
NextToken: nextToken,
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, param := range it.Parameters { for _, param := range it.Parameters {
err = pm.fetchAndSet(data, *param.Name) err = pm.fetchAndSet(ctx, data, *param.Name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -154,8 +299,8 @@ func (pm *ParameterStore) findByTags(ref esv1beta1.ExternalSecretFind) (map[stri
return data, nil return data, nil
} }
func (pm *ParameterStore) fetchAndSet(data map[string][]byte, name string) error { func (pm *ParameterStore) fetchAndSet(ctx context.Context, data map[string][]byte, name string) error {
out, err := pm.client.GetParameter(&ssm.GetParameterInput{ out, err := pm.client.GetParameterWithContext(ctx, &ssm.GetParameterInput{
Name: utilpointer.StringPtr(name), Name: utilpointer.StringPtr(name),
WithDecryption: aws.Bool(true), WithDecryption: aws.Bool(true),
}) })
@ -169,13 +314,14 @@ func (pm *ParameterStore) fetchAndSet(data map[string][]byte, name string) error
// GetSecret returns a single secret from the provider. // GetSecret returns a single secret from the provider.
func (pm *ParameterStore) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) { func (pm *ParameterStore) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
out, err := pm.client.GetParameter(&ssm.GetParameterInput{ out, err := pm.client.GetParameterWithContext(ctx, &ssm.GetParameterInput{
Name: &ref.Key, Name: &ref.Key,
WithDecryption: aws.Bool(true), WithDecryption: aws.Bool(true),
}) })
nsf := esv1beta1.NoSecretError{}
var nf *ssm.ParameterNotFound var nf *ssm.ParameterNotFound
if errors.As(err, &nf) { if errors.As(err, &nf) || errors.As(err, &nsf) {
return nil, esv1beta1.NoSecretErr return nil, esv1beta1.NoSecretErr
} }
if err != nil { if err != nil {

View file

@ -15,20 +15,23 @@ package parameterstore
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
"testing" "testing"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ssm" "github.com/aws/aws-sdk-go/service/ssm"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
fake "github.com/external-secrets/external-secrets/pkg/provider/aws/parameterstore/fake" fakeps "github.com/external-secrets/external-secrets/pkg/provider/aws/parameterstore/fake"
) )
type parameterstoreTestCase struct { type parameterstoreTestCase struct {
fakeClient *fake.Client fakeClient *fakeps.Client
apiInput *ssm.GetParameterInput apiInput *ssm.GetParameterInput
apiOutput *ssm.GetParameterOutput apiOutput *ssm.GetParameterOutput
remoteRef *esv1beta1.ExternalSecretDataRemoteRef remoteRef *esv1beta1.ExternalSecretDataRemoteRef
@ -38,9 +41,17 @@ type parameterstoreTestCase struct {
expectedData map[string][]byte expectedData map[string][]byte
} }
type fakeRef struct {
key string
}
func (f fakeRef) GetRemoteKey() string {
return f.key
}
func makeValidParameterStoreTestCase() *parameterstoreTestCase { func makeValidParameterStoreTestCase() *parameterstoreTestCase {
return &parameterstoreTestCase{ return &parameterstoreTestCase{
fakeClient: &fake.Client{}, fakeClient: &fakeps.Client{},
apiInput: makeValidAPIInput(), apiInput: makeValidAPIInput(),
apiOutput: makeValidAPIOutput(), apiOutput: makeValidAPIOutput(),
remoteRef: makeValidRemoteRef(), remoteRef: makeValidRemoteRef(),
@ -81,6 +92,356 @@ func makeValidParameterStoreTestCaseCustom(tweaks ...func(pstc *parameterstoreTe
return pstc return pstc
} }
func TestDeleteSecret(t *testing.T) {
fakeClient := fakeps.Client{}
parameterName := "parameter"
managedBy := "managed-by"
manager := "external-secrets"
ssmTag := ssm.Tag{
Key: &managedBy,
Value: &manager,
}
type args struct {
client fakeps.Client
getParameterOutput *ssm.GetParameterOutput
listTagsOutput *ssm.ListTagsForResourceOutput
deleteParameterOutput *ssm.DeleteParameterOutput
getParameterError error
listTagsError error
deleteParameterError error
}
type want struct {
err error
}
type testCase struct {
args args
want want
reason string
}
tests := map[string]testCase{
"Deletes Successfully": {
args: args{
client: fakeClient,
getParameterOutput: &ssm.GetParameterOutput{
Parameter: &ssm.Parameter{
Name: &parameterName,
},
},
listTagsOutput: &ssm.ListTagsForResourceOutput{
TagList: []*ssm.Tag{&ssmTag},
},
deleteParameterOutput: nil,
getParameterError: nil,
listTagsError: nil,
deleteParameterError: nil,
},
want: want{
err: nil,
},
reason: "",
},
"Secret Not Found": {
args: args{
client: fakeClient,
getParameterOutput: nil,
listTagsOutput: nil,
deleteParameterOutput: nil,
getParameterError: awserr.New(ssm.ErrCodeParameterNotFound, "not here, sorry dude", nil),
listTagsError: nil,
deleteParameterError: nil,
},
want: want{
err: nil,
},
reason: "",
},
"No permissions to get secret": {
args: args{
client: fakeClient,
getParameterOutput: nil,
listTagsOutput: nil,
deleteParameterOutput: nil,
getParameterError: errors.New("no permissions"),
listTagsError: nil,
deleteParameterError: nil,
},
want: want{
err: errors.New("no permissions"),
},
reason: "",
},
"No permissions to get tags": {
args: args{
client: fakeClient,
getParameterOutput: &ssm.GetParameterOutput{
Parameter: &ssm.Parameter{
Name: &parameterName,
},
},
listTagsOutput: nil,
deleteParameterOutput: nil,
getParameterError: nil,
listTagsError: errors.New("no permissions"),
deleteParameterError: nil,
},
want: want{
err: errors.New("no permissions"),
},
reason: "",
},
"Secret Not Managed by External Secrets": {
args: args{
client: fakeClient,
getParameterOutput: &ssm.GetParameterOutput{
Parameter: &ssm.Parameter{
Name: &parameterName,
},
},
listTagsOutput: &ssm.ListTagsForResourceOutput{
TagList: []*ssm.Tag{},
},
deleteParameterOutput: nil,
getParameterError: nil,
listTagsError: nil,
deleteParameterError: nil,
},
want: want{
err: nil,
},
reason: "",
},
"No permissions delete secret": {
args: args{
client: fakeClient,
getParameterOutput: &ssm.GetParameterOutput{
Parameter: &ssm.Parameter{
Name: &parameterName,
},
},
listTagsOutput: &ssm.ListTagsForResourceOutput{
TagList: []*ssm.Tag{&ssmTag},
},
deleteParameterOutput: nil,
getParameterError: nil,
listTagsError: nil,
deleteParameterError: errors.New("no permissions"),
},
want: want{
err: errors.New("no permissions"),
},
reason: "",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
ref := fakeRef{key: "fake-key"}
ps := ParameterStore{
client: &tc.args.client,
}
tc.args.client.GetParameterWithContextFn = fakeps.NewGetParameterWithContextFn(tc.args.getParameterOutput, tc.args.getParameterError)
tc.args.client.ListTagsForResourceWithContextFn = fakeps.NewListTagsForResourceWithContextFn(tc.args.listTagsOutput, tc.args.listTagsError)
tc.args.client.DeleteParameterWithContextFn = fakeps.NewDeleteParameterWithContextFn(tc.args.deleteParameterOutput, tc.args.deleteParameterError)
err := ps.DeleteSecret(context.TODO(), ref)
// Error nil XOR tc.want.err nil
if ((err == nil) || (tc.want.err == nil)) && !((err == nil) && (tc.want.err == nil)) {
t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error: %v", name, tc.reason, tc.want.err, err)
}
// if errors are the same type but their contents do not match
if err != nil && tc.want.err != nil {
if !strings.Contains(err.Error(), tc.want.err.Error()) {
t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error got nil", name, tc.reason, tc.want.err)
}
}
})
}
}
func TestPushSecret(t *testing.T) {
invalidParameters := errors.New(ssm.ErrCodeInvalidParameters)
alreadyExistsError := errors.New(ssm.ErrCodeAlreadyExistsException)
fakeValue := "fakeValue"
managedByESO := ssm.Tag{
Key: &managedBy,
Value: &externalSecrets,
}
putParameterOutput := &ssm.PutParameterOutput{}
getParameterOutput := &ssm.GetParameterOutput{}
describeParameterOutput := &ssm.DescribeParametersOutput{}
validListTagsForResourceOutput := &ssm.ListTagsForResourceOutput{
TagList: []*ssm.Tag{&managedByESO},
}
noTagsResourceOutput := &ssm.ListTagsForResourceOutput{}
validGetParameterOutput := &ssm.GetParameterOutput{
Parameter: &ssm.Parameter{
ARN: nil,
DataType: nil,
LastModifiedDate: nil,
Name: nil,
Selector: nil,
SourceResult: nil,
Type: nil,
Value: nil,
Version: nil,
},
}
sameGetParameterOutput := &ssm.GetParameterOutput{
Parameter: &ssm.Parameter{
Value: &fakeValue,
},
}
type args struct {
store *esv1beta1.AWSProvider
client fakeps.Client
}
type want struct {
err error
}
tests := map[string]struct {
reason string
args args
want want
}{
"PutParameterSucceeds": {
reason: "a parameter can be successfully pushed to aws parameter store",
args: args{
store: makeValidParameterStore().Spec.Provider.AWS,
client: fakeps.Client{
PutParameterWithContextFn: fakeps.NewPutParameterWithContextFn(putParameterOutput, nil),
GetParameterWithContextFn: fakeps.NewGetParameterWithContextFn(getParameterOutput, nil),
DescribeParametersWithContextFn: fakeps.NewDescribeParametersWithContextFn(describeParameterOutput, nil),
ListTagsForResourceWithContextFn: fakeps.NewListTagsForResourceWithContextFn(validListTagsForResourceOutput, nil),
},
},
want: want{
err: nil,
},
},
"SetParameterFailsWhenNoNameProvided": {
reason: "test push secret with no name gives error",
args: args{
store: makeValidParameterStore().Spec.Provider.AWS,
client: fakeps.Client{
PutParameterWithContextFn: fakeps.NewPutParameterWithContextFn(putParameterOutput, nil),
GetParameterWithContextFn: fakeps.NewGetParameterWithContextFn(getParameterOutput, invalidParameters),
DescribeParametersWithContextFn: fakeps.NewDescribeParametersWithContextFn(describeParameterOutput, nil),
ListTagsForResourceWithContextFn: fakeps.NewListTagsForResourceWithContextFn(validListTagsForResourceOutput, nil),
},
},
want: want{
err: invalidParameters,
},
},
"SetSecretWhenAlreadyExists": {
reason: "test push secret with secret that already exists gives error",
args: args{
store: makeValidParameterStore().Spec.Provider.AWS,
client: fakeps.Client{
PutParameterWithContextFn: fakeps.NewPutParameterWithContextFn(putParameterOutput, alreadyExistsError),
GetParameterWithContextFn: fakeps.NewGetParameterWithContextFn(getParameterOutput, nil),
DescribeParametersWithContextFn: fakeps.NewDescribeParametersWithContextFn(describeParameterOutput, nil),
ListTagsForResourceWithContextFn: fakeps.NewListTagsForResourceWithContextFn(validListTagsForResourceOutput, nil),
},
},
want: want{
err: alreadyExistsError,
},
},
"GetSecretWithValidParameters": {
reason: "Get secret with valid parameters",
args: args{
store: makeValidParameterStore().Spec.Provider.AWS,
client: fakeps.Client{
PutParameterWithContextFn: fakeps.NewPutParameterWithContextFn(putParameterOutput, nil),
GetParameterWithContextFn: fakeps.NewGetParameterWithContextFn(validGetParameterOutput, nil),
DescribeParametersWithContextFn: fakeps.NewDescribeParametersWithContextFn(describeParameterOutput, nil),
ListTagsForResourceWithContextFn: fakeps.NewListTagsForResourceWithContextFn(validListTagsForResourceOutput, nil),
},
},
want: want{
err: nil,
},
},
"SetSecretNotManagedByESO": {
reason: "SetSecret to the parameter store but tags are not managed by ESO",
args: args{
store: makeValidParameterStore().Spec.Provider.AWS,
client: fakeps.Client{
PutParameterWithContextFn: fakeps.NewPutParameterWithContextFn(putParameterOutput, nil),
GetParameterWithContextFn: fakeps.NewGetParameterWithContextFn(validGetParameterOutput, nil),
DescribeParametersWithContextFn: fakeps.NewDescribeParametersWithContextFn(describeParameterOutput, nil),
ListTagsForResourceWithContextFn: fakeps.NewListTagsForResourceWithContextFn(noTagsResourceOutput, nil),
},
},
want: want{
err: fmt.Errorf("secret not managed by external-secrets"),
},
},
"SetSecretGetTagsError": {
reason: "SetSecret to the parameter store returns error while obtaining tags",
args: args{
store: makeValidParameterStore().Spec.Provider.AWS,
client: fakeps.Client{
PutParameterWithContextFn: fakeps.NewPutParameterWithContextFn(putParameterOutput, nil),
GetParameterWithContextFn: fakeps.NewGetParameterWithContextFn(validGetParameterOutput, nil),
DescribeParametersWithContextFn: fakeps.NewDescribeParametersWithContextFn(describeParameterOutput, nil),
ListTagsForResourceWithContextFn: fakeps.NewListTagsForResourceWithContextFn(nil, fmt.Errorf("you shall not tag")),
},
},
want: want{
err: fmt.Errorf("you shall not tag"),
},
},
"SetSecretContentMatches": {
reason: "No ops",
args: args{
store: makeValidParameterStore().Spec.Provider.AWS,
client: fakeps.Client{
PutParameterWithContextFn: fakeps.NewPutParameterWithContextFn(putParameterOutput, nil),
GetParameterWithContextFn: fakeps.NewGetParameterWithContextFn(sameGetParameterOutput, nil),
DescribeParametersWithContextFn: fakeps.NewDescribeParametersWithContextFn(describeParameterOutput, nil),
ListTagsForResourceWithContextFn: fakeps.NewListTagsForResourceWithContextFn(validListTagsForResourceOutput, nil),
},
},
want: want{
err: nil,
},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
ref := fakeRef{key: "fake-key"}
ps := ParameterStore{
client: &tc.args.client,
}
err := ps.PushSecret(context.TODO(), []byte(fakeValue), ref)
// Error nil XOR tc.want.err nil
if ((err == nil) || (tc.want.err == nil)) && !((err == nil) && (tc.want.err == nil)) {
t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error: %v", name, tc.reason, tc.want.err, err)
}
// if errors are the same type but their contents do not match
if err != nil && tc.want.err != nil {
if !strings.Contains(err.Error(), tc.want.err.Error()) {
t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error got nil", name, tc.reason, tc.want.err)
}
}
})
}
}
// test the ssm<->aws interface // test the ssm<->aws interface
// make sure correct values are passed and errors are handled accordingly. // make sure correct values are passed and errors are handled accordingly.
func TestGetSecret(t *testing.T) { func TestGetSecret(t *testing.T) {
@ -110,6 +471,13 @@ func TestGetSecret(t *testing.T) {
pstc.expectError = "key INVALPROP does not exist in secret" pstc.expectError = "key INVALPROP does not exist in secret"
} }
// bad case: parameter.Value not found
setParameterValueNotFound := func(pstc *parameterstoreTestCase) {
pstc.apiOutput.Parameter.Value = aws.String("NONEXISTENT")
pstc.apiErr = esv1beta1.NoSecretErr
pstc.expectError = "Secret does not exist"
}
// bad case: extract property failure due to invalid json // bad case: extract property failure due to invalid json
setPropertyFail := func(pstc *parameterstoreTestCase) { setPropertyFail := func(pstc *parameterstoreTestCase) {
pstc.apiOutput.Parameter.Value = aws.String(`------`) pstc.apiOutput.Parameter.Value = aws.String(`------`)
@ -138,6 +506,7 @@ func TestGetSecret(t *testing.T) {
makeValidParameterStoreTestCaseCustom(setParameterValueNil), makeValidParameterStoreTestCaseCustom(setParameterValueNil),
makeValidParameterStoreTestCaseCustom(setAPIError), makeValidParameterStoreTestCaseCustom(setAPIError),
makeValidParameterStoreTestCaseCustom(setExtractPropertyWithDot), makeValidParameterStoreTestCaseCustom(setExtractPropertyWithDot),
makeValidParameterStoreTestCaseCustom(setParameterValueNotFound),
} }
ps := ParameterStore{} ps := ParameterStore{}
@ -200,6 +569,23 @@ func TestGetSecretMap(t *testing.T) {
} }
} }
func makeValidParameterStore() *esv1beta1.SecretStore {
return &esv1beta1.SecretStore{
ObjectMeta: metav1.ObjectMeta{
Name: "aws-parameterstore",
Namespace: "default",
},
Spec: esv1beta1.SecretStoreSpec{
Provider: &esv1beta1.SecretStoreProvider{
AWS: &esv1beta1.AWSProvider{
Service: esv1beta1.AWSServiceParameterStore,
Region: "us-east-1",
},
},
},
}
}
func ErrorContains(out error, want string) bool { func ErrorContains(out error, want string) bool {
if out == nil { if out == nil {
return want == "" return want == ""

View file

@ -46,6 +46,11 @@ const (
errInitAWSProvider = "unable to initialize aws provider: %s" errInitAWSProvider = "unable to initialize aws provider: %s"
) )
// Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadWrite
}
// NewClient constructs a new secrets client based on the provided store. // 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) (esv1beta1.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) return newClient(ctx, store, kube, namespace, awsauth.DefaultSTSProvider)

View file

@ -17,14 +17,76 @@ package fake
import ( import (
"fmt" "fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
awssm "github.com/aws/aws-sdk-go/service/secretsmanager" awssm "github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
) )
// Client implements the aws secretsmanager interface. // Client implements the aws secretsmanager interface.
type Client struct { type Client struct {
ExecutionCounter int ExecutionCounter int
valFn map[string]func(*awssm.GetSecretValueInput) (*awssm.GetSecretValueOutput, error) valFn map[string]func(*awssm.GetSecretValueInput) (*awssm.GetSecretValueOutput, error)
CreateSecretWithContextFn CreateSecretWithContextFn
GetSecretValueWithContextFn GetSecretValueWithContextFn
PutSecretValueWithContextFn PutSecretValueWithContextFn
DescribeSecretWithContextFn DescribeSecretWithContextFn
DeleteSecretWithContextFn DeleteSecretWithContextFn
}
type CreateSecretWithContextFn func(aws.Context, *awssm.CreateSecretInput, ...request.Option) (*awssm.CreateSecretOutput, error)
type GetSecretValueWithContextFn func(aws.Context, *awssm.GetSecretValueInput, ...request.Option) (*awssm.GetSecretValueOutput, error)
type PutSecretValueWithContextFn func(aws.Context, *awssm.PutSecretValueInput, ...request.Option) (*awssm.PutSecretValueOutput, error)
type DescribeSecretWithContextFn func(aws.Context, *awssm.DescribeSecretInput, ...request.Option) (*awssm.DescribeSecretOutput, error)
type DeleteSecretWithContextFn func(ctx aws.Context, input *awssm.DeleteSecretInput, opts ...request.Option) (*awssm.DeleteSecretOutput, error)
func (sm Client) CreateSecretWithContext(ctx aws.Context, input *awssm.CreateSecretInput, options ...request.Option) (*awssm.CreateSecretOutput, error) {
return sm.CreateSecretWithContextFn(ctx, input, options...)
}
func NewCreateSecretWithContextFn(output *awssm.CreateSecretOutput, err error) CreateSecretWithContextFn {
return func(ctx aws.Context, input *awssm.CreateSecretInput, options ...request.Option) (*awssm.CreateSecretOutput, error) {
return output, err
}
}
func (sm Client) DeleteSecretWithContext(ctx aws.Context, input *awssm.DeleteSecretInput, opts ...request.Option) (*awssm.DeleteSecretOutput, error) {
return sm.DeleteSecretWithContextFn(ctx, input, opts...)
}
func NewDeleteSecretWithContextFn(output *awssm.DeleteSecretOutput, err error) DeleteSecretWithContextFn {
return func(ctx aws.Context, input *awssm.DeleteSecretInput, opts ...request.Option) (output *awssm.DeleteSecretOutput, err error) {
return output, err
}
}
func (sm Client) GetSecretValueWithContext(ctx aws.Context, input *awssm.GetSecretValueInput, options ...request.Option) (*awssm.GetSecretValueOutput, error) {
return sm.GetSecretValueWithContextFn(ctx, input, options...)
}
func NewGetSecretValueWithContextFn(output *awssm.GetSecretValueOutput, err error) GetSecretValueWithContextFn {
return func(aws.Context, *awssm.GetSecretValueInput, ...request.Option) (*awssm.GetSecretValueOutput, error) {
return output, err
}
}
func (sm Client) PutSecretValueWithContext(ctx aws.Context, input *awssm.PutSecretValueInput, options ...request.Option) (*awssm.PutSecretValueOutput, error) {
return sm.PutSecretValueWithContextFn(ctx, input, options...)
}
func NewPutSecretValueWithContextFn(output *awssm.PutSecretValueOutput, err error) PutSecretValueWithContextFn {
return func(aws.Context, *awssm.PutSecretValueInput, ...request.Option) (*awssm.PutSecretValueOutput, error) {
return output, err
}
}
func (sm Client) DescribeSecretWithContext(ctx aws.Context, input *awssm.DescribeSecretInput, options ...request.Option) (*awssm.DescribeSecretOutput, error) {
return sm.DescribeSecretWithContextFn(ctx, input, options...)
}
func NewDescribeSecretWithContextFn(output *awssm.DescribeSecretOutput, err error) DescribeSecretWithContextFn {
return func(aws.Context, *awssm.DescribeSecretInput, ...request.Option) (*awssm.DescribeSecretOutput, error) {
return output, err
}
} }
// NewClient init a new fake client. // NewClient init a new fake client.

View file

@ -15,6 +15,7 @@ limitations under the License.
package secretsmanager package secretsmanager
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
@ -22,6 +23,8 @@ import (
"strings" "strings"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/aws/session"
awssm "github.com/aws/aws-sdk-go/service/secretsmanager" awssm "github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
@ -46,12 +49,19 @@ type SecretsManager struct {
// SMInterface is a subset of the smiface api. // SMInterface is a subset of the smiface api.
// see: https://docs.aws.amazon.com/sdk-for-go/api/service/secretsmanager/secretsmanageriface/ // see: https://docs.aws.amazon.com/sdk-for-go/api/service/secretsmanager/secretsmanageriface/
type SMInterface interface { type SMInterface interface {
GetSecretValue(*awssm.GetSecretValueInput) (*awssm.GetSecretValueOutput, error)
ListSecrets(*awssm.ListSecretsInput) (*awssm.ListSecretsOutput, error) ListSecrets(*awssm.ListSecretsInput) (*awssm.ListSecretsOutput, error)
GetSecretValue(*awssm.GetSecretValueInput) (*awssm.GetSecretValueOutput, error)
CreateSecretWithContext(aws.Context, *awssm.CreateSecretInput, ...request.Option) (*awssm.CreateSecretOutput, error)
GetSecretValueWithContext(aws.Context, *awssm.GetSecretValueInput, ...request.Option) (*awssm.GetSecretValueOutput, error)
PutSecretValueWithContext(aws.Context, *awssm.PutSecretValueInput, ...request.Option) (*awssm.PutSecretValueOutput, error)
DescribeSecretWithContext(aws.Context, *awssm.DescribeSecretInput, ...request.Option) (*awssm.DescribeSecretOutput, error)
DeleteSecretWithContext(ctx aws.Context, input *awssm.DeleteSecretInput, opts ...request.Option) (*awssm.DeleteSecretOutput, error)
} }
const ( const (
errUnexpectedFindOperator = "unexpected find operator" errUnexpectedFindOperator = "unexpected find operator"
managedBy = "managed-by"
externalSecrets = "external-secrets"
) )
var log = ctrl.Log.WithName("provider").WithName("aws").WithName("secretsmanager") var log = ctrl.Log.WithName("provider").WithName("aws").WithName("secretsmanager")
@ -104,6 +114,104 @@ func (sm *SecretsManager) fetch(_ context.Context, ref esv1beta1.ExternalSecretD
return secretOut, nil return secretOut, nil
} }
func (sm *SecretsManager) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
secretName := remoteRef.GetRemoteKey()
secretValue := awssm.GetSecretValueInput{
SecretId: &secretName,
}
secretInput := awssm.DescribeSecretInput{
SecretId: &secretName,
}
awsSecret, err := sm.client.GetSecretValueWithContext(ctx, &secretValue)
var aerr awserr.Error
if err != nil {
if ok := errors.As(err, &aerr); !ok {
return err
}
if aerr.Code() == awssm.ErrCodeResourceNotFoundException {
return nil
}
return err
}
data, err := sm.client.DescribeSecretWithContext(ctx, &secretInput)
if err != nil {
return err
}
if !isManagedByESO(data) {
return nil
}
deleteInput := &awssm.DeleteSecretInput{
SecretId: awsSecret.ARN,
}
_, err = sm.client.DeleteSecretWithContext(ctx, deleteInput)
return err
}
func (sm *SecretsManager) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
secretName := remoteRef.GetRemoteKey()
managedBy := managedBy
externalSecrets := externalSecrets
externalSecretsTag := []*awssm.Tag{
{
Key: &managedBy,
Value: &externalSecrets,
},
}
secretRequest := awssm.CreateSecretInput{
Name: &secretName,
SecretBinary: value,
Tags: externalSecretsTag,
}
secretValue := awssm.GetSecretValueInput{
SecretId: &secretName,
}
secretInput := awssm.DescribeSecretInput{
SecretId: &secretName,
}
awsSecret, err := sm.client.GetSecretValueWithContext(ctx, &secretValue)
var aerr awserr.Error
if err != nil {
if ok := errors.As(err, &aerr); !ok {
return err
}
if aerr.Code() == awssm.ErrCodeResourceNotFoundException {
_, err = sm.client.CreateSecretWithContext(ctx, &secretRequest)
return err
}
return err
}
data, err := sm.client.DescribeSecretWithContext(ctx, &secretInput)
if err != nil {
return err
}
if !isManagedByESO(data) {
return fmt.Errorf("secret not managed by external-secrets")
}
if awsSecret != nil && bytes.Equal(awsSecret.SecretBinary, value) {
return nil
}
input := &awssm.PutSecretValueInput{
SecretId: awsSecret.ARN,
SecretBinary: value,
}
_, err = sm.client.PutSecretValueWithContext(ctx, input)
return err
}
func isManagedByESO(data *awssm.DescribeSecretOutput) bool {
managedBy := managedBy
externalSecrets := externalSecrets
for _, tag := range data.Tags {
if *tag.Key == managedBy && *tag.Value == externalSecrets {
return true
}
}
return false
}
// GetAllSecrets syncs multiple secrets from aws provider into a single Kubernetes Secret. // GetAllSecrets syncs multiple secrets from aws provider into a single Kubernetes Secret.
func (sm *SecretsManager) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) { func (sm *SecretsManager) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
if ref.Name != nil { if ref.Name != nil {
@ -305,3 +413,7 @@ func (sm *SecretsManager) Validate() (esv1beta1.ValidationResult, error) {
} }
return esv1beta1.ValidationResultReady, nil return esv1beta1.ValidationResultReady, nil
} }
func (sm *SecretsManager) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadWrite
}

View file

@ -16,13 +16,16 @@ package secretsmanager
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
"testing" "testing"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
awssm "github.com/aws/aws-sdk-go/service/secretsmanager" awssm "github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
fakesm "github.com/external-secrets/external-secrets/pkg/provider/aws/secretsmanager/fake" fakesm "github.com/external-secrets/external-secrets/pkg/provider/aws/secretsmanager/fake"
@ -316,3 +319,384 @@ func ErrorContains(out error, want string) bool {
} }
return strings.Contains(out.Error(), want) return strings.Contains(out.Error(), want)
} }
type fakeRef struct {
key string
}
func (f fakeRef) GetRemoteKey() string {
return f.key
}
func TestSetSecret(t *testing.T) {
managedBy := managedBy
notManagedBy := "not-managed-by"
secretValue := []byte("fake-value")
externalSecrets := externalSecrets
noPermission := errors.New("no permission")
arn := "arn:aws:secretsmanager:us-east-1:702902267788:secret:foo-bar5-Robbgh"
getSecretCorrectErr := awssm.ResourceNotFoundException{}
getSecretWrongErr := awssm.InvalidRequestException{}
secretOutput := &awssm.CreateSecretOutput{
ARN: &arn,
}
externalSecretsTag := []*awssm.Tag{
{
Key: &managedBy,
Value: &externalSecrets,
},
}
externalSecretsTagFaulty := []*awssm.Tag{
{
Key: &notManagedBy,
Value: &externalSecrets,
},
}
tagSecretOutput := &awssm.DescribeSecretOutput{
ARN: &arn,
Tags: externalSecretsTag,
}
tagSecretOutputFaulty := &awssm.DescribeSecretOutput{
ARN: &arn,
Tags: externalSecretsTagFaulty,
}
secretValueOutput := &awssm.GetSecretValueOutput{
ARN: &arn,
}
secretValueOutput2 := &awssm.GetSecretValueOutput{
ARN: &arn,
SecretBinary: secretValue,
}
blankSecretValueOutput := &awssm.GetSecretValueOutput{}
putSecretOutput := &awssm.PutSecretValueOutput{
ARN: &arn,
}
type args struct {
store *esv1beta1.AWSProvider
client fakesm.Client
}
type want struct {
err error
}
tests := map[string]struct {
reason string
args args
want want
}{
"SetSecretSucceedsWithExistingSecret": {
reason: "a secret can be pushed to aws secrets manager when it already exists",
args: args{
store: makeValidSecretStore().Spec.Provider.AWS,
client: fakesm.Client{
GetSecretValueWithContextFn: fakesm.NewGetSecretValueWithContextFn(secretValueOutput, nil),
CreateSecretWithContextFn: fakesm.NewCreateSecretWithContextFn(secretOutput, nil),
PutSecretValueWithContextFn: fakesm.NewPutSecretValueWithContextFn(putSecretOutput, nil),
DescribeSecretWithContextFn: fakesm.NewDescribeSecretWithContextFn(tagSecretOutput, nil),
},
},
want: want{
err: nil,
},
},
"SetSecretSucceedsWithNewSecret": {
reason: "a secret can be pushed to aws secrets manager if it doesn't already exist",
args: args{
store: makeValidSecretStore().Spec.Provider.AWS,
client: fakesm.Client{
GetSecretValueWithContextFn: fakesm.NewGetSecretValueWithContextFn(blankSecretValueOutput, &getSecretCorrectErr),
CreateSecretWithContextFn: fakesm.NewCreateSecretWithContextFn(secretOutput, nil),
},
},
want: want{
err: nil,
},
},
"SetSecretCreateSecretFails": {
reason: "CreateSecretWithContext returns an error if it fails",
args: args{
store: makeValidSecretStore().Spec.Provider.AWS,
client: fakesm.Client{
GetSecretValueWithContextFn: fakesm.NewGetSecretValueWithContextFn(blankSecretValueOutput, &getSecretCorrectErr),
CreateSecretWithContextFn: fakesm.NewCreateSecretWithContextFn(nil, noPermission),
},
},
want: want{
err: noPermission,
},
},
"SetSecretGetSecretFails": {
reason: "GetSecretValueWithContext returns an error if it fails",
args: args{
store: makeValidSecretStore().Spec.Provider.AWS,
client: fakesm.Client{
GetSecretValueWithContextFn: fakesm.NewGetSecretValueWithContextFn(blankSecretValueOutput, noPermission),
},
},
want: want{
err: noPermission,
},
},
"SetSecretWillNotPushSameSecret": {
reason: "secret with the same value will not be pushed",
args: args{
store: makeValidSecretStore().Spec.Provider.AWS,
client: fakesm.Client{
GetSecretValueWithContextFn: fakesm.NewGetSecretValueWithContextFn(secretValueOutput2, nil),
DescribeSecretWithContextFn: fakesm.NewDescribeSecretWithContextFn(tagSecretOutput, nil),
},
},
want: want{
err: nil,
},
},
"SetSecretPutSecretValueFails": {
reason: "PutSecretValueWithContext returns an error if it fails",
args: args{
store: makeValidSecretStore().Spec.Provider.AWS,
client: fakesm.Client{
GetSecretValueWithContextFn: fakesm.NewGetSecretValueWithContextFn(secretValueOutput, nil),
PutSecretValueWithContextFn: fakesm.NewPutSecretValueWithContextFn(nil, noPermission),
DescribeSecretWithContextFn: fakesm.NewDescribeSecretWithContextFn(tagSecretOutput, nil),
},
},
want: want{
err: noPermission,
},
},
"SetSecretWrongGetSecretErrFails": {
reason: "GetSecretValueWithContext errors out when anything except awssm.ErrCodeResourceNotFoundException",
args: args{
store: makeValidSecretStore().Spec.Provider.AWS,
client: fakesm.Client{
GetSecretValueWithContextFn: fakesm.NewGetSecretValueWithContextFn(blankSecretValueOutput, &getSecretWrongErr),
},
},
want: want{
err: &getSecretWrongErr,
},
},
"SetSecretDescribeSecretFails": {
reason: "secret cannot be described",
args: args{
store: makeValidSecretStore().Spec.Provider.AWS,
client: fakesm.Client{
GetSecretValueWithContextFn: fakesm.NewGetSecretValueWithContextFn(secretValueOutput, nil),
DescribeSecretWithContextFn: fakesm.NewDescribeSecretWithContextFn(nil, noPermission),
},
},
want: want{
err: noPermission,
},
},
"SetSecretDoesNotOverwriteUntaggedSecret": {
reason: "secret cannot be described",
args: args{
store: makeValidSecretStore().Spec.Provider.AWS,
client: fakesm.Client{
GetSecretValueWithContextFn: fakesm.NewGetSecretValueWithContextFn(secretValueOutput, nil),
DescribeSecretWithContextFn: fakesm.NewDescribeSecretWithContextFn(tagSecretOutputFaulty, nil),
},
},
want: want{
err: fmt.Errorf("secret not managed by external-secrets"),
},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
ref := fakeRef{key: "fake-key"}
sm := SecretsManager{
client: &tc.args.client,
}
err := sm.PushSecret(context.Background(), []byte("fake-value"), ref)
// Error nil XOR tc.want.err nil
if ((err == nil) || (tc.want.err == nil)) && !((err == nil) && (tc.want.err == nil)) {
t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error: %v", name, tc.reason, tc.want.err, err)
}
// if errors are the same type but their contents do not match
if err != nil && tc.want.err != nil {
if !strings.Contains(err.Error(), tc.want.err.Error()) {
t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error got nil", name, tc.reason, tc.want.err)
}
}
})
}
}
func TestDeleteSecret(t *testing.T) {
fakeClient := fakesm.Client{}
managed := managedBy
manager := externalSecrets
secretTag := awssm.Tag{
Key: &managed,
Value: &manager,
}
type args struct {
client fakesm.Client
getSecretOutput *awssm.GetSecretValueOutput
describeSecretOutput *awssm.DescribeSecretOutput
deleteSecretOutput *awssm.DeleteSecretOutput
getSecretErr error
describeSecretErr error
deleteSecretErr error
}
type want struct {
err error
}
type testCase struct {
args args
want want
reason string
}
tests := map[string]testCase{
"Deletes Successfully": {
args: args{
client: fakeClient,
getSecretOutput: &awssm.GetSecretValueOutput{},
describeSecretOutput: &awssm.DescribeSecretOutput{
Tags: []*awssm.Tag{&secretTag},
},
deleteSecretOutput: &awssm.DeleteSecretOutput{},
getSecretErr: nil,
describeSecretErr: nil,
deleteSecretErr: nil,
},
want: want{
err: nil,
},
reason: "",
},
"Not Managed by ESO": {
args: args{
client: fakeClient,
getSecretOutput: &awssm.GetSecretValueOutput{},
describeSecretOutput: &awssm.DescribeSecretOutput{
Tags: []*awssm.Tag{},
},
deleteSecretOutput: &awssm.DeleteSecretOutput{},
getSecretErr: nil,
describeSecretErr: nil,
deleteSecretErr: nil,
},
want: want{
err: nil,
},
reason: "",
},
"Failed to get Tags": {
args: args{
client: fakeClient,
getSecretOutput: &awssm.GetSecretValueOutput{},
describeSecretOutput: nil,
deleteSecretOutput: nil,
getSecretErr: nil,
describeSecretErr: errors.New("failed to get tags"),
deleteSecretErr: nil,
},
want: want{
err: errors.New("failed to get tags"),
},
reason: "",
},
"Secret Not Found": {
args: args{
client: fakeClient,
getSecretOutput: nil,
describeSecretOutput: nil,
deleteSecretOutput: nil,
getSecretErr: awserr.New(awssm.ErrCodeResourceNotFoundException, "not here, sorry dude", nil),
describeSecretErr: nil,
deleteSecretErr: nil,
},
want: want{
err: nil,
},
},
"Not expected AWS error": {
args: args{
client: fakeClient,
getSecretOutput: nil,
describeSecretOutput: nil,
deleteSecretOutput: nil,
getSecretErr: awserr.New(awssm.ErrCodeEncryptionFailure, "aws unavailable", nil),
describeSecretErr: nil,
deleteSecretErr: nil,
},
want: want{
err: errors.New("aws unavailable"),
},
},
"unexpected error": {
args: args{
client: fakeClient,
getSecretOutput: nil,
describeSecretOutput: nil,
deleteSecretOutput: nil,
getSecretErr: errors.New("timeout"),
describeSecretErr: nil,
deleteSecretErr: nil,
},
want: want{
err: errors.New("timeout"),
},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
ref := fakeRef{key: "fake-key"}
sm := SecretsManager{
client: &tc.args.client,
}
tc.args.client.GetSecretValueWithContextFn = fakesm.NewGetSecretValueWithContextFn(tc.args.getSecretOutput, tc.args.getSecretErr)
tc.args.client.DescribeSecretWithContextFn = fakesm.NewDescribeSecretWithContextFn(tc.args.describeSecretOutput, tc.args.describeSecretErr)
tc.args.client.DeleteSecretWithContextFn = fakesm.NewDeleteSecretWithContextFn(tc.args.deleteSecretOutput, tc.args.deleteSecretErr)
err := sm.DeleteSecret(context.TODO(), ref)
// Error nil XOR tc.want.err nil
if ((err == nil) || (tc.want.err == nil)) && !((err == nil) && (tc.want.err == nil)) {
t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error: %v", name, tc.reason, tc.want.err, err)
}
// if errors are the same type but their contents do not match
if err != nil && tc.want.err != nil {
if !strings.Contains(err.Error(), tc.want.err.Error()) {
t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error got nil", name, tc.reason, tc.want.err)
}
}
})
}
}
func makeValidSecretStore() *esv1beta1.SecretStore {
return &esv1beta1.SecretStore{
ObjectMeta: metav1.ObjectMeta{
Name: "aws-secret-store",
Namespace: "default",
},
Spec: esv1beta1.SecretStoreSpec{
Provider: &esv1beta1.SecretStoreProvider{
AWS: &esv1beta1.AWSProvider{
Service: esv1beta1.AWSServiceSecretsManager,
Region: "eu-west-2",
},
},
},
}
}

View file

@ -107,6 +107,11 @@ func init() {
}) })
} }
// Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
func (a *Azure) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadOnly
}
// NewClient constructs a new secrets client based on the provided store. // 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) (esv1beta1.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) return newClient(ctx, store, kube, namespace)
@ -196,6 +201,15 @@ func (a *Azure) ValidateStore(store esv1beta1.GenericStore) error {
return nil return nil
} }
func (a *Azure) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
// Not Implemented PushSecret.
func (a *Azure) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
// Implements store.Client.GetAllSecrets Interface. // Implements store.Client.GetAllSecrets Interface.
// Retrieves a map[string][]byte with the secret names as key and the secret itself as the calue. // Retrieves a map[string][]byte with the secret names as key and the secret itself as the calue.
func (a *Azure) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) { func (a *Azure) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {

View file

@ -115,6 +115,14 @@ func (c *Client) Validate() (esv1beta1.ValidationResult, error) {
return esv1beta1.ValidationResultReady, nil return esv1beta1.ValidationResultReady, nil
} }
func (c *Client) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
func (c *Client) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
func (c *Client) GetSecret(_ context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) { func (c *Client) GetSecret(_ context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
request := dClient.SecretRequest{ request := dClient.SecretRequest{
Name: ref.Key, Name: ref.Key,

View file

@ -46,6 +46,10 @@ func init() {
}) })
} }
func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadOnly
}
func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) { func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
storeSpec := store.GetSpec() storeSpec := store.GetSpec()

View file

@ -30,15 +30,61 @@ var (
errMissingValueField = "at least one of value or valueMap must be set in data %v" errMissingValueField = "at least one of value or valueMap must be set in data %v"
) )
type SourceOrigin string
const (
FakeSecretStore SourceOrigin = "SecretStore"
FakeSetSecret SourceOrigin = "SetSecret"
)
type Data struct {
Value string
Version string
ValueMap map[string]string
Origin SourceOrigin
}
type Config map[string]*Data
type Provider struct { type Provider struct {
config *esv1beta1.FakeProvider config Config
database map[string]Config
}
// Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadWrite
} }
func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.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 p.database == nil {
p.database = make(map[string]Config)
}
c, err := getProvider(store)
if err != nil { if err != nil {
return nil, err return nil, err
} }
cfg := p.database[store.GetName()]
if cfg == nil {
cfg = Config{}
}
// We want to remove any FakeSecretStore entry from memory
// this will ensure SecretStores can delete from memory.
for key, data := range cfg {
if data.Origin == FakeSecretStore {
delete(cfg, key)
}
}
for _, data := range c.Data {
mapKey := fmt.Sprintf("%v%v", data.Key, data.Version)
cfg[mapKey] = &Data{
Value: data.Value,
Version: data.Version,
Origin: FakeSecretStore,
}
if data.ValueMap != nil {
cfg[mapKey].ValueMap = data.ValueMap
}
}
p.database[store.GetName()] = cfg
return &Provider{ return &Provider{
config: cfg, config: cfg,
}, nil }, nil
@ -55,6 +101,26 @@ func getProvider(store esv1beta1.GenericStore) (*esv1beta1.FakeProvider, error)
return spc.Provider.Fake, nil return spc.Provider.Fake, nil
} }
func (p *Provider) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
return nil
}
func (p *Provider) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
currentData, ok := p.config[remoteRef.GetRemoteKey()]
if !ok {
p.config[remoteRef.GetRemoteKey()] = &Data{
Value: string(value),
Origin: FakeSetSecret,
}
return nil
}
if currentData.Origin != FakeSetSecret {
return fmt.Errorf("key already exists")
}
currentData.Value = string(value)
return nil
}
// Empty GetAllSecrets. // Empty GetAllSecrets.
func (p *Provider) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) { func (p *Provider) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
// TO be implemented // TO be implemented
@ -63,23 +129,22 @@ func (p *Provider) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecr
// GetSecret returns a single secret from the provider. // GetSecret returns a single secret from the provider.
func (p *Provider) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) { func (p *Provider) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
for _, data := range p.config.Data { mapKey := fmt.Sprintf("%v%v", ref.Key, ref.Version)
if data.Key == ref.Key && data.Version == ref.Version { data, ok := p.config[mapKey]
return []byte(data.Value), nil if !ok || data.Version != ref.Version {
} return nil, esv1beta1.NoSecretErr
} }
return nil, esv1beta1.NoSecretErr return []byte(data.Value), nil
} }
// GetSecretMap returns multiple k/v pairs from the provider. // GetSecretMap returns multiple k/v pairs from the provider.
func (p *Provider) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) { func (p *Provider) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
for _, data := range p.config.Data { mapKey := fmt.Sprintf("%v%v", ref.Key, ref.Version)
if data.Key != ref.Key || data.Version != ref.Version || data.ValueMap == nil { data, ok := p.config[mapKey]
continue if !ok || data.Version != ref.Version || data.ValueMap == nil {
} return nil, esv1beta1.NoSecretErr
return convertMap(data.ValueMap), nil
} }
return nil, esv1beta1.NoSecretErr return convertMap(data.ValueMap), nil
} }
func convertMap(in map[string]string) map[string][]byte { func convertMap(in map[string]string) map[string][]byte {

View file

@ -15,11 +15,14 @@ package fake
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"testing" "testing"
"github.com/onsi/gomega" "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
) )
@ -123,9 +126,12 @@ func TestGetSecret(t *testing.T) {
}, },
} }
for _, row := range tbl { for i, row := range tbl {
t.Run(row.name, func(t *testing.T) { t.Run(row.name, func(t *testing.T) {
cl, err := p.NewClient(context.Background(), &esv1beta1.SecretStore{ cl, err := p.NewClient(context.Background(), &esv1beta1.SecretStore{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("secret-store-%v", i),
},
Spec: esv1beta1.SecretStoreSpec{ Spec: esv1beta1.SecretStoreSpec{
Provider: &esv1beta1.SecretStoreProvider{ Provider: &esv1beta1.SecretStoreProvider{
Fake: &esv1beta1.FakeProvider{ Fake: &esv1beta1.FakeProvider{
@ -146,6 +152,69 @@ func TestGetSecret(t *testing.T) {
} }
} }
type setSecretTestCase struct {
name string
input []esv1beta1.FakeProviderData
requestKey string
expValue string
expErr string
}
func TestSetSecret(t *testing.T) {
gomega.RegisterTestingT(t)
p := &Provider{}
tbl := []setSecretTestCase{
{
name: "return nil if no existing secret",
input: []esv1beta1.FakeProviderData{},
requestKey: "/foo",
expValue: "my-secret-value",
},
{
name: "return err if existing secret",
input: []esv1beta1.FakeProviderData{
{
Key: "/foo",
Value: "bar2",
},
},
requestKey: "/foo",
expErr: errors.New("key already exists").Error(),
},
}
for i, row := range tbl {
t.Run(row.name, func(t *testing.T) {
cl, err := p.NewClient(context.Background(), &esv1beta1.SecretStore{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("secret-store-%v", i),
},
Spec: esv1beta1.SecretStoreSpec{
Provider: &esv1beta1.SecretStoreProvider{
Fake: &esv1beta1.FakeProvider{
Data: row.input,
},
},
},
}, nil, "")
gomega.Expect(err).ToNot(gomega.HaveOccurred())
err = cl.PushSecret(context.TODO(), []byte(row.expValue), esv1alpha1.PushSecretRemoteRef{
RemoteKey: row.requestKey,
})
if row.expErr != "" {
gomega.Expect(err).To(gomega.MatchError(row.expErr))
} else {
gomega.Expect(err).ToNot(gomega.HaveOccurred())
out, err := cl.GetSecret(context.Background(), esv1beta1.ExternalSecretDataRemoteRef{
Key: row.requestKey,
})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
gomega.Expect(string(out)).To(gomega.Equal(row.expValue))
}
})
}
}
type testMapCase struct { type testMapCase struct {
name string name string
input []esv1beta1.FakeProviderData input []esv1beta1.FakeProviderData
@ -204,9 +273,12 @@ func TestGetSecretMap(t *testing.T) {
}, },
} }
for _, row := range tbl { for i, row := range tbl {
t.Run(row.name, func(t *testing.T) { t.Run(row.name, func(t *testing.T) {
cl, err := p.NewClient(context.Background(), &esv1beta1.SecretStore{ cl, err := p.NewClient(context.Background(), &esv1beta1.SecretStore{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("secret-store-%v", i),
},
Spec: esv1beta1.SecretStoreSpec{ Spec: esv1beta1.SecretStoreSpec{
Provider: &esv1beta1.SecretStoreProvider{ Provider: &esv1beta1.SecretStoreProvider{
Fake: &esv1beta1.FakeProvider{ Fake: &esv1beta1.FakeProvider{

View file

@ -14,6 +14,7 @@ limitations under the License.
package secretmanager package secretmanager
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
@ -24,8 +25,10 @@ import (
secretmanager "cloud.google.com/go/secretmanager/apiv1" secretmanager "cloud.google.com/go/secretmanager/apiv1"
"cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
"github.com/googleapis/gax-go/v2" "github.com/googleapis/gax-go/v2"
"github.com/googleapis/gax-go/v2/apierror"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"google.golang.org/api/iterator" "google.golang.org/api/iterator"
"google.golang.org/grpc/codes"
ctrl "sigs.k8s.io/controller-runtime" ctrl "sigs.k8s.io/controller-runtime"
kclient "sigs.k8s.io/controller-runtime/pkg/client" kclient "sigs.k8s.io/controller-runtime/pkg/client"
@ -71,13 +74,122 @@ type Client struct {
} }
type GoogleSecretManagerClient interface { type GoogleSecretManagerClient interface {
DeleteSecret(ctx context.Context, req *secretmanagerpb.DeleteSecretRequest, opts ...gax.CallOption) error
AccessSecretVersion(ctx context.Context, req *secretmanagerpb.AccessSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.AccessSecretVersionResponse, error) AccessSecretVersion(ctx context.Context, req *secretmanagerpb.AccessSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.AccessSecretVersionResponse, error)
ListSecrets(ctx context.Context, req *secretmanagerpb.ListSecretsRequest, opts ...gax.CallOption) *secretmanager.SecretIterator ListSecrets(ctx context.Context, req *secretmanagerpb.ListSecretsRequest, opts ...gax.CallOption) *secretmanager.SecretIterator
AddSecretVersion(ctx context.Context, req *secretmanagerpb.AddSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.SecretVersion, error)
CreateSecret(ctx context.Context, req *secretmanagerpb.CreateSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error)
Close() error Close() error
GetSecret(ctx context.Context, req *secretmanagerpb.GetSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error)
} }
var log = ctrl.Log.WithName("provider").WithName("gcp").WithName("secretsmanager") var log = ctrl.Log.WithName("provider").WithName("gcp").WithName("secretsmanager")
func (c *Client) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
var gcpSecret *secretmanagerpb.Secret
var err error
gcpSecret, err = c.smClient.GetSecret(ctx, &secretmanagerpb.GetSecretRequest{
Name: fmt.Sprintf("projects/%s/secrets/%s", c.store.ProjectID, remoteRef.GetRemoteKey()),
})
var gErr *apierror.APIError
if errors.As(err, &gErr) {
if gErr.GRPCStatus().Code() == codes.NotFound {
return nil
}
return err
}
if err != nil {
return err
}
manager, ok := gcpSecret.Labels["managed-by"]
if !ok || manager != "external-secrets" {
return nil
}
deleteSecretVersionReq := &secretmanagerpb.DeleteSecretRequest{
Name: fmt.Sprintf("projects/%s/secrets/%s", c.store.ProjectID, remoteRef.GetRemoteKey()),
}
return c.smClient.DeleteSecret(ctx, deleteSecretVersionReq)
}
// PushSecret pushes a kubernetes secret key into gcp provider Secret.
func (c *Client) PushSecret(ctx context.Context, payload []byte, remoteRef esv1beta1.PushRemoteRef) error {
createSecretReq := &secretmanagerpb.CreateSecretRequest{
Parent: fmt.Sprintf("projects/%s", c.store.ProjectID),
SecretId: remoteRef.GetRemoteKey(),
Secret: &secretmanagerpb.Secret{
Labels: map[string]string{
"managed-by": "external-secrets",
},
Replication: &secretmanagerpb.Replication{
Replication: &secretmanagerpb.Replication_Automatic_{
Automatic: &secretmanagerpb.Replication_Automatic{},
},
},
},
}
var gcpSecret *secretmanagerpb.Secret
var err error
gcpSecret, err = c.smClient.GetSecret(ctx, &secretmanagerpb.GetSecretRequest{
Name: fmt.Sprintf("projects/%s/secrets/%s", c.store.ProjectID, remoteRef.GetRemoteKey()),
})
var gErr *apierror.APIError
if err != nil && errors.As(err, &gErr) {
if gErr.GRPCStatus().Code() == codes.NotFound {
gcpSecret, err = c.smClient.CreateSecret(ctx, createSecretReq)
if err != nil {
return err
}
} else {
return err
}
}
manager, ok := gcpSecret.Labels["managed-by"]
if !ok || manager != "external-secrets" {
return fmt.Errorf("secret %v is not managed by external secrets", remoteRef.GetRemoteKey())
}
gcpVersion, err := c.smClient.AccessSecretVersion(ctx, &secretmanagerpb.AccessSecretVersionRequest{
Name: fmt.Sprintf("projects/%s/secrets/%s/versions/latest", c.store.ProjectID, remoteRef.GetRemoteKey()),
})
if errors.As(err, &gErr) {
if err != nil && gErr.GRPCStatus().Code() != codes.NotFound {
return err
}
} else if err != nil {
return err
}
if gcpVersion != nil && gcpVersion.Payload != nil && bytes.Equal(payload, gcpVersion.Payload.Data) {
return nil
}
addSecretVersionReq := &secretmanagerpb.AddSecretVersionRequest{
Parent: fmt.Sprintf("projects/%s/secrets/%s", c.store.ProjectID, remoteRef.GetRemoteKey()),
Payload: &secretmanagerpb.SecretPayload{
Data: payload,
},
}
_, err = c.smClient.AddSecretVersion(ctx, addSecretVersionReq)
if err != nil {
return err
}
return nil
}
// GetAllSecrets syncs multiple secrets from gcp provider into a single Kubernetes Secret. // GetAllSecrets syncs multiple secrets from gcp provider into a single Kubernetes Secret.
func (c *Client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) { func (c *Client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
if ref.Name != nil { if ref.Name != nil {
@ -86,6 +198,7 @@ func (c *Client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecret
if len(ref.Tags) > 0 { if len(ref.Tags) > 0 {
return c.findByTags(ctx, ref) return c.findByTags(ctx, ref)
} }
return nil, errors.New(errUnexpectedFindOperator) return nil, errors.New(errUnexpectedFindOperator)
} }
@ -194,8 +307,8 @@ func (c *Client) trimName(name string) string {
// (and users would always use the name, while requests accept both). // (and users would always use the name, while requests accept both).
func (c *Client) extractProjectIDNumber(secretFullName string) string { func (c *Client) extractProjectIDNumber(secretFullName string) string {
s := strings.Split(secretFullName, "/") s := strings.Split(secretFullName, "/")
projectIDNumuber := s[1] ProjectIDNumuber := s[1]
return projectIDNumuber return ProjectIDNumuber
} }
// GetSecret returns a single secret from the provider. // GetSecret returns a single secret from the provider.

View file

@ -15,12 +15,16 @@ package secretmanager
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
"github.com/googleapis/gax-go/v2/apierror"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"k8s.io/utils/pointer" "k8s.io/utils/pointer"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
@ -97,7 +101,7 @@ var setAPIErr = func(smtc *secretManagerTestCase) {
var setNilMockClient = func(smtc *secretManagerTestCase) { var setNilMockClient = func(smtc *secretManagerTestCase) {
smtc.mockClient = nil smtc.mockClient = nil
smtc.expectError = errUninitalizedGCPProvider smtc.expectError = "provider GCP is not initialized"
} }
// test the sm<->gcp interface // test the sm<->gcp interface
@ -181,6 +185,335 @@ func TestSecretManagerGetSecret(t *testing.T) {
} }
} }
type fakeRef struct {
key string
}
func (f fakeRef) GetRemoteKey() string {
return f.key
}
func TestDeleteSecret(t *testing.T) {
fErr := status.Error(codes.NotFound, "failed")
notFoundError, _ := apierror.FromError(fErr)
pErr := status.Error(codes.PermissionDenied, "failed")
permissionDeniedError, _ := apierror.FromError(pErr)
fakeClient := fakesm.MockSMClient{}
type args struct {
client fakesm.MockSMClient
getSecretOutput fakesm.GetSecretMockReturn
deleteSecretErr error
}
type want struct {
err error
}
type testCase struct {
args args
want want
reason string
}
tests := map[string]testCase{
"Deletes Successfully": {
args: args{
client: fakeClient,
getSecretOutput: fakesm.GetSecretMockReturn{
Secret: &secretmanagerpb.Secret{
Name: "projects/foo/secret/bar",
Labels: map[string]string{
"managed-by": "external-secrets",
},
},
Err: nil,
},
},
},
"Not Managed by ESO": {
args: args{
client: fakeClient,
getSecretOutput: fakesm.GetSecretMockReturn{
Secret: &secretmanagerpb.Secret{
Name: "projects/foo/secret/bar",
Labels: map[string]string{},
},
Err: nil,
},
},
},
"Secret Not Found": {
args: args{
client: fakeClient,
getSecretOutput: fakesm.GetSecretMockReturn{
Secret: nil,
Err: notFoundError,
},
},
},
"Random Error": {
args: args{
client: fakeClient,
getSecretOutput: fakesm.GetSecretMockReturn{
Secret: nil,
Err: errors.New("This errored out"),
},
},
want: want{
err: errors.New("This errored out"),
},
},
"Random GError": {
args: args{
client: fakeClient,
getSecretOutput: fakesm.GetSecretMockReturn{
Secret: nil,
Err: permissionDeniedError,
},
},
want: want{
err: errors.New("failed"),
},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
ref := fakeRef{key: "fake-key"}
client := Client{
smClient: &tc.args.client,
store: &esv1beta1.GCPSMProvider{
ProjectID: "foo",
},
}
tc.args.client.NewGetSecretFn(tc.args.getSecretOutput)
tc.args.client.NewDeleteSecretFn(tc.args.deleteSecretErr)
err := client.DeleteSecret(context.TODO(), ref)
// Error nil XOR tc.want.err nil
if ((err == nil) || (tc.want.err == nil)) && !((err == nil) && (tc.want.err == nil)) {
t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error: %v", name, tc.reason, tc.want.err, err)
}
// if errors are the same type but their contents do not match
if err != nil && tc.want.err != nil {
if !strings.Contains(err.Error(), tc.want.err.Error()) {
t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error got nil", name, tc.reason, tc.want.err)
}
}
})
}
}
func TestSetSecret(t *testing.T) {
ref := fakeRef{key: "/baz"}
notFoundError := status.Error(codes.NotFound, "failed")
notFoundError, _ = apierror.FromError(notFoundError)
canceledError := status.Error(codes.Canceled, "canceled")
canceledError, _ = apierror.FromError(canceledError)
APIerror := fmt.Errorf("API Error")
labelError := fmt.Errorf("secret %v is not managed by external secrets", ref.GetRemoteKey())
secret := secretmanagerpb.Secret{
Name: "projects/default/secrets/baz",
Replication: &secretmanagerpb.Replication{
Replication: &secretmanagerpb.Replication_Automatic_{
Automatic: &secretmanagerpb.Replication_Automatic{},
},
},
Labels: map[string]string{
"managed-by": "external-secrets",
},
}
wrongLabelSecret := secretmanagerpb.Secret{
Name: "projects/default/secrets/foo-bar",
Replication: &secretmanagerpb.Replication{
Replication: &secretmanagerpb.Replication_Automatic_{
Automatic: &secretmanagerpb.Replication_Automatic{},
},
},
Labels: map[string]string{
"managed-by": "not-external-secrets",
},
}
smtc := secretManagerTestCase{
mockClient: &fakesm.MockSMClient{},
apiInput: makeValidAPIInput(),
ref: makeValidRef(),
apiOutput: makeValidAPIOutput(),
projectID: "default",
apiErr: nil,
expectError: "",
expectedSecret: "",
expectedData: map[string][]byte{},
}
var payload = secretmanagerpb.SecretPayload{
Data: []byte("payload"),
}
var payload2 = secretmanagerpb.SecretPayload{
Data: []byte("fake-value"),
}
var res = secretmanagerpb.AccessSecretVersionResponse{
Name: "projects/default/secrets/foo-bar",
Payload: &payload,
}
var res2 = secretmanagerpb.AccessSecretVersionResponse{
Name: "projects/default/secrets/baz",
Payload: &payload2,
}
var secretVersion = secretmanagerpb.SecretVersion{}
type args struct {
mock *fakesm.MockSMClient
GetSecretMockReturn fakesm.GetSecretMockReturn
AccessSecretVersionMockReturn fakesm.AccessSecretVersionMockReturn
AddSecretVersionMockReturn fakesm.AddSecretVersionMockReturn
CreateSecretMockReturn fakesm.CreateSecretMockReturn
}
type want struct {
err error
}
tests := map[string]struct {
reason string
args args
want want
}{
"SetSecret": {
reason: "SetSecret successfully pushes a secret",
args: args{
mock: smtc.mockClient,
GetSecretMockReturn: fakesm.GetSecretMockReturn{Secret: &secret, Err: nil},
AccessSecretVersionMockReturn: fakesm.AccessSecretVersionMockReturn{Res: &res, Err: nil},
AddSecretVersionMockReturn: fakesm.AddSecretVersionMockReturn{SecretVersion: &secretVersion, Err: nil}},
want: want{
err: nil,
},
},
"AddSecretVersion": {
reason: "secret not pushed if AddSecretVersion errors",
args: args{
mock: smtc.mockClient,
GetSecretMockReturn: fakesm.GetSecretMockReturn{Secret: &secret, Err: nil},
AccessSecretVersionMockReturn: fakesm.AccessSecretVersionMockReturn{Res: &res, Err: nil},
AddSecretVersionMockReturn: fakesm.AddSecretVersionMockReturn{SecretVersion: nil, Err: APIerror},
},
want: want{
err: APIerror,
},
},
"AccessSecretVersion": {
reason: "secret not pushed if AccessSecretVersion errors",
args: args{
mock: smtc.mockClient,
GetSecretMockReturn: fakesm.GetSecretMockReturn{Secret: &secret, Err: nil},
AccessSecretVersionMockReturn: fakesm.AccessSecretVersionMockReturn{Res: nil, Err: APIerror},
},
want: want{
err: APIerror,
},
},
"NotManagedByESO": {
reason: "secret not pushed if not managed-by external-secrets",
args: args{
mock: smtc.mockClient,
GetSecretMockReturn: fakesm.GetSecretMockReturn{Secret: &wrongLabelSecret, Err: nil},
},
want: want{
err: labelError,
},
},
"SecretAlreadyExists": {
reason: "don't push a secret with the same key and value",
args: args{
mock: smtc.mockClient,
AccessSecretVersionMockReturn: fakesm.AccessSecretVersionMockReturn{Res: &res2, Err: nil},
GetSecretMockReturn: fakesm.GetSecretMockReturn{Secret: &secret, Err: nil},
},
want: want{
err: nil,
},
},
"GetSecretNotFound": {
reason: "secret is created if one doesn't already exist",
args: args{
mock: smtc.mockClient,
GetSecretMockReturn: fakesm.GetSecretMockReturn{Secret: nil, Err: notFoundError},
AccessSecretVersionMockReturn: fakesm.AccessSecretVersionMockReturn{Res: nil, Err: notFoundError},
AddSecretVersionMockReturn: fakesm.AddSecretVersionMockReturn{SecretVersion: &secretVersion, Err: nil},
CreateSecretMockReturn: fakesm.CreateSecretMockReturn{Secret: &secret, Err: nil},
},
want: want{
err: nil,
},
},
"CreateSecretReturnsNotFoundError": {
reason: "secret not created if CreateSecret returns not found error",
args: args{
mock: smtc.mockClient,
GetSecretMockReturn: fakesm.GetSecretMockReturn{Secret: nil, Err: notFoundError},
CreateSecretMockReturn: fakesm.CreateSecretMockReturn{Secret: &secret, Err: notFoundError},
},
want: want{
err: notFoundError,
},
},
"CreateSecretReturnsError": {
reason: "secret not created if CreateSecret returns error",
args: args{
mock: smtc.mockClient,
GetSecretMockReturn: fakesm.GetSecretMockReturn{Secret: nil, Err: canceledError},
},
want: want{
err: canceledError,
},
},
"AccessSecretVersionReturnsError": {
reason: "access secret version for an existing secret returns error",
args: args{
mock: smtc.mockClient,
GetSecretMockReturn: fakesm.GetSecretMockReturn{Secret: &secret, Err: nil},
AccessSecretVersionMockReturn: fakesm.AccessSecretVersionMockReturn{Res: nil, Err: canceledError},
},
want: want{
err: canceledError,
},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
tc.args.mock.NewGetSecretFn(tc.args.GetSecretMockReturn)
tc.args.mock.NewCreateSecretFn(tc.args.CreateSecretMockReturn)
tc.args.mock.NewAccessSecretVersionFn(tc.args.AccessSecretVersionMockReturn)
tc.args.mock.NewAddSecretVersionFn(tc.args.AddSecretVersionMockReturn)
c := Client{
smClient: tc.args.mock,
store: &esv1beta1.GCPSMProvider{
ProjectID: smtc.projectID,
},
}
err := c.PushSecret(context.Background(), []byte("fake-value"), ref)
// Error nil XOR tc.want.err nil
if ((err == nil) || (tc.want.err == nil)) && !((err == nil) && (tc.want.err == nil)) {
t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error: %v", name, tc.reason, tc.want.err, err)
}
// if errors are the same type but their contents do not match
if err != nil && tc.want.err != nil {
if !strings.Contains(err.Error(), tc.want.err.Error()) {
t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error got nil", name, tc.reason, tc.want.err)
}
}
})
}
}
func TestGetSecretMap(t *testing.T) { func TestGetSecretMap(t *testing.T) {
// good case: default version & deserialization // good case: default version & deserialization
setDeserialization := func(smtc *secretManagerTestCase) { setDeserialization := func(smtc *secretManagerTestCase) {

View file

@ -15,6 +15,7 @@ package fake
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
secretmanager "cloud.google.com/go/secretmanager/apiv1" secretmanager "cloud.google.com/go/secretmanager/apiv1"
@ -27,13 +28,61 @@ import (
type MockSMClient struct { type MockSMClient struct {
accessSecretFn func(ctx context.Context, req *secretmanagerpb.AccessSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.AccessSecretVersionResponse, error) accessSecretFn func(ctx context.Context, req *secretmanagerpb.AccessSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.AccessSecretVersionResponse, error)
ListSecretsFn func(ctx context.Context, req *secretmanagerpb.ListSecretsRequest, opts ...gax.CallOption) *secretmanager.SecretIterator ListSecretsFn func(ctx context.Context, req *secretmanagerpb.ListSecretsRequest, opts ...gax.CallOption) *secretmanager.SecretIterator
addSecretFn func(ctx context.Context, req *secretmanagerpb.AddSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.SecretVersion, error)
createSecretFn func(ctx context.Context, req *secretmanagerpb.CreateSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error)
closeFn func() error closeFn func() error
GetSecretFn func(ctx context.Context, req *secretmanagerpb.GetSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error)
DeleteSecretFn func(ctx context.Context, req *secretmanagerpb.DeleteSecretRequest, opts ...gax.CallOption) error
}
type AccessSecretVersionMockReturn struct {
Res *secretmanagerpb.AccessSecretVersionResponse
Err error
}
type AddSecretVersionMockReturn struct {
SecretVersion *secretmanagerpb.SecretVersion
Err error
}
type GetSecretMockReturn struct {
Secret *secretmanagerpb.Secret
Err error
}
type CreateSecretMockReturn struct {
Secret *secretmanagerpb.Secret
Err error
}
func (mc *MockSMClient) DeleteSecret(ctx context.Context, req *secretmanagerpb.DeleteSecretRequest, opts ...gax.CallOption) error {
return mc.DeleteSecretFn(ctx, req)
}
func (mc *MockSMClient) NewDeleteSecretFn(err error) {
mc.DeleteSecretFn = func(ctx context.Context, req *secretmanagerpb.DeleteSecretRequest, opts ...gax.CallOption) error {
return err
}
}
func (mc *MockSMClient) GetSecret(ctx context.Context, req *secretmanagerpb.GetSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error) {
return mc.GetSecretFn(ctx, req)
}
func (mc *MockSMClient) NewGetSecretFn(mock GetSecretMockReturn) {
mc.GetSecretFn = func(ctx context.Context, req *secretmanagerpb.GetSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error) {
return mock.Secret, mock.Err
}
} }
func (mc *MockSMClient) AccessSecretVersion(ctx context.Context, req *secretmanagerpb.AccessSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.AccessSecretVersionResponse, error) { func (mc *MockSMClient) AccessSecretVersion(ctx context.Context, req *secretmanagerpb.AccessSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.AccessSecretVersionResponse, error) {
return mc.accessSecretFn(ctx, req) return mc.accessSecretFn(ctx, req)
} }
func (mc *MockSMClient) NewAccessSecretVersionFn(mock AccessSecretVersionMockReturn) {
mc.accessSecretFn = func(ctx context.Context, req *secretmanagerpb.AccessSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.AccessSecretVersionResponse, error) {
return mock.Res, mock.Err
}
}
func (mc *MockSMClient) ListSecrets(ctx context.Context, req *secretmanagerpb.ListSecretsRequest, opts ...gax.CallOption) *secretmanager.SecretIterator { func (mc *MockSMClient) ListSecrets(ctx context.Context, req *secretmanagerpb.ListSecretsRequest, opts ...gax.CallOption) *secretmanager.SecretIterator {
return mc.ListSecretsFn(ctx, req) return mc.ListSecretsFn(ctx, req)
} }
@ -41,12 +90,93 @@ func (mc *MockSMClient) Close() error {
return mc.closeFn() return mc.closeFn()
} }
func (mc *MockSMClient) AddSecretVersion(ctx context.Context, req *secretmanagerpb.AddSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.SecretVersion, error) {
return mc.addSecretFn(ctx, req)
}
func (mc *MockSMClient) NewAddSecretVersionFn(mock AddSecretVersionMockReturn) {
mc.addSecretFn = func(ctx context.Context, req *secretmanagerpb.AddSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.SecretVersion, error) {
return mock.SecretVersion, mock.Err
}
}
func (mc *MockSMClient) CreateSecret(ctx context.Context, req *secretmanagerpb.CreateSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error) {
return mc.createSecretFn(ctx, req)
}
func (mc *MockSMClient) NewCreateSecretFn(mock CreateSecretMockReturn) {
mc.createSecretFn = func(ctx context.Context, req *secretmanagerpb.CreateSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error) {
return mock.Secret, mock.Err
}
}
func (mc *MockSMClient) NilClose() { func (mc *MockSMClient) NilClose() {
mc.closeFn = func() error { mc.closeFn = func() error {
return nil return nil
} }
} }
func (mc *MockSMClient) CreateSecretError() {
mc.createSecretFn = func(ctx context.Context, req *secretmanagerpb.CreateSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error) {
return nil, errors.New("something went wrong")
}
}
func (mc *MockSMClient) CreateSecretGetError() {
mc.createSecretFn = func(ctx context.Context, req *secretmanagerpb.CreateSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error) {
mc.accessSecretFn = func(ctx context.Context, req *secretmanagerpb.AccessSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.AccessSecretVersionResponse, error) {
return nil, errors.New("no, this broke")
}
return nil, nil
}
}
func (mc *MockSMClient) DefaultCreateSecret(wantedSecretID, wantedParent string) {
mc.createSecretFn = func(ctx context.Context, req *secretmanagerpb.CreateSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error) {
if req.SecretId != wantedSecretID {
return nil, fmt.Errorf("create secret req wrong key: got %v want %v", req.SecretId, wantedSecretID)
}
if req.Parent != wantedParent {
return nil, fmt.Errorf("create secret req wrong parent: got %v want %v", req.Parent, wantedParent)
}
return &secretmanagerpb.Secret{
Name: fmt.Sprintf("%s/%s", req.Parent, req.SecretId),
}, nil
}
}
func (mc *MockSMClient) DefaultAddSecretVersion(wantedData, wantedParent, versionName string) {
mc.addSecretFn = func(ctx context.Context, req *secretmanagerpb.AddSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.SecretVersion, error) {
if string(req.Payload.Data) != wantedData {
return nil, fmt.Errorf("add version req wrong data got: %v want %v ", req.Payload.Data, wantedData)
}
if req.Parent != wantedParent {
return nil, fmt.Errorf("add version req has wrong parent: got %v want %v", req.Parent, wantedParent)
}
return &secretmanagerpb.SecretVersion{
Name: versionName,
}, nil
}
}
func (mc *MockSMClient) DefaultAccessSecretVersion(wantedVersionName string) {
mc.accessSecretFn = func(ctx context.Context, req *secretmanagerpb.AccessSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.AccessSecretVersionResponse, error) {
if req.Name != wantedVersionName {
return nil, fmt.Errorf("access req has wrong version name: got %v want %v", req.Name, wantedVersionName)
}
return &secretmanagerpb.AccessSecretVersionResponse{
Name: req.Name,
Payload: &secretmanagerpb.SecretPayload{Data: []byte("bar")},
}, nil
}
}
func (mc *MockSMClient) AccessSecretVersionWithError(err error) {
mc.accessSecretFn = func(ctx context.Context, req *secretmanagerpb.AccessSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.AccessSecretVersionResponse, error) {
return nil, err
}
}
func (mc *MockSMClient) WithValue(ctx context.Context, req *secretmanagerpb.AccessSecretVersionRequest, val *secretmanagerpb.AccessSecretVersionResponse, err error) { func (mc *MockSMClient) WithValue(ctx context.Context, req *secretmanagerpb.AccessSecretVersionRequest, val *secretmanagerpb.AccessSecretVersionResponse, err error) {
if mc != nil { if mc != nil {
mc.accessSecretFn = func(paramCtx context.Context, paramReq *secretmanagerpb.AccessSecretVersionRequest, paramOpts ...gax.CallOption) (*secretmanagerpb.AccessSecretVersionResponse, error) { mc.accessSecretFn = func(paramCtx context.Context, paramReq *secretmanagerpb.AccessSecretVersionRequest, paramOpts ...gax.CallOption) (*secretmanagerpb.AccessSecretVersionResponse, error) {

View file

@ -49,6 +49,10 @@ A Mutex was implemented to make sure only one connection can be in place at a ti
*/ */
var useMu = sync.Mutex{} var useMu = sync.Mutex{}
func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadWrite
}
// NewClient constructs a GCP Provider. // NewClient constructs a GCP Provider.
func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) { func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
storeSpec := store.GetSpec() storeSpec := store.GetSpec()

View file

@ -139,6 +139,11 @@ func NewGitlabProvider() *Gitlab {
return &Gitlab{} return &Gitlab{}
} }
// Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
func (g *Gitlab) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadOnly
}
// Method on Gitlab Provider to set up projectVariablesClient with credentials, populate projectID and environment. // Method on Gitlab Provider to set up projectVariablesClient with credentials, populate projectID and environment.
func (g *Gitlab) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) { func (g *Gitlab) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
storeSpec := store.GetSpec() storeSpec := store.GetSpec()
@ -187,6 +192,15 @@ func (g *Gitlab) NewClient(ctx context.Context, store esv1beta1.GenericStore, ku
return g, nil return g, nil
} }
func (g *Gitlab) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
// Not Implemented PushSecret.
func (g *Gitlab) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
// GetAllSecrets syncs all gitlab project and group variables into a single Kubernetes Secret. // GetAllSecrets syncs all gitlab project and group variables into a single Kubernetes Secret.
func (g *Gitlab) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) { func (g *Gitlab) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
if utils.IsNil(g.projectVariablesClient) { if utils.IsNil(g.projectVariablesClient) {

View file

@ -101,6 +101,15 @@ func (c *client) setAuth(ctx context.Context) error {
return nil return nil
} }
func (ibm *providerIBM) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
// Not Implemented PushSecret.
func (ibm *providerIBM) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
// Empty GetAllSecrets. // Empty GetAllSecrets.
func (ibm *providerIBM) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) { func (ibm *providerIBM) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
// TO be implemented // TO be implemented
@ -578,6 +587,11 @@ func (ibm *providerIBM) ValidateStore(store esv1beta1.GenericStore) error {
return nil return nil
} }
// Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
func (ibm *providerIBM) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadOnly
}
func (ibm *providerIBM) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) { func (ibm *providerIBM) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
storeSpec := store.GetSpec() storeSpec := store.GetSpec()
ibmSpec := storeSpec.Provider.IBM ibmSpec := storeSpec.Provider.IBM

View file

@ -49,6 +49,15 @@ func (c *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretData
return jsonStr, nil return jsonStr, nil
} }
func (c *Client) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
// Not Implemented PushSecret.
func (c *Client) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
func (c *Client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) { func (c *Client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
secret, err := c.userSecretClient.Get(ctx, ref.Key, metav1.GetOptions{}) secret, err := c.userSecretClient.Get(ctx, ref.Key, metav1.GetOptions{})
if err != nil { if err != nil {

View file

@ -83,6 +83,10 @@ func init() {
}) })
} }
func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadOnly
}
// NewClient constructs a Kubernetes Provider. // NewClient constructs a Kubernetes Provider.
func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) { func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
restCfg, err := ctrlcfg.GetConfig() restCfg, err := ctrlcfg.GetConfig()

View file

@ -68,6 +68,11 @@ type ProviderOnePassword struct {
var _ esv1beta1.SecretsClient = &ProviderOnePassword{} var _ esv1beta1.SecretsClient = &ProviderOnePassword{}
var _ esv1beta1.Provider = &ProviderOnePassword{} var _ esv1beta1.Provider = &ProviderOnePassword{}
// Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
func (provider *ProviderOnePassword) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadOnly
}
// NewClient constructs a 1Password Provider. // NewClient constructs a 1Password Provider.
func (provider *ProviderOnePassword) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) { func (provider *ProviderOnePassword) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
config := store.GetSpec().Provider.OnePassword config := store.GetSpec().Provider.OnePassword
@ -147,6 +152,15 @@ func validateStore(store esv1beta1.GenericStore) error {
return nil return nil
} }
func (provider *ProviderOnePassword) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
// Not Implemented PushSecret.
func (provider *ProviderOnePassword) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
// GetSecret returns a single secret from the provider. // GetSecret returns a single secret from the provider.
func (provider *ProviderOnePassword) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) { func (provider *ProviderOnePassword) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
if ref.Version != "" { if ref.Version != "" {

View file

@ -73,6 +73,15 @@ type KmsVCInterface interface {
GetVault(ctx context.Context, request keymanagement.GetVaultRequest) (response keymanagement.GetVaultResponse, err error) GetVault(ctx context.Context, request keymanagement.GetVaultRequest) (response keymanagement.GetVaultResponse, err error)
} }
// Not Implemented PushSecret.
func (vms *VaultManagementService) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
func (vms *VaultManagementService) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
// Empty GetAllSecrets. // Empty GetAllSecrets.
func (vms *VaultManagementService) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) { func (vms *VaultManagementService) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
// TO be implemented // TO be implemented
@ -133,6 +142,11 @@ func (vms *VaultManagementService) GetSecretMap(ctx context.Context, ref esv1bet
return secretData, nil return secretData, nil
} }
// Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
func (vms *VaultManagementService) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadOnly
}
// NewClient constructs a new secrets client based on the provided store. // 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) (esv1beta1.SecretsClient, error) { func (vms *VaultManagementService) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
storeSpec := store.GetSpec() storeSpec := store.GetSpec()

View file

@ -90,6 +90,15 @@ func New(isoSession *senhaseguraAuth.SenhaseguraIsoSession) (*DSM, error) {
}, nil }, nil
} }
func (dsm *DSM) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
// Not Implemented PushSecret.
func (dsm *DSM) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
/* /*
GetSecret implements ESO interface and get a single secret from senhasegura provider with DSM service. GetSecret implements ESO interface and get a single secret from senhasegura provider with DSM service.
*/ */

View file

@ -43,6 +43,11 @@ const (
errMissingClientID = "missing senhasegura authentication Client ID" errMissingClientID = "missing senhasegura authentication Client ID"
) )
// Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadOnly
}
/* /*
Construct a new secrets client based on provided store. Construct a new secrets client based on provided store.
*/ */

View file

@ -24,12 +24,20 @@ import (
var _ esv1beta1.Provider = &Client{} var _ esv1beta1.Provider = &Client{}
type SetSecretCallArgs struct {
Value []byte
RemoteRef esv1beta1.PushRemoteRef
}
// Client is a fake client for testing. // Client is a fake client for testing.
type Client struct { type Client struct {
SetSecretArgs map[string]SetSecretCallArgs
NewFn func(context.Context, esv1beta1.GenericStore, client.Client, string) (esv1beta1.SecretsClient, error) NewFn func(context.Context, esv1beta1.GenericStore, client.Client, string) (esv1beta1.SecretsClient, error)
GetSecretFn func(context.Context, esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) GetSecretFn func(context.Context, esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error)
GetSecretMapFn func(context.Context, esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) GetSecretMapFn func(context.Context, esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error)
GetAllSecretsFn func(context.Context, esv1beta1.ExternalSecretFind) (map[string][]byte, error) GetAllSecretsFn func(context.Context, esv1beta1.ExternalSecretFind) (map[string][]byte, error)
SetSecretFn func() error
DeleteSecretFn func() error
} }
// New returns a fake provider/client. // New returns a fake provider/client.
@ -44,6 +52,13 @@ func New() *Client {
GetAllSecretsFn: func(context.Context, esv1beta1.ExternalSecretFind) (map[string][]byte, error) { GetAllSecretsFn: func(context.Context, esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
return nil, nil return nil, nil
}, },
SetSecretFn: func() error {
return nil
},
DeleteSecretFn: func() error {
return nil
},
SetSecretArgs: map[string]SetSecretCallArgs{},
} }
v.NewFn = func(context.Context, esv1beta1.GenericStore, client.Client, string) (esv1beta1.SecretsClient, error) { v.NewFn = func(context.Context, esv1beta1.GenericStore, client.Client, string) (esv1beta1.SecretsClient, error) {
@ -63,6 +78,19 @@ func (v *Client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecret
return v.GetAllSecretsFn(ctx, ref) return v.GetAllSecretsFn(ctx, ref)
} }
// Not Implemented PushSecret.
func (v *Client) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
v.SetSecretArgs[remoteRef.GetRemoteKey()] = SetSecretCallArgs{
Value: value,
RemoteRef: remoteRef,
}
return v.SetSecretFn()
}
func (v *Client) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
return v.DeleteSecretFn()
}
// GetSecret implements the provider.Provider interface. // GetSecret implements the provider.Provider interface.
func (v *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) { func (v *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
return v.GetSecretFn(ctx, ref) return v.GetSecretFn(ctx, ref)
@ -109,6 +137,14 @@ func (v *Client) WithGetAllSecrets(secData map[string][]byte, err error) *Client
return v return v
} }
// WithSetSecret wraps the secret response to the fake provider.
func (v *Client) WithSetSecret(err error) *Client {
v.SetSecretFn = func() error {
return err
}
return v
}
// WithNew wraps the fake provider factory function. // WithNew wraps the fake provider factory function.
func (v *Client) WithNew(f func(context.Context, esv1beta1.GenericStore, client.Client, func (v *Client) WithNew(f func(context.Context, esv1beta1.GenericStore, client.Client,
string) (esv1beta1.SecretsClient, error)) *Client { string) (esv1beta1.SecretsClient, error)) *Client {
@ -116,6 +152,11 @@ func (v *Client) WithNew(f func(context.Context, esv1beta1.GenericStore, client.
return v return v
} }
// Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
func (v *Client) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadOnly
}
// NewClient returns a new fake provider. // NewClient returns a new fake provider.
func (v *Client) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.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) c, err := v.NewFn(ctx, store, kube, namespace)

View file

@ -32,11 +32,24 @@ func (f Auth) Login(ctx context.Context, authMethod vault.AuthMethod) (*vault.Se
type ReadWithDataWithContextFn func(ctx context.Context, path string, data map[string][]string) (*vault.Secret, error) type ReadWithDataWithContextFn func(ctx context.Context, path string, data map[string][]string) (*vault.Secret, error)
type ListWithContextFn func(ctx context.Context, path string) (*vault.Secret, error) type ListWithContextFn func(ctx context.Context, path string) (*vault.Secret, error)
type WriteWithContextFn func(ctx context.Context, path string, data map[string]interface{}) (*vault.Secret, error) type WriteWithContextFn func(ctx context.Context, path string, data map[string]interface{}) (*vault.Secret, error)
type DeleteWithContextFn func(ctx context.Context, path string) (*vault.Secret, error)
type Logical struct { type Logical struct {
ReadWithDataWithContextFn ReadWithDataWithContextFn ReadWithDataWithContextFn ReadWithDataWithContextFn
ListWithContextFn ListWithContextFn ListWithContextFn ListWithContextFn
WriteWithContextFn WriteWithContextFn WriteWithContextFn WriteWithContextFn
DeleteWithContextFn DeleteWithContextFn
}
func (f Logical) DeleteWithContext(ctx context.Context, path string) (*vault.Secret, error) {
return f.DeleteWithContextFn(ctx, path)
}
func NewDeleteWithContextFn(secret map[string]interface{}, err error) DeleteWithContextFn {
return func(ctx context.Context, path string) (*vault.Secret, error) {
vault := &vault.Secret{
Data: secret,
}
return vault, err
}
} }
func NewReadWithContextFn(secret map[string]interface{}, err error) ReadWithDataWithContextFn { func NewReadWithContextFn(secret map[string]interface{}, err error) ReadWithDataWithContextFn {
@ -48,6 +61,27 @@ func NewReadWithContextFn(secret map[string]interface{}, err error) ReadWithData
} }
} }
func NewWriteWithContextFn(secret map[string]interface{}, err error) WriteWithContextFn {
return func(ctx context.Context, path string, data map[string]interface{}) (*vault.Secret, error) {
vault := &vault.Secret{
Data: secret,
}
return vault, err
}
}
func WriteChangingReadContext(secret map[string]interface{}, l Logical) WriteWithContextFn {
v := &vault.Secret{
Data: secret,
}
return func(ctx context.Context, path string, data map[string]interface{}) (*vault.Secret, error) {
l.ReadWithDataWithContextFn = func(ctx context.Context, path string, data map[string][]string) (*vault.Secret, error) {
return v, nil
}
return v, nil
}
}
func (f Logical) ReadWithDataWithContext(ctx context.Context, path string, data map[string][]string) (*vault.Secret, error) { func (f Logical) ReadWithDataWithContext(ctx context.Context, path string, data map[string][]string) (*vault.Secret, error) {
return f.ReadWithDataWithContextFn(ctx, path, data) return f.ReadWithDataWithContextFn(ctx, path, data)
} }

View file

@ -127,6 +127,7 @@ type Logical interface {
ReadWithDataWithContext(ctx context.Context, path string, data map[string][]string) (*vault.Secret, error) ReadWithDataWithContext(ctx context.Context, path string, data map[string][]string) (*vault.Secret, error)
ListWithContext(ctx context.Context, path string) (*vault.Secret, error) ListWithContext(ctx context.Context, path string) (*vault.Secret, error)
WriteWithContext(ctx context.Context, path string, data map[string]interface{}) (*vault.Secret, error) WriteWithContext(ctx context.Context, path string, data map[string]interface{}) (*vault.Secret, error)
DeleteWithContext(ctx context.Context, path string) (*vault.Secret, error)
} }
type Client interface { type Client interface {
@ -272,6 +273,11 @@ type connector struct {
newVaultClient func(c *vault.Config) (Client, error) newVaultClient func(c *vault.Config) (Client, error)
} }
// Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
func (c *connector) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadWrite
}
func (c *connector) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) { func (c *connector) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
// controller-runtime/client does not support TokenRequest or other subresource APIs // controller-runtime/client does not support TokenRequest or other subresource APIs
// so we need to construct our own client and use it to fetch tokens // so we need to construct our own client and use it to fetch tokens
@ -284,6 +290,7 @@ func (c *connector) NewClient(ctx context.Context, store esv1beta1.GenericStore,
if err != nil { if err != nil {
return nil, err return nil, err
} }
return c.newClient(ctx, store, kube, clientset.CoreV1(), namespace) return c.newClient(ctx, store, kube, clientset.CoreV1(), namespace)
} }
@ -403,9 +410,94 @@ func (c *connector) ValidateStore(store esv1beta1.GenericStore) error {
return nil return nil
} }
// Empty GetAllSecrets. func (v *client) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
// GetAllSecrets path := v.buildPath(remoteRef.GetRemoteKey())
// First load all secrets from secretStore path configuration. metaPath, err := v.buildMetadataPath(remoteRef.GetRemoteKey())
if err != nil {
return err
}
// Retrieve the secret map from vault and convert the secret value in string form.
_, err = v.logical.ReadWithDataWithContext(ctx, path, nil)
// If error is not of type secret not found, we should error
if err != nil && !strings.Contains(err.Error(), "secret not found") {
return nil
}
if err != nil {
return err
}
metadata, err := v.readSecretMetadata(ctx, remoteRef.GetRemoteKey())
if err != nil {
return err
}
manager, ok := metadata["managed-by"]
if !ok || manager != "external-secrets" {
return nil
}
_, err = v.logical.DeleteWithContext(ctx, path)
if err != nil {
return fmt.Errorf("could not delete secret %v: %w", remoteRef.GetRemoteKey(), err)
}
_, err = v.logical.DeleteWithContext(ctx, metaPath)
if err != nil {
return fmt.Errorf("could not delete secret metadata %v: %w", remoteRef.GetRemoteKey(), err)
}
return nil
}
func (v *client) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
label := map[string]interface{}{
"custom_metadata": map[string]string{
"managed-by": "external-secrets",
},
}
secretToPush := map[string]interface{}{
"data": map[string]string{
remoteRef.GetRemoteKey(): string(value),
},
}
path := v.buildPath(remoteRef.GetRemoteKey())
metaPath, err := v.buildMetadataPath(remoteRef.GetRemoteKey())
if err != nil {
return err
}
// Retrieve the secret map from vault and convert the secret value in string form.
vaultSecret, err := v.GetSecretMap(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: path})
vaultSecretValue := string(vaultSecret[remoteRef.GetRemoteKey()])
// If error is not of type secret not found, we should error
if err != nil && !strings.Contains(err.Error(), "secret not found") {
return err
}
// Retrieve the secret value to be pushed and convert it to string form.
pushSecretValue := string(value)
if vaultSecretValue == pushSecretValue {
return nil
}
// If the secret exists (err == nil), we should check if it is managed by external-secrets
if err == nil {
metadata, err := v.readSecretMetadata(ctx, remoteRef.GetRemoteKey())
if err != nil {
return err
}
manager, ok := metadata["managed-by"]
if !ok || manager != "external-secrets" {
return fmt.Errorf("secret not managed by external-secrets")
}
}
_, err = v.logical.WriteWithContext(ctx, metaPath, label)
if err != nil {
return err
}
// Otherwise, create or update the version.
_, err = v.logical.WriteWithContext(ctx, path, secretToPush)
return err
}
// GetAllSecrets gets multiple secrets from the provider and loads into a kubernetes secret.
// First load all secrets from secretStore path configuration
// Then, gets secrets from a matching name or matching custom_metadata. // Then, gets secrets from a matching name or matching custom_metadata.
func (v *client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) { func (v *client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
if v.store.Version == esv1beta1.VaultKVStoreV1 { if v.store.Version == esv1beta1.VaultKVStoreV1 {
@ -791,7 +883,6 @@ func (v *client) readSecret(ctx context.Context, path, version string) (map[stri
// Vault KV2 has data embedded within sub-field // Vault KV2 has data embedded within sub-field
// reference - https://www.vaultproject.io/api/secret/kv/kv-v2#read-secret-version // reference - https://www.vaultproject.io/api/secret/kv/kv-v2#read-secret-version
dataInt, ok := vaultSecret.Data["data"] dataInt, ok := vaultSecret.Data["data"]
if !ok { if !ok {
return nil, errors.New(errDataField) return nil, errors.New(errDataField)
} }

View file

@ -1376,6 +1376,134 @@ func TestValidateStore(t *testing.T) {
} }
} }
type fakeRef struct {
key string
}
func (f fakeRef) GetRemoteKey() string {
return f.key
}
func TestSetSecret(t *testing.T) {
noPermission := errors.New("no permission")
secretNotFound := errors.New("secret not found")
type args struct {
store *esv1beta1.VaultProvider
vLogical Logical
}
type want struct {
err error
}
tests := map[string]struct {
reason string
args args
want want
}{
"SetSecret": {
reason: "secret is successfully set, with no existing vault secret",
args: args{
store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
vLogical: &fake.Logical{
ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, secretNotFound),
WriteWithContextFn: fake.NewWriteWithContextFn(nil, nil),
},
},
want: want{
err: nil,
},
},
"SetSecretWithWriteError": {
reason: "secret cannot be pushed if write fails",
args: args{
store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
vLogical: &fake.Logical{
ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, secretNotFound),
WriteWithContextFn: fake.NewWriteWithContextFn(nil, noPermission),
},
},
want: want{
err: noPermission,
},
},
"SetSecretEqualsPushSecret": {
reason: "vault secret kv equals secret to push kv",
args: args{
store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
vLogical: &fake.Logical{
ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
"data": map[string]interface{}{
"fake-key": "fake-value",
},
}, nil),
},
},
want: want{
err: nil,
},
},
"SetSecretErrorReadingSecret": {
reason: "error occurs if secret cannot be read",
args: args{
store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
vLogical: &fake.Logical{
ReadWithDataWithContextFn: fake.NewReadWithContextFn(nil, noPermission),
},
},
want: want{
err: fmt.Errorf(errReadSecret, noPermission),
},
},
"SetSecretNotManagedByESO": {
reason: "a secret not managed by ESO cannot be updated",
args: args{
store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV2).Spec.Provider.Vault,
vLogical: &fake.Logical{
ReadWithDataWithContextFn: fake.NewReadWithContextFn(map[string]interface{}{
"data": map[string]interface{}{
"fake-key": "fake-value2",
"custom_metadata": map[string]interface{}{
"managed-by": "not-external-secrets",
},
},
}, nil),
},
},
want: want{
err: errors.New("secret not managed by external-secrets"),
},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
ref := fakeRef{key: "fake-key"}
client := &client{
logical: tc.args.vLogical,
store: tc.args.store,
}
err := client.PushSecret(context.Background(), []byte("fake-value"), ref)
// Error nil XOR tc.want.err nil
if ((err == nil) || (tc.want.err == nil)) && !((err == nil) && (tc.want.err == nil)) {
t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error: %v", name, tc.reason, tc.want.err, err)
}
// if errors are the same type but their contents do not match
if err != nil && tc.want.err != nil {
if !strings.Contains(err.Error(), tc.want.err.Error()) {
t.Errorf("\nTesting SetSecret:\nName: %v\nReason: %v\nWant error: %v\nGot error got nil", name, tc.reason, tc.want.err)
}
}
})
}
}
// EquateErrors returns true if the supplied errors are of the same type and // EquateErrors returns true if the supplied errors are of the same type and
// produce identical strings. This mirrors the error comparison behavior of // produce identical strings. This mirrors the error comparison behavior of
// https://github.com/go-test/deep, which most Crossplane tests targeted before // https://github.com/go-test/deep, which most Crossplane tests targeted before

View file

@ -61,6 +61,11 @@ func init() {
}) })
} }
// Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadOnly
}
func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) { func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
whClient := &WebHook{ whClient := &WebHook{
kube: kube, kube: kube,
@ -111,6 +116,15 @@ func (w *WebHook) getStoreSecret(ctx context.Context, ref esmeta.SecretKeySelect
return secret, nil return secret, nil
} }
func (w *WebHook) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
// Not Implemented PushSecret.
func (w *WebHook) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
// Empty GetAllSecrets. // Empty GetAllSecrets.
func (w *WebHook) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) { func (w *WebHook) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
// TO be implemented // TO be implemented

View file

@ -88,6 +88,7 @@ func InitYandexCloudProvider(
return provider return provider
} }
type NewSecretSetterFunc func()
type AdaptInputFunc func(store esv1beta1.GenericStore) (*SecretsClientInput, error) type AdaptInputFunc func(store esv1beta1.GenericStore) (*SecretsClientInput, error)
type NewSecretGetterFunc func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (SecretGetter, error) type NewSecretGetterFunc func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (SecretGetter, error)
type NewIamTokenFunc func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (*IamToken, error) type NewIamTokenFunc func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (*IamToken, error)
@ -103,6 +104,10 @@ type SecretsClientInput struct {
CACertificate *esmeta.SecretKeySelector CACertificate *esmeta.SecretKeySelector
} }
func (p *YandexCloudProvider) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadOnly
}
// NewClient constructs a Yandex.Cloud Provider. // NewClient constructs a Yandex.Cloud Provider.
func (p *YandexCloudProvider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) { func (p *YandexCloudProvider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
input, err := p.adaptInputFunc(store) input, err := p.adaptInputFunc(store)
@ -177,7 +182,7 @@ func (p *YandexCloudProvider) NewClient(ctx context.Context, store esv1beta1.Gen
return nil, fmt.Errorf("failed to create IAM token: %w", err) return nil, fmt.Errorf("failed to create IAM token: %w", err)
} }
return &yandexCloudSecretsClient{secretGetter, iamToken.Token}, nil return &yandexCloudSecretsClient{secretGetter, nil, iamToken.Token}, nil
} }
func (p *YandexCloudProvider) getOrCreateSecretGetter(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (SecretGetter, error) { func (p *YandexCloudProvider) getOrCreateSecretGetter(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (SecretGetter, error) {

View file

@ -26,26 +26,35 @@ var _ esv1beta1.SecretsClient = &yandexCloudSecretsClient{}
// Implementation of v1beta1.SecretsClient. // Implementation of v1beta1.SecretsClient.
type yandexCloudSecretsClient struct { type yandexCloudSecretsClient struct {
secretGetter SecretGetter secretGetter SecretGetter
secretSetter SecretSetter
iamToken string iamToken string
} }
func (c *yandexCloudSecretsClient) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
return c.secretGetter.GetSecret(ctx, c.iamToken, ref.Key, ref.Version, ref.Property)
}
func (c *yandexCloudSecretsClient) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
func (c *yandexCloudSecretsClient) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
return fmt.Errorf("not implemented")
}
func (c *yandexCloudSecretsClient) Validate() (esv1beta1.ValidationResult, error) {
return esv1beta1.ValidationResultReady, nil
}
func (c *yandexCloudSecretsClient) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
return c.secretGetter.GetSecretMap(ctx, c.iamToken, ref.Key, ref.Version)
}
func (c *yandexCloudSecretsClient) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) { func (c *yandexCloudSecretsClient) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
// TO be implemented // TO be implemented
return nil, fmt.Errorf("GetAllSecrets not supported") return nil, fmt.Errorf("GetAllSecrets not supported")
} }
func (c *yandexCloudSecretsClient) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
return c.secretGetter.GetSecret(ctx, c.iamToken, ref.Key, ref.Version, ref.Property)
}
func (c *yandexCloudSecretsClient) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
return c.secretGetter.GetSecretMap(ctx, c.iamToken, ref.Key, ref.Version)
}
func (c *yandexCloudSecretsClient) Close(ctx context.Context) error { func (c *yandexCloudSecretsClient) Close(ctx context.Context) error {
return nil return nil
} }
func (c *yandexCloudSecretsClient) Validate() (esv1beta1.ValidationResult, error) {
return esv1beta1.ValidationResultReady, nil
}

View file

@ -0,0 +1,18 @@
/*
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 common
type SecretSetter interface {
SetSecret() error
}

View file

@ -15,7 +15,6 @@ limitations under the License.
package utils package utils
import ( import (
//nolint:gosec //nolint:gosec
"crypto/md5" "crypto/md5"
"encoding/base64" "encoding/base64"

View file

@ -5,6 +5,7 @@ package tools
import ( import (
_ "github.com/ahmetb/gen-crd-api-reference-docs" _ "github.com/ahmetb/gen-crd-api-reference-docs"
_ "github.com/maxbrunsfeld/counterfeiter/v6"
_ "github.com/onsi/ginkgo/v2/ginkgo" _ "github.com/onsi/ginkgo/v2/ginkgo"
_ "sigs.k8s.io/controller-tools/cmd/controller-gen" _ "sigs.k8s.io/controller-tools/cmd/controller-gen"
) )