diff --git a/api/crds/manifests/openmcp.cloud_clusterproviders.yaml b/api/crds/manifests/openmcp.cloud_clusterproviders.yaml index d95a13f..446a013 100644 --- a/api/crds/manifests/openmcp.cloud_clusterproviders.yaml +++ b/api/crds/manifests/openmcp.cloud_clusterproviders.yaml @@ -39,7 +39,7 @@ spec: metadata: type: object spec: - description: DeploymentSpec defines the desired state of a provider. + description: ClusterProviderSpec defines the desired state of ClusterProvider. properties: image: description: Image is the name of the image of a provider. diff --git a/api/crds/manifests/openmcp.cloud_platformservices.yaml b/api/crds/manifests/openmcp.cloud_platformservices.yaml index 2992fd9..1691f7d 100644 --- a/api/crds/manifests/openmcp.cloud_platformservices.yaml +++ b/api/crds/manifests/openmcp.cloud_platformservices.yaml @@ -39,7 +39,7 @@ spec: metadata: type: object spec: - description: DeploymentSpec defines the desired state of a provider. + description: PlatformServiceSpec defines the desired state of PlatformService. properties: image: description: Image is the name of the image of a provider. diff --git a/api/crds/manifests/openmcp.cloud_serviceproviders.yaml b/api/crds/manifests/openmcp.cloud_serviceproviders.yaml index ccef419..cea54f6 100644 --- a/api/crds/manifests/openmcp.cloud_serviceproviders.yaml +++ b/api/crds/manifests/openmcp.cloud_serviceproviders.yaml @@ -39,7 +39,7 @@ spec: metadata: type: object spec: - description: DeploymentSpec defines the desired state of a provider. + description: ServiceProviderSpec defines the desired state of ServiceProvider. properties: image: description: Image is the name of the image of a provider. diff --git a/api/provider/v1alpha1/clusterprovider_types.go b/api/provider/v1alpha1/clusterprovider_types.go index 513f99b..6a05d9f 100644 --- a/api/provider/v1alpha1/clusterprovider_types.go +++ b/api/provider/v1alpha1/clusterprovider_types.go @@ -26,11 +26,7 @@ import ( // ClusterProviderSpec defines the desired state of ClusterProvider. type ClusterProviderSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - - // Foo is an example field of ClusterProvider. Edit clusterprovider_types.go to remove/update - Foo string `json:"foo,omitempty"` + DeploymentSpec `json:",inline"` } // ClusterProviderStatus defines the observed state of ClusterProvider. @@ -48,8 +44,8 @@ type ClusterProviderStatus struct { type ClusterProvider struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec DeploymentSpec `json:"spec,omitempty"` - Status DeploymentStatus `json:"status,omitempty"` + Spec ClusterProviderSpec `json:"spec,omitempty"` + Status DeploymentStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true diff --git a/api/provider/v1alpha1/platformservice_types.go b/api/provider/v1alpha1/platformservice_types.go index f2b8c0f..cdabae6 100644 --- a/api/provider/v1alpha1/platformservice_types.go +++ b/api/provider/v1alpha1/platformservice_types.go @@ -26,11 +26,7 @@ import ( // PlatformServiceSpec defines the desired state of PlatformService. type PlatformServiceSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - - // Foo is an example field of PlatformService. Edit platformservice_types.go to remove/update - Foo string `json:"foo,omitempty"` + DeploymentSpec `json:",inline"` } // PlatformServiceStatus defines the observed state of PlatformService. @@ -48,8 +44,8 @@ type PlatformServiceStatus struct { type PlatformService struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec DeploymentSpec `json:"spec,omitempty"` - Status DeploymentStatus `json:"status,omitempty"` + Spec PlatformServiceSpec `json:"spec,omitempty"` + Status DeploymentStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true diff --git a/api/provider/v1alpha1/serviceprovider_types.go b/api/provider/v1alpha1/serviceprovider_types.go index 09d2be6..54a43d0 100644 --- a/api/provider/v1alpha1/serviceprovider_types.go +++ b/api/provider/v1alpha1/serviceprovider_types.go @@ -26,11 +26,7 @@ import ( // ServiceProviderSpec defines the desired state of ServiceProvider. type ServiceProviderSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - - // Foo is an example field of ServiceProvider. Edit serviceprovider_types.go to remove/update - Foo string `json:"foo,omitempty"` + DeploymentSpec `json:",inline"` } // ServiceProviderStatus defines the observed state of ServiceProvider. @@ -48,8 +44,8 @@ type ServiceProviderStatus struct { type ServiceProvider struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec DeploymentSpec `json:"spec,omitempty"` - Status DeploymentStatus `json:"status,omitempty"` + Spec ServiceProviderSpec `json:"spec,omitempty"` + Status DeploymentStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true diff --git a/api/provider/v1alpha1/zz_generated.deepcopy.go b/api/provider/v1alpha1/zz_generated.deepcopy.go index cec1b18..0bc7f43 100644 --- a/api/provider/v1alpha1/zz_generated.deepcopy.go +++ b/api/provider/v1alpha1/zz_generated.deepcopy.go @@ -71,6 +71,7 @@ func (in *ClusterProviderList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterProviderSpec) DeepCopyInto(out *ClusterProviderSpec) { *out = *in + in.DeploymentSpec.DeepCopyInto(&out.DeploymentSpec) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterProviderSpec. @@ -217,6 +218,7 @@ func (in *PlatformServiceList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PlatformServiceSpec) DeepCopyInto(out *PlatformServiceSpec) { *out = *in + in.DeploymentSpec.DeepCopyInto(&out.DeploymentSpec) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformServiceSpec. @@ -306,6 +308,7 @@ func (in *ServiceProviderList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceProviderSpec) DeepCopyInto(out *ServiceProviderSpec) { *out = *in + in.DeploymentSpec.DeepCopyInto(&out.DeploymentSpec) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceProviderSpec. diff --git a/cmd/openmcp-operator/app/run.go b/cmd/openmcp-operator/app/run.go index fa754e2..a2c1a32 100644 --- a/cmd/openmcp-operator/app/run.go +++ b/cmd/openmcp-operator/app/run.go @@ -5,13 +5,16 @@ import ( "crypto/tls" "fmt" "path/filepath" + "slices" + "strings" "github.com/openmcp-project/controller-utils/pkg/logging" - "github.com/spf13/cobra" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/clientcmd/api" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/certwatcher" "sigs.k8s.io/controller-runtime/pkg/healthz" @@ -22,6 +25,7 @@ import ( "github.com/openmcp-project/openmcp-operator/api/install" "github.com/openmcp-project/openmcp-operator/api/provider/v1alpha1" + "github.com/openmcp-project/openmcp-operator/internal/controllers/provider" ) var setupLog logging.Logger @@ -287,6 +291,16 @@ func (o *RunOptions) Run(ctx context.Context) error { // } // } + // setup deployment controller + if slices.Contains(o.Controllers, strings.ToLower("deploymentcontroller")) { + utilruntime.Must(clientgoscheme.AddToScheme(mgr.GetScheme())) + utilruntime.Must(api.AddToScheme(mgr.GetScheme())) + + if err = provider.NewDeploymentController().SetupWithManager(mgr, o.ProviderGVKList); err != nil { + return fmt.Errorf("unable to setup provider controllers: %w", err) + } + } + if o.MetricsCertWatcher != nil { setupLog.Info("Adding metrics certificate watcher to manager") if err := mgr.Add(o.MetricsCertWatcher); err != nil { diff --git a/go.mod b/go.mod index 78945d7..2f104c2 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,8 @@ require ( k8s.io/api v0.33.0 k8s.io/apiextensions-apiserver v0.33.0 k8s.io/apimachinery v0.33.0 + k8s.io/client-go v0.33.0 + k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 sigs.k8s.io/controller-runtime v0.20.4 sigs.k8s.io/yaml v1.4.0 ) @@ -93,11 +95,9 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiserver v0.33.0 // indirect - k8s.io/client-go v0.33.0 // indirect k8s.io/component-base v0.33.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/gateway-api v1.3.0 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect diff --git a/internal/controllers/provider/controller.go b/internal/controllers/provider/controller.go index 34037dc..2bacf9b 100644 --- a/internal/controllers/provider/controller.go +++ b/internal/controllers/provider/controller.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/openmcp-project/controller-utils/pkg/controller" "github.com/openmcp-project/controller-utils/pkg/logging" apimeta "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -21,7 +22,6 @@ import ( ) const ( - openmcpNamespace = "openmcp-system" openmcpDomain = "openmcp.cloud" openmcpFinalizer = openmcpDomain + "/finalizer" openmcpOperationAnnotation = openmcpDomain + "/operation" @@ -41,27 +41,6 @@ func (r *ProviderReconciler) ControllerName() string { return r.GroupVersionKind.String() } -// HandleJob creates a reconcile request for the provider associated with the job. -func (r *ProviderReconciler) HandleJob(_ context.Context, job client.Object) []reconcile.Request { - providerKind, found := job.GetLabels()[install.ProviderKindLabel] - if !found || providerKind != r.GroupVersionKind.Kind { - return nil - } - - providerName, found := job.GetLabels()[install.ProviderNameLabel] - if !found { - return nil - } - - return []reconcile.Request{ - { - NamespacedName: client.ObjectKey{ - Name: providerName, - }, - }, - } -} - func (r *ProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) { log := logging.FromContextOrPanic(ctx).WithName(r.ControllerName()) ctx = logging.NewContext(ctx, log) @@ -79,7 +58,11 @@ func (r *ProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r if provider.GetDeletionTimestamp().IsZero() { res, err = r.handleCreateUpdateOperation(ctx, provider) } else { - res, err = r.handleDeleteOperation(ctx, provider) + deleted := false + deleted, res, err = r.handleDeleteOperation(ctx, provider) + if deleted { + return ctrl.Result{}, nil + } } // patch the status @@ -88,6 +71,29 @@ func (r *ProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r return res, err } +// HandleJob - the ProviderReconciler reconciles the provider resources (ClusterProviders, ServiceProviders, PlatformServices). +// During a reconcile, the ProviderReconciler creates the init job of the provider and must wait for it to complete. +// Therefore, the ProviderReconciler watches also jobs. The present method handles the job events and creates a reconcile request. +func (r *ProviderReconciler) HandleJob(_ context.Context, job client.Object) []reconcile.Request { + providerKind, found := job.GetLabels()[install.ProviderKindLabel] + if !found || providerKind != r.GroupVersionKind.Kind { + return nil + } + + providerName, found := job.GetLabels()[install.ProviderNameLabel] + if !found { + return nil + } + + return []reconcile.Request{ + { + NamespacedName: client.ObjectKey{ + Name: providerName, + }, + }, + } +} + func (r *ProviderReconciler) handleCreateUpdateOperation(ctx context.Context, provider *unstructured.Unstructured) (ctrl.Result, error) { log := logging.FromContextOrPanic(ctx) @@ -107,8 +113,10 @@ func (r *ProviderReconciler) handleCreateUpdateOperation(ctx context.Context, pr // If the provider is not yet in phase ready, we continue with the installation. // If the provider is already in phase ready, we install it again if the generation has changed or if it has a reconcile annotation. - // TODO: checkReconcileAnnotation + if err := r.checkReconcileAnnotation(ctx, provider, deploymentStatus); err != nil { + return reconcile.Result{}, err + } r.observeGeneration(provider, deploymentStatus) if deploymentStatus.Phase == phaseReady { @@ -136,24 +144,20 @@ func (r *ProviderReconciler) install( PlatformClient: r.PlatformClient, Provider: provider, DeploymentSpec: deploymentSpec, - Namespace: openmcpNamespace, } if !isInitialized(deploymentStatus) { log.Debug("installing init job") - _, err := installer.InstallInitJob(ctx) + completed, err := installer.InstallInitJob(ctx) if err != nil { log.Error(err, "failed to install init job") apimeta.SetStatusCondition(&deploymentStatus.Conditions, conditionInitJobCreationFailed(provider, err)) return reconcile.Result{}, err } - - log.Debug("checking init job readiness") - if readinessCheckResult := installer.CheckInitJobReadiness(ctx); !readinessCheckResult.IsReady() { - log.Debug("init job has not yet finished") - apimeta.SetStatusCondition(&deploymentStatus.Conditions, conditionInitRunning(provider, readinessCheckResult.Message())) - // TODO: RequeueAfter is unnecessary, if we watch jobs - return reconcile.Result{RequeueAfter: 30 * time.Second}, nil + if !completed { + log.Debug("init job has not yet completed") + apimeta.SetStatusCondition(&deploymentStatus.Conditions, conditionInitRunning(provider, "init job has not yet completed")) + return reconcile.Result{}, nil } log.Debug("init job completed") @@ -183,11 +187,11 @@ func (r *ProviderReconciler) install( return reconcile.Result{}, nil } -func (r *ProviderReconciler) handleDeleteOperation(ctx context.Context, provider *unstructured.Unstructured) (ctrl.Result, error) { +func (r *ProviderReconciler) handleDeleteOperation(ctx context.Context, provider *unstructured.Unstructured) (deleted bool, res ctrl.Result, err error) { deploymentStatus, err := r.deploymentStatusFromUnstructured(provider) if err != nil { - return reconcile.Result{}, err + return false, reconcile.Result{}, err } installer := install.Installer{ @@ -198,12 +202,50 @@ func (r *ProviderReconciler) handleDeleteOperation(ctx context.Context, provider deploymentStatus.Phase = phaseTerminating - err = installer.UninstallProvider(ctx) + deleted, err = installer.UninstallProvider(ctx) + if err != nil { + conversionErr := r.deploymentStatusIntoUnstructured(deploymentStatus, provider) + err = errors.Join(err, conversionErr) + return false, reconcile.Result{}, err - conversionErr := r.deploymentStatusIntoUnstructured(deploymentStatus, provider) - err = errors.Join(err, conversionErr) + } + + if !deleted { + return false, reconcile.Result{Requeue: true}, nil + } + + if err = r.removeFinalizer(ctx, provider); err != nil { + return false, reconcile.Result{}, err + } + + return true, reconcile.Result{}, nil +} - return ctrl.Result{}, err +func (r *ProviderReconciler) checkReconcileAnnotation( + ctx context.Context, + provider *unstructured.Unstructured, + deploymentStatus *v1alpha1.DeploymentStatus, +) error { + if controller.HasAnnotationWithValue(provider, openmcpOperationAnnotation, operationReconcile) && deploymentStatus.Phase == phaseReady { + log := logging.FromContextOrPanic(ctx) + deploymentStatus.Phase = phaseProgressing + + if err := r.deploymentStatusIntoUnstructured(deploymentStatus, provider); err != nil { + log.Error(err, "failed to handle reconcile annotation") + return fmt.Errorf("failed to handle reconcile annotation: %w", err) + } + + if err := r.PlatformClient.Status().Update(ctx, provider); err != nil { + log.Error(err, "failed to handle reconcile annotation: unable to change phase of provider to progressing") + return fmt.Errorf("failed to handle reconcile annotation: unable to change phase of provider %s/%s to progressing: %w", provider.GetNamespace(), provider.GetName(), err) + } + + if err := controller.EnsureAnnotation(ctx, r.PlatformClient, provider, openmcpOperationAnnotation, operationReconcile, true, controller.DELETE); err != nil { + log.Error(err, "failed to remove reconcile annotation from provider") + return fmt.Errorf("failed to remove reconcile annotation from provider %s/%s: %w", provider.GetNamespace(), provider.GetName(), err) + } + } + return nil } func (r *ProviderReconciler) ensureFinalizer(ctx context.Context, provider *unstructured.Unstructured) error { @@ -218,6 +260,18 @@ func (r *ProviderReconciler) ensureFinalizer(ctx context.Context, provider *unst return nil } +func (r *ProviderReconciler) removeFinalizer(ctx context.Context, provider *unstructured.Unstructured) error { + if controllerutil.ContainsFinalizer(provider, openmcpFinalizer) { + controllerutil.RemoveFinalizer(provider, openmcpFinalizer) + if err := r.PlatformClient.Update(ctx, provider); err != nil { + log := logging.FromContextOrPanic(ctx) + log.Error(err, "failed to remove finalizer from provider") + return fmt.Errorf("failed to remove finalizer from provider %s: %w", provider.GetName(), err) + } + } + return nil +} + func (r *ProviderReconciler) observeGeneration(provider *unstructured.Unstructured, status *v1alpha1.DeploymentStatus) { gen := provider.GetGeneration() if status.ObservedGeneration != gen { diff --git a/internal/controllers/provider/controller_list.go b/internal/controllers/provider/deployment_controller.go similarity index 82% rename from internal/controllers/provider/controller_list.go rename to internal/controllers/provider/deployment_controller.go index c8b0ad9..cddecef 100644 --- a/internal/controllers/provider/controller_list.go +++ b/internal/controllers/provider/deployment_controller.go @@ -20,25 +20,27 @@ import ( "github.com/openmcp-project/controller-utils/pkg/controller" v1 "k8s.io/api/batch/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/handler" "github.com/openmcp-project/openmcp-operator/internal/controllers/provider/install" ) -type ProviderReconcilerList struct { - PlatformClient client.Client - Scheme *runtime.Scheme - Reconcilers []*ProviderReconciler +// DeploymentController is not a controller, but a collection of controllers reconciling +// ClusterProviders, ServiceProviders, and PlatformServices. +type DeploymentController struct { + Reconcilers []*ProviderReconciler +} + +func NewDeploymentController() *DeploymentController { + return &DeploymentController{} } // SetupWithManager sets up the controllers with the Manager. -func (r *ProviderReconcilerList) SetupWithManager(mgr ctrl.Manager, providerGKVList []schema.GroupVersionKind) error { +func (r *DeploymentController) SetupWithManager(mgr ctrl.Manager, providerGKVList []schema.GroupVersionKind) error { allErrs := field.ErrorList{} r.Reconcilers = make([]*ProviderReconciler, len(providerGKVList)) diff --git a/internal/controllers/provider/install/deployment.go b/internal/controllers/provider/install/deployment.go new file mode 100644 index 0000000..691439a --- /dev/null +++ b/internal/controllers/provider/install/deployment.go @@ -0,0 +1,76 @@ +package install + +import ( + "fmt" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openmcp-project/controller-utils/pkg/resources" +) + +const ( + deploymentSelectorLabelKey = "app" + deploymentSelectorLabelValue = "openmcp.cloud/deployment" +) + +type deploymentMutator struct { + values *Values + meta resources.Mutator[client.Object] +} + +var _ resources.Mutator[*appsv1.Deployment] = &deploymentMutator{} + +func newDeploymentMutator(values *Values) resources.Mutator[*appsv1.Deployment] { + return &deploymentMutator{ + values: values, + meta: resources.NewMetadataMutator(values.LabelsController(), nil), + } +} + +func (m *deploymentMutator) String() string { + return fmt.Sprintf("deployment %s/%s", m.values.Namespace(), m.values.NamespacedDefaultResourceName()) +} + +func (m *deploymentMutator) Empty() *appsv1.Deployment { + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: m.values.NamespacedDefaultResourceName(), + Namespace: m.values.Namespace(), + }, + } +} + +func (m *deploymentMutator) Mutate(r *appsv1.Deployment) error { + r.Spec = appsv1.DeploymentSpec{ + Replicas: ptr.To[int32](1), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + deploymentSelectorLabelKey: deploymentSelectorLabelValue, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: m.values.LabelsController(), + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: m.values.NamespacedDefaultResourceName(), + Image: m.values.Image(), + ImagePullPolicy: corev1.PullIfNotPresent, + Args: []string{ + "run", + }, + }, + }, + ImagePullSecrets: m.values.ImagePullSecrets(), + ServiceAccountName: m.values.NamespacedDefaultResourceName(), + }, + }, + } + return m.meta.Mutate(r) +} diff --git a/internal/controllers/provider/install/installer.go b/internal/controllers/provider/install/installer.go index 92d5001..50bbf8e 100644 --- a/internal/controllers/provider/install/installer.go +++ b/internal/controllers/provider/install/installer.go @@ -7,7 +7,7 @@ import ( "github.com/openmcp-project/controller-utils/pkg/logging" "github.com/openmcp-project/controller-utils/pkg/readiness" - "github.com/openmcp-project/controller-utils/pkg/resources" + . "github.com/openmcp-project/controller-utils/pkg/resources" v1 "k8s.io/api/batch/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -26,7 +26,6 @@ type Installer struct { PlatformClient client.Client Provider *unstructured.Unstructured DeploymentSpec *v1alpha1.DeploymentSpec - Namespace string } // InstallInitJob installs the init job of a provider. @@ -35,13 +34,26 @@ type Installer struct { // Adds provider generation as annotation to the job. func (a *Installer) InstallInitJob(ctx context.Context) (completed bool, err error) { - // Create namespace if it does not exist - n := resources.NewNamespaceMutator(a.Namespace, nil, nil) - if err := resources.CreateOrUpdateResource(ctx, a.PlatformClient, n); err != nil { - return false, fmt.Errorf("failed to create namespace %s: %w", a.Namespace, err) + values := NewValues(a.Provider, a.DeploymentSpec) + + if err := CreateOrUpdateResource(ctx, a.PlatformClient, NewNamespaceMutator(values.Namespace(), nil, nil)); err != nil { + return false, err + } + + if err = CreateOrUpdateResource(ctx, a.PlatformClient, newInitServiceAccountMutator(values)); err != nil { + return false, err + } + + if err = CreateOrUpdateResource(ctx, a.PlatformClient, newInitClusterRoleMutator(values)); err != nil { + return false, err } - j := newJobMutator(a.Provider.GetName(), a.Namespace, a.DeploymentSpec, nil, map[string]string{ + if err = CreateOrUpdateResource(ctx, a.PlatformClient, newInitClusterRoleBindingMutator(values)); err != nil { + return false, err + } + + // Create job + j := newJobMutator(values, a.DeploymentSpec, map[string]string{ ProviderKindLabel: a.Provider.GetKind(), ProviderNameLabel: a.Provider.GetName(), ProviderGenerationLabel: fmt.Sprintf("%d", a.Provider.GetGeneration()), @@ -52,34 +64,49 @@ func (a *Installer) InstallInitJob(ctx context.Context) (completed bool, err err if apierrors.IsNotFound(err) { found = false } else { - return false, fmt.Errorf("failed to get job %s/%s: %w", a.Namespace, a.Provider.GetName(), err) + return false, fmt.Errorf("failed to get job %s/%s: %w", values.Namespace(), a.Provider.GetName(), err) } } - if found { - // Delete and re-create job if it has an old provider generation or is failed. - if !a.isGenerationUpToDate(ctx, job) || a.isJobFailed(job) { - if err := resources.DeleteResource(ctx, a.PlatformClient, j); err != nil { - return false, fmt.Errorf("failed to delete job %s/%s: %w", a.Namespace, a.Provider.GetName(), err) - } - if err := resources.CreateOrUpdateResource(ctx, a.PlatformClient, j); err != nil { - return false, fmt.Errorf("failed to re-create job %s/%s: %w", a.Namespace, a.Provider.GetName(), err) - } + if !found { + // Job does not exist, create it + if err := CreateOrUpdateResource(ctx, a.PlatformClient, j); err != nil { + return false, fmt.Errorf("failed to create job %s/%s: %w", values.Namespace(), a.Provider.GetName(), err) } - } else { - if err := resources.CreateOrUpdateResource(ctx, a.PlatformClient, j); err != nil { - return false, fmt.Errorf("failed to create job %s/%s: %w", a.Namespace, a.Provider.GetName(), err) - } - } + return false, nil - return true, nil -} + } else if !a.isGenerationUpToDate(ctx, job) || a.isJobFailed(job) { + // Job exists, but needs to be deleted and re-created + if err := DeleteResource(ctx, a.PlatformClient, j); err != nil { + return false, fmt.Errorf("failed to delete job %s/%s: %w", values.Namespace(), a.Provider.GetName(), err) + } + if err := CreateOrUpdateResource(ctx, a.PlatformClient, j); err != nil { + return false, fmt.Errorf("failed to re-create job %s/%s: %w", values.Namespace(), a.Provider.GetName(), err) + } + return false, nil -func (a *Installer) CheckInitJobReadiness(ctx context.Context) readiness.CheckResult { - return nil + } else { + // Job exists, check completion + return job.Status.Succeeded > 0, nil + } } func (a *Installer) InstallProvider(ctx context.Context) error { + + values := NewValues(a.Provider, a.DeploymentSpec) + + if err := CreateOrUpdateResource(ctx, a.PlatformClient, newProviderServiceAccountMutator(values)); err != nil { + return err + } + + if err := CreateOrUpdateResource(ctx, a.PlatformClient, newProviderClusterRoleBindingMutator(values)); err != nil { + return err + } + + if err := CreateOrUpdateResource(ctx, a.PlatformClient, newDeploymentMutator(values)); err != nil { + return err + } + return nil } @@ -87,8 +114,27 @@ func (a *Installer) CheckProviderReadiness(ctx context.Context) readiness.CheckR return nil } -func (a *Installer) UninstallProvider(ctx context.Context) error { - return nil +func (a *Installer) UninstallProvider(ctx context.Context) (deleted bool, err error) { + + values := NewValues(a.Provider, a.DeploymentSpec) + + if err := DeleteResource(ctx, a.PlatformClient, newJobMutator(values, a.DeploymentSpec, nil)); err != nil { + return false, err + } + + if err := DeleteResource(ctx, a.PlatformClient, newInitClusterRoleBindingMutator(values)); err != nil { + return false, err + } + + if err := DeleteResource(ctx, a.PlatformClient, newInitClusterRoleMutator(values)); err != nil { + return false, err + } + + if err := DeleteResource(ctx, a.PlatformClient, newInitServiceAccountMutator(values)); err != nil { + return false, err + } + + return true, nil } func (a *Installer) isJobFailed(job *v1.Job) bool { diff --git a/internal/controllers/provider/install/job.go b/internal/controllers/provider/install/job.go index 8bd55fd..a874009 100644 --- a/internal/controllers/provider/install/job.go +++ b/internal/controllers/provider/install/job.go @@ -13,8 +13,7 @@ import ( ) type jobMutator struct { - name string - namespace string + values *Values deploymentSpec *v1alpha1.DeploymentSpec meta resources.Mutator[client.Object] } @@ -22,22 +21,19 @@ type jobMutator struct { var _ resources.Mutator[*v1.Job] = &jobMutator{} func newJobMutator( - name string, - namespace string, + values *Values, deploymentSpec *v1alpha1.DeploymentSpec, - labels map[string]string, annotations map[string]string, ) resources.Mutator[*v1.Job] { return &jobMutator{ - name: name, - namespace: namespace, + values: values, deploymentSpec: deploymentSpec, - meta: resources.NewMetadataMutator(labels, annotations), + meta: resources.NewMetadataMutator(values.LabelsInitJob(), annotations), } } func (m *jobMutator) String() string { - return fmt.Sprintf("job %s/%s", m.namespace, m.name) + return fmt.Sprintf("job %s/%s", m.values.Namespace(), m.values.NamespacedResourceName(initPrefix)) } func (m *jobMutator) Empty() *v1.Job { @@ -47,8 +43,8 @@ func (m *jobMutator) Empty() *v1.Job { Kind: "Job", }, ObjectMeta: metav1.ObjectMeta{ - Name: m.name, - Namespace: m.namespace, + Name: m.values.NamespacedResourceName(initPrefix), + Namespace: m.values.Namespace(), }, } } @@ -57,35 +53,24 @@ func (m *jobMutator) Mutate(j *v1.Job) error { j.Spec = v1.JobSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Name: m.name, + Labels: m.values.LabelsInitJob(), }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "init", - Image: m.deploymentSpec.Image, + Image: m.values.Image(), ImagePullPolicy: corev1.PullIfNotPresent, - Command: []string{ - "/service-provider-landscaper", + Args: []string{ "init", - "--verbosity=info", }, }, }, - ImagePullSecrets: m.imagePullSecrets(), - RestartPolicy: corev1.RestartPolicyNever, + ServiceAccountName: m.values.NamespacedResourceName(initPrefix), + ImagePullSecrets: m.values.ImagePullSecrets(), + RestartPolicy: corev1.RestartPolicyNever, }, }, } return m.meta.Mutate(j) } - -func (m *jobMutator) imagePullSecrets() []corev1.LocalObjectReference { - secrets := make([]corev1.LocalObjectReference, len(m.deploymentSpec.ImagePullSecrets)) - for i, s := range m.deploymentSpec.ImagePullSecrets { - secrets[i] = corev1.LocalObjectReference{ - Name: s.Name, - } - } - return secrets -} diff --git a/internal/controllers/provider/install/rbac.go b/internal/controllers/provider/install/rbac.go new file mode 100644 index 0000000..5c328f5 --- /dev/null +++ b/internal/controllers/provider/install/rbac.go @@ -0,0 +1,30 @@ +package install + +import ( + "github.com/openmcp-project/controller-utils/pkg/resources" + corev1 "k8s.io/api/core/v1" + rbac "k8s.io/api/rbac/v1" +) + +func newProviderServiceAccountMutator(values *Values) resources.Mutator[*corev1.ServiceAccount] { + return resources.NewServiceAccountMutator( + values.NamespacedDefaultResourceName(), + values.Namespace(), + values.LabelsController(), + nil) +} + +func newProviderClusterRoleBindingMutator(values *Values) resources.Mutator[*rbac.ClusterRoleBinding] { + return resources.NewClusterRoleBindingMutator( + values.ClusterScopedDefaultResourceName(), + []rbac.Subject{ + { + Kind: rbac.ServiceAccountKind, + Name: values.NamespacedDefaultResourceName(), + Namespace: values.Namespace(), + }, + }, + resources.NewClusterRoleRef("cluster-admin"), + values.LabelsController(), + nil) +} diff --git a/internal/controllers/provider/install/rbac_init.go b/internal/controllers/provider/install/rbac_init.go new file mode 100644 index 0000000..4be1597 --- /dev/null +++ b/internal/controllers/provider/install/rbac_init.go @@ -0,0 +1,45 @@ +package install + +import ( + "github.com/openmcp-project/controller-utils/pkg/resources" + corev1 "k8s.io/api/core/v1" + rbac "k8s.io/api/rbac/v1" +) + +func newInitServiceAccountMutator(values *Values) resources.Mutator[*corev1.ServiceAccount] { + return resources.NewServiceAccountMutator( + values.NamespacedResourceName(initPrefix), + values.Namespace(), + values.LabelsInitJob(), + nil) +} + +func newInitClusterRoleBindingMutator(values *Values) resources.Mutator[*rbac.ClusterRoleBinding] { + clusterRoleName := values.ClusterScopedResourceName(initPrefix) + return resources.NewClusterRoleBindingMutator( + clusterRoleName, + []rbac.Subject{ + { + Kind: rbac.ServiceAccountKind, + Name: values.NamespacedResourceName(initPrefix), + Namespace: values.Namespace(), + }, + }, + resources.NewClusterRoleRef(clusterRoleName), + values.LabelsInitJob(), + nil) +} + +func newInitClusterRoleMutator(values *Values) resources.Mutator[*rbac.ClusterRole] { + return resources.NewClusterRoleMutator( + values.ClusterScopedResourceName(initPrefix), + []rbac.PolicyRule{ + { + APIGroups: []string{"apiextensions.k8s.io"}, + Resources: []string{"customresourcedefinitions"}, + Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"}, + }, + }, + values.LabelsInitJob(), + nil) +} diff --git a/internal/controllers/provider/install/values.go b/internal/controllers/provider/install/values.go new file mode 100644 index 0000000..4023390 --- /dev/null +++ b/internal/controllers/provider/install/values.go @@ -0,0 +1,94 @@ +package install + +import ( + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/openmcp-project/openmcp-operator/api/provider/v1alpha1" +) + +const initPrefix = "init" + +func NewValues(provider *unstructured.Unstructured, deploymentSpec *v1alpha1.DeploymentSpec) *Values { + var namespacePrefix string + switch provider.GroupVersionKind().Kind { + case "ServiceProvider": + namespacePrefix = "sp" + case "ClusterProvider": + namespacePrefix = "cp" + case "PlatformService": + namespacePrefix = "ps" + default: + namespacePrefix = strings.ToLower(provider.GroupVersionKind().Kind) + } + namespace := fmt.Sprintf("%s-%s", namespacePrefix, provider.GetName()) + + return &Values{ + provider: provider, + deploymentSpec: deploymentSpec, + namespace: namespace, + } +} + +type Values struct { + provider *unstructured.Unstructured + deploymentSpec *v1alpha1.DeploymentSpec + namespace string +} + +func (v *Values) Namespace() string { + return v.namespace +} + +func (v *Values) NamespacedDefaultResourceName() string { + return v.provider.GetName() +} + +func (v *Values) NamespacedResourceName(suffix string) string { + return fmt.Sprintf("%s-%s", v.provider.GetName(), suffix) +} + +func (v *Values) ClusterScopedDefaultResourceName() string { + return fmt.Sprintf("%s:%s", v.Namespace(), v.NamespacedDefaultResourceName()) +} + +func (v *Values) ClusterScopedResourceName(suffix string) string { + return fmt.Sprintf("%s:%s", v.Namespace(), v.NamespacedResourceName(suffix)) +} + +func (v *Values) Image() string { + return v.deploymentSpec.Image +} + +func (v *Values) ImagePullSecrets() []corev1.LocalObjectReference { + secrets := make([]corev1.LocalObjectReference, len(v.deploymentSpec.ImagePullSecrets)) + for i, s := range v.deploymentSpec.ImagePullSecrets { + secrets[i] = corev1.LocalObjectReference{ + Name: s.Name, + } + } + return secrets +} + +func (v *Values) LabelsCommon() map[string]string { + return map[string]string{ + "app.kubernetes.io/managed-by": "openmcp-operator", + "app.kubernetes.io/name": v.provider.GetKind(), + "app.kubernetes.io/instance": v.provider.GetName(), + } +} + +func (v *Values) LabelsInitJob() map[string]string { + m := v.LabelsCommon() + m["app.kubernetes.io/component"] = "init-job" + return m +} + +func (v *Values) LabelsController() map[string]string { + m := v.LabelsCommon() + m["app.kubernetes.io/component"] = "controller" + return m +}