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:
parent
d71e905a47
commit
0cb799b5cf
71 changed files with 5016 additions and 172 deletions
157
apis/externalsecrets/v1alpha1/pushsecret_types.go
Normal file
157
apis/externalsecrets/v1alpha1/pushsecret_types.go
Normal 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"`
|
||||
}
|
|
@ -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{})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
102
apis/externalsecrets/v1beta1/fakes/pushremoteref.go
Normal file
102
apis/externalsecrets/v1beta1/fakes/pushremoteref.go
Normal 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)
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
24
apis/externalsecrets/v1beta1/pushsecret_interfaces.go
Normal file
24
apis/externalsecrets/v1beta1/pushsecret_interfaces.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
|
|
23
cmd/root.go
23
cmd/root.go
|
@ -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.")
|
||||
|
|
|
@ -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:
|
||||
|
|
232
config/crds/bases/external-secrets.io_pushsecrets.yaml
Normal file
232
config/crds/bases/external-secrets.io_pushsecrets.yaml
Normal 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: {}
|
|
@ -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:
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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: ""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
5
docs/api/pushsecret.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
The `PushSecret` is namespaced and specifies how to push secrets to secret stores.
|
||||
|
||||
``` yaml
|
||||
{% include 'full-pushsecret.yaml' %}
|
||||
```
|
|
@ -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>"ReadOnly"</p></td>
|
||||
<td></td>
|
||||
</tr><tr><td><p>"ReadWrite"</p></td>
|
||||
<td></td>
|
||||
</tr><tr><td><p>"WriteOnly"</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
|
||||
|
|
|
@ -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"
|
||||
|
|
6
docs/provider/aws-pushsecret.md
Normal file
6
docs/provider/aws-pushsecret.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
|
||||
## Push Secret
|
||||
|
||||
### IAM Policy
|
||||
|
18
docs/snippets/full-pushsecret.yaml
Normal file
18
docs/snippets/full-pushsecret.yaml
Normal 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)
|
|
@ -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
1
go.mod
|
@ -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
3
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
398
pkg/controllers/pushsecret/pushsecret_controller.go
Normal file
398
pkg/controllers/pushsecret/pushsecret_controller.go
Normal 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
|
||||
}
|
727
pkg/controllers/pushsecret/pushsecret_controller_test.go
Normal file
727
pkg/controllers/pushsecret/pushsecret_controller_test.go
Normal 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),
|
||||
)
|
||||
})
|
105
pkg/controllers/pushsecret/suite_test.go
Normal file
105
pkg/controllers/pushsecret/suite_test.go
Normal 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())
|
||||
})
|
|
@ -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())
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
|
||||
})
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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: ¶meterType,
|
||||
}
|
||||
|
||||
data, err := pm.client.ListTagsForResourceWithContext(ctx, ¶meterTags)
|
||||
|
||||
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: ¶meterType,
|
||||
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 {
|
||||
|
|
|
@ -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 ¶meterstoreTestCase{
|
||||
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: ¶meterName,
|
||||
},
|
||||
},
|
||||
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: ¶meterName,
|
||||
},
|
||||
},
|
||||
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: ¶meterName,
|
||||
},
|
||||
},
|
||||
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: ¶meterName,
|
||||
},
|
||||
},
|
||||
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 == ""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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: ¬ManagedBy,
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 != "" {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
18
pkg/provider/yandex/common/secretsetter.go
Normal file
18
pkg/provider/yandex/common/secretsetter.go
Normal 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
|
||||
}
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||
package utils
|
||||
|
||||
import (
|
||||
|
||||
//nolint:gosec
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
|
|
1
tools.go
1
tools.go
|
@ -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"
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue