Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial LvmCluster controller #12

Merged
merged 4 commits into from
Dec 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/v1alpha1/lvmcluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a placeholder field in the status. We will probably end up removing this.

}

//+kubebuilder:object:root=true
Expand Down
1 change: 0 additions & 1 deletion api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions config/crd/bases/lvm.topolvm.io_lvmclusters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 48 additions & 0 deletions controllers/defaults.go
Original file line number Diff line number Diff line change
@@ -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]
}
146 changes: 102 additions & 44 deletions controllers/lvmcluster_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,20 @@ 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"
"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"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

var lvmClusterFinalizer = "lvmcluster.topolvm.io"

const (
ControllerName = "lvmcluster-controller"
)
Expand Down Expand Up @@ -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
}
67 changes: 67 additions & 0 deletions controllers/lvmcluster_controller_test.go
Original file line number Diff line number Diff line change
@@ -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))
})

})

})
Loading