From 104b77fafa0ed2cd31bfd2486959e77e05df74f6 Mon Sep 17 00:00:00 2001 From: Hardik Dodiya Date: Mon, 6 May 2024 12:29:49 +0000 Subject: [PATCH] Implement httpbootconfig controller --- api/v1alpha1/httpbootconfig_types.go | 12 +- cmd/main.go | 20 ++- .../boot.ironcore.dev_httpbootconfigs.yaml | 9 +- .../controller/httpbootconfig_controller.go | 123 ++++++++++++++++-- 4 files changed, 140 insertions(+), 24 deletions(-) diff --git a/api/v1alpha1/httpbootconfig_types.go b/api/v1alpha1/httpbootconfig_types.go index e1092050..d07702e6 100644 --- a/api/v1alpha1/httpbootconfig_types.go +++ b/api/v1alpha1/httpbootconfig_types.go @@ -16,19 +16,21 @@ type HTTPBootConfigSpec struct { // HTTPBootConfigStatus defines the observed state of HTTPBootConfig type HTTPBootConfigStatus struct { - State HTTPConfigState `json:"state,omitempty"` + State HTTPBootConfigState `json:"state,omitempty"` } -type HTTPConfigState string +type HTTPBootConfigState string const ( - HTTPConfigStateReady HTTPConfigState = "Ready" - HTTPConfigStatePending HTTPConfigState = "Pending" - HTTPConfigStateError HTTPConfigState = "Error" + HTTPBootConfigStateReady HTTPBootConfigState = "Ready" + HTTPBootConfigStatePending HTTPBootConfigState = "Pending" + HTTPBootConfigStateError HTTPBootConfigState = "Error" ) //+kubebuilder:object:root=true //+kubebuilder:subresource:status +//+kubebuilder:printcolumn:name="State",type=string,JSONPath=`.status.state` +//+kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` // HTTPBootConfig is the Schema for the httpbootconfigs API type HTTPBootConfig struct { diff --git a/cmd/main.go b/cmd/main.go index c9027196..dcc48f13 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -48,6 +48,7 @@ const ( // core controllers ipxeBootConfigController = "ipxebootconfig" serverBootConfigController = "serverbootconfig" + httpBootConfigController = "httpbootconfig" ) func init() { @@ -92,6 +93,7 @@ func main() { // core controllers ipxeBootConfigController, serverBootConfigController, + httpBootConfigController, ) flag.Var(controllers, "controllers", @@ -184,17 +186,21 @@ func main() { Scheme: mgr.GetScheme(), IPXEServiceURL: ipxeServiceURL, }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "ServerBootConfiguration") + setupLog.Error(err, "unable to create controller", "controller", "ServerBootConfig") os.Exit(1) } } - if err = (&controller.HTTPBootConfigReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "HTTPBootConfig") - os.Exit(1) + + if controllers.Enabled(httpBootConfigController) { + if err = (&controller.HTTPBootConfigReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "HTTPBootConfig") + os.Exit(1) + } } + //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/config/crd/bases/boot.ironcore.dev_httpbootconfigs.yaml b/config/crd/bases/boot.ironcore.dev_httpbootconfigs.yaml index bb49fe93..670616db 100644 --- a/config/crd/bases/boot.ironcore.dev_httpbootconfigs.yaml +++ b/config/crd/bases/boot.ironcore.dev_httpbootconfigs.yaml @@ -14,7 +14,14 @@ spec: singular: httpbootconfig scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - jsonPath: .status.state + name: State + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 schema: openAPIV3Schema: description: HTTPBootConfig is the Schema for the httpbootconfigs API diff --git a/internal/controller/httpbootconfig_controller.go b/internal/controller/httpbootconfig_controller.go index 8267278a..cdb8dc5e 100644 --- a/internal/controller/httpbootconfig_controller.go +++ b/internal/controller/httpbootconfig_controller.go @@ -5,12 +5,18 @@ package controller import ( "context" + "fmt" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/go-logr/logr" bootv1alpha1 "github.com/ironcore-dev/ipxe-operator/api/v1alpha1" ) @@ -23,20 +29,69 @@ type HTTPBootConfigReconciler struct { //+kubebuilder:rbac:groups=boot.ironcore.dev,resources=httpbootconfigs,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=boot.ironcore.dev,resources=httpbootconfigs/status,verbs=get;update;patch //+kubebuilder:rbac:groups=boot.ironcore.dev,resources=httpbootconfigs/finalizers,verbs=update +//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch -// 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 HTTPBootConfig 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.17.0/pkg/reconcile func (r *HTTPBootConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) + log := log.FromContext(ctx) - // TODO(user): your logic here + HTTPBootConfig := &bootv1alpha1.HTTPBootConfig{} + if err := r.Get(ctx, req.NamespacedName, HTTPBootConfig); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + return r.reconcileExists(ctx, log, HTTPBootConfig) +} + +func (r *HTTPBootConfigReconciler) reconcileExists(ctx context.Context, log logr.Logger, HTTPBootConfig *bootv1alpha1.HTTPBootConfig) (ctrl.Result, error) { + if !HTTPBootConfig.DeletionTimestamp.IsZero() { + return r.delete(ctx, log, HTTPBootConfig) + } + + return r.reconcile(ctx, log, HTTPBootConfig) +} + +func (r *HTTPBootConfigReconciler) reconcile(ctx context.Context, log logr.Logger, HTTPBootConfig *bootv1alpha1.HTTPBootConfig) (ctrl.Result, error) { + log.V(1).Info("Ensuring Ignition") + state, ignitionErr := r.ensureIgnition(ctx, log, HTTPBootConfig) + if ignitionErr != nil { + patchError := r.patchStatus(ctx, HTTPBootConfig, state) + if patchError != nil { + return ctrl.Result{}, fmt.Errorf("failed to patch status %w %w", ignitionErr, patchError) + } + + log.V(1).Info("Failed to Ensure Ignition", "Error", ignitionErr) + return ctrl.Result{}, nil + } + + patchErr := r.patchStatus(ctx, HTTPBootConfig, state) + if patchErr != nil { + return ctrl.Result{}, fmt.Errorf("failed to patch status %w", patchErr) + } + + return ctrl.Result{}, nil +} + +func (r *HTTPBootConfigReconciler) ensureIgnition(ctx context.Context, _ logr.Logger, HTTPBootConfig *bootv1alpha1.HTTPBootConfig) (bootv1alpha1.HTTPBootConfigState, error) { + // Verify if the IgnitionRef is set, and it has the intended data key. + if HTTPBootConfig.Spec.IgnitionSecretRef != nil { + IgnitionSecret := &corev1.Secret{} + if err := r.Get(ctx, client.ObjectKey{Name: HTTPBootConfig.Spec.IgnitionSecretRef.Name, Namespace: HTTPBootConfig.Namespace}, IgnitionSecret); err != nil { + return bootv1alpha1.HTTPBootConfigStateError, err + // TODO: Add some validation steps to ensure that the IgntionData is compliant, if necessary. + // Assume for now, that it's going to json format. + } + if IgnitionSecret.Data[bootv1alpha1.DefaultIgnitionKey] == nil { + return bootv1alpha1.HTTPBootConfigStateError, fmt.Errorf("ignition data is missing") + } + } + + return bootv1alpha1.HTTPBootConfigStateReady, nil +} + +func (r *HTTPBootConfigReconciler) delete(_ context.Context, log logr.Logger, HTTPBootConfig *bootv1alpha1.HTTPBootConfig) (ctrl.Result, error) { + log.V(1).Info("Deleting HTTPBootConfig") + + // TODO return ctrl.Result{}, nil } @@ -45,5 +100,51 @@ func (r *HTTPBootConfigReconciler) Reconcile(ctx context.Context, req ctrl.Reque func (r *HTTPBootConfigReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&bootv1alpha1.HTTPBootConfig{}). + Watches( + &corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(r.enqueueHTTPBootConfigReferencingIgnitionSecret), + ). Complete(r) } + +func (r *HTTPBootConfigReconciler) patchStatus( + ctx context.Context, + HTTPBootConfig *bootv1alpha1.HTTPBootConfig, + state bootv1alpha1.HTTPBootConfigState, +) error { + base := HTTPBootConfig.DeepCopy() + HTTPBootConfig.Status.State = state + + if err := r.Status().Patch(ctx, HTTPBootConfig, client.MergeFrom(base)); err != nil { + return fmt.Errorf("error patching HTTPBootConfig: %w", err) + } + return nil +} + +func (r *HTTPBootConfigReconciler) enqueueHTTPBootConfigReferencingIgnitionSecret(ctx context.Context, secret client.Object) []reconcile.Request { + log := log.Log.WithValues("secret", secret.GetName()) + secretObj, ok := secret.(*corev1.Secret) + if !ok { + log.Error(nil, "cant decode object into Secret", secret) + return nil + } + + list := &bootv1alpha1.HTTPBootConfigList{} + if err := r.Client.List(ctx, list, client.InNamespace(secretObj.Namespace)); err != nil { + log.Error(err, "failed to list HTTPBootConfig for secret", secret) + return nil + } + + var requests []reconcile.Request + for _, HTTPBootConfig := range list.Items { + if HTTPBootConfig.Spec.IgnitionSecretRef != nil && HTTPBootConfig.Spec.IgnitionSecretRef.Name == secretObj.Name { + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: HTTPBootConfig.Name, + Namespace: HTTPBootConfig.Namespace, + }, + }) + } + } + return requests +}