mirror of
https://github.com/external-secrets/external-secrets.git
synced 2024-12-15 17:51:01 +00:00
0cb799b5cf
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>
259 lines
8.2 KiB
Go
259 lines
8.2 KiB
Go
/*
|
|
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 secretstore
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/go-logr/logr"
|
|
"golang.org/x/exp/slices"
|
|
v1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
|
)
|
|
|
|
const (
|
|
errGetClusterSecretStore = "could not get ClusterSecretStore %q, %w"
|
|
errGetSecretStore = "could not get SecretStore %q, %w"
|
|
errSecretStoreNotReady = "the desired SecretStore %s is not ready"
|
|
errClusterStoreMismatch = "using cluster store %q is not allowed from namespace %q: denied by spec.condition"
|
|
)
|
|
|
|
// Manager stores instances of provider clients
|
|
// At any given time we must have no more than one instance
|
|
// of a client (due to limitations in GCP / see mutexlock there)
|
|
// If the controller requests another instance of a given client
|
|
// we will close the old client first and then construct a new one.
|
|
type Manager struct {
|
|
log logr.Logger
|
|
client client.Client
|
|
controllerClass string
|
|
enableFloodgate bool
|
|
|
|
// store clients by provider type
|
|
clientMap map[clientKey]*clientVal
|
|
}
|
|
|
|
type clientKey struct {
|
|
providerType string
|
|
}
|
|
|
|
type clientVal struct {
|
|
client esv1beta1.SecretsClient
|
|
store esv1beta1.GenericStore
|
|
}
|
|
|
|
// New constructs a new manager with defaults.
|
|
func NewManager(ctrlClient client.Client, controllerClass string, enableFloodgate bool) *Manager {
|
|
log := ctrl.Log.WithName("clientmanager")
|
|
return &Manager{
|
|
log: log,
|
|
client: ctrlClient,
|
|
controllerClass: controllerClass,
|
|
enableFloodgate: enableFloodgate,
|
|
clientMap: make(map[clientKey]*clientVal),
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
secretClient := m.getStoredClient(ctx, storeProvider, store)
|
|
if secretClient != nil {
|
|
return secretClient, nil
|
|
}
|
|
m.log.V(1).Info("creating new client",
|
|
"provider", fmt.Sprintf("%T", storeProvider),
|
|
"store", fmt.Sprintf("%s/%s", store.GetNamespace(), store.GetName()))
|
|
// secret client is created only if we are going to refresh
|
|
// this skip an unnecessary check/request in the case we are not going to do anything
|
|
secretClient, err = storeProvider.NewClient(ctx, store, m.client, namespace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
idx := storeKey(storeProvider)
|
|
m.clientMap[idx] = &clientVal{
|
|
client: secretClient,
|
|
store: store,
|
|
}
|
|
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.
|
|
func (m *Manager) getStoredClient(ctx context.Context, storeProvider esv1beta1.Provider, store esv1beta1.GenericStore) esv1beta1.SecretsClient {
|
|
idx := storeKey(storeProvider)
|
|
val, ok := m.clientMap[idx]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
storeName := fmt.Sprintf("%s/%s", store.GetNamespace(), store.GetName())
|
|
// return client if it points to the very same store
|
|
if val.store.GetObjectMeta().Generation == store.GetGeneration() &&
|
|
val.store.GetTypeMeta().Kind == store.GetTypeMeta().Kind &&
|
|
val.store.GetName() == store.GetName() &&
|
|
val.store.GetNamespace() == store.GetNamespace() {
|
|
m.log.V(1).Info("reusing stored client",
|
|
"provider", fmt.Sprintf("%T", storeProvider),
|
|
"store", storeName)
|
|
return val.client
|
|
}
|
|
m.log.V(1).Info("cleaning up client",
|
|
"provider", fmt.Sprintf("%T", storeProvider),
|
|
"store", storeName)
|
|
// if we have a client but it points to a different store
|
|
// we must clean it up
|
|
val.client.Close(ctx)
|
|
delete(m.clientMap, idx)
|
|
return nil
|
|
}
|
|
|
|
func storeKey(storeProvider esv1beta1.Provider) clientKey {
|
|
return clientKey{
|
|
providerType: fmt.Sprintf("%T", storeProvider),
|
|
}
|
|
}
|
|
|
|
// getStore fetches the (Cluster)SecretStore from the kube-apiserver
|
|
// and returns a GenericStore representing it.
|
|
func (m *Manager) getStore(ctx context.Context, storeRef *esv1beta1.SecretStoreRef, namespace string) (esv1beta1.GenericStore, error) {
|
|
ref := types.NamespacedName{
|
|
Name: storeRef.Name,
|
|
}
|
|
if storeRef.Kind == esv1beta1.ClusterSecretStoreKind {
|
|
var store esv1beta1.ClusterSecretStore
|
|
err := m.client.Get(ctx, ref, &store)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(errGetClusterSecretStore, ref.Name, err)
|
|
}
|
|
return &store, nil
|
|
}
|
|
ref.Namespace = namespace
|
|
var store esv1beta1.SecretStore
|
|
err := m.client.Get(ctx, ref, &store)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(errGetSecretStore, ref.Name, err)
|
|
}
|
|
return &store, nil
|
|
}
|
|
|
|
// Close cleans up all clients.
|
|
func (m *Manager) Close(ctx context.Context) error {
|
|
var errs []string
|
|
for key, val := range m.clientMap {
|
|
err := val.client.Close(ctx)
|
|
if err != nil {
|
|
errs = append(errs, err.Error())
|
|
}
|
|
delete(m.clientMap, key)
|
|
}
|
|
if len(errs) != 0 {
|
|
return fmt.Errorf("errors while closing clients: %s", strings.Join(errs, ", "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) shouldProcessSecret(store esv1beta1.GenericStore, ns string) (bool, error) {
|
|
if store.GetKind() != esv1beta1.ClusterSecretStoreKind {
|
|
return true, nil
|
|
}
|
|
|
|
if len(store.GetSpec().Conditions) == 0 {
|
|
return true, nil
|
|
}
|
|
|
|
namespaceList := &v1.NamespaceList{}
|
|
|
|
for _, condition := range store.GetSpec().Conditions {
|
|
if condition.NamespaceSelector != nil {
|
|
namespaceSelector, err := metav1.LabelSelectorAsSelector(condition.NamespaceSelector)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if err := m.client.List(context.Background(), namespaceList, client.MatchingLabelsSelector{Selector: namespaceSelector}); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
for _, namespace := range namespaceList.Items {
|
|
if namespace.GetName() == ns {
|
|
return true, nil // namespace matches the labelselector
|
|
}
|
|
}
|
|
}
|
|
|
|
if condition.Namespaces != nil {
|
|
if slices.Contains(condition.Namespaces, ns) {
|
|
return true, nil // namespace in the namespaces list
|
|
}
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// assertStoreIsUsable assert that the store is ready to use.
|
|
func assertStoreIsUsable(store esv1beta1.GenericStore) error {
|
|
if store == nil {
|
|
return nil
|
|
}
|
|
condition := GetSecretStoreCondition(store.GetStatus(), esv1beta1.SecretStoreReady)
|
|
if condition == nil || condition.Status != v1.ConditionTrue {
|
|
return fmt.Errorf(errSecretStoreNotReady, store.GetName())
|
|
}
|
|
return nil
|
|
}
|