diff --git a/PROJECT b/PROJECT index d4e2702..f4188f5 100644 --- a/PROJECT +++ b/PROJECT @@ -24,4 +24,8 @@ resources: kind: Identity path: github.com/jumpstarter-dev/jumpstarter-controller/api/v1alpha1 version: v1alpha1 +- controller: true + domain: jumpstarter.dev + kind: Lease + version: v1alpha1 version: "3" diff --git a/cmd/main.go b/cmd/main.go index 5cbff17..4b4f55e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -138,6 +138,13 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "Identity") os.Exit(1) } + if err = (&controller.LeaseReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Lease") + os.Exit(1) + } // +kubebuilder:scaffold:builder if err = (&service.ControllerService{ diff --git a/internal/controller/lease_controller.go b/internal/controller/lease_controller.go new file mode 100644 index 0000000..ebfba6a --- /dev/null +++ b/internal/controller/lease_controller.go @@ -0,0 +1,119 @@ +/* +Copyright 2024. + +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 controller + +import ( + "context" + "fmt" + "time" + + jumpstarterdevv1alpha1 "github.com/jumpstarter-dev/jumpstarter-controller/api/v1alpha1" + 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/log" +) + +// LeaseReconciler reconciles a Lease object +type LeaseReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=jumpstarter.dev,resources=leases,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=jumpstarter.dev,resources=leases/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=jumpstarter.dev,resources=leases/finalizers,verbs=update + +// 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 Lease 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.18.4/pkg/reconcile +func (r *LeaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := log.FromContext(ctx) + + var lease jumpstarterdevv1alpha1.Lease + if err := r.Get(ctx, req.NamespacedName, &lease); err != nil { + log.Error(err, "unable to fetch Lease") + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // Ignore invalid lease + if !lease.Spec.BeginTime.Before(&lease.Spec.EndTime) { + log.Error(fmt.Errorf("BeginTime not before EndTime"), "invalid lease") + return ctrl.Result{}, nil + } + + // Ignore leases that are yet to begin + // Requeue at BeginTime + if lease.Spec.BeginTime.After(time.Now()) { + return ctrl.Result{ + RequeueAfter: lease.Spec.BeginTime.Sub(time.Now()), + }, nil + } + + if !lease.Spec.EndTime.After(time.Now()) { + // Update status for expired leases + lease.Status.Ended = true + // TODO: release exporter + lease.Status.ExporterName = "" + } else { + // Update status for active leases + // TODO: filter exporter + selector, err := metav1.LabelSelectorAsSelector(&lease.Spec.Selector) + if err != nil { + log.Error(err, "Error creating selector for label selector") + return ctrl.Result{}, err + } + + var exporters jumpstarterdevv1alpha1.ExporterList + err = r.List(ctx, &exporters, client.InNamespace(req.Namespace), client.MatchingLabelsSelector{Selector: selector}) + if err != nil { + log.Error(err, "Error listing exporters") + return ctrl.Result{}, err + } + + // No matching exporter available + // Try again later + if len(exporters.Items) == 0 { + return ctrl.Result{ + RequeueAfter: time.Second, + }, nil + } + + lease.Status.ExporterName = exporters.Items[0].Name + } + + if err := r.Status().Update(ctx, &lease); err != nil { + log.Error(err, "unable to update Lease status") + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *LeaseReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&jumpstarterdevv1alpha1.Lease{}). + Complete(r) +} diff --git a/internal/controller/lease_controller_test.go b/internal/controller/lease_controller_test.go new file mode 100644 index 0000000..3bb7805 --- /dev/null +++ b/internal/controller/lease_controller_test.go @@ -0,0 +1,32 @@ +/* +Copyright 2024. + +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 controller + +import ( + . "github.com/onsi/ginkgo/v2" +) + +var _ = Describe("Lease Controller", func() { + Context("When reconciling a resource", func() { + + It("should successfully reconcile the resource", func() { + + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +})