Просмотр исходного кода

wip: implement workflow

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Moritz Johner 2 лет назад
Родитель
Сommit
ee6eb7b1e6

+ 8 - 0
apis/externalsecrets/v1alpha1/register.go

@@ -67,9 +67,17 @@ var (
 	PushSecretGroupVersionKind = SchemeGroupVersion.WithKind(PushSecretKind)
 )
 
+var (
+	WorkflowKind             = reflect.TypeOf(Workflow{}).Name()
+	WorkflowGroupKind        = schema.GroupKind{Group: Group, Kind: WorkflowKind}.String()
+	WorkflowKindAPIVersion   = WorkflowKind + "." + SchemeGroupVersion.String()
+	WorkflowGroupVersionKind = SchemeGroupVersion.WithKind(WorkflowKind)
+)
+
 func init() {
 	SchemeBuilder.Register(&ExternalSecret{}, &ExternalSecretList{})
 	SchemeBuilder.Register(&SecretStore{}, &SecretStoreList{})
 	SchemeBuilder.Register(&ClusterSecretStore{}, &ClusterSecretStoreList{})
 	SchemeBuilder.Register(&PushSecret{}, &PushSecretList{})
+	SchemeBuilder.Register(&Workflow{}, &WorkflowList{})
 }

+ 140 - 0
apis/externalsecrets/v1alpha1/workflow_types.go

@@ -0,0 +1,140 @@
+package v1alpha1
+
+import (
+	"github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+type WorkflowSpec struct {
+	// RefreshInterval is the amount of time before the workflow is being reconciled.
+	// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h"
+	// May be set to zero to fetch and create it once. Defaults to 1h.
+	// +kubebuilder:default="1h"
+	RefreshInterval *metav1.Duration `json:"refreshInterval,omitempty"`
+
+	// Workflows are a list of workflows that are being executed in order.
+	// +optional
+	Workflows []WorkflowItem `json:"workflows"`
+}
+type WorkflowItem struct {
+	// Name of the workflow.
+	// It will be used as the index in the workflows data map.
+	// +kubebuilder:validation:MinLength=1
+	// +kubebuilder:validation:MaxLength=63
+	// +kubebuilder:validation:Required
+	Name string `json:"name"`
+
+	// Steps of the workflow, they are executed in order.
+	// +optional
+	Steps []WorkflowStep `json:"steps,omitempty"`
+}
+
+type WorkflowStep struct {
+	// Name of the workflow step.
+	Name string `json:"name"`
+
+	// Pull allows you to fetch secrets from a SecretStore.
+	// The secret data will be stored in the workflow data map.
+	// +optional
+	Pull *WorkflowStepPull `json:"pull,omitempty"`
+
+	// Push allows you to push secrets to a SecretStore.
+	// The secret data will be read from the workflow data map.
+	// +optional
+	Push *WorkflowStepPush `json:"push,omitempty"`
+
+	// Template allows you to compose data from the workflow.
+	// The result will be stored in the workflow data map.
+	// +optional
+	Template *WorkflowTemplate `json:"template,omitempty"`
+
+	// Manifests allows you to apply manifests to the cluster. The manifests are applied in order.
+	// The manifests can be templated and have access to the workflow data map.
+	// +optional
+	Manifests []string `json:"manifests,omitempty"`
+}
+
+type WorkflowTemplate struct {
+	// Metadata allows you to set metadata on the workflow data map.
+	// +optional
+	Metadata v1beta1.ExternalSecretTemplateMetadata `json:"metadata,omitempty"`
+
+	// Data allows you to compose data from the workflow. It is stored in the workflow data map.
+	// Previous data can be accessed from the workflow data map.
+	Data map[string]string `json:"data,omitempty"`
+}
+
+type WorkflowStepPull struct {
+	// Source allows you to fetch secrets from a SecretStore.
+	Source v1beta1.StoreSourceRef `json:"source"`
+
+	// Data allows you to fetch specific data from the secret.
+	// +optional
+	Data []v1beta1.ExternalSecretData `json:"data,omitempty"`
+
+	// DataFrom allows you to find multiple secrets in a store or extract structured data from a secret.
+	// +optional
+	DataFrom []v1beta1.ExternalSecretDataFromRemoteRef `json:"dataFrom,omitempty"`
+}
+
+type WorkflowStepPush struct {
+	Destination DestinationRef   `json:"destination,omitempty"`
+	Data        []PushSecretData `json:"data,omitempty"`
+}
+
+// DestinationRef allows you to override the SecretStore destination
+// where the secret will be pushed to.
+// +kubebuilder:validation:MaxProperties=1
+type DestinationRef struct {
+	// +optional
+	SecretStoreRef v1beta1.SecretStoreRef `json:"storeRef,omitempty"`
+}
+
+type WorkflowStatus struct {
+	Conditions []WorkflowStatusCondition `json:"conditions,omitempty"`
+}
+
+type WorkflowStatusCondition struct {
+	Type   WorkflowConditionType  `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 WorkflowConditionType string
+
+const (
+	WorkflowReady WorkflowConditionType = "Ready"
+)
+
+// +kubebuilder:object:root=true
+// +kubebuilder:storageversion
+// +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={workflows}
+
+type Workflow struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   WorkflowSpec   `json:"spec,omitempty"`
+	Status WorkflowStatus `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`
+type WorkflowList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []Workflow `json:"items"`
+}

+ 273 - 0
apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go

@@ -442,6 +442,22 @@ func (in *ClusterSecretStoreList) DeepCopyObject() runtime.Object {
 	return nil
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DestinationRef) DeepCopyInto(out *DestinationRef) {
+	*out = *in
+	out.SecretStoreRef = in.SecretStoreRef
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DestinationRef.
+func (in *DestinationRef) DeepCopy() *DestinationRef {
+	if in == nil {
+		return nil
+	}
+	out := new(DestinationRef)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ExternalSecret) DeepCopyInto(out *ExternalSecret) {
 	*out = *in
@@ -2015,6 +2031,263 @@ func (in *WebhookSecret) DeepCopy() *WebhookSecret {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Workflow) DeepCopyInto(out *Workflow) {
+	*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 Workflow.
+func (in *Workflow) DeepCopy() *Workflow {
+	if in == nil {
+		return nil
+	}
+	out := new(Workflow)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *Workflow) 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 *WorkflowItem) DeepCopyInto(out *WorkflowItem) {
+	*out = *in
+	if in.Steps != nil {
+		in, out := &in.Steps, &out.Steps
+		*out = make([]WorkflowStep, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkflowItem.
+func (in *WorkflowItem) DeepCopy() *WorkflowItem {
+	if in == nil {
+		return nil
+	}
+	out := new(WorkflowItem)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WorkflowList) DeepCopyInto(out *WorkflowList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]Workflow, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkflowList.
+func (in *WorkflowList) DeepCopy() *WorkflowList {
+	if in == nil {
+		return nil
+	}
+	out := new(WorkflowList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *WorkflowList) 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 *WorkflowSpec) DeepCopyInto(out *WorkflowSpec) {
+	*out = *in
+	if in.RefreshInterval != nil {
+		in, out := &in.RefreshInterval, &out.RefreshInterval
+		*out = new(v1.Duration)
+		**out = **in
+	}
+	if in.Workflows != nil {
+		in, out := &in.Workflows, &out.Workflows
+		*out = make([]WorkflowItem, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkflowSpec.
+func (in *WorkflowSpec) DeepCopy() *WorkflowSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(WorkflowSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WorkflowStatus) DeepCopyInto(out *WorkflowStatus) {
+	*out = *in
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]WorkflowStatusCondition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkflowStatus.
+func (in *WorkflowStatus) DeepCopy() *WorkflowStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(WorkflowStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WorkflowStatusCondition) DeepCopyInto(out *WorkflowStatusCondition) {
+	*out = *in
+	in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkflowStatusCondition.
+func (in *WorkflowStatusCondition) DeepCopy() *WorkflowStatusCondition {
+	if in == nil {
+		return nil
+	}
+	out := new(WorkflowStatusCondition)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WorkflowStep) DeepCopyInto(out *WorkflowStep) {
+	*out = *in
+	if in.Pull != nil {
+		in, out := &in.Pull, &out.Pull
+		*out = new(WorkflowStepPull)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Push != nil {
+		in, out := &in.Push, &out.Push
+		*out = new(WorkflowStepPush)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Template != nil {
+		in, out := &in.Template, &out.Template
+		*out = new(WorkflowTemplate)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Manifests != nil {
+		in, out := &in.Manifests, &out.Manifests
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkflowStep.
+func (in *WorkflowStep) DeepCopy() *WorkflowStep {
+	if in == nil {
+		return nil
+	}
+	out := new(WorkflowStep)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WorkflowStepPull) DeepCopyInto(out *WorkflowStepPull) {
+	*out = *in
+	in.Source.DeepCopyInto(&out.Source)
+	if in.Data != nil {
+		in, out := &in.Data, &out.Data
+		*out = make([]v1beta1.ExternalSecretData, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	if in.DataFrom != nil {
+		in, out := &in.DataFrom, &out.DataFrom
+		*out = make([]v1beta1.ExternalSecretDataFromRemoteRef, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkflowStepPull.
+func (in *WorkflowStepPull) DeepCopy() *WorkflowStepPull {
+	if in == nil {
+		return nil
+	}
+	out := new(WorkflowStepPull)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WorkflowStepPush) DeepCopyInto(out *WorkflowStepPush) {
+	*out = *in
+	out.Destination = in.Destination
+	if in.Data != nil {
+		in, out := &in.Data, &out.Data
+		*out = make([]PushSecretData, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkflowStepPush.
+func (in *WorkflowStepPush) DeepCopy() *WorkflowStepPush {
+	if in == nil {
+		return nil
+	}
+	out := new(WorkflowStepPush)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WorkflowTemplate) DeepCopyInto(out *WorkflowTemplate) {
+	*out = *in
+	in.Metadata.DeepCopyInto(&out.Metadata)
+	if in.Data != nil {
+		in, out := &in.Data, &out.Data
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkflowTemplate.
+func (in *WorkflowTemplate) DeepCopy() *WorkflowTemplate {
+	if in == nil {
+		return nil
+	}
+	out := new(WorkflowTemplate)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *YandexLockboxAuth) DeepCopyInto(out *YandexLockboxAuth) {
 	*out = *in

+ 10 - 0
cmd/root.go

@@ -47,6 +47,7 @@ import (
 	"github.com/external-secrets/external-secrets/pkg/controllers/secretstore"
 	"github.com/external-secrets/external-secrets/pkg/controllers/secretstore/cssmetrics"
 	"github.com/external-secrets/external-secrets/pkg/controllers/secretstore/ssmetrics"
+	"github.com/external-secrets/external-secrets/pkg/controllers/workflow"
 	"github.com/external-secrets/external-secrets/pkg/feature"
 
 	// To allow using gcp auth.
@@ -221,6 +222,15 @@ var rootCmd = &cobra.Command{
 				os.Exit(1)
 			}
 		}
+		if err = (&workflow.Reconciler{
+			Client:          mgr.GetClient(),
+			Log:             ctrl.Log.WithName("controllers").WithName("Workflow"),
+			Scheme:          mgr.GetScheme(),
+			RequeueInterval: time.Hour,
+		}).SetupWithManager(mgr); err != nil {
+			setupLog.Error(err, errCreateController, "controller", "Workflow")
+			os.Exit(1)
+		}
 		if enableClusterExternalSecretReconciler {
 			cesmetrics.SetUpMetrics()
 

+ 564 - 0
config/crds/bases/external-secrets.io_workflows.yaml

@@ -0,0 +1,564 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.15.0
+  name: workflows.external-secrets.io
+spec:
+  group: external-secrets.io
+  names:
+    categories:
+    - workflows
+    kind: Workflow
+    listKind: WorkflowList
+    plural: workflows
+    singular: workflow
+  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:
+            properties:
+              refreshInterval:
+                default: 1h
+                description: |-
+                  RefreshInterval is the amount of time before the workflow is being reconciled.
+                  Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h"
+                  May be set to zero to fetch and create it once. Defaults to 1h.
+                type: string
+              workflows:
+                description: Workflows are a list of workflows that are being executed
+                  in order.
+                items:
+                  properties:
+                    name:
+                      description: |-
+                        Name of the workflow.
+                        It will be used as the index in the workflows data map.
+                      maxLength: 63
+                      minLength: 1
+                      type: string
+                    steps:
+                      description: Steps of the workflow, they are executed in order.
+                      items:
+                        properties:
+                          manifests:
+                            description: |-
+                              Manifests allows you to apply manifests to the cluster. The manifests are applied in order.
+                              The manifests can be templated and have access to the workflow data map.
+                            items:
+                              type: string
+                            type: array
+                          name:
+                            description: Name of the workflow step.
+                            type: string
+                          pull:
+                            description: |-
+                              Pull allows you to fetch secrets from a SecretStore.
+                              The secret data will be stored in the workflow data map.
+                            properties:
+                              data:
+                                description: Data allows you to fetch specific data
+                                  from the secret.
+                                items:
+                                  description: ExternalSecretData defines the connection
+                                    between the Kubernetes Secret key (spec.data.<key>)
+                                    and the Provider data.
+                                  properties:
+                                    remoteRef:
+                                      description: |-
+                                        RemoteRef points to the remote secret and defines
+                                        which secret (version/property/..) to fetch.
+                                      properties:
+                                        conversionStrategy:
+                                          default: Default
+                                          description: Used to define a conversion
+                                            Strategy
+                                          enum:
+                                          - Default
+                                          - Unicode
+                                          type: string
+                                        decodingStrategy:
+                                          default: None
+                                          description: Used to define a decoding Strategy
+                                          enum:
+                                          - Auto
+                                          - Base64
+                                          - Base64URL
+                                          - None
+                                          type: string
+                                        key:
+                                          description: Key is the key used in the
+                                            Provider, mandatory
+                                          type: string
+                                        metadataPolicy:
+                                          default: None
+                                          description: Policy for fetching tags/labels
+                                            from provider secrets, possible options
+                                            are Fetch, None. Defaults to None
+                                          enum:
+                                          - None
+                                          - Fetch
+                                          type: string
+                                        property:
+                                          description: Used to select a specific property
+                                            of the Provider value (if a map), if supported
+                                          type: string
+                                        version:
+                                          description: Used to select a specific version
+                                            of the Provider value, if supported
+                                          type: string
+                                      required:
+                                      - key
+                                      type: object
+                                    secretKey:
+                                      description: |-
+                                        SecretKey defines the key in which the controller stores
+                                        the value. This is the key in the Kind=Secret
+                                      type: string
+                                    sourceRef:
+                                      description: |-
+                                        SourceRef allows you to override the source
+                                        from which the value will pulled from.
+                                      maxProperties: 1
+                                      properties:
+                                        generatorRef:
+                                          description: |-
+                                            GeneratorRef points to a generator custom resource.
+
+
+                                            Deprecated: The generatorRef is not implemented in .data[].
+                                            this will be removed with v1.
+                                          properties:
+                                            apiVersion:
+                                              default: generators.external-secrets.io/v1alpha1
+                                              description: Specify the apiVersion
+                                                of the generator resource
+                                              type: string
+                                            kind:
+                                              description: Specify the Kind of the
+                                                resource, e.g. Password, ACRAccessToken
+                                                etc.
+                                              type: string
+                                            name:
+                                              description: Specify the name of the
+                                                generator resource
+                                              type: string
+                                          required:
+                                          - kind
+                                          - name
+                                          type: object
+                                        storeRef:
+                                          description: SecretStoreRef defines which
+                                            SecretStore to fetch the ExternalSecret
+                                            data.
+                                          properties:
+                                            kind:
+                                              description: |-
+                                                Kind of the SecretStore resource (SecretStore or ClusterSecretStore)
+                                                Defaults to `SecretStore`
+                                              type: string
+                                            name:
+                                              description: Name of the SecretStore
+                                                resource
+                                              type: string
+                                          required:
+                                          - name
+                                          type: object
+                                      type: object
+                                  required:
+                                  - remoteRef
+                                  - secretKey
+                                  type: object
+                                type: array
+                              dataFrom:
+                                description: DataFrom allows you to find multiple
+                                  secrets in a store or extract structured data from
+                                  a secret.
+                                items:
+                                  properties:
+                                    extract:
+                                      description: |-
+                                        Used to extract multiple key/value pairs from one secret
+                                        Note: Extract does not support sourceRef.Generator or sourceRef.GeneratorRef.
+                                      properties:
+                                        conversionStrategy:
+                                          default: Default
+                                          description: Used to define a conversion
+                                            Strategy
+                                          enum:
+                                          - Default
+                                          - Unicode
+                                          type: string
+                                        decodingStrategy:
+                                          default: None
+                                          description: Used to define a decoding Strategy
+                                          enum:
+                                          - Auto
+                                          - Base64
+                                          - Base64URL
+                                          - None
+                                          type: string
+                                        key:
+                                          description: Key is the key used in the
+                                            Provider, mandatory
+                                          type: string
+                                        metadataPolicy:
+                                          default: None
+                                          description: Policy for fetching tags/labels
+                                            from provider secrets, possible options
+                                            are Fetch, None. Defaults to None
+                                          enum:
+                                          - None
+                                          - Fetch
+                                          type: string
+                                        property:
+                                          description: Used to select a specific property
+                                            of the Provider value (if a map), if supported
+                                          type: string
+                                        version:
+                                          description: Used to select a specific version
+                                            of the Provider value, if supported
+                                          type: string
+                                      required:
+                                      - key
+                                      type: object
+                                    find:
+                                      description: |-
+                                        Used to find secrets based on tags or regular expressions
+                                        Note: Find does not support sourceRef.Generator or sourceRef.GeneratorRef.
+                                      properties:
+                                        conversionStrategy:
+                                          default: Default
+                                          description: Used to define a conversion
+                                            Strategy
+                                          enum:
+                                          - Default
+                                          - Unicode
+                                          type: string
+                                        decodingStrategy:
+                                          default: None
+                                          description: Used to define a decoding Strategy
+                                          enum:
+                                          - Auto
+                                          - Base64
+                                          - Base64URL
+                                          - None
+                                          type: string
+                                        name:
+                                          description: Finds secrets based on the
+                                            name.
+                                          properties:
+                                            regexp:
+                                              description: Finds secrets base
+                                              type: string
+                                          type: object
+                                        path:
+                                          description: A root path to start the find
+                                            operations.
+                                          type: string
+                                        tags:
+                                          additionalProperties:
+                                            type: string
+                                          description: Find secrets based on tags.
+                                          type: object
+                                      type: object
+                                    rewrite:
+                                      description: |-
+                                        Used to rewrite secret Keys after getting them from the secret Provider
+                                        Multiple Rewrite operations can be provided. They are applied in a layered order (first to last)
+                                      items:
+                                        properties:
+                                          regexp:
+                                            description: |-
+                                              Used to rewrite with regular expressions.
+                                              The resulting key will be the output of a regexp.ReplaceAll operation.
+                                            properties:
+                                              source:
+                                                description: Used to define the regular
+                                                  expression of a re.Compiler.
+                                                type: string
+                                              target:
+                                                description: Used to define the target
+                                                  pattern of a ReplaceAll operation.
+                                                type: string
+                                            required:
+                                            - source
+                                            - target
+                                            type: object
+                                          transform:
+                                            description: |-
+                                              Used to apply string transformation on the secrets.
+                                              The resulting key will be the output of the template applied by the operation.
+                                            properties:
+                                              template:
+                                                description: |-
+                                                  Used to define the template to apply on the secret name.
+                                                  `.value ` will specify the secret name in the template.
+                                                type: string
+                                            required:
+                                            - template
+                                            type: object
+                                        type: object
+                                      type: array
+                                    sourceRef:
+                                      description: |-
+                                        SourceRef points to a store or generator
+                                        which contains secret values ready to use.
+                                        Use this in combination with Extract or Find pull values out of
+                                        a specific SecretStore.
+                                        When sourceRef points to a generator Extract or Find is not supported.
+                                        The generator returns a static map of values
+                                      maxProperties: 1
+                                      properties:
+                                        generatorRef:
+                                          description: GeneratorRef points to a generator
+                                            custom resource.
+                                          properties:
+                                            apiVersion:
+                                              default: generators.external-secrets.io/v1alpha1
+                                              description: Specify the apiVersion
+                                                of the generator resource
+                                              type: string
+                                            kind:
+                                              description: Specify the Kind of the
+                                                resource, e.g. Password, ACRAccessToken
+                                                etc.
+                                              type: string
+                                            name:
+                                              description: Specify the name of the
+                                                generator resource
+                                              type: string
+                                          required:
+                                          - kind
+                                          - name
+                                          type: object
+                                        storeRef:
+                                          description: SecretStoreRef defines which
+                                            SecretStore to fetch the ExternalSecret
+                                            data.
+                                          properties:
+                                            kind:
+                                              description: |-
+                                                Kind of the SecretStore resource (SecretStore or ClusterSecretStore)
+                                                Defaults to `SecretStore`
+                                              type: string
+                                            name:
+                                              description: Name of the SecretStore
+                                                resource
+                                              type: string
+                                          required:
+                                          - name
+                                          type: object
+                                      type: object
+                                  type: object
+                                type: array
+                              source:
+                                description: Source allows you to fetch secrets from
+                                  a SecretStore.
+                                maxProperties: 1
+                                properties:
+                                  generatorRef:
+                                    description: |-
+                                      GeneratorRef points to a generator custom resource.
+
+
+                                      Deprecated: The generatorRef is not implemented in .data[].
+                                      this will be removed with v1.
+                                    properties:
+                                      apiVersion:
+                                        default: generators.external-secrets.io/v1alpha1
+                                        description: Specify the apiVersion of the
+                                          generator resource
+                                        type: string
+                                      kind:
+                                        description: Specify the Kind of the resource,
+                                          e.g. Password, ACRAccessToken etc.
+                                        type: string
+                                      name:
+                                        description: Specify the name of the generator
+                                          resource
+                                        type: string
+                                    required:
+                                    - kind
+                                    - name
+                                    type: object
+                                  storeRef:
+                                    description: SecretStoreRef defines which SecretStore
+                                      to fetch the ExternalSecret data.
+                                    properties:
+                                      kind:
+                                        description: |-
+                                          Kind of the SecretStore resource (SecretStore or ClusterSecretStore)
+                                          Defaults to `SecretStore`
+                                        type: string
+                                      name:
+                                        description: Name of the SecretStore resource
+                                        type: string
+                                    required:
+                                    - name
+                                    type: object
+                                type: object
+                            required:
+                            - source
+                            type: object
+                          push:
+                            description: |-
+                              Push allows you to push secrets to a SecretStore.
+                              The secret data will be read from the workflow data map.
+                            properties:
+                              data:
+                                items:
+                                  properties:
+                                    conversionStrategy:
+                                      default: None
+                                      description: Used to define a conversion Strategy
+                                        for the secret keys
+                                      enum:
+                                      - None
+                                      - ReverseUnicode
+                                      type: string
+                                    match:
+                                      description: Match a given Secret Key to be
+                                        pushed to the provider.
+                                      properties:
+                                        remoteRef:
+                                          description: Remote Refs to push to providers.
+                                          properties:
+                                            property:
+                                              description: Name of the property in
+                                                the resulting secret
+                                              type: string
+                                            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
+                                      type: object
+                                    metadata:
+                                      description: |-
+                                        Metadata is metadata attached to the secret.
+                                        The structure of metadata is provider specific, please look it up in the provider documentation.
+                                      x-kubernetes-preserve-unknown-fields: true
+                                  required:
+                                  - match
+                                  type: object
+                                type: array
+                              destination:
+                                description: |-
+                                  DestinationRef allows you to override the SecretStore destination
+                                  where the secret will be pushed to.
+                                maxProperties: 1
+                                properties:
+                                  storeRef:
+                                    description: SecretStoreRef defines which SecretStore
+                                      to fetch the ExternalSecret data.
+                                    properties:
+                                      kind:
+                                        description: |-
+                                          Kind of the SecretStore resource (SecretStore or ClusterSecretStore)
+                                          Defaults to `SecretStore`
+                                        type: string
+                                      name:
+                                        description: Name of the SecretStore resource
+                                        type: string
+                                    required:
+                                    - name
+                                    type: object
+                                type: object
+                            type: object
+                          template:
+                            description: |-
+                              Template allows you to compose data from the workflow.
+                              The result will be stored in the workflow data map.
+                            properties:
+                              data:
+                                additionalProperties:
+                                  type: string
+                                description: |-
+                                  Data allows you to compose data from the workflow. It is stored in the workflow data map.
+                                  Previous data can be accessed from the workflow data map.
+                                type: object
+                              metadata:
+                                description: Metadata allows you to set metadata on
+                                  the workflow data map.
+                                properties:
+                                  annotations:
+                                    additionalProperties:
+                                      type: string
+                                    type: object
+                                  labels:
+                                    additionalProperties:
+                                      type: string
+                                    type: object
+                                type: object
+                            type: object
+                        required:
+                        - name
+                        type: object
+                      type: array
+                  required:
+                  - name
+                  type: object
+                type: array
+            type: object
+          status:
+            properties:
+              conditions:
+                items:
+                  properties:
+                    lastTransitionTime:
+                      format: date-time
+                      type: string
+                    message:
+                      type: string
+                    reason:
+                      type: string
+                    status:
+                      type: string
+                    type:
+                      type: string
+                  required:
+                  - status
+                  - type
+                  type: object
+                type: array
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}

+ 1 - 0
config/crds/bases/kustomization.yaml

@@ -7,6 +7,7 @@ resources:
   - external-secrets.io_externalsecrets.yaml
   - external-secrets.io_pushsecrets.yaml
   - external-secrets.io_secretstores.yaml
+  - external-secrets.io_workflows.yaml
   - generators.external-secrets.io_acraccesstokens.yaml
   - generators.external-secrets.io_ecrauthorizationtokens.yaml
   - generators.external-secrets.io_fakes.yaml

+ 526 - 0
deploy/crds/bundle.yaml

@@ -10215,6 +10215,532 @@ spec:
 ---
 apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.15.0
+  name: workflows.external-secrets.io
+spec:
+  group: external-secrets.io
+  names:
+    categories:
+      - workflows
+    kind: Workflow
+    listKind: WorkflowList
+    plural: workflows
+    singular: workflow
+  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:
+              properties:
+                refreshInterval:
+                  default: 1h
+                  description: |-
+                    RefreshInterval is the amount of time before the workflow is being reconciled.
+                    Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h"
+                    May be set to zero to fetch and create it once. Defaults to 1h.
+                  type: string
+                workflows:
+                  description: Workflows are a list of workflows that are being executed in order.
+                  items:
+                    properties:
+                      name:
+                        description: |-
+                          Name of the workflow.
+                          It will be used as the index in the workflows data map.
+                        maxLength: 63
+                        minLength: 1
+                        type: string
+                      steps:
+                        description: Steps of the workflow, they are executed in order.
+                        items:
+                          properties:
+                            manifests:
+                              description: |-
+                                Manifests allows you to apply manifests to the cluster. The manifests are applied in order.
+                                The manifests can be templated and have access to the workflow data map.
+                              items:
+                                type: string
+                              type: array
+                            name:
+                              description: Name of the workflow step.
+                              type: string
+                            pull:
+                              description: |-
+                                Pull allows you to fetch secrets from a SecretStore.
+                                The secret data will be stored in the workflow data map.
+                              properties:
+                                data:
+                                  description: Data allows you to fetch specific data from the secret.
+                                  items:
+                                    description: ExternalSecretData defines the connection between the Kubernetes Secret key (spec.data.<key>) and the Provider data.
+                                    properties:
+                                      remoteRef:
+                                        description: |-
+                                          RemoteRef points to the remote secret and defines
+                                          which secret (version/property/..) to fetch.
+                                        properties:
+                                          conversionStrategy:
+                                            default: Default
+                                            description: Used to define a conversion Strategy
+                                            enum:
+                                              - Default
+                                              - Unicode
+                                            type: string
+                                          decodingStrategy:
+                                            default: None
+                                            description: Used to define a decoding Strategy
+                                            enum:
+                                              - Auto
+                                              - Base64
+                                              - Base64URL
+                                              - None
+                                            type: string
+                                          key:
+                                            description: Key is the key used in the Provider, mandatory
+                                            type: string
+                                          metadataPolicy:
+                                            default: None
+                                            description: Policy for fetching tags/labels from provider secrets, possible options are Fetch, None. Defaults to None
+                                            enum:
+                                              - None
+                                              - Fetch
+                                            type: string
+                                          property:
+                                            description: Used to select a specific property of the Provider value (if a map), if supported
+                                            type: string
+                                          version:
+                                            description: Used to select a specific version of the Provider value, if supported
+                                            type: string
+                                        required:
+                                          - key
+                                        type: object
+                                      secretKey:
+                                        description: |-
+                                          SecretKey defines the key in which the controller stores
+                                          the value. This is the key in the Kind=Secret
+                                        type: string
+                                      sourceRef:
+                                        description: |-
+                                          SourceRef allows you to override the source
+                                          from which the value will pulled from.
+                                        maxProperties: 1
+                                        properties:
+                                          generatorRef:
+                                            description: |-
+                                              GeneratorRef points to a generator custom resource.
+
+
+                                              Deprecated: The generatorRef is not implemented in .data[].
+                                              this will be removed with v1.
+                                            properties:
+                                              apiVersion:
+                                                default: generators.external-secrets.io/v1alpha1
+                                                description: Specify the apiVersion of the generator resource
+                                                type: string
+                                              kind:
+                                                description: Specify the Kind of the resource, e.g. Password, ACRAccessToken etc.
+                                                type: string
+                                              name:
+                                                description: Specify the name of the generator resource
+                                                type: string
+                                            required:
+                                              - kind
+                                              - name
+                                            type: object
+                                          storeRef:
+                                            description: SecretStoreRef defines which SecretStore to fetch the ExternalSecret data.
+                                            properties:
+                                              kind:
+                                                description: |-
+                                                  Kind of the SecretStore resource (SecretStore or ClusterSecretStore)
+                                                  Defaults to `SecretStore`
+                                                type: string
+                                              name:
+                                                description: Name of the SecretStore resource
+                                                type: string
+                                            required:
+                                              - name
+                                            type: object
+                                        type: object
+                                    required:
+                                      - remoteRef
+                                      - secretKey
+                                    type: object
+                                  type: array
+                                dataFrom:
+                                  description: DataFrom allows you to find multiple secrets in a store or extract structured data from a secret.
+                                  items:
+                                    properties:
+                                      extract:
+                                        description: |-
+                                          Used to extract multiple key/value pairs from one secret
+                                          Note: Extract does not support sourceRef.Generator or sourceRef.GeneratorRef.
+                                        properties:
+                                          conversionStrategy:
+                                            default: Default
+                                            description: Used to define a conversion Strategy
+                                            enum:
+                                              - Default
+                                              - Unicode
+                                            type: string
+                                          decodingStrategy:
+                                            default: None
+                                            description: Used to define a decoding Strategy
+                                            enum:
+                                              - Auto
+                                              - Base64
+                                              - Base64URL
+                                              - None
+                                            type: string
+                                          key:
+                                            description: Key is the key used in the Provider, mandatory
+                                            type: string
+                                          metadataPolicy:
+                                            default: None
+                                            description: Policy for fetching tags/labels from provider secrets, possible options are Fetch, None. Defaults to None
+                                            enum:
+                                              - None
+                                              - Fetch
+                                            type: string
+                                          property:
+                                            description: Used to select a specific property of the Provider value (if a map), if supported
+                                            type: string
+                                          version:
+                                            description: Used to select a specific version of the Provider value, if supported
+                                            type: string
+                                        required:
+                                          - key
+                                        type: object
+                                      find:
+                                        description: |-
+                                          Used to find secrets based on tags or regular expressions
+                                          Note: Find does not support sourceRef.Generator or sourceRef.GeneratorRef.
+                                        properties:
+                                          conversionStrategy:
+                                            default: Default
+                                            description: Used to define a conversion Strategy
+                                            enum:
+                                              - Default
+                                              - Unicode
+                                            type: string
+                                          decodingStrategy:
+                                            default: None
+                                            description: Used to define a decoding Strategy
+                                            enum:
+                                              - Auto
+                                              - Base64
+                                              - Base64URL
+                                              - None
+                                            type: string
+                                          name:
+                                            description: Finds secrets based on the name.
+                                            properties:
+                                              regexp:
+                                                description: Finds secrets base
+                                                type: string
+                                            type: object
+                                          path:
+                                            description: A root path to start the find operations.
+                                            type: string
+                                          tags:
+                                            additionalProperties:
+                                              type: string
+                                            description: Find secrets based on tags.
+                                            type: object
+                                        type: object
+                                      rewrite:
+                                        description: |-
+                                          Used to rewrite secret Keys after getting them from the secret Provider
+                                          Multiple Rewrite operations can be provided. They are applied in a layered order (first to last)
+                                        items:
+                                          properties:
+                                            regexp:
+                                              description: |-
+                                                Used to rewrite with regular expressions.
+                                                The resulting key will be the output of a regexp.ReplaceAll operation.
+                                              properties:
+                                                source:
+                                                  description: Used to define the regular expression of a re.Compiler.
+                                                  type: string
+                                                target:
+                                                  description: Used to define the target pattern of a ReplaceAll operation.
+                                                  type: string
+                                              required:
+                                                - source
+                                                - target
+                                              type: object
+                                            transform:
+                                              description: |-
+                                                Used to apply string transformation on the secrets.
+                                                The resulting key will be the output of the template applied by the operation.
+                                              properties:
+                                                template:
+                                                  description: |-
+                                                    Used to define the template to apply on the secret name.
+                                                    `.value ` will specify the secret name in the template.
+                                                  type: string
+                                              required:
+                                                - template
+                                              type: object
+                                          type: object
+                                        type: array
+                                      sourceRef:
+                                        description: |-
+                                          SourceRef points to a store or generator
+                                          which contains secret values ready to use.
+                                          Use this in combination with Extract or Find pull values out of
+                                          a specific SecretStore.
+                                          When sourceRef points to a generator Extract or Find is not supported.
+                                          The generator returns a static map of values
+                                        maxProperties: 1
+                                        properties:
+                                          generatorRef:
+                                            description: GeneratorRef points to a generator custom resource.
+                                            properties:
+                                              apiVersion:
+                                                default: generators.external-secrets.io/v1alpha1
+                                                description: Specify the apiVersion of the generator resource
+                                                type: string
+                                              kind:
+                                                description: Specify the Kind of the resource, e.g. Password, ACRAccessToken etc.
+                                                type: string
+                                              name:
+                                                description: Specify the name of the generator resource
+                                                type: string
+                                            required:
+                                              - kind
+                                              - name
+                                            type: object
+                                          storeRef:
+                                            description: SecretStoreRef defines which SecretStore to fetch the ExternalSecret data.
+                                            properties:
+                                              kind:
+                                                description: |-
+                                                  Kind of the SecretStore resource (SecretStore or ClusterSecretStore)
+                                                  Defaults to `SecretStore`
+                                                type: string
+                                              name:
+                                                description: Name of the SecretStore resource
+                                                type: string
+                                            required:
+                                              - name
+                                            type: object
+                                        type: object
+                                    type: object
+                                  type: array
+                                source:
+                                  description: Source allows you to fetch secrets from a SecretStore.
+                                  maxProperties: 1
+                                  properties:
+                                    generatorRef:
+                                      description: |-
+                                        GeneratorRef points to a generator custom resource.
+
+
+                                        Deprecated: The generatorRef is not implemented in .data[].
+                                        this will be removed with v1.
+                                      properties:
+                                        apiVersion:
+                                          default: generators.external-secrets.io/v1alpha1
+                                          description: Specify the apiVersion of the generator resource
+                                          type: string
+                                        kind:
+                                          description: Specify the Kind of the resource, e.g. Password, ACRAccessToken etc.
+                                          type: string
+                                        name:
+                                          description: Specify the name of the generator resource
+                                          type: string
+                                      required:
+                                        - kind
+                                        - name
+                                      type: object
+                                    storeRef:
+                                      description: SecretStoreRef defines which SecretStore to fetch the ExternalSecret data.
+                                      properties:
+                                        kind:
+                                          description: |-
+                                            Kind of the SecretStore resource (SecretStore or ClusterSecretStore)
+                                            Defaults to `SecretStore`
+                                          type: string
+                                        name:
+                                          description: Name of the SecretStore resource
+                                          type: string
+                                      required:
+                                        - name
+                                      type: object
+                                  type: object
+                              required:
+                                - source
+                              type: object
+                            push:
+                              description: |-
+                                Push allows you to push secrets to a SecretStore.
+                                The secret data will be read from the workflow data map.
+                              properties:
+                                data:
+                                  items:
+                                    properties:
+                                      conversionStrategy:
+                                        default: None
+                                        description: Used to define a conversion Strategy for the secret keys
+                                        enum:
+                                          - None
+                                          - ReverseUnicode
+                                        type: string
+                                      match:
+                                        description: Match a given Secret Key to be pushed to the provider.
+                                        properties:
+                                          remoteRef:
+                                            description: Remote Refs to push to providers.
+                                            properties:
+                                              property:
+                                                description: Name of the property in the resulting secret
+                                                type: string
+                                              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
+                                        type: object
+                                      metadata:
+                                        description: |-
+                                          Metadata is metadata attached to the secret.
+                                          The structure of metadata is provider specific, please look it up in the provider documentation.
+                                        x-kubernetes-preserve-unknown-fields: true
+                                    required:
+                                      - match
+                                    type: object
+                                  type: array
+                                destination:
+                                  description: |-
+                                    DestinationRef allows you to override the SecretStore destination
+                                    where the secret will be pushed to.
+                                  maxProperties: 1
+                                  properties:
+                                    storeRef:
+                                      description: SecretStoreRef defines which SecretStore to fetch the ExternalSecret data.
+                                      properties:
+                                        kind:
+                                          description: |-
+                                            Kind of the SecretStore resource (SecretStore or ClusterSecretStore)
+                                            Defaults to `SecretStore`
+                                          type: string
+                                        name:
+                                          description: Name of the SecretStore resource
+                                          type: string
+                                      required:
+                                        - name
+                                      type: object
+                                  type: object
+                              type: object
+                            template:
+                              description: |-
+                                Template allows you to compose data from the workflow.
+                                The result will be stored in the workflow data map.
+                              properties:
+                                data:
+                                  additionalProperties:
+                                    type: string
+                                  description: |-
+                                    Data allows you to compose data from the workflow. It is stored in the workflow data map.
+                                    Previous data can be accessed from the workflow data map.
+                                  type: object
+                                metadata:
+                                  description: Metadata allows you to set metadata on the workflow data map.
+                                  properties:
+                                    annotations:
+                                      additionalProperties:
+                                        type: string
+                                      type: object
+                                    labels:
+                                      additionalProperties:
+                                        type: string
+                                      type: object
+                                  type: object
+                              type: object
+                          required:
+                            - name
+                          type: object
+                        type: array
+                    required:
+                      - name
+                    type: object
+                  type: array
+              type: object
+            status:
+              properties:
+                conditions:
+                  items:
+                    properties:
+                      lastTransitionTime:
+                        format: date-time
+                        type: string
+                      message:
+                        type: string
+                      reason:
+                        type: string
+                      status:
+                        type: string
+                      type:
+                        type: string
+                    required:
+                      - status
+                      - type
+                    type: object
+                  type: array
+              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.15.0

+ 163 - 0
pkg/controllers/workflow/workflow_controller.go

@@ -0,0 +1,163 @@
+/*
+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 workflow
+
+import (
+	"context"
+	"fmt"
+	"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/client-go/tools/record"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	"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"
+	errSetSecretFailed       = "could not write remote ref %v to target secretstore %v: %v"
+	errFailedSetSecret       = "set secret failed: %v"
+	errConvert               = "could not apply conversion strategy to keys: %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) SetupWithManager(mgr ctrl.Manager) error {
+	r.recorder = mgr.GetEventRecorderFor("workflow")
+
+	return ctrl.NewControllerManagedBy(mgr).
+		For(&esapi.Workflow{}).
+		Complete(r)
+}
+
+func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+	log := r.Log.WithValues("workflow", req.NamespacedName)
+
+	var workflow esapi.Workflow
+	mgr := secretstore.NewManager(r.Client, r.ControllerClass, false)
+	defer mgr.Close(ctx)
+
+	if err := r.Get(ctx, req.NamespacedName, &workflow); err != nil {
+		if apierrors.IsNotFound(err) {
+			return ctrl.Result{}, nil
+		}
+		msg := "unable to get Workflow"
+		r.recorder.Event(&workflow, v1.EventTypeWarning, esapi.ReasonErrored, msg)
+		log.Error(err, msg)
+		return ctrl.Result{}, fmt.Errorf("get resource: %w", err)
+	}
+
+	refreshInt := r.RequeueInterval
+	if workflow.Spec.RefreshInterval != nil {
+		refreshInt = workflow.Spec.RefreshInterval.Duration
+	}
+
+	p := client.MergeFrom(workflow.DeepCopy())
+	defer func() {
+		if err := r.Client.Status().Patch(ctx, &workflow, p); err != nil {
+			log.Error(err, errPatchStatus)
+		}
+	}()
+
+	err := NewWorkflowRunner(ctx, r.Client, workflow.Namespace, workflow.Spec.Workflows, log).Run()
+	if err != nil {
+		r.markAsFailed(workflow, err)
+		return ctrl.Result{RequeueAfter: refreshInt}, nil
+	}
+
+	r.markAsDone(&workflow)
+	return ctrl.Result{RequeueAfter: refreshInt}, nil
+}
+
+func (r *Reconciler) markAsFailed(workflow esapi.Workflow, err error) {
+	msg := err.Error()
+	r.recorder.Event(&workflow, v1.EventTypeWarning, esapi.ReasonErrored, msg)
+	r.Log.Error(err, msg)
+	cond := newWorkflowCondition(esapi.WorkflowReady, v1.ConditionFalse, esapi.ReasonErrored, msg)
+	setWorkflowCondition(&workflow, *cond)
+}
+
+func (r *Reconciler) markAsDone(workflow *esapi.Workflow) {
+	msg := "Workflow ran successfully"
+	cond := newWorkflowCondition(esapi.WorkflowReady, v1.ConditionTrue, esapi.ReasonSynced, msg)
+	setWorkflowCondition(workflow, *cond)
+	r.recorder.Event(workflow, v1.EventTypeNormal, esapi.ReasonSynced, msg)
+}
+
+func newWorkflowCondition(condType esapi.WorkflowConditionType, status v1.ConditionStatus, reason, message string) *esapi.WorkflowStatusCondition {
+	return &esapi.WorkflowStatusCondition{
+		Type:               condType,
+		Status:             status,
+		LastTransitionTime: metav1.Now(),
+		Reason:             reason,
+		Message:            message,
+	}
+}
+
+func setWorkflowCondition(workflow *esapi.Workflow, condition esapi.WorkflowStatusCondition) {
+	currentCond := getWorkflowCondition(workflow.Status, condition.Type)
+	if currentCond != nil && currentCond.Status == condition.Status &&
+		currentCond.Reason == condition.Reason && currentCond.Message == condition.Message {
+		return
+	}
+
+	// Do not update lastTransitionTime if the status of the condition doesn't change.
+	if currentCond != nil && currentCond.Status == condition.Status {
+		condition.LastTransitionTime = currentCond.LastTransitionTime
+	}
+
+	workflow.Status.Conditions = append(filterOutCondition(workflow.Status.Conditions, condition.Type), condition)
+}
+
+// filterOutCondition returns an empty set of conditions with the provided type.
+func filterOutCondition(conditions []esapi.WorkflowStatusCondition, condType esapi.WorkflowConditionType) []esapi.WorkflowStatusCondition {
+	newConditions := make([]esapi.WorkflowStatusCondition, 0, len(conditions))
+	for _, c := range conditions {
+		if c.Type == condType {
+			continue
+		}
+		newConditions = append(newConditions, c)
+	}
+	return newConditions
+}
+
+// getWorkflowCondition returns the condition with the provided type.
+func getWorkflowCondition(status esapi.WorkflowStatus, condType esapi.WorkflowConditionType) *esapi.WorkflowStatusCondition {
+	for i := range status.Conditions {
+		c := status.Conditions[i]
+		if c.Type == condType {
+			return &c
+		}
+	}
+	return nil
+}

+ 334 - 0
pkg/controllers/workflow/workflow_engine.go

@@ -0,0 +1,334 @@
+package workflow
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"text/template"
+
+	"github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	"github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/controllers/secretstore"
+	templatev2 "github.com/external-secrets/external-secrets/pkg/template/v2"
+	"github.com/external-secrets/external-secrets/pkg/utils"
+	"github.com/go-logr/logr"
+	corev1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+type WorkflowRunner struct {
+	ctx       context.Context
+	client    client.Client
+	namespace string
+	workflows []v1alpha1.WorkflowItem
+	log       logr.Logger
+	inputs    WorkflowInputs
+}
+
+type WorkflowOutput struct {
+	Data     map[string]string            `json:"data"`
+	Metadata map[string]map[string]string `json:"metadata"`
+}
+
+type WorkflowInputs struct {
+	Workflow  *WorkflowOutput            `json:"workflow"`
+	Workflows map[string]*WorkflowOutput `json:"workflows"`
+}
+
+func NewWorkflowRunner(ctx context.Context, client client.Client, namespace string, workflows []v1alpha1.WorkflowItem, log logr.Logger) *WorkflowRunner {
+	return &WorkflowRunner{
+		ctx:       ctx,
+		client:    client,
+		namespace: namespace,
+		workflows: workflows,
+		log:       log,
+		inputs: WorkflowInputs{
+			Workflows: make(map[string]*WorkflowOutput),
+			Workflow:  newWorkflowOutput(),
+		},
+	}
+}
+
+func (w *WorkflowRunner) Run() error {
+	for _, workflow := range w.workflows {
+		err := w.runWorkflow(workflow)
+		if err != nil {
+			return fmt.Errorf("error running workflow %s: %w", workflow.Name, err)
+		}
+	}
+	return nil
+}
+
+func (w *WorkflowRunner) runWorkflow(workflow v1alpha1.WorkflowItem) error {
+	log := w.log.WithValues("workflow", workflow.Name)
+	log.Info("running workflow")
+
+	// workflowData persists the state across all steps in the workflow
+	workflowData := newWorkflowOutput()
+	w.inputs.Workflow = workflowData
+	w.inputs.Workflows[workflow.Name] = workflowData
+
+	for _, step := range workflow.Steps {
+		stepData, err := w.runStep(step, workflowData, log)
+		if err != nil {
+			return fmt.Errorf("error running step %s: %w", step.Name, err)
+		}
+		// accumulate and update workflow output after each step
+		workflowData = mergeStepOutput(workflowData, stepData)
+		w.inputs.Workflow = workflowData
+		w.inputs.Workflows[workflow.Name] = workflowData
+	}
+
+	return nil
+}
+
+func newWorkflowOutput() *WorkflowOutput {
+	return &WorkflowOutput{
+		Data: make(map[string]string),
+		Metadata: map[string]map[string]string{
+			"labels":      make(map[string]string),
+			"annotations": make(map[string]string),
+		},
+	}
+}
+
+func mergeStepOutput(step1, step2 *WorkflowOutput) *WorkflowOutput {
+	return &WorkflowOutput{
+		Data: mergeStringMap(step1.Data, step2.Data),
+		Metadata: map[string]map[string]string{
+			"labels":      mergeStringMap(step1.Metadata["labels"], step2.Metadata["labels"]),
+			"annotations": mergeStringMap(step1.Metadata["annotations"], step2.Metadata["annotations"]),
+		},
+	}
+}
+
+func mergeStringMap(map1, map2 map[string]string) map[string]string {
+	for k, v := range map2 {
+		map1[k] = v
+	}
+	return map1
+}
+
+func (w *WorkflowRunner) runStep(step v1alpha1.WorkflowStep, workflowOutput *WorkflowOutput, workflowLog logr.Logger) (*WorkflowOutput, error) {
+	log := workflowLog.WithValues("step", step.Name)
+	log.Info("running step")
+	stepOutput := newWorkflowOutput()
+	if step.Pull != nil {
+		if err := w.runPullStep(step.Pull, stepOutput, log); err != nil {
+			return nil, fmt.Errorf("error running pull step: %w", err)
+		}
+	}
+	if step.Push != nil {
+		if err := w.runPushStep(step.Push, workflowOutput, log); err != nil {
+			return nil, fmt.Errorf("error running push step: %w", err)
+		}
+	}
+	if step.Template != nil {
+		if err := w.runTemplateStep(step.Template, stepOutput, log); err != nil {
+			return nil, fmt.Errorf("error running template step: %w", err)
+		}
+	}
+	if step.Manifests != nil {
+		if err := w.runManifestsStep(step.Manifests, stepOutput, log); err != nil {
+			return nil, fmt.Errorf("error running manifests step: %w", err)
+
+		}
+	}
+	return stepOutput, nil
+}
+
+const (
+	errFetchTplFrom = "error fetching templateFrom data: %w"
+	errExecTpl      = "could not execute template: %w"
+)
+
+func (w *WorkflowRunner) runTemplateStep(tpl *v1alpha1.WorkflowTemplate, stepOutput *WorkflowOutput, log logr.Logger) error {
+	log.Info("running template step")
+
+	for k, v := range tpl.Data {
+		templatedValue, err := templateValue(string(v), w.inputs.ToMap())
+		if err != nil {
+			return fmt.Errorf(errExecTpl, err)
+		}
+		templatedKey, err := templateValue(k, w.inputs.ToMap())
+		if err != nil {
+			return fmt.Errorf(errExecTpl, err)
+		}
+		delete(stepOutput.Data, k)
+		stepOutput.Data[templatedKey] = templatedValue
+	}
+
+	for k, v := range tpl.Metadata.Annotations {
+		templatedValue, err := templateValue(string(v), w.inputs.ToMap())
+		if err != nil {
+			return fmt.Errorf(errExecTpl, err)
+		}
+		delete(stepOutput.Metadata["annotations"], k)
+		stepOutput.Metadata["annotations"][k] = templatedValue
+	}
+
+	for k, v := range tpl.Metadata.Labels {
+		templatedValue, err := templateValue(string(v), w.inputs.ToMap())
+		if err != nil {
+			return fmt.Errorf(errExecTpl, err)
+		}
+		delete(stepOutput.Metadata["labels"], k)
+		stepOutput.Metadata["labels"][k] = templatedValue
+	}
+
+	return nil
+}
+
+func (w *WorkflowRunner) runManifestsStep(manifests []string, stepOutput *WorkflowOutput, log logr.Logger) error {
+	return fmt.Errorf("not implemented")
+}
+
+func templateValue(tpl string, data any) (string, error) {
+	t, err := template.New("template-step").
+		Funcs(templatev2.FuncMap()).
+		Option("missingkey=error").
+		Parse(tpl)
+
+	if err != nil {
+		return "", fmt.Errorf("error parsing template: %w", err)
+	}
+	buf := bytes.NewBuffer(nil)
+	err = t.Execute(buf, data)
+	if err != nil {
+		return "", fmt.Errorf("error executing template: %w", err)
+	}
+	return buf.String(), nil
+}
+
+func (w *WorkflowRunner) runPullStep(pull *v1alpha1.WorkflowStepPull, stepOutput *WorkflowOutput, log logr.Logger) error {
+	log.Info("running pull step")
+	mgr := secretstore.NewManager(w.client, "", true)
+	defer mgr.Close(w.ctx)
+
+	for i, data := range pull.Data {
+		client, err := mgr.Get(w.ctx, pull.Source.SecretStoreRef, w.namespace, nil)
+		if err != nil {
+			return fmt.Errorf("error getting client for secret store [%d]: %w", i, err)
+		}
+		secretData, err := client.GetSecret(w.ctx, data.RemoteRef)
+		if err != nil {
+			return fmt.Errorf("error getting secret data [%d]: %w", i, err)
+		}
+		stepOutput.Data[data.SecretKey] = string(secretData)
+	}
+
+	for i, remoteRef := range pull.DataFrom {
+		var secretMap map[string][]byte
+		var err error
+
+		if remoteRef.Find != nil {
+			secretMap, err = w.handleFindAllSecrets(pull.Source, remoteRef, mgr, i)
+		} else if remoteRef.Extract != nil {
+			secretMap, err = w.handleExtractSecrets(pull.Source, remoteRef, mgr, i)
+		} else if remoteRef.SourceRef != nil && remoteRef.SourceRef.GeneratorRef != nil {
+			secretMap, err = w.handleGenerateSecrets(pull.Source, remoteRef, i)
+		}
+		if err != nil {
+			return err
+		}
+		for k, v := range secretMap {
+			stepOutput.Data[k] = string(v)
+		}
+	}
+	return nil
+}
+
+func (w *WorkflowRunner) handleFindAllSecrets(sourceRef v1beta1.StoreSourceRef, remoteRef v1beta1.ExternalSecretDataFromRemoteRef, mgr *secretstore.Manager, i int) (map[string][]byte, error) {
+	client, err := mgr.Get(w.ctx, sourceRef.SecretStoreRef, w.namespace, remoteRef.SourceRef)
+	if err != nil {
+		return nil, err
+	}
+	secretMap, err := client.GetAllSecrets(w.ctx, *remoteRef.Find)
+	if err != nil {
+		return nil, err
+	}
+	secretMap, err = utils.RewriteMap(remoteRef.Rewrite, secretMap)
+	if err != nil {
+		return nil, fmt.Errorf("unable to rewrite map .dataFrom[%d]: %w", i, err)
+	}
+	secretMap, err = utils.DecodeMap(remoteRef.Find.DecodingStrategy, secretMap)
+	if err != nil {
+		return nil, fmt.Errorf("unablet to decode map %s[%d]: %w", ".dataFrom", i, err)
+	}
+	return secretMap, err
+}
+
+func (w *WorkflowRunner) handleExtractSecrets(sourceRef v1beta1.StoreSourceRef, remoteRef v1beta1.ExternalSecretDataFromRemoteRef, mgr *secretstore.Manager, i int) (map[string][]byte, error) {
+	client, err := mgr.Get(w.ctx, sourceRef.SecretStoreRef, w.namespace, remoteRef.SourceRef)
+	if err != nil {
+		return nil, err
+	}
+	secretMap, err := client.GetSecretMap(w.ctx, *remoteRef.Extract)
+	if err != nil {
+		return nil, err
+	}
+	secretMap, err = utils.RewriteMap(remoteRef.Rewrite, secretMap)
+	if err != nil {
+		return nil, fmt.Errorf("unable to rewrite map at .dataFrom[%d]: %w", i, err)
+	}
+	if len(remoteRef.Rewrite) == 0 {
+		secretMap, err = utils.ConvertKeys(remoteRef.Extract.ConversionStrategy, secretMap)
+		if err != nil {
+			return nil, fmt.Errorf(errConvert, err)
+		}
+	}
+	secretMap, err = utils.DecodeMap(remoteRef.Extract.DecodingStrategy, secretMap)
+	if err != nil {
+		return nil, fmt.Errorf("unable to decode map at %s[%d]: %w", ".dataFrom", i, err)
+	}
+	return secretMap, err
+}
+
+func (w *WorkflowRunner) handleGenerateSecrets(sourceRef v1beta1.StoreSourceRef, remoteRef v1beta1.ExternalSecretDataFromRemoteRef, i int) (map[string][]byte, error) {
+	// TODO
+	return nil, fmt.Errorf("not implemented")
+}
+
+func (w *WorkflowRunner) runPushStep(push *v1alpha1.WorkflowStepPush, stepOutput *WorkflowOutput, log logr.Logger) error {
+	log.Info("running push step")
+	mgr := secretstore.NewManager(w.client, "", true)
+	defer mgr.Close(w.ctx)
+
+	byteData := make(map[string][]byte)
+	for k, v := range stepOutput.Data {
+		byteData[k] = []byte(v)
+	}
+	secret := corev1.Secret{
+		Data: byteData,
+	}
+
+	for i, data := range push.Data {
+		client, err := mgr.Get(w.ctx, push.Destination.SecretStoreRef, w.namespace, nil)
+		if err != nil {
+			return fmt.Errorf("error getting client for secret store [%d]: %w", i, err)
+		}
+		err = client.PushSecret(w.ctx, &secret, data)
+		if err != nil {
+			return fmt.Errorf("error setting secret data [%d]: %w", i, err)
+		}
+	}
+	return nil
+}
+
+func (wo WorkflowOutput) ToMap() map[string]interface{} {
+	return map[string]interface{}{
+		"data":     wo.Data,
+		"metadata": wo.Metadata,
+	}
+}
+
+func (wi WorkflowInputs) ToMap() map[string]interface{} {
+	workflows := make(map[string]interface{})
+	for k, v := range wi.Workflows {
+		workflows[k] = v.ToMap()
+	}
+	return map[string]interface{}{
+		"workflow":  wi.Workflow.ToMap(),
+		"workflows": workflows,
+	}
+}

+ 386 - 0
pkg/controllers/workflow/workflow_engine_test.go

@@ -0,0 +1,386 @@
+package workflow
+
+import (
+	"context"
+	"reflect"
+	"testing"
+
+	"github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	"github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/go-logr/logr"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+)
+
+func TestWorkflowRunner_Run(t *testing.T) {
+	tests := []struct {
+		name      string
+		workflows []v1alpha1.WorkflowItem
+		expected  map[string]interface{}
+	}{
+		{
+			name: "SingleStepWorkflow",
+			workflows: []v1alpha1.WorkflowItem{
+				{
+					Name: "workflow1",
+					Steps: []v1alpha1.WorkflowStep{
+						{
+							Name: "step1",
+							Pull: &v1alpha1.WorkflowStepPull{
+								Source: v1beta1.StoreSourceRef{
+									SecretStoreRef: v1beta1.SecretStoreRef{
+										Name: "store1",
+									},
+								},
+								Data: []v1beta1.ExternalSecretData{
+									{
+										SecretKey: "key1",
+										RemoteRef: v1beta1.ExternalSecretDataRemoteRef{
+											Key: "key1",
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expected: map[string]interface{}{
+				"workflow": map[string]interface{}{
+					"data": map[string]string{
+						"key1": "value1",
+					},
+					"metadata": map[string]map[string]string{
+						"annotations": {},
+						"labels":      {},
+					},
+				},
+				"workflows": map[string]interface{}{
+					"workflow1": map[string]interface{}{
+						"data": map[string]string{
+							"key1": "value1",
+						},
+						"metadata": map[string]map[string]string{
+							"annotations": {},
+							"labels":      {},
+						},
+					},
+				},
+			},
+		},
+		{
+
+			name: "MultipleStepWorkflow",
+			workflows: []v1alpha1.WorkflowItem{
+				{
+					Name: "workflow1",
+					Steps: []v1alpha1.WorkflowStep{
+						{
+							Name: "step1",
+							Pull: &v1alpha1.WorkflowStepPull{
+								Source: v1beta1.StoreSourceRef{
+									SecretStoreRef: v1beta1.SecretStoreRef{
+										Name: "store1",
+									},
+								},
+								Data: []v1beta1.ExternalSecretData{
+									{
+										SecretKey: "key1",
+										RemoteRef: v1beta1.ExternalSecretDataRemoteRef{
+											Key: "key1",
+										},
+									},
+								},
+							},
+						},
+						{
+							Name: "step2",
+							Pull: &v1alpha1.WorkflowStepPull{
+								Source: v1beta1.StoreSourceRef{
+									SecretStoreRef: v1beta1.SecretStoreRef{
+										Name: "store1",
+									},
+								},
+								Data: []v1beta1.ExternalSecretData{
+									{
+										SecretKey: "key2",
+										RemoteRef: v1beta1.ExternalSecretDataRemoteRef{
+											Key: "key2",
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expected: map[string]interface{}{
+				"workflow": map[string]interface{}{
+					"data": map[string]string{
+						"key1": "value1",
+						"key2": "value2",
+					},
+					"metadata": map[string]map[string]string{
+						"annotations": {},
+						"labels":      {},
+					},
+				},
+				"workflows": map[string]interface{}{
+					"workflow1": map[string]interface{}{
+						"data": map[string]string{
+							"key1": "value1",
+							"key2": "value2",
+						},
+						"metadata": map[string]map[string]string{
+							"annotations": {},
+							"labels":      {},
+						},
+					},
+				},
+			},
+		},
+		{
+
+			name: "MultipleWorkflows",
+			workflows: []v1alpha1.WorkflowItem{
+				{
+					Name: "workflow1",
+					Steps: []v1alpha1.WorkflowStep{
+						{
+							Name: "step1",
+							Pull: &v1alpha1.WorkflowStepPull{
+								Source: v1beta1.StoreSourceRef{
+									SecretStoreRef: v1beta1.SecretStoreRef{
+										Name: "store1",
+									},
+								},
+								Data: []v1beta1.ExternalSecretData{
+									{
+										SecretKey: "key1",
+										RemoteRef: v1beta1.ExternalSecretDataRemoteRef{
+											Key: "key1",
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+				{
+					Name: "workflow2",
+					Steps: []v1alpha1.WorkflowStep{
+						{
+							Name: "step1",
+							Pull: &v1alpha1.WorkflowStepPull{
+								Source: v1beta1.StoreSourceRef{
+									SecretStoreRef: v1beta1.SecretStoreRef{
+										Name: "store1",
+									},
+								},
+								Data: []v1beta1.ExternalSecretData{
+									{
+										SecretKey: "key2",
+										RemoteRef: v1beta1.ExternalSecretDataRemoteRef{
+											Key: "key2",
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expected: map[string]interface{}{
+				"workflow": map[string]interface{}{
+					"data": map[string]string{
+						"key2": "value2",
+					},
+					"metadata": map[string]map[string]string{
+						"annotations": {},
+						"labels":      {},
+					},
+				},
+				"workflows": map[string]interface{}{
+					"workflow1": map[string]interface{}{
+						"data": map[string]string{
+							"key1": "value1",
+						},
+						"metadata": map[string]map[string]string{
+							"annotations": {},
+							"labels":      {},
+						},
+					},
+					"workflow2": map[string]interface{}{
+						"data": map[string]string{
+							"key2": "value2",
+						},
+						"metadata": map[string]map[string]string{
+							"annotations": {},
+							"labels":      {},
+						},
+					},
+				},
+			},
+		},
+		{
+
+			name: "ChainWorkflows",
+			workflows: []v1alpha1.WorkflowItem{
+				{
+					Name: "workflow1",
+					Steps: []v1alpha1.WorkflowStep{
+						{
+							Name: "step1",
+							Pull: &v1alpha1.WorkflowStepPull{
+								Source: v1beta1.StoreSourceRef{
+									SecretStoreRef: v1beta1.SecretStoreRef{
+										Name: "store1",
+									},
+								},
+								Data: []v1beta1.ExternalSecretData{
+									{
+										SecretKey: "key1",
+										RemoteRef: v1beta1.ExternalSecretDataRemoteRef{
+											Key: "key1",
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+				{
+					Name: "workflow2",
+					Steps: []v1alpha1.WorkflowStep{
+						{
+							Name: "step1",
+							Pull: &v1alpha1.WorkflowStepPull{
+								Source: v1beta1.StoreSourceRef{
+									SecretStoreRef: v1beta1.SecretStoreRef{
+										Name: "store1",
+									},
+								},
+								Data: []v1beta1.ExternalSecretData{
+									{
+										SecretKey: "key2",
+										RemoteRef: v1beta1.ExternalSecretDataRemoteRef{
+											Key: "key2",
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+				{
+					Name: "workflow3",
+					Steps: []v1alpha1.WorkflowStep{
+						{
+							Name: "step1",
+							Template: &v1alpha1.WorkflowTemplate{
+								Data: map[string]string{
+									"aggregated": "{{ .workflows.workflow1.data.key1 }} and {{ .workflows.workflow2.data.key2 }}",
+								},
+							},
+						},
+					},
+				},
+			},
+			expected: map[string]interface{}{
+				"workflow": map[string]interface{}{
+					"data": map[string]string{
+						"aggregated": "value1 and value2",
+					},
+					"metadata": map[string]map[string]string{
+						"annotations": {},
+						"labels":      {},
+					},
+				},
+				"workflows": map[string]interface{}{
+					"workflow1": map[string]interface{}{
+						"data": map[string]string{
+							"key1": "value1",
+						},
+						"metadata": map[string]map[string]string{
+							"annotations": {},
+							"labels":      {},
+						},
+					},
+					"workflow2": map[string]interface{}{
+						"data": map[string]string{
+							"key2": "value2",
+						},
+						"metadata": map[string]map[string]string{
+							"annotations": {},
+							"labels":      {},
+						},
+					},
+					"workflow3": map[string]interface{}{
+						"data": map[string]string{
+							"aggregated": "value1 and value2",
+						},
+						"metadata": map[string]map[string]string{
+							"annotations": {},
+							"labels":      {},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	ctx := context.TODO()
+	scheme := runtime.NewScheme()
+	v1alpha1.AddToScheme(scheme)
+	v1beta1.AddToScheme(scheme)
+
+	namespace := "test-namespace"
+	store1 := &v1beta1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "store1",
+			Namespace: namespace,
+		},
+		Status: v1beta1.SecretStoreStatus{
+			Conditions: []v1beta1.SecretStoreStatusCondition{
+				{
+					Type:   v1beta1.SecretStoreReady,
+					Status: corev1.ConditionTrue,
+				},
+			},
+		},
+		Spec: v1beta1.SecretStoreSpec{
+			Provider: &v1beta1.SecretStoreProvider{
+				Fake: &v1beta1.FakeProvider{
+					Data: []v1beta1.FakeProviderData{
+						{
+							Key:   "key1",
+							Value: "value1",
+						},
+						{
+							Key:   "key2",
+							Value: "value2",
+						},
+					},
+				},
+			},
+		},
+	}
+
+	client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(store1).Build()
+	log := logr.Discard()
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			runner := NewWorkflowRunner(ctx, client, namespace, tt.workflows, log)
+			err := runner.Run()
+			if err != nil {
+				t.Errorf("unexpected error: %v", err)
+			}
+			if !reflect.DeepEqual(runner.inputs.ToMap(), tt.expected) {
+				t.Errorf("unexpected result: got %v, want %v", runner.inputs.ToMap(), tt.expected)
+			}
+		})
+	}
+}

+ 163 - 0
workflow.yaml

@@ -0,0 +1,163 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: my-store
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  namespace: default
+  name: eso-store-role
+rules:
+- apiGroups: [""]
+  resources:
+  - secrets
+  verbs:
+  - get
+  - list
+  - watch
+  - create
+  - update
+  - delete
+- apiGroups:
+  - authorization.k8s.io
+  resources:
+  - selfsubjectrulesreviews
+  verbs:
+  - create
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: my-store
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: eso-store-role
+subjects:
+- kind: ServiceAccount
+  name: my-store
+  namespace: default
+---
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: kubernetes
+spec:
+  provider:
+    kubernetes:
+      remoteNamespace: default
+      server:
+        url: https://localhost:44245
+        caProvider:
+          type: ConfigMap
+          name: kube-root-ca.crt
+          key: ca.crt
+      auth:
+        serviceAccount:
+          name: "my-store"
+---
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: aws-parameterstore
+spec:
+  provider:
+    aws:
+      service: ParameterStore
+      region: eu-central-1
+---
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: aws-secrets-manager
+spec:
+  provider:
+    aws:
+      region: eu-central-1
+      service: SecretsManager
+---
+apiVersion: external-secrets.io/v1alpha1
+kind: Workflow
+metadata:
+  name: "backend-secrets-with-config"
+spec:
+  workflows:
+  
+  # 1: fetch the database credentials from AWS Secrets Manager
+  - name: "db_credentials"
+
+    # steps are executed in order
+    steps:
+    - name: "fetch-mysql-credentials"
+      pull:
+        source:
+          storeRef:
+            name: "aws-secrets-manager"
+        dataFrom:
+        - extract:
+            key: "app-creds"
+        data:
+        - secretKey: "color"
+          remoteRef:
+            metadataPolicy: Fetch
+            key: "app-creds"
+            property: "color"
+    
+    - name: "encode_db_credentials"
+      template:
+        data:
+          color: "{{ .workflow.data.color }}"
+          encodedAppCreds: mysql://{{ .workflow.data.foo }}:{{ .workflow.data.baz }}@db.mycorp:3306/{{ .workflow.data.color }}
+  
+  # 2. fetch the configuration from SSM     
+  - name: "ami_config"
+    steps:
+    - name: "fetch-config"
+      pull:
+        source:
+          storeRef:
+            name: "aws-parameterstore"
+        data:
+        - secretKey: "ami"
+          remoteRef:
+            key: "/aws/service/eks/optimized-ami/1.29/amazon-linux-2/recommended/image_id"
+  
+  # 3. aggregate the secrets
+  - name: "aggregate"
+    steps:
+    - name: "aggregate-secrets"
+      # takes inputs from previous workflows
+      # inputs
+      template:
+        metadata:
+          labels:
+            color: "{{ .workflows.db_credentials.data.color }}"
+        data:
+          credentials: "{{ .workflows.db_credentials.data.encodedAppCreds }}"
+          ami: "{{ .workflows.ami_config.data.ami }}"
+    
+    # Note: A workflow always starts a new output map which aggregates values over the steps in a workflow.
+    # 
+    # For that reason, the "push" step needs a preceding step to have a value for the secret
+    # which is about to be pushed.
+    - name: "push-secrets"
+      push:
+        destination:
+          storeRef:
+            name: "kubernetes"
+            # TODO: support pushing to multiple stores with matchLabels
+            # TODO: allow Kubernetes provider (CSS) to push to multiple namespaces
+        data:
+        - match:
+            # TODO: support accessing previous workflow outputs
+            secretKey: "credentials"
+            remoteRef:
+              remoteKey: "app-credentials"
+              property: "credentials"
+        - match:
+            secretKey: "ami"
+            remoteRef:
+              remoteKey: "app-credentials"
+              property: "ami"
+