diff --git a/api/v1alpha1/lvmcluster_types.go b/api/v1alpha1/lvmcluster_types.go index dfa8ab00b..910c8865d 100644 --- a/api/v1alpha1/lvmcluster_types.go +++ b/api/v1alpha1/lvmcluster_types.go @@ -65,10 +65,15 @@ type DeviceSelector struct { // type LVMConfig struct { // thinProvision bool `json:"thinProvision,omitempty"` // } + // LVMClusterStatus defines the observed state of LVMCluster type LVMClusterStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file + + // ready describes if the LvmCluster is ready. + // +optional + Ready bool `json:"ready,omitempty"` } //+kubebuilder:object:root=true diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 0e76f99e9..c15b1e4bb 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1,4 +1,3 @@ -//go:build !ignore_autogenerated // +build !ignore_autogenerated /* diff --git a/config/crd/bases/lvm.topolvm.io_lvmclusters.yaml b/config/crd/bases/lvm.topolvm.io_lvmclusters.yaml index a0989dea1..0285a1653 100644 --- a/config/crd/bases/lvm.topolvm.io_lvmclusters.yaml +++ b/config/crd/bases/lvm.topolvm.io_lvmclusters.yaml @@ -137,8 +137,11 @@ spec: type: array type: object status: - description: "type LVMConfig struct { \tthinProvision bool `json:\"thinProvision,omitempty\"` - } LVMClusterStatus defines the observed state of LVMCluster" + description: LVMClusterStatus defines the observed state of LVMCluster + properties: + ready: + description: ready describes if the LvmCluster is ready. + type: boolean type: object type: object served: true diff --git a/controllers/defaults.go b/controllers/defaults.go new file mode 100644 index 000000000..8ceff8ac9 --- /dev/null +++ b/controllers/defaults.go @@ -0,0 +1,48 @@ +/* +Copyright 2021. + +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 controllers + +import ( + "os" +) + +var ( + defaultValMap = map[string]string{ + "OPERATOR_NAMESPACE": "openshift-storage", + "TOPOLVM_CSI_IMAGE": "quay.io/topolvm/topolvm:0.10.3", + "CSI_REGISTRAR_IMAGE": "k8s.gcr.io/sig-storage/csi-node-driver-registrar:v2.3.0", + "CSI_PROVISIONER_IMAGE": "k8s.gcr.io/sig-storage/csi-provisioner:v3.0.0", + "CSI_LIVENESSPROBE_IMAGE": "k8s.gcr.io/sig-storage/livenessprobe:v2.5.0", + "CSI_RESIZER_IMAGE": "k8s.gcr.io/sig-storage/csi-resizer:v1.3.0", + } + + OperatorNamespace = GetEnvOrDefault("OPERATOR_NAMESPACE") + + //CSI + TopolvmCsiImage = GetEnvOrDefault("TOPOLVM_CSI_IMAGE") + CsiRegistrarImage = GetEnvOrDefault("CSI_REGISTRAR_IMAGE") + CsiProvisionerImage = GetEnvOrDefault("CSI_PROVISIONER_IMAGE") + CsiLivenessProbeImage = GetEnvOrDefault("CSI_LIVENESSPROBE_IMAGE") + CsiResizerImage = GetEnvOrDefault("CSI_RESIZER_IMAGE") +) + +func GetEnvOrDefault(env string) string { + if val := os.Getenv(env); val != "" { + return val + } + return defaultValMap[env] +} diff --git a/controllers/lvmcluster_controller.go b/controllers/lvmcluster_controller.go index 80aa9f89b..6b04ecafa 100644 --- a/controllers/lvmcluster_controller.go +++ b/controllers/lvmcluster_controller.go @@ -19,7 +19,8 @@ package controllers import ( "context" "fmt" - "strings" + + "k8s.io/apimachinery/pkg/api/errors" "github.com/go-logr/logr" lvmv1alpha1 "github.com/red-hat-storage/lvm-operator/api/v1alpha1" @@ -27,8 +28,11 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) +var lvmClusterFinalizer = "lvmcluster.topolvm.io" + const ( ControllerName = "lvmcluster-controller" ) @@ -56,81 +60,135 @@ type LVMClusterReconciler struct { func (r *LVMClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { r.Log = log.FromContext(ctx).WithName(ControllerName) r.Log.Info("reconciling", "lvmcluster", req) - result, err := r.reconcile(ctx, req) - // TODO: update status with condition describing whether reconcile succeeded - if err != nil { - r.Log.Error(err, "reconcile error") - } - - return result, err -} - -// errors returned by this will be updated in the reconcileSucceeded condition of the LVMCluster -func (r *LVMClusterReconciler) reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - result := ctrl.Result{} // get lvmcluster lvmCluster := &lvmv1alpha1.LVMCluster{} err := r.Client.Get(ctx, req.NamespacedName, lvmCluster) if err != nil { - return result, fmt.Errorf("failed to fetch lvmCluster: %w", err) + if errors.IsNotFound(err) { + r.Log.Info("lvmCluster instance not found") + return ctrl.Result{}, nil + } + // Error reading the object - requeue the request. + return ctrl.Result{}, err } - unitList := []resourceManager{} + result, reconcileError := r.reconcile(ctx, lvmCluster) + + // Apply status changes + statusError := r.Client.Status().Update(ctx, lvmCluster) + if statusError != nil { + if errors.IsNotFound(err) { + r.Log.Error(statusError, "failed to update status") + } + } + + // Reconcile errors have higher priority than status update errors + if reconcileError != nil { + return result, reconcileError + } else if statusError != nil && errors.IsNotFound(statusError) { + return result, statusError + } else { + return result, nil + } +} - // handle deletion - if !lvmCluster.DeletionTimestamp.IsZero() { - for _, unit := range unitList { - err := unit.ensureDeleted(r, ctx, *lvmCluster) - if err != nil { - return result, fmt.Errorf("failed cleaning up: %s %w", unit.getName(), err) +// errors returned by this will be updated in the reconcileSucceeded condition of the LVMCluster +func (r *LVMClusterReconciler) reconcile(ctx context.Context, instance *lvmv1alpha1.LVMCluster) (ctrl.Result, error) { + resourceList := []resourceManager{} + + //The resource was deleted + if !instance.DeletionTimestamp.IsZero() { + if contains(instance.GetFinalizers(), lvmClusterFinalizer) { + for _, unit := range resourceList { + err := unit.ensureDeleted(r, ctx, instance) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed cleaning up: %s %w", unit.getName(), err) + } + } + instance.ObjectMeta.Finalizers = remove(instance.ObjectMeta.Finalizers, lvmClusterFinalizer) + if err := r.Client.Update(context.TODO(), instance); err != nil { + r.Log.Info("failed to remove finalizer from LvmCluster", "LvmCluster", instance.Name) + return reconcile.Result{}, err } } + return ctrl.Result{}, nil } - // handle create/update - for _, unit := range unitList { - err := unit.ensureCreated(r, ctx, *lvmCluster) - if err != nil { - return result, fmt.Errorf("failed reconciling: %s %w", unit.getName(), err) + if !contains(instance.GetFinalizers(), lvmClusterFinalizer) { + r.Log.Info("Finalizer not found for LvmCluster. Adding finalizer.", "LvmCluster", instance.Name) + instance.ObjectMeta.Finalizers = append(instance.ObjectMeta.Finalizers, lvmClusterFinalizer) + if err := r.Client.Update(context.TODO(), instance); err != nil { + r.Log.Info("failed to update LvmCluster with finalizer.", "LvmCluster", instance.Name) + return reconcile.Result{}, err } } - // check and report deployment status - var failedStatusUpdates []string - var lastError error - for _, unit := range unitList { - err := unit.updateStatus(r, ctx, *lvmCluster) + // handle create/update + for _, unit := range resourceList { + err := unit.ensureCreated(r, ctx, instance) if err != nil { - failedStatusUpdates = append(failedStatusUpdates, unit.getName()) - unitError := fmt.Errorf("failed updating status for: %s %w", unit.getName(), err) - r.Log.Error(unitError, "") + return ctrl.Result{}, fmt.Errorf("failed reconciling: %s %w", unit.getName(), err) } } - // return simple message that will fit in status reconcileSucceeded condition, don't put all the errors there - if len(failedStatusUpdates) > 0 { - return ctrl.Result{}, fmt.Errorf("status update failed for %s: %w", strings.Join(failedStatusUpdates, ","), lastError) - } + /* // check and report deployment status + var failedStatusUpdates []string + var lastError error + for _, unit := range resourceList { + err := unit.updateStatus(r, ctx, instance) + if err != nil { + failedStatusUpdates = append(failedStatusUpdates, unit.getName()) + unitError := fmt.Errorf("failed updating status for: %s %w", unit.getName(), err) + r.Log.Error(unitError, "") + } + } */ + /* // return simple message that will fit in status reconcileSucceeded condition, don't put all the errors there + if len(failedStatusUpdates) > 0 { + return ctrl.Result{}, fmt.Errorf("status update failed for %s: %w", strings.Join(failedStatusUpdates, ","), lastError) + } + */ + //ToDo: Change the status to something useful + instance.Status.Ready = true return ctrl.Result{}, nil - } -// NOTE: when updating this, please also update doc/design/README.md +// NOTE: when updating this, please also update doc/design/operator.md type resourceManager interface { // getName should return a camelCase name of this unit of reconciliation getName() string // ensureCreated should check the resources managed by this unit - ensureCreated(*LVMClusterReconciler, context.Context, lvmv1alpha1.LVMCluster) error + ensureCreated(*LVMClusterReconciler, context.Context, *lvmv1alpha1.LVMCluster) error // ensureDeleted should wait for the resources to be cleaned up - ensureDeleted(*LVMClusterReconciler, context.Context, lvmv1alpha1.LVMCluster) error + ensureDeleted(*LVMClusterReconciler, context.Context, *lvmv1alpha1.LVMCluster) error // updateStatus should optionally update the CR's status about the health of the managed resource - // each unit will have updateStatus called induvidually so + // each unit will have updateStatus called individually so // avoid status fields like lastHeartbeatTime and have a // status that changes only when the operands change. - updateStatus(*LVMClusterReconciler, context.Context, lvmv1alpha1.LVMCluster) error + updateStatus(*LVMClusterReconciler, context.Context, *lvmv1alpha1.LVMCluster) error +} + +// Checks whether a string is contained within a slice +func contains(slice []string, s string) bool { + for _, item := range slice { + if item == s { + return true + } + } + return false +} + +// Removes a given string from a slice and returns the new slice +func remove(slice []string, s string) (result []string) { + for _, item := range slice { + if item == s { + continue + } + result = append(result, item) + } + return } diff --git a/controllers/lvmcluster_controller_test.go b/controllers/lvmcluster_controller_test.go new file mode 100644 index 000000000..9ae9e21ca --- /dev/null +++ b/controllers/lvmcluster_controller_test.go @@ -0,0 +1,67 @@ +/* +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 controllers + +import ( + "context" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + lvmv1alpha1 "github.com/red-hat-storage/lvm-operator/api/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("LVMCluster controller", func() { + + const ( + timeout = time.Second * 10 + interval = time.Millisecond * 250 + ) + + Context("LvmCluster reconcile", func() { + It("Reconciles an LvmCluster, ", func() { + By("Indicate setting status to ready") + ctx := context.Background() + lvmCluster := &lvmv1alpha1.LVMCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: testLvmClusterName, + Namespace: testLvmClusterNamespace, + }, + Spec: lvmv1alpha1.LVMClusterSpec{ + DeviceClasses: []lvmv1alpha1.DeviceClass{{Name: "test"}}, + }, + } + Expect(k8sClient.Create(ctx, lvmCluster)).Should(Succeed()) + + //Check that the status.Ready field has been set to true. This is a placeholder test and will + // be modified to check for the actual resources once they are implemented. + + lvmClusterLookupName := types.NamespacedName{Name: testLvmClusterName, Namespace: testLvmClusterNamespace} + lvmCluster1 := &lvmv1alpha1.LVMCluster{} + + Eventually(func() bool { + err := k8sClient.Get(ctx, lvmClusterLookupName, lvmCluster1) + if err != nil { + return false + } + return lvmCluster1.Status.Ready + }, timeout, interval).Should(BeTrue()) + // Let's make sure our Schedule string value was properly converted/handled. + Expect(lvmCluster1.Status.Ready).Should(Equal(true)) + }) + + }) + +}) diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 2ab9d2cec..4b5eac62d 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -17,13 +17,16 @@ limitations under the License. package controllers import ( + "context" "path/filepath" "testing" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" "sigs.k8s.io/controller-runtime/pkg/envtest/printer" @@ -37,9 +40,13 @@ import ( // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. -var cfg *rest.Config -var k8sClient client.Client -var testEnv *envtest.Environment +var ( + cfg *rest.Config + k8sClient client.Client + testEnv *envtest.Environment + ctx context.Context + cancel context.CancelFunc +) func TestAPIs(t *testing.T) { RegisterFailHandler(Fail) @@ -49,9 +56,16 @@ func TestAPIs(t *testing.T) { []Reporter{printer.NewlineReporter{}}) } +const ( + testLvmClusterName = "test-lvmcluster" + testLvmClusterNamespace = "openshift-storage" +) + var _ = BeforeSuite(func() { logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + ctx, cancel = context.WithCancel(context.TODO()) + By("bootstrapping test environment") testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, @@ -71,9 +85,33 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + }) + Expect(err).ToNot(HaveOccurred()) + + // Create the primary namespace to be used by some of the tests + testNamespace := &corev1.Namespace{} + testNamespace.Name = testLvmClusterNamespace + Expect(k8sClient.Create(ctx, testNamespace)).Should(Succeed()) + + err = (&LVMClusterReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + Log: ctrl.Log.WithName("controllers").WithName("LvmCluster"), + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + go func() { + defer GinkgoRecover() + err = k8sManager.Start(ctx) + Expect(err).ToNot(HaveOccurred(), "failed to run manager") + }() + }, 60) var _ = AfterSuite(func() { + cancel() By("tearing down the test environment") err := testEnv.Stop() Expect(err).NotTo(HaveOccurred())