---
title: Provider Separation on specific CRDs
version: v1alpha1
authors: Gustavo Carvalho
creation-date: 2023-08-25
status: approved
---
WRT: https://github.com/external-secrets/external-secrets/issues/694
We want to separate provider configuration from the SecretStore, in a way that allows us to install providers only when needed. This also allows us to version provider fields accordingly to their maturity without impacting the SecretStore Manifest
The changes to the code proposed are summarized below:
providers where all provider configuration will reside as individual CRD.cluster.providers where all provider configuration will reside as individual CRD for ClusterScoped providers.ProviderRef to the SecretStore/ClusterSecretStore manifests.RefRegister, which registers based on a provider kind.NewClient to receive the provider interfaceConvert and ApplyReferent on the provider interface, to be able to customize SecretStore.provider vs provider.spec differences, and apply Referent logic on Client Managerspec.provider field and on the Convert methodproviderRef or to generate a providerRef from spec.provider.The Following diagram shows how the new sequence would work:
sequenceDiagram
Reconciler ->> Reconciler: Check for spec.providers
Reconciler ->> APIServer: Creates Providers based on ProviderRef
Reconciler ->> APIServer: GetProvider
Reconciler ->> Provider: ValidateStore
This is a basic reconciler using APIDiscovery just to prepare if we decide to deprecate the whole SecretStore structure. It could be a no-op as well.
sequenceDiagram
Reconciler ->> Reconciler: ValidateStore
sequenceDiagram
Reconciler ->> ClientManager: GetClient
ClientManager ->> Store: GetProviderRef
Store ->> ClientManager: Ref
alt ProviderRef not defined:
ClientManager ->> ClientManager: GetProviderRefFromStoreName
end
ClientManager ->> ClientManager: GetProviderSpecFromK8s
ClientManager ->> Provider: ApplyReferent(spec)
Provider ->> ClientManager: spec
ClientManager ->> Provider: NewClientFromObj(spec)
Provider ->> ClientManager: Client
An example of how this implementation would look like is available on here - This example still needs to be updated to take into account some SecretStore changes after community meeting discussions
Fake Provider Basic Convert function (very similar to other ):
func (p *Provider) Convert(in esv1.GenericStore) (client.Object, error) {
out := &prov.Fake{}
tmp := map[string]any{
"spec": in.GetSpec().Provider.Fake,
}
d, err := json.Marshal(tmp)
if err != nil {
return nil, err
}
err = json.Unmarshal(d, out)
if err != nil {
return nil, fmt.Errorf("could not convert %v in a valid fake provider: %w", in.GetName(), err)
}
return out, nil
}
Gitlab Provider ApplyReferent Implementation:
func (g *Provider) ApplyReferent(spec kclient.Object, caller esmeta.ReferentCallOrigin, namespace string) (kclient.Object, error) {
conv, ok := spec.(*prov.Gitlab)
if !ok {
return nil, fmt.Errorf("could not convert spec %v onto a Gitlab Provider type: current type: %T", spec.GetName(), spec)
}
out := conv.DeepCopy()
switch caller {
case esmeta.ReferentCallSecretStore:
out.Spec.Auth.SecretRef.AccessToken.Namespace = &namespace
case esmeta.ReferentCallProvider:
out.Spec.Auth.SecretRef.AccessToken.Namespace = &namespace
case esmeta.ReferentCallClusterSecretStore:
default:
}
return out, nil
}
Gitlab Provider new getAuth method:
func (g *gitlabBase) getAuth(ctx context.Context) ([]byte, error) {
credentialsSecret := &corev1.Secret{}
credentialsSecretName := g.store.Auth.SecretRef.AccessToken.Name
if credentialsSecretName == "" {
return nil, fmt.Errorf(errGitlabCredSecretName)
}
objectKey := types.NamespacedName{
Name: credentialsSecretName,
Namespace: g.namespace,
}
// If namespace is set, it means we must use it (non-referrent call either from local SecretStore or defined ClusterSecretStore)
if g.store.Auth.SecretRef.AccessToken.Namespace != nil {
objectKey.Namespace = *g.store.Auth.SecretRef.AccessToken.Namespace
}
err := g.kube.Get(ctx, objectKey, credentialsSecret)
if err != nil {
return nil, fmt.Errorf(errFetchSAKSecret, err)
}
credentials := credentialsSecret.Data[g.store.Auth.SecretRef.AccessToken.Key]
if len(credentials) == 0 {
return nil, errors.New(errMissingSAK)
}
return credentials, nil
}
Gitlab Provider NewClient implementations:
func (g *Provider) NewClient(ctx context.Context, obj kclient.Object, kube kclient.Client, namespace string) (esv1.SecretsClient, error) {
prov, ok := obj.(*prov.Gitlab)
if !ok {
return nil, fmt.Errorf("could not convert spec %v onto a Gitlab Provider type: current type: %T", obj.GetName(), obj)
}
gl := &gitlabBase{
kube: kube,
store: &prov.Spec,
namespace: namespace,
}
client, err := gl.getClient(ctx, &prov.Spec)
if err != nil {
return nil, err
}
gl.projectsClient = client.Projects
gl.projectVariablesClient = client.ProjectVariables
gl.groupVariablesClient = client.GroupVariables
return gl, nil
}
Client Manager reconciler changes:
func (m *Manager) GetProviderRefFromStore(store esv1.GenericStore) (esv1.ProviderRef, error) {
providerRef := store.GetSpec().ProviderRef
if providerRef != nil {
return *providerRef, nil
}
provider, err := esv1.GetProvider(store)
if err != nil {
return esv1.ProviderRef{}, err
}
providerRef := esv1.GetProviderRefByProvider(provider)
providerRef.Name = store.GetName()
return *providerRef, nil
}
func (m *Manager) GetFromStore(ctx context.Context, store esv1.GenericStore, namespace string) (esv1.SecretsClient, error) {
var storeProvider esv1.Provider
var err error
var spec client.Object
prov, err := GetProviderRefFromStore(store)
if err != nil {
return nil, err
}
storeProvider, _ = esv1.GetProviderByRef(*prov)
spec, err = m.getProviderSpec(ctx, prov, namespace)
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()))
caller := esmetav1.ReferentCallSecretStore
storeKind := store.GetObjectKind().GroupVersionKind().Kind
if storeKind == esv1.ClusterSecretStoreKind {
caller = esmetav1.ReferentCallClusterSecretStore
}
referredSpec, err := storeProvider.ApplyReferent(spec, caller, namespace)
if err != nil {
return nil, fmt.Errorf("could not apply referrent logic to spec on %v: %w", store.GetName(), err)
}
secretClient, err = storeProvider.NewClientFromObj(ctx, referredSpec, m.client, namespace)
if err != nil {
return nil, err
}
idx := storeKey(storeProvider)
m.clientMap[idx] = &clientVal{
client: secretClient,
store: store,
}
return secretClient, nil
}
v1alpha1)Convert method