Browse Source

feat(component): implement component controller

Thomas Zhang 3 months ago
parent
commit
36827924bb

+ 26 - 0
config/rbac/role.yaml

@@ -4,6 +4,32 @@ kind: ClusterRole
 metadata:
   name: manager-role
 rules:
+- apiGroups:
+  - ""
+  resources:
+  - configmaps
+  - persistentvolumeclaims
+  - persistentvolumes
+  - secrets
+  - services
+  verbs:
+  - create
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - apps
+  resources:
+  - deployments
+  verbs:
+  - create
+  - get
+  - list
+  - patch
+  - update
+  - watch
 - apiGroups:
   - locostack.com
   resources:

+ 116 - 11
internal/controller/component_controller.go

@@ -18,13 +18,22 @@ package controller
 
 import (
 	"context"
+	"fmt"
 
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	apimeta "k8s.io/apimachinery/pkg/api/meta"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
 
 	"github.com/LocoStack/loco-operator/api/v1alpha1"
+	"github.com/LocoStack/loco-operator/internal/reconciler"
+	"github.com/LocoStack/loco-operator/pkg/templates"
 )
 
 // ComponentReconciler reconciles a Component object
@@ -36,28 +45,124 @@ type ComponentReconciler struct {
 // +kubebuilder:rbac:groups=locostack.com,resources=components,verbs=get;list;watch;create;update;patch;delete
 // +kubebuilder:rbac:groups=locostack.com,resources=components/status,verbs=get;update;patch
 // +kubebuilder:rbac:groups=locostack.com,resources=components/finalizers,verbs=update
+// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch
+// +kubebuilder:rbac:groups="",resources=persistentvolumes,verbs=get;list;watch;create;update;patch
+// +kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;watch;create;update;patch
+// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch
+// +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch
+// +kubebuilder:rbac:groups="apps",resources=deployments,verbs=get;list;watch;create;update;patch
 
-// Reconcile is part of the main kubernetes reconciliation loop which aims to
-// move the current state of the cluster closer to the desired state.
-// TODO(user): Modify the Reconcile function to compare the state specified by
-// the Component object against the actual cluster state, and then
-// perform operations to make the cluster state reflect the state specified by
-// the user.
-//
-// For more details, check Reconcile and its Result here:
-// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
 func (r *ComponentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
-	_ = logf.FromContext(ctx)
+	log := logf.FromContext(ctx)
 
-	// TODO(user): your logic here
+	comp := &v1alpha1.Component{}
+	if err := r.Get(ctx, req.NamespacedName, comp); err != nil {
+		if apierrors.IsNotFound(err) {
+			return ctrl.Result{}, nil
+		}
+		log.Error(err, "Failed to get Component", "namespace", req.NamespacedName.Namespace, "name", req.NamespacedName.Name)
+		return ctrl.Result{}, err
+	}
 
+	patch := client.MergeFrom(comp.DeepCopy())
+
+	tmpl, err := templates.Manager.ResolveTemplate(comp.Spec.Template, "")
+	if err != nil {
+		apimeta.SetStatusCondition(&comp.Status.Conditions, metav1.Condition{
+			Type:               "Reconciled",
+			Status:             metav1.ConditionFalse,
+			Reason:             "TemplateResolutionFailed",
+			Message:            fmt.Sprintf("Could not resolve template: %v", err),
+			ObservedGeneration: comp.Generation,
+		})
+		log.Error(err, "Failed to resolve template for Component", "namespace", req.NamespacedName.Namespace, "name", req.NamespacedName.Name)
+		_ = r.Status().Patch(ctx, comp, patch)
+		return ctrl.Result{}, nil
+	}
+
+	componentReconciler := r.getComponentReconciler(comp)
+	dependencies, err := componentReconciler.ReconcileComponent(ctx, &tmpl, make(map[string]string))
+	if err != nil {
+		apimeta.SetStatusCondition(&comp.Status.Conditions, metav1.Condition{
+			Type:               "Reconciled",
+			Status:             metav1.ConditionFalse,
+			Reason:             "ReconcileFailed",
+			Message:            fmt.Sprintf("Could not reconcile component: %v", err),
+			ObservedGeneration: comp.Generation,
+		})
+		log.Error(err, "Failed to reconcile Component", "namespace", req.NamespacedName.Namespace, "name", req.NamespacedName.Name)
+		_ = r.Status().Patch(ctx, comp, patch)
+		return ctrl.Result{}, nil
+	}
+
+	comp.Status.Endpoint = fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", componentReconciler.ResourceName(""), comp.Namespace, tmpl.Spec.Runtime.Port)
+	comp.Status.ObservedGeneration = comp.Generation
+	if len(dependencies) > 0 {
+		comp.Status.Dependencies = make(map[string]map[string]int64)
+		for _, dep := range dependencies {
+			gvk, err := apiutil.GVKForObject(dep, r.Scheme)
+			if err != nil {
+				log.Error(err, "Failed to get GVK for dependency", "dependency", dep)
+				continue
+			}
+			if gvk.Group != v1alpha1.GroupVersion.Group {
+				continue
+			}
+			kind := gvk.Kind
+			name := dep.GetName()
+			generation := dep.GetGeneration()
+
+			if _, exists := comp.Status.Dependencies[kind]; !exists {
+				comp.Status.Dependencies[kind] = make(map[string]int64)
+			}
+			comp.Status.Dependencies[kind][name] = generation
+		}
+	}
+	apimeta.SetStatusCondition(&comp.Status.Conditions, metav1.Condition{
+		Type:               "Reconciled",
+		Status:             metav1.ConditionTrue,
+		Reason:             "ReconcileSucceeded",
+		Message:            "Component reconciled",
+		ObservedGeneration: comp.Generation,
+	})
+	if available := componentReconciler.ComponentAvailable(ctx); !available {
+		apimeta.SetStatusCondition(&comp.Status.Conditions, metav1.Condition{
+			Type:               "Available",
+			Status:             metav1.ConditionFalse,
+			Reason:             "ServiceUnavailable",
+			Message:            "Component is not available",
+			ObservedGeneration: comp.Generation,
+		})
+	} else {
+		apimeta.SetStatusCondition(&comp.Status.Conditions, metav1.Condition{
+			Type:               "Available",
+			Status:             metav1.ConditionTrue,
+			Reason:             "ComponentReady",
+			Message:            "Component is available",
+			ObservedGeneration: comp.Generation,
+		})
+	}
+	if err := r.Status().Patch(ctx, comp, patch); err != nil {
+		return ctrl.Result{}, client.IgnoreNotFound(err)
+	}
+
+	log.Info("Reconciled Component", "namespace", comp.Namespace, "name", comp.Name)
 	return ctrl.Result{}, nil
 }
 
+func (r *ComponentReconciler) getComponentReconciler(comp *v1alpha1.Component) reconciler.ComponentReconciler {
+	return reconciler.NewDefaultComponentReconciler(r.Client, r.Scheme, comp)
+}
+
 // SetupWithManager sets up the controller with the Manager.
 func (r *ComponentReconciler) SetupWithManager(mgr ctrl.Manager) error {
 	return ctrl.NewControllerManagedBy(mgr).
 		For(&v1alpha1.Component{}).
+		Owns(&corev1.ConfigMap{}).
+		Owns(&corev1.Secret{}).
+		Owns(&corev1.PersistentVolumeClaim{}).
+		Owns(&corev1.Service{}).
+		Owns(&appsv1.Deployment{}).
 		Named("component").
 		Complete(r)
 }

+ 174 - 0
internal/reconciler/common.go

@@ -0,0 +1,174 @@
+/*
+Copyright 2026 LocoStack.
+
+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 reconciler
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/LocoStack/loco-operator/api/v1alpha1"
+	corev1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+type ComponentReconciler interface {
+	// create all resources for the component, and return the list of dependencies
+	ReconcileComponent(ctx context.Context, tmpl *v1alpha1.Template, variables map[string]string) ([]client.Object, error)
+	ComponentAvailable(ctx context.Context) bool
+	ResourceName(name string) string
+}
+
+func ResourceLabels(component, name, namespace string) map[string]string {
+	labels := map[string]string{
+		"app.kubernetes.io/managed-by": "loco-operator",
+		"locostack.com/component":      component,
+		"locostack.com/name":           name,
+	}
+	if namespace != "" {
+		labels["locostack.com/namespace"] = namespace
+	}
+	return labels
+}
+
+// substituteVariables replaces placeholders in the input string with actual values from the variables map.
+// Placeholders are in the format $(placeholder).
+func substituteVariables(variables map[string]string, input string) (bool, string) {
+	for placeholder, value := range variables {
+		if !strings.Contains(input, "$(") {
+			return true, input
+		}
+		if strings.Contains(input, fmt.Sprintf("$(%s)", placeholder)) {
+			input = strings.ReplaceAll(input, fmt.Sprintf("$(%s)", placeholder), value)
+		}
+	}
+	return false, input
+}
+
+func mustSubstituteVariables(variables map[string]string, input string) string {
+	_, substituted := substituteVariables(variables, input)
+	return substituted
+}
+
+func buildVolumes(variables map[string]string, tmpl *v1alpha1.Template) []corev1.Volume {
+	volumes := make([]corev1.Volume, len(tmpl.Spec.Volumes))
+	if len(volumes) == 0 {
+		volumes = nil
+	} else {
+		for i, vol := range tmpl.Spec.Volumes {
+			volumes[i] = vol
+			if vol.VolumeSource.ConfigMap != nil {
+				volumes[i].VolumeSource.ConfigMap.LocalObjectReference.Name = mustSubstituteVariables(variables, vol.VolumeSource.ConfigMap.LocalObjectReference.Name)
+			}
+			if vol.VolumeSource.Secret != nil {
+				volumes[i].VolumeSource.Secret.SecretName = mustSubstituteVariables(variables, vol.VolumeSource.Secret.SecretName)
+			}
+			if vol.VolumeSource.PersistentVolumeClaim != nil {
+				volumes[i].VolumeSource.PersistentVolumeClaim.ClaimName = mustSubstituteVariables(variables, vol.VolumeSource.PersistentVolumeClaim.ClaimName)
+			}
+		}
+	}
+	return volumes
+}
+
+func buildContainerSpec(variables map[string]string, tmpl *v1alpha1.Template) corev1.Container {
+	command := make([]string, len(tmpl.Spec.Runtime.Command))
+	if len(command) == 0 {
+		command = nil
+	} else {
+		for i, cmd := range tmpl.Spec.Runtime.Command {
+			command[i] = mustSubstituteVariables(variables, cmd)
+		}
+	}
+	args := make([]string, len(tmpl.Spec.Runtime.Args))
+	if len(args) == 0 {
+		args = nil
+	} else {
+		for i, arg := range tmpl.Spec.Runtime.Args {
+			args[i] = mustSubstituteVariables(variables, arg)
+		}
+		for _, arg := range tmpl.Spec.Runtime.ExtraArgs {
+			args = append(args, mustSubstituteVariables(variables, arg))
+		}
+		for _, arg := range tmpl.Spec.Runtime.ConditionalArgs {
+			complete, substituted := substituteVariables(variables, arg.When)
+			if complete && substituted != "" {
+				for _, a := range arg.Args {
+					args = append(args, mustSubstituteVariables(variables, a))
+				}
+			}
+		}
+	}
+	env := make([]corev1.EnvVar, len(tmpl.Spec.Runtime.Env))
+	if len(env) == 0 {
+		env = nil
+	} else {
+		for i, envVar := range tmpl.Spec.Runtime.Env {
+			env[i] = envVar
+			env[i].Value = mustSubstituteVariables(variables, envVar.Value)
+			if envVar.ValueFrom != nil && envVar.ValueFrom.SecretKeyRef != nil {
+				env[i].ValueFrom.SecretKeyRef.LocalObjectReference.Name = mustSubstituteVariables(variables, envVar.ValueFrom.SecretKeyRef.LocalObjectReference.Name)
+				env[i].ValueFrom.SecretKeyRef.Key = mustSubstituteVariables(variables, envVar.ValueFrom.SecretKeyRef.Key)
+			}
+		}
+	}
+	envFrom := make([]corev1.EnvFromSource, len(tmpl.Spec.Runtime.EnvFrom))
+	if len(envFrom) == 0 {
+		envFrom = nil
+	} else {
+		for i, envFromSource := range tmpl.Spec.Runtime.EnvFrom {
+			envFrom[i] = envFromSource
+			if envFromSource.ConfigMapRef != nil {
+				envFrom[i].ConfigMapRef.LocalObjectReference.Name = mustSubstituteVariables(variables, envFromSource.ConfigMapRef.LocalObjectReference.Name)
+			}
+			if envFromSource.SecretRef != nil {
+				envFrom[i].SecretRef.LocalObjectReference.Name = mustSubstituteVariables(variables, envFromSource.SecretRef.LocalObjectReference.Name)
+			}
+		}
+	}
+	ports := make([]corev1.ContainerPort, 0)
+	if tmpl.Spec.Runtime.Port != 0 {
+		ports = append(ports, corev1.ContainerPort{
+			ContainerPort: tmpl.Spec.Runtime.Port,
+			Protocol:      corev1.ProtocolTCP,
+		})
+	}
+	return corev1.Container{
+		Name:            "main",
+		Image:           tmpl.Spec.Runtime.Image,
+		ImagePullPolicy: corev1.PullIfNotPresent,
+		Command:         command,
+		Args:            args,
+		Env:             env,
+		EnvFrom:         envFrom,
+		VolumeMounts:    tmpl.Spec.Runtime.VolumeMounts,
+		Ports:           ports,
+		Resources:       tmpl.Spec.Resources,
+	}
+}
+
+func buildPodSpec(variables map[string]string, tmpl *v1alpha1.Template) (corev1.PodSpec, error) {
+	return corev1.PodSpec{
+		NodeSelector:              tmpl.Spec.NodeSelector,
+		Affinity:                  tmpl.Spec.Affinity,
+		Tolerations:               tmpl.Spec.Tolerations,
+		TopologySpreadConstraints: tmpl.Spec.TopologySpreadConstraints,
+		Containers:                []corev1.Container{buildContainerSpec(variables, tmpl)},
+		SecurityContext:           tmpl.Spec.SecurityContext,
+		Volumes:                   buildVolumes(variables, tmpl),
+	}, nil
+}

+ 306 - 0
internal/reconciler/component.go

@@ -0,0 +1,306 @@
+/*
+Copyright 2026 LocoStack.
+
+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 reconciler
+
+import (
+	"context"
+	"crypto/sha256"
+	"encoding/hex"
+	"fmt"
+	"maps"
+	"path"
+	"strings"
+
+	"github.com/LocoStack/loco-operator/api/v1alpha1"
+	"go.yaml.in/yaml/v2"
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	discoveryv1 "k8s.io/api/discovery/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/util/intstr"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+)
+
+type DefaultComponentReconciler struct {
+	client    client.Client
+	scheme    *runtime.Scheme
+	component *v1alpha1.Component
+}
+
+func NewDefaultComponentReconciler(client client.Client, scheme *runtime.Scheme, component *v1alpha1.Component) *DefaultComponentReconciler {
+	reconciler := &DefaultComponentReconciler{
+		client:    client,
+		scheme:    scheme,
+		component: component,
+	}
+	return reconciler
+}
+
+func (r *DefaultComponentReconciler) ReconcileComponent(ctx context.Context, tmpl *v1alpha1.Template, variables map[string]string) ([]client.Object, error) {
+	r.component.Labels = r.ResourceLabels()
+	tmplVars := map[string]string{
+		"metadata.name":     r.ResourceName(""),
+		"spec.runtime.port": fmt.Sprintf("%d", tmpl.Spec.Runtime.Port),
+	}
+	maps.Copy(tmplVars, variables)
+	maps.Copy(tmplVars, r.component.Spec.Variables)
+	err := r.reconcileService(ctx, tmpl)
+	if err != nil {
+		return nil, fmt.Errorf("Failed to reconcile service: %w", err)
+	}
+	err = r.reconcileDeployment(ctx, tmpl, tmplVars)
+	if err != nil {
+		return nil, fmt.Errorf("Failed to reconcile deployment: %w", err)
+	}
+	return nil, nil
+}
+
+func (r *DefaultComponentReconciler) ComponentAvailable(ctx context.Context) bool {
+	endpoints := &discoveryv1.EndpointSliceList{}
+	err := r.client.List(ctx, endpoints, client.InNamespace(r.component.GetNamespace()), client.MatchingLabels{"kubernetes.io/service-name": r.component.Name})
+	if err != nil {
+		return false
+	}
+	for _, endpoint := range endpoints.Items {
+		if len(endpoint.Endpoints) > 0 {
+			return true
+		}
+	}
+	return false
+}
+
+func (r *DefaultComponentReconciler) ResourceName(name string) string {
+	if name == "" {
+		return r.component.GetName()
+	}
+	return fmt.Sprintf("%s-%s", r.component.GetName(), name)
+}
+
+func (r *DefaultComponentReconciler) ResourceLabels() map[string]string {
+	return ResourceLabels(r.component.Spec.Category, r.ResourceName(""), "")
+}
+
+func (r *DefaultComponentReconciler) ResourceLabelsWithNamespace() map[string]string {
+	return ResourceLabels(r.component.Spec.Category, r.ResourceName(""), r.component.GetNamespace())
+}
+
+func (r *DefaultComponentReconciler) reconcileService(ctx context.Context, tmpl *v1alpha1.Template) error {
+	labels := r.ResourceLabels()
+	svc := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName(""), Namespace: r.component.GetNamespace()}}
+	_, err := controllerutil.CreateOrUpdate(ctx, r.client, svc, func() error {
+		svc.Labels = labels
+		svc.Spec.Type = corev1.ServiceTypeLoadBalancer
+		svc.Spec.Selector = labels
+		svc.Spec.Ports = []corev1.ServicePort{{
+			Port:       tmpl.Spec.Runtime.Port,
+			TargetPort: intstr.FromInt32(tmpl.Spec.Runtime.Port),
+			Protocol:   corev1.ProtocolTCP,
+		}}
+		return controllerutil.SetControllerReference(r.component, svc, r.scheme)
+	})
+	return err
+}
+
+func (r *DefaultComponentReconciler) reconcileDeployment(ctx context.Context, tmpl *v1alpha1.Template, variables map[string]string) error {
+	podSpec, err := buildPodSpec(variables, tmpl)
+	if err != nil {
+		return fmt.Errorf("Failed to build pod spec: %w", err)
+	}
+
+	labels := r.ResourceLabels()
+	maps.Copy(labels, tmpl.Metadata.Labels)
+	replicas := int32(1)
+
+	dep := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName(""), Namespace: r.component.GetNamespace()}}
+	_, err = controllerutil.CreateOrUpdate(ctx, r.client, dep, func() error {
+		dep.Labels = labels
+		dep.Spec.Replicas = &replicas
+		dep.Spec.Selector = &metav1.LabelSelector{MatchLabels: labels}
+		dep.Spec.Template = corev1.PodTemplateSpec{
+			ObjectMeta: metav1.ObjectMeta{
+				Labels:      labels,
+				Annotations: tmpl.Metadata.Annotations,
+			},
+			Spec: podSpec,
+		}
+		return controllerutil.SetControllerReference(r.component, dep, r.scheme)
+	})
+	return err
+}
+
+func (r *DefaultComponentReconciler) ReconcileStorage(ctx context.Context, name, size string) error {
+	pvName, err := r.reconcilePersistentVolume(ctx, name, size)
+	if err != nil {
+		return fmt.Errorf("Failed to reconcile persistent volume: %w", err)
+	}
+	if err := r.reconcilePersistentVolumeClaim(ctx, pvName, name, size); err != nil {
+		return fmt.Errorf("Failed to reconcile persistent volume claim: %w", err)
+	}
+	return nil
+}
+
+func (r *DefaultComponentReconciler) reconcilePersistentVolume(ctx context.Context, name, size string) (string, error) {
+	pvName := fmt.Sprintf("%s-%s", r.component.GetNamespace(), r.ResourceName(name))
+	pv := &corev1.PersistentVolume{ObjectMeta: metav1.ObjectMeta{Name: pvName}}
+	storage := resource.MustParse(size)
+	hostPathType := corev1.HostPathDirectoryOrCreate
+	hostPath := path.Join("/var/lib/locostack", r.component.GetNamespace(), r.ResourceName(""))
+
+	_, err := controllerutil.CreateOrUpdate(ctx, r.client, pv, func() error {
+		if pv.Labels == nil {
+			pv.Labels = map[string]string{}
+		}
+		maps.Copy(pv.Labels, r.ResourceLabelsWithNamespace())
+		pv.Spec.Capacity = corev1.ResourceList{corev1.ResourceStorage: storage}
+		pv.Spec.AccessModes = []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}
+		pv.Spec.PersistentVolumeReclaimPolicy = corev1.PersistentVolumeReclaimRetain
+		pv.Spec.StorageClassName = ""
+		pv.Spec.VolumeMode = nil
+		pv.Spec.PersistentVolumeSource = corev1.PersistentVolumeSource{
+			HostPath: &corev1.HostPathVolumeSource{
+				Path: hostPath,
+				Type: &hostPathType,
+			},
+		}
+		return nil
+	})
+	return pvName, err
+}
+
+func (r *DefaultComponentReconciler) reconcilePersistentVolumeClaim(ctx context.Context, pvName, name, size string) error {
+	pvc := &corev1.PersistentVolumeClaim{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName(name), Namespace: r.component.GetNamespace()}}
+	storage := resource.MustParse(size)
+	emptyStorageClass := ""
+
+	_, err := controllerutil.CreateOrUpdate(ctx, r.client, pvc, func() error {
+		if pvc.Labels == nil {
+			pvc.Labels = map[string]string{}
+		}
+		maps.Copy(pvc.Labels, r.ResourceLabels())
+
+		pvc.Spec.AccessModes = []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}
+		pvc.Spec.VolumeName = pvName
+		pvc.Spec.StorageClassName = &emptyStorageClass
+		if pvc.Spec.Resources.Requests == nil {
+			pvc.Spec.Resources.Requests = corev1.ResourceList{}
+		}
+		pvc.Spec.Resources.Requests[corev1.ResourceStorage] = storage
+		return nil
+	})
+	return err
+}
+
+func (r *DefaultComponentReconciler) ReconcileKey(ctx context.Context, name, key string, keygen func() (string, error)) error {
+	secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName(name), Namespace: r.component.GetNamespace()}}
+	_, err := controllerutil.CreateOrUpdate(ctx, r.client, secret, func() error {
+		if secret.Labels == nil {
+			secret.Labels = map[string]string{}
+		}
+		maps.Copy(secret.Labels, r.ResourceLabels())
+		if secret.Data == nil {
+			value, err := keygen()
+			if err != nil {
+				return err
+			}
+			secret.Data = map[string][]byte{key: []byte(value)}
+		}
+		return controllerutil.SetControllerReference(r.component, secret, r.scheme)
+	})
+	return err
+}
+
+func (r *DefaultComponentReconciler) ReconcileConfig(ctx context.Context, key string, config map[string]string, isSecret bool) (string, error) {
+	if err := r.reconcileEnvVarConfig(ctx, config, isSecret); err != nil {
+		return "", fmt.Errorf("Failed to reconcile env var config map: %w", err)
+	}
+	configHash, err := r.reconcileFileConfig(ctx, key, config, isSecret)
+	if err != nil {
+		return "", fmt.Errorf("Failed to reconcile file config map: %w", err)
+	}
+	return configHash, nil
+}
+
+func (r *DefaultComponentReconciler) reconcileEnvVarConfig(ctx context.Context, config map[string]string, isSecret bool) error {
+	configData := make(map[string]string)
+	for k, v := range config {
+		envVarName := strings.NewReplacer("-", "_", ".", "_").Replace(k)
+		configData[strings.ToUpper(envVarName)] = v
+	}
+	var obj client.Object
+	if isSecret {
+		obj = &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName("env"), Namespace: r.component.GetNamespace()}}
+	} else {
+		obj = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName("env"), Namespace: r.component.GetNamespace()}}
+	}
+	_, err := controllerutil.CreateOrUpdate(ctx, r.client, obj, func() error {
+		if obj.GetLabels() == nil {
+			obj.SetLabels(map[string]string{})
+		}
+		maps.Copy(obj.GetLabels(), r.ResourceLabels())
+		switch o := obj.(type) {
+		case *corev1.ConfigMap:
+			o.Data = configData
+		case *corev1.Secret:
+			o.Data = make(map[string][]byte)
+			for k, v := range configData {
+				o.Data[k] = []byte(v)
+			}
+		}
+		return controllerutil.SetControllerReference(r.component, obj, r.scheme)
+	})
+	return err
+}
+
+func (r *DefaultComponentReconciler) reconcileFileConfig(ctx context.Context, key string, config map[string]string, isSecret bool) (string, error) {
+	yamlConfig, err := yaml.Marshal(config)
+	if err != nil {
+		return "", fmt.Errorf("Failed to marshal config to yaml: %w", err)
+	}
+	h := sha256.Sum256(yamlConfig)
+	configHash := hex.EncodeToString(h[:])
+	var obj client.Object
+	if isSecret {
+		obj = &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName("config"), Namespace: r.component.GetNamespace()}}
+	} else {
+		obj = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: r.ResourceName("config"), Namespace: r.component.GetNamespace()}}
+	}
+	_, err = controllerutil.CreateOrUpdate(ctx, r.client, obj, func() error {
+		if obj.GetLabels() == nil {
+			obj.SetLabels(map[string]string{})
+		}
+		maps.Copy(obj.GetLabels(), r.ResourceLabels())
+		if obj.GetAnnotations() == nil {
+			obj.SetAnnotations(map[string]string{})
+		}
+		obj.GetAnnotations()["hash"] = configHash
+		switch o := obj.(type) {
+		case *corev1.ConfigMap:
+			o.Data = map[string]string{
+				key: string(yamlConfig),
+			}
+		case *corev1.Secret:
+			o.Data = map[string][]byte{
+				key: yamlConfig,
+			}
+		}
+		return controllerutil.SetControllerReference(r.component, obj, r.scheme)
+	})
+	return configHash, err
+}