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)
)
var (
PushSecretKind = reflect.TypeOf(PushSecret{}).Name()
PushSecretGroupKind = schema.GroupKind{Group: Group, Kind: PushSecretKind}.String()
PushSecretKindAPIVersion = PushSecretKind + "." + SchemeGroupVersion.String()
PushSecretGroupVersionKind = SchemeGroupVersion.WithKind(PushSecretKind)
)
func init() {
SchemeBuilder.Register(&ExternalSecret{}, &ExternalSecretList{})
SchemeBuilder.Register(&SecretStore{}, &SecretStoreList{})
SchemeBuilder.Register(&ClusterSecretStore{}, &ClusterSecretStoreList{})
SchemeBuilder.Register(&PushSecret{}, &PushSecretList{})
}

View file

@ -994,6 +994,252 @@ func (in *OracleSecretRef) DeepCopy() *OracleSecretRef {
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.
func (in *SecretStore) DeepCopyInto(out *SecretStore) {
*out = *in
@ -1252,6 +1498,37 @@ func (in *ServiceAccountAuth) DeepCopy() *ServiceAccountAuth {
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.
func (in *TemplateFrom) DeepCopyInto(out *TemplateFrom) {
*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(store GenericStore) error
// Capabilities returns the provider Capabilities (Read, Write, ReadWrite)
Capabilities() SecretStoreCapabilities
}
// +kubebuilder:object:root=false
@ -65,6 +67,12 @@ type SecretsClient interface {
// then the secret entry will be deleted depending on the deletionPolicy.
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
// and is able to retrieve secrets from the provider.
// 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"
func (p *PP) Capabilities() SecretStoreCapabilities {
return SecretStoreReadOnly
}
// New constructs a SecretsManager Provider.
func (p *PP) NewClient(ctx context.Context, store GenericStore, kube client.Client, namespace string) (SecretsClient, error) {
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.
func (p *PP) GetSecret(ctx context.Context, ref ExternalSecretDataRemoteRef) ([]byte, error) {
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"`
}
// 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.
type SecretStoreStatus struct {
// +optional
Conditions []SecretStoreStatusCondition `json:"conditions"`
// +optional
Capabilities SecretStoreCapabilities `json:"capabilities"`
}
// +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.
// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp"
// +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:subresource:status
// +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.
// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp"
// +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:subresource:status
// +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"
"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/pushsecret"
"github.com/external-secrets/external-secrets/pkg/controllers/secretstore"
awsauth "github.com/external-secrets/external-secrets/pkg/provider/aws/auth"
"github.com/external-secrets/external-secrets/pkg/provider/vault"
@ -61,6 +62,7 @@ var (
namespace string
enableClusterStoreReconciler bool
enableClusterExternalSecretReconciler bool
enablePushSecretReconciler bool
enableFloodGate bool
storeRequeueInterval time.Duration
serviceName, serviceNamespace string
@ -166,6 +168,18 @@ var rootCmd = &cobra.Command{
setupLog.Error(err, errCreateController, "controller", "ExternalSecret")
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 err = (&clusterexternalsecret.Reconciler{
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().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().BoolVar(&enableClusterStoreReconciler, "enable-cluster-store-reconciler", true, "Enables the cluster store reconciler.")
rootCmd.Flags().BoolVar(&enableClusterExternalSecretReconciler, "enable-cluster-external-secret-reconciler", true, "Enables the cluster external secret reconciler.")
rootCmd.Flags().BoolVar(&enableSecretsCache, "enable-secrets-caching", false, "Enables the secrets caching for external-secrets pod.")
rootCmd.Flags().BoolVar(&enableConfigMapsCache, "enable-configmaps-caching", false, "Enables the ConfigMap caching for external-secrets pod.")
rootCmd.Flags().BoolVar(&enableClusterStoreReconciler, "enable-cluster-store-reconciler", true, "Enable cluster store reconciler.")
rootCmd.Flags().BoolVar(&enableClusterExternalSecretReconciler, "enable-cluster-external-secret-reconciler", true, "Enable cluster external secret reconciler.")
rootCmd.Flags().BoolVar(&enablePushSecretReconciler, "enable-push-secret-reconciler", true, "Enable push secret reconciler.")
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().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.")

View file

@ -1512,6 +1512,9 @@ spec:
- jsonPath: .status.conditions[?(@.type=="Ready")].reason
name: Status
type: string
- jsonPath: .status.capabilities
name: Capabilities
type: string
- jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready
type: string
@ -3331,6 +3334,10 @@ spec:
status:
description: SecretStoreStatus defines the observed state of the SecretStore.
properties:
capabilities:
description: SecretStoreCapabilities defines the possible operations
a SecretStore can do.
type: string
conditions:
items:
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
name: Status
type: string
- jsonPath: .status.capabilities
name: Capabilities
type: string
- jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready
type: string
@ -3331,6 +3334,10 @@ spec:
status:
description: SecretStoreStatus defines the observed state of the SecretStore.
properties:
capabilities:
description: SecretStoreCapabilities defines the possible operations
a SecretStore can do.
type: string
conditions:
items:
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. |
| 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.createPushSecret | bool | `true` | If true, create CRDs for Push Secret. |
| createOperator | bool | `true` | Specifies whether an external secret operator deployment be created. |
| deploymentAnnotations | object | `{}` | Annotations to add to Deployment |
| dnsConfig | object | `{}` | Specifies `dnsOptions` to deployment |

View file

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

View file

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

View file

@ -1545,6 +1545,9 @@ spec:
- jsonPath: .status.conditions[?(@.type=="Ready")].reason
name: Status
type: string
- jsonPath: .status.capabilities
name: Capabilities
type: string
- jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready
type: string
@ -2857,6 +2860,9 @@ spec:
status:
description: SecretStoreStatus defines the observed state of the SecretStore.
properties:
capabilities:
description: SecretStoreCapabilities defines the possible operations a SecretStore can do.
type: string
conditions:
items:
properties:
@ -3504,6 +3510,220 @@ spec:
---
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: {}
conversion:
strategy: Webhook
webhook:
conversionReviewVersions:
- v1
clientConfig:
service:
name: kubernetes
namespace: default
path: /convert
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.10.0
@ -4604,6 +4824,9 @@ spec:
- jsonPath: .status.conditions[?(@.type=="Ready")].reason
name: Status
type: string
- jsonPath: .status.capabilities
name: Capabilities
type: string
- jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready
type: string
@ -5916,6 +6139,9 @@ spec:
status:
description: SecretStoreStatus defines the observed state of the SecretStore.
properties:
capabilities:
description: SecretStoreCapabilities defines the possible operations a SecretStore can do.
type: string
conditions:
items:
properties:

View file

@ -1,8 +1,8 @@
```yaml
---
title: SecretSink
title: PushSecret
version: v1alpha1
authors:
authors:
creation-date: 2022-01-25
status: draft
---
@ -18,7 +18,7 @@ status: draft
## 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
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
- CRD Design for the SecretSink
- Define the need for a SinkStore
-
-
### Non-Goals
Do not implement full compatibility mechanisms with each provider (we are not Terraform neither Crossplane)
@ -113,7 +113,7 @@ spec:
```yaml
apiVersion: external-secrets.io/v1alpha1
kind: SecretSink
kind: PushSecret
metadata:
name: "hello-world"
namespace: my-ns # Same of the SecretStores
@ -130,17 +130,17 @@ spec:
secret:
name: foobar
data:
match:
- secretKey: foobar
- match:
secretKey: foobar
remoteRefs:
- remoteKey: my/path/foobar
- remoteKey: my/path/foobar
property: my-property #optional. To allow coming back from a 'dataFrom'
- remoteKey: secret/my-path-foobar
property: another-property
rewrite:
- secretKey: game-(.+).(.+)
secretKey: game-(.+).(.+)
remoteRefs:
- remoteKey: my/path/($1)
- remoteKey: my/path/($1)
property: prop-($2)
- remoteKey: my-path-($1)-($2) #Applies this way to all other secretStores
@ -148,7 +148,7 @@ status:
refreshTime: "2019-08-12T12:33:02Z"
conditions:
- type: Ready
status: "True"
status: "True"
reason: "SecretSynced"
message: "Secret was synced" #Fully synced
lastTransitionTime: "2019-08-12T12:33:02Z"
@ -165,7 +165,7 @@ status:
```
### 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).
@ -177,9 +177,9 @@ We had several discussions on how to implement this feature, and it turns out ju
### Acceptance Criteria
+ ExternalSecrets create appropriate labels on generated Secrets
+ SecretSinks can read labels on source Secrets
+ SecretSinks cannot have same references to SecretStores
+ SecretSinks respect refreshInterval
+ PushSecrets can read labels on source Secrets
+ PushSecrets cannot have same references to SecretStores
+ PushSecrets respect refreshInterval
## 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.

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>Provider is a common interface for interacting with secret backends.</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>
<p>
@ -3730,6 +3735,30 @@ SecretStoreStatus
</tr>
</tbody>
</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
(<code>string</code> alias)</p></h3>
<p>
@ -4190,6 +4219,19 @@ int
<em>(Optional)</em>
</td>
</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>
</table>
<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",
"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
```
### 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"

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"
// nolint
. "github.com/onsi/ginkgo/v2"
ginkgo "github.com/onsi/ginkgo/v2"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -93,7 +93,7 @@ type OperatorInitResponse struct {
}
func (l *Vault) Install() error {
By("Installing vault in " + l.Namespace)
ginkgo.By("Installing vault in " + l.Namespace)
err := l.chart.Install()
if err != nil {
return err
@ -168,13 +168,13 @@ func (l *Vault) initVault() error {
l.KubernetesAuthPath = "mykubernetes" // 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)
if err != nil {
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{
LabelSelector: "app.kubernetes.io/name=vault",
})
@ -183,7 +183,7 @@ func (l *Vault) initVault() error {
}
l.PodName = pl.Items[0].Name
By("Initializing vault")
ginkgo.By("Initializing vault")
out, err := util.ExecCmd(
l.chart.config.KubeClientSet,
l.chart.config.KubeConfig,
@ -192,7 +192,7 @@ func (l *Vault) initVault() error {
return fmt.Errorf("error initializing vault: %w", err)
}
By("Parsing init response")
ginkgo.By("Parsing init response")
var res OperatorInitResponse
err = json.Unmarshal([]byte(out), &res)
if err != nil {
@ -200,7 +200,7 @@ func (l *Vault) initVault() error {
}
l.RootToken = res.RootToken
By("Unsealing vault")
ginkgo.By("Unsealing vault")
for _, k := range res.UnsealKeysB64 {
_, err = util.ExecCmd(
l.chart.config.KubeClientSet,
@ -238,7 +238,7 @@ func (l *Vault) initVault() error {
}
func (l *Vault) configureVault() error {
By("configuring vault")
ginkgo.By("configuring vault")
cmd := `sh /etc/vault-config/configure-vault.sh %s`
_, err := util.ExecCmd(
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/azidentity v1.2.0
github.com/hashicorp/golang-lru v0.5.4
github.com/maxbrunsfeld/counterfeiter/v6 v6.5.0
github.com/sethvargo/go-password v0.2.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.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/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/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
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/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
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/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI=
github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE=

View file

@ -49,6 +49,7 @@ nav:
- SecretStore: api/secretstore.md
- ClusterSecretStore: api/clustersecretstore.md
- ClusterExternalSecret: api/clusterexternalsecret.md
- PushSecret: api/pushsecret.md
- Generators:
- "api/generator/index.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"
if [[ "$CRDS_FLAG_NAME" == *"Cluster"* ]]; then
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
echo "{{- if .Values.installCRDs }}" > "$i"
fi

View file

@ -29,10 +29,10 @@ import (
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
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.
_ "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/utils"
)
@ -43,7 +43,7 @@ func (r *Reconciler) getProviderSecretData(ctx context.Context, externalSecret *
// Clientmanager keeps track of the client instances
// that are created during the fetching process and closes clients
// if needed.
mgr := clientmanager.New(r.Client, r.ControllerClass, r.EnableFloodGate)
mgr := secretstore.NewManager(r.Client, r.ControllerClass, r.EnableFloodGate)
defer mgr.Close(ctx)
providerData := make(map[string][]byte)
@ -87,7 +87,7 @@ func (r *Reconciler) getProviderSecretData(ctx context.Context, externalSecret *
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)
if err != nil {
return err
@ -170,7 +170,7 @@ func (r *Reconciler) getGeneratorDefinition(ctx context.Context, namespace strin
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)
if err != nil {
return nil, err
@ -199,7 +199,7 @@ func (r *Reconciler) handleExtractSecrets(ctx context.Context, externalSecret *e
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)
if err != nil {
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.
*/
package clientmanager
package secretstore
import (
"context"
@ -28,7 +28,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/controllers/secretstore"
)
const (
@ -63,7 +62,7 @@ type clientVal struct {
}
// 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")
return &Manager{
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
// 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
}
}
func (m *Manager) GetFromStore(ctx context.Context, store esv1beta1.GenericStore, namespace string) (esv1beta1.SecretsClient, error) {
storeProvider, err := esv1beta1.GetProvider(store)
if err != nil {
return nil, err
@ -129,6 +99,39 @@ func (m *Manager) Get(ctx context.Context, storeRef esv1beta1.SecretStoreRef, na
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
// if a client exists for the same provider which points to a different store or store version
// it will be cleaned up.
@ -248,7 +251,7 @@ func assertStoreIsUsable(store esv1beta1.GenericStore) error {
if store == nil {
return nil
}
condition := secretstore.GetSecretStoreCondition(store.GetStatus(), esv1beta1.SecretStoreReady)
condition := GetSecretStoreCondition(store.GetStatus(), esv1beta1.SecretStoreReady)
if condition == nil || condition.Status != v1.ConditionTrue {
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.
*/
package clientmanager
package secretstore
import (
"context"
@ -20,6 +20,7 @@ import (
"github.com/go-logr/logr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -41,13 +42,13 @@ func TestManagerGet(t *testing.T) {
// the behavior of the NewClient func.
fakeProvider := &WrapProvider{}
esv1beta1.ForceRegister(fakeProvider, &esv1beta1.SecretStoreProvider{
Fake: &esv1beta1.FakeProvider{},
AWS: &esv1beta1.AWSProvider{},
})
// fake clients are re-used to compare the
// in-memory reference
clientA := &FakeClient{id: "1"}
clientB := &FakeClient{id: "2"}
clientA := &MockFakeClient{id: "1"}
clientB := &MockFakeClient{id: "2"}
const testNamespace = "foo"
@ -62,14 +63,7 @@ func TestManagerGet(t *testing.T) {
fakeSpec := esv1beta1.SecretStoreSpec{
Provider: &esv1beta1.SecretStoreProvider{
Fake: &esv1beta1.FakeProvider{
Data: []esv1beta1.FakeProviderData{
{
Key: "foo",
Value: "bar",
},
},
},
AWS: &esv1beta1.AWSProvider{},
},
}
@ -96,7 +90,7 @@ func TestManagerGet(t *testing.T) {
var mgr *Manager
provKey := clientKey{
providerType: "*clientmanager.WrapProvider",
providerType: "*secretstore.WrapProvider",
}
type fields struct {
@ -148,7 +142,7 @@ func TestManagerGet(t *testing.T) {
// and it mustbe the client defined in clientConstructor
assert.NotNil(t, sc)
c, ok := mgr.clientMap[provKey]
assert.True(t, ok)
require.True(t, ok)
assert.Same(t, c.client, clientA)
},
@ -332,35 +326,47 @@ func (f *WrapProvider) NewClient(
return f.newClientFunc(ctx, store, kube, namespace)
}
func (f *WrapProvider) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadOnly
}
// ValidateStore checks if the provided store is valid.
func (f *WrapProvider) ValidateStore(store esv1beta1.GenericStore) error {
return nil
}
type FakeClient struct {
type MockFakeClient struct {
id string
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
}
func (c *FakeClient) Validate() (esv1beta1.ValidationResult, error) {
func (c *MockFakeClient) Validate() (esv1beta1.ValidationResult, error) {
return esv1beta1.ValidationResultReady, nil
}
// 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
}
// 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
}
func (c *FakeClient) Close(ctx context.Context) error {
func (c *MockFakeClient) Close(ctx context.Context) error {
c.closeCalled = true
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
// we have to patch the status
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 {
log.Error(err, "unable to validate store")
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)
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
// 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 {
storeProvider, err := esapi.GetProvider(store)
if err != nil {
cond := NewSecretStoreCondition(esapi.SecretStoreReady, v1.ConditionFalse, esapi.ReasonInvalidStore, errUnableGetProvider)
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)
mgr := NewManager(client, controllerClass, false)
defer mgr.Close(ctx)
cl, err := mgr.GetFromStore(ctx, store, namespace)
if err != nil {
cond := NewSecretStoreCondition(esapi.SecretStoreReady, v1.ConditionFalse, esapi.ReasonInvalidProviderConfig, errUnableCreateClient)
SetExternalSecretCondition(store, *cond)
recorder.Event(store, v1.EventTypeWarning, esapi.ReasonInvalidProviderConfig, err.Error())
return fmt.Errorf(errStoreClient, err)
}
defer cl.Close(ctx)
validationResult, err := cl.Validate()
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)) {
for _, mut := range muts {
mut(test)
@ -137,11 +168,13 @@ var _ = Describe("SecretStore reconcile", func() {
Entry("[namespace] invalid provider with secretStore should set InvalidStore condition", invalidProvider),
Entry("[namespace] ignore stores with non-matching class", ignoreControllerClass),
Entry("[namespace] valid provider has status=ready", validProvider),
Entry("[namespace] valid provider has capabilities=ReadWrite", readWrite),
// cluster store
Entry("[cluster] invalid provider with secretStore should set InvalidStore condition", invalidProvider, 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 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.
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
@ -204,6 +209,14 @@ func (a *Akeyless) Validate() (esv1beta1.ValidationResult, error) {
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.
// Retrieves a secret with the secret name defined in ref.Name.
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
}
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.
func (kms *KeyManagementService) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
// TO be implemented
@ -168,6 +176,11 @@ func (kms *KeyManagementService) GetSecretMap(ctx context.Context, ref esv1beta1
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.
func (kms *KeyManagementService) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
storeSpec := store.GetSpec()

View file

@ -14,27 +14,82 @@ limitations under the License.
package fake
import (
"context"
"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/google/go-cmp/cmp"
)
// Client implements the aws parameterstore interface.
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) {
return sm.valFn(in)
type GetParameterWithContextFn func(aws.Context, *ssm.GetParameterInput, ...request.Option) (*ssm.GetParameterOutput, error)
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) {
return nil, nil
func NewListTagsForResourceWithContextFn(output *ssm.ListTagsForResourceOutput, err error) ListTagsForResourceWithContextFn {
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) {
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) {
return nil, fmt.Errorf("unexpected test argument")
}

View file

@ -21,6 +21,8 @@ import (
"strings"
"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/service/ssm"
"github.com/tidwall/gjson"
@ -32,7 +34,11 @@ import (
)
// 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.
type ParameterStore struct {
@ -43,8 +49,11 @@ type ParameterStore struct {
// PMInterface is a subset of the parameterstore api.
// see: https://docs.aws.amazon.com/sdk-for-go/api/service/ssm/ssmiface/
type PMInterface interface {
GetParameter(*ssm.GetParameterInput) (*ssm.GetParameterOutput, error)
DescribeParameters(*ssm.DescribeParametersInput) (*ssm.DescribeParametersOutput, error)
GetParameterWithContext(aws.Context, *ssm.GetParameterInput, ...request.Option) (*ssm.GetParameterOutput, 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 (
@ -59,18 +68,150 @@ func New(sess *session.Session, cfg *aws.Config) (*ParameterStore, error) {
}, 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) {
if ref.Name != nil {
return pm.findByName(ref)
return pm.findByName(ctx, ref)
}
if ref.Tags != nil {
return pm.findByTags(ref)
return pm.findByTags(ctx, ref)
}
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)
if err != nil {
return nil, err
@ -86,10 +227,12 @@ func (pm *ParameterStore) findByName(ref esv1beta1.ExternalSecretFind) (map[stri
data := make(map[string][]byte)
var nextToken *string
for {
it, err := pm.client.DescribeParameters(&ssm.DescribeParametersInput{
NextToken: nextToken,
ParameterFilters: pathFilter,
})
it, err := pm.client.DescribeParametersWithContext(
ctx,
&ssm.DescribeParametersInput{
NextToken: nextToken,
ParameterFilters: pathFilter,
})
if err != nil {
return nil, err
}
@ -97,7 +240,7 @@ func (pm *ParameterStore) findByName(ref esv1beta1.ExternalSecretFind) (map[stri
if !matcher.MatchName(*param.Name) {
continue
}
err = pm.fetchAndSet(data, *param.Name)
err = pm.fetchAndSet(ctx, data, *param.Name)
if err != nil {
return nil, err
}
@ -111,7 +254,7 @@ func (pm *ParameterStore) findByName(ref esv1beta1.ExternalSecretFind) (map[stri
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)
for k, v := range ref.Tags {
filters = append(filters, &ssm.ParameterStringFilter{
@ -132,15 +275,17 @@ func (pm *ParameterStore) findByTags(ref esv1beta1.ExternalSecretFind) (map[stri
data := make(map[string][]byte)
var nextToken *string
for {
it, err := pm.client.DescribeParameters(&ssm.DescribeParametersInput{
ParameterFilters: filters,
NextToken: nextToken,
})
it, err := pm.client.DescribeParametersWithContext(
ctx,
&ssm.DescribeParametersInput{
ParameterFilters: filters,
NextToken: nextToken,
})
if err != nil {
return nil, err
}
for _, param := range it.Parameters {
err = pm.fetchAndSet(data, *param.Name)
err = pm.fetchAndSet(ctx, data, *param.Name)
if err != nil {
return nil, err
}
@ -154,8 +299,8 @@ func (pm *ParameterStore) findByTags(ref esv1beta1.ExternalSecretFind) (map[stri
return data, nil
}
func (pm *ParameterStore) fetchAndSet(data map[string][]byte, name string) error {
out, err := pm.client.GetParameter(&ssm.GetParameterInput{
func (pm *ParameterStore) fetchAndSet(ctx context.Context, data map[string][]byte, name string) error {
out, err := pm.client.GetParameterWithContext(ctx, &ssm.GetParameterInput{
Name: utilpointer.StringPtr(name),
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.
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,
WithDecryption: aws.Bool(true),
})
nsf := esv1beta1.NoSecretError{}
var nf *ssm.ParameterNotFound
if errors.As(err, &nf) {
if errors.As(err, &nf) || errors.As(err, &nsf) {
return nil, esv1beta1.NoSecretErr
}
if err != nil {

View file

@ -15,20 +15,23 @@ package parameterstore
import (
"context"
"errors"
"fmt"
"strings"
"testing"
"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/google/go-cmp/cmp"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
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 {
fakeClient *fake.Client
fakeClient *fakeps.Client
apiInput *ssm.GetParameterInput
apiOutput *ssm.GetParameterOutput
remoteRef *esv1beta1.ExternalSecretDataRemoteRef
@ -38,9 +41,17 @@ type parameterstoreTestCase struct {
expectedData map[string][]byte
}
type fakeRef struct {
key string
}
func (f fakeRef) GetRemoteKey() string {
return f.key
}
func makeValidParameterStoreTestCase() *parameterstoreTestCase {
return &parameterstoreTestCase{
fakeClient: &fake.Client{},
fakeClient: &fakeps.Client{},
apiInput: makeValidAPIInput(),
apiOutput: makeValidAPIOutput(),
remoteRef: makeValidRemoteRef(),
@ -81,6 +92,356 @@ func makeValidParameterStoreTestCaseCustom(tweaks ...func(pstc *parameterstoreTe
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
// make sure correct values are passed and errors are handled accordingly.
func TestGetSecret(t *testing.T) {
@ -110,6 +471,13 @@ func TestGetSecret(t *testing.T) {
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
setPropertyFail := func(pstc *parameterstoreTestCase) {
pstc.apiOutput.Parameter.Value = aws.String(`------`)
@ -138,6 +506,7 @@ func TestGetSecret(t *testing.T) {
makeValidParameterStoreTestCaseCustom(setParameterValueNil),
makeValidParameterStoreTestCaseCustom(setAPIError),
makeValidParameterStoreTestCaseCustom(setExtractPropertyWithDot),
makeValidParameterStoreTestCaseCustom(setParameterValueNotFound),
}
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 {
if out == nil {
return want == ""

View file

@ -46,6 +46,11 @@ const (
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.
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)

View file

@ -17,14 +17,76 @@ package fake
import (
"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"
"github.com/google/go-cmp/cmp"
)
// Client implements the aws secretsmanager interface.
type Client struct {
ExecutionCounter int
valFn map[string]func(*awssm.GetSecretValueInput) (*awssm.GetSecretValueOutput, error)
ExecutionCounter int
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.

View file

@ -15,6 +15,7 @@ limitations under the License.
package secretsmanager
import (
"bytes"
"context"
"encoding/json"
"errors"
@ -22,6 +23,8 @@ import (
"strings"
"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"
awssm "github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/tidwall/gjson"
@ -46,12 +49,19 @@ type SecretsManager struct {
// SMInterface is a subset of the smiface api.
// see: https://docs.aws.amazon.com/sdk-for-go/api/service/secretsmanager/secretsmanageriface/
type SMInterface interface {
GetSecretValue(*awssm.GetSecretValueInput) (*awssm.GetSecretValueOutput, 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 (
errUnexpectedFindOperator = "unexpected find operator"
managedBy = "managed-by"
externalSecrets = "external-secrets"
)
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
}
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.
func (sm *SecretsManager) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
if ref.Name != nil {
@ -305,3 +413,7 @@ func (sm *SecretsManager) Validate() (esv1beta1.ValidationResult, error) {
}
return esv1beta1.ValidationResultReady, nil
}
func (sm *SecretsManager) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadWrite
}

View file

@ -16,13 +16,16 @@ package secretsmanager
import (
"context"
"errors"
"fmt"
"strings"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
awssm "github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/google/go-cmp/cmp"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
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)
}
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.
func (a *Azure) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
return newClient(ctx, store, kube, namespace)
@ -196,6 +201,15 @@ func (a *Azure) ValidateStore(store esv1beta1.GenericStore) error {
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.
// 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) {

View file

@ -115,6 +115,14 @@ func (c *Client) Validate() (esv1beta1.ValidationResult, error) {
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) {
request := dClient.SecretRequest{
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) {
storeSpec := store.GetSpec()

View file

@ -30,15 +30,61 @@ var (
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 {
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) {
cfg, err := getProvider(store)
if p.database == nil {
p.database = make(map[string]Config)
}
c, err := getProvider(store)
if err != nil {
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{
config: cfg,
}, nil
@ -55,6 +101,26 @@ func getProvider(store esv1beta1.GenericStore) (*esv1beta1.FakeProvider, error)
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.
func (p *Provider) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
// 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.
func (p *Provider) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
for _, data := range p.config.Data {
if data.Key == ref.Key && data.Version == ref.Version {
return []byte(data.Value), nil
}
mapKey := fmt.Sprintf("%v%v", ref.Key, ref.Version)
data, ok := p.config[mapKey]
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.
func (p *Provider) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
for _, data := range p.config.Data {
if data.Key != ref.Key || data.Version != ref.Version || data.ValueMap == nil {
continue
}
return convertMap(data.ValueMap), nil
mapKey := fmt.Sprintf("%v%v", ref.Key, ref.Version)
data, ok := p.config[mapKey]
if !ok || data.Version != ref.Version || data.ValueMap == nil {
return nil, esv1beta1.NoSecretErr
}
return nil, esv1beta1.NoSecretErr
return convertMap(data.ValueMap), nil
}
func convertMap(in map[string]string) map[string][]byte {

View file

@ -15,11 +15,14 @@ package fake
import (
"context"
"errors"
"fmt"
"testing"
"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"
)
@ -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) {
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{
@ -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 {
name string
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) {
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{

View file

@ -14,6 +14,7 @@ limitations under the License.
package secretmanager
import (
"bytes"
"context"
"encoding/json"
"errors"
@ -24,8 +25,10 @@ import (
secretmanager "cloud.google.com/go/secretmanager/apiv1"
"cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
"github.com/googleapis/gax-go/v2"
"github.com/googleapis/gax-go/v2/apierror"
"github.com/tidwall/gjson"
"google.golang.org/api/iterator"
"google.golang.org/grpc/codes"
ctrl "sigs.k8s.io/controller-runtime"
kclient "sigs.k8s.io/controller-runtime/pkg/client"
@ -71,13 +74,122 @@ type Client struct {
}
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)
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
GetSecret(ctx context.Context, req *secretmanagerpb.GetSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error)
}
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.
func (c *Client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
if ref.Name != nil {
@ -86,6 +198,7 @@ func (c *Client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecret
if len(ref.Tags) > 0 {
return c.findByTags(ctx, ref)
}
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).
func (c *Client) extractProjectIDNumber(secretFullName string) string {
s := strings.Split(secretFullName, "/")
projectIDNumuber := s[1]
return projectIDNumuber
ProjectIDNumuber := s[1]
return ProjectIDNumuber
}
// GetSecret returns a single secret from the provider.

View file

@ -15,12 +15,16 @@ package secretmanager
import (
"context"
"errors"
"fmt"
"reflect"
"strings"
"testing"
"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"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
@ -97,7 +101,7 @@ var setAPIErr = func(smtc *secretManagerTestCase) {
var setNilMockClient = func(smtc *secretManagerTestCase) {
smtc.mockClient = nil
smtc.expectError = errUninitalizedGCPProvider
smtc.expectError = "provider GCP is not initialized"
}
// 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) {
// good case: default version & deserialization
setDeserialization := func(smtc *secretManagerTestCase) {

View file

@ -15,6 +15,7 @@ package fake
import (
"context"
"errors"
"fmt"
secretmanager "cloud.google.com/go/secretmanager/apiv1"
@ -27,13 +28,61 @@ import (
type MockSMClient struct {
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
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
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) {
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 {
return mc.ListSecretsFn(ctx, req)
}
@ -41,12 +90,93 @@ func (mc *MockSMClient) Close() error {
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() {
mc.closeFn = func() error {
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) {
if mc != nil {
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{}
func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadWrite
}
// NewClient constructs a GCP Provider.
func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
storeSpec := store.GetSpec()

View file

@ -139,6 +139,11 @@ func NewGitlabProvider() *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.
func (g *Gitlab) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
storeSpec := store.GetSpec()
@ -187,6 +192,15 @@ func (g *Gitlab) NewClient(ctx context.Context, store esv1beta1.GenericStore, ku
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.
func (g *Gitlab) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
if utils.IsNil(g.projectVariablesClient) {

View file

@ -101,6 +101,15 @@ func (c *client) setAuth(ctx context.Context) error {
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.
func (ibm *providerIBM) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
// TO be implemented
@ -578,6 +587,11 @@ func (ibm *providerIBM) ValidateStore(store esv1beta1.GenericStore) error {
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) {
storeSpec := store.GetSpec()
ibmSpec := storeSpec.Provider.IBM

View file

@ -49,6 +49,15 @@ func (c *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretData
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) {
secret, err := c.userSecretClient.Get(ctx, ref.Key, metav1.GetOptions{})
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.
func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
restCfg, err := ctrlcfg.GetConfig()

View file

@ -68,6 +68,11 @@ type ProviderOnePassword struct {
var _ esv1beta1.SecretsClient = &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.
func (provider *ProviderOnePassword) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
config := store.GetSpec().Provider.OnePassword
@ -147,6 +152,15 @@ func validateStore(store esv1beta1.GenericStore) error {
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.
func (provider *ProviderOnePassword) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
if ref.Version != "" {

View file

@ -73,6 +73,15 @@ type KmsVCInterface interface {
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.
func (vms *VaultManagementService) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
// TO be implemented
@ -133,6 +142,11 @@ func (vms *VaultManagementService) GetSecretMap(ctx context.Context, ref esv1bet
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.
func (vms *VaultManagementService) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
storeSpec := store.GetSpec()

View file

@ -90,6 +90,15 @@ func New(isoSession *senhaseguraAuth.SenhaseguraIsoSession) (*DSM, error) {
}, 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.
*/

View file

@ -43,6 +43,11 @@ const (
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.
*/

View file

@ -24,12 +24,20 @@ import (
var _ esv1beta1.Provider = &Client{}
type SetSecretCallArgs struct {
Value []byte
RemoteRef esv1beta1.PushRemoteRef
}
// Client is a fake client for testing.
type Client struct {
SetSecretArgs map[string]SetSecretCallArgs
NewFn func(context.Context, esv1beta1.GenericStore, client.Client, string) (esv1beta1.SecretsClient, error)
GetSecretFn func(context.Context, esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error)
GetSecretMapFn func(context.Context, esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error)
GetAllSecretsFn func(context.Context, esv1beta1.ExternalSecretFind) (map[string][]byte, error)
SetSecretFn func() error
DeleteSecretFn func() error
}
// New returns a fake provider/client.
@ -44,6 +52,13 @@ func New() *Client {
GetAllSecretsFn: func(context.Context, esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
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) {
@ -63,6 +78,19 @@ func (v *Client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecret
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.
func (v *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
return v.GetSecretFn(ctx, ref)
@ -109,6 +137,14 @@ func (v *Client) WithGetAllSecrets(secData map[string][]byte, err error) *Client
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.
func (v *Client) WithNew(f func(context.Context, esv1beta1.GenericStore, client.Client,
string) (esv1beta1.SecretsClient, error)) *Client {
@ -116,6 +152,11 @@ func (v *Client) WithNew(f func(context.Context, esv1beta1.GenericStore, client.
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.
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)

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 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 DeleteWithContextFn func(ctx context.Context, path string) (*vault.Secret, error)
type Logical struct {
ReadWithDataWithContextFn ReadWithDataWithContextFn
ListWithContextFn ListWithContextFn
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 {
@ -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) {
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)
ListWithContext(ctx context.Context, path string) (*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 {
@ -272,6 +273,11 @@ type connector struct {
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) {
// 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
@ -284,6 +290,7 @@ func (c *connector) NewClient(ctx context.Context, store esv1beta1.GenericStore,
if err != nil {
return nil, err
}
return c.newClient(ctx, store, kube, clientset.CoreV1(), namespace)
}
@ -403,9 +410,94 @@ func (c *connector) ValidateStore(store esv1beta1.GenericStore) error {
return nil
}
// Empty GetAllSecrets.
// GetAllSecrets
// First load all secrets from secretStore path configuration.
func (v *client) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
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.
_, 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.
func (v *client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
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
// reference - https://www.vaultproject.io/api/secret/kv/kv-v2#read-secret-version
dataInt, ok := vaultSecret.Data["data"]
if !ok {
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
// produce identical strings. This mirrors the error comparison behavior of
// 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) {
whClient := &WebHook{
kube: kube,
@ -111,6 +116,15 @@ func (w *WebHook) getStoreSecret(ctx context.Context, ref esmeta.SecretKeySelect
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.
func (w *WebHook) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
// TO be implemented

View file

@ -88,6 +88,7 @@ func InitYandexCloudProvider(
return provider
}
type NewSecretSetterFunc func()
type AdaptInputFunc func(store esv1beta1.GenericStore) (*SecretsClientInput, 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)
@ -103,6 +104,10 @@ type SecretsClientInput struct {
CACertificate *esmeta.SecretKeySelector
}
func (p *YandexCloudProvider) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadOnly
}
// NewClient constructs a Yandex.Cloud Provider.
func (p *YandexCloudProvider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
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 &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) {

View file

@ -26,26 +26,35 @@ var _ esv1beta1.SecretsClient = &yandexCloudSecretsClient{}
// Implementation of v1beta1.SecretsClient.
type yandexCloudSecretsClient struct {
secretGetter SecretGetter
secretSetter SecretSetter
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) {
// TO be implemented
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 {
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
import (
//nolint:gosec
"crypto/md5"
"encoding/base64"

View file

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