diff --git a/api/falcon/v1alpha1/conditions.go b/api/falcon/v1alpha1/conditions.go index 0e6dbbf6..a837610a 100644 --- a/api/falcon/v1alpha1/conditions.go +++ b/api/falcon/v1alpha1/conditions.go @@ -1,5 +1,7 @@ package v1alpha1 +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + const ( // Following strings are condition types @@ -25,6 +27,23 @@ const ( ReasonSucceeded string = "Succeeded" ReasonUpdateSucceeded string = "UpdateSucceeded" ReasonUpdateFailed string = "UpdateFailed" + ReasonDeleteSucceeded string = "DeleteSucceeded" + ReasonDeleteFailed string = "DeleteFailed" ReasonFailed string = "Failed" ReasonDiscovered string = "Discovered" ) + +// FalconAdmissionStatus defines the observed state of FalconAdmission +type FalconCRStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // Version of the CrowdStrike Falcon Sensor + Sensor *string `json:"sensor,omitempty"` + + // Version of the CrowdStrike Falcon Operator + Version string `json:"version,omitempty"` + + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} diff --git a/internal/controller/common/utils.go b/internal/controller/common/utils.go index c0b93548..a71ee62e 100644 --- a/internal/controller/common/utils.go +++ b/internal/controller/common/utils.go @@ -3,11 +3,188 @@ package common import ( "context" "fmt" + "strings" + "github.com/crowdstrike/falcon-operator/api/falcon/v1alpha1" + falconv1alpha1 "github.com/crowdstrike/falcon-operator/api/falcon/v1alpha1" + "github.com/go-logr/logr" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + 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" ) +func Create(r client.Client, sch *runtime.Scheme, ctx context.Context, req ctrl.Request, log logr.Logger, falconObject client.Object, falconStatus *v1alpha1.FalconCRStatus, obj runtime.Object) error { + switch o := obj.(type) { + case client.Object: + name := o.GetName() + namespace := o.GetNamespace() + gvk := o.GetObjectKind().GroupVersionKind() + fgvk := falconObject.GetObjectKind().GroupVersionKind() + condType := fmt.Sprintf("%sReady", strings.ToUpper(gvk.Kind[:1])+gvk.Kind[1:]) + + err := ctrl.SetControllerReference(falconObject, o, sch) + if err != nil { + log.Error(err, fmt.Sprintf("unable to set controller reference for %s %s", fgvk.Kind, gvk.Kind)) + return err + } + + log.Info(logMessage("Creating a new", fgvk.Kind, gvk.Kind), oLogMessage(gvk.Kind, "Name"), name, oLogMessage(gvk.Kind, "Namespace"), namespace) + err = r.Create(ctx, o) + if err != nil { + log.Error(err, logMessage("Failed to create", fgvk.Kind, gvk.Kind), oLogMessage(gvk.Kind, "Name"), name, oLogMessage(gvk.Kind, "Namespace"), namespace) + + if err := ConditionsUpdate(r, ctx, req, log, falconObject, falconStatus, + metav1.Condition{ + Status: metav1.ConditionFalse, + Reason: falconv1alpha1.ReasonInstallFailed, + Type: condType, + Message: fmt.Sprintf("%s %s creation has failed", fgvk.Kind, gvk.Kind), + ObservedGeneration: falconObject.GetGeneration(), + }); err != nil { + return err + } + + return err + } + + if err := ConditionsUpdate(r, ctx, req, log, falconObject, falconStatus, + metav1.Condition{ + Status: metav1.ConditionTrue, + Reason: falconv1alpha1.ReasonInstallSucceeded, + Type: condType, + Message: fmt.Sprintf("%s %s has been successfully created", fgvk.Kind, gvk.Kind), + ObservedGeneration: falconObject.GetGeneration(), + }); err != nil { + return err + } + + return nil + default: + return fmt.Errorf("Unrecognized kubernetes object type: %T", obj) + } +} + +func Update(r client.Client, ctx context.Context, req ctrl.Request, log logr.Logger, falconObject client.Object, falconStatus *v1alpha1.FalconCRStatus, obj runtime.Object) error { + switch o := obj.(type) { + case client.Object: + name := o.GetName() + namespace := o.GetNamespace() + gvk := o.GetObjectKind().GroupVersionKind() + fgvk := falconObject.GetObjectKind().GroupVersionKind() + condType := fmt.Sprintf("%sReady", strings.ToUpper(gvk.Kind[:1])+gvk.Kind[1:]) + + log.Info(logMessage("Updating", fgvk.Kind, gvk.Kind), oLogMessage(gvk.Kind, "Name"), name, oLogMessage(gvk.Kind, "Namespace"), namespace) + err := r.Update(ctx, o) + if err != nil { + log.Error(err, logMessage("Failed to update", fgvk.Kind, gvk.Kind), oLogMessage(gvk.Kind, "Name"), name, oLogMessage(gvk.Kind, "Namespace"), namespace) + + if err := ConditionsUpdate(r, ctx, req, log, falconObject, falconStatus, + metav1.Condition{ + Status: metav1.ConditionFalse, + Reason: falconv1alpha1.ReasonUpdateFailed, + Type: condType, + Message: fmt.Sprintf("%s %s update has failed", fgvk.Kind, gvk.Kind), + ObservedGeneration: falconObject.GetGeneration(), + }); err != nil { + return err + } + + return err + } + + if err := ConditionsUpdate(r, ctx, req, log, falconObject, falconStatus, + metav1.Condition{ + Status: metav1.ConditionTrue, + Reason: falconv1alpha1.ReasonUpdateSucceeded, + Type: condType, + Message: fmt.Sprintf("%s %s has been successfully updated", fgvk.Kind, gvk.Kind), + ObservedGeneration: falconObject.GetGeneration(), + }); err != nil { + return err + } + + return nil + default: + return fmt.Errorf("Unrecognized kubernetes object type: %T", obj) + } +} + +func Delete(r client.Client, ctx context.Context, req ctrl.Request, log logr.Logger, falconObject client.Object, falconStatus *v1alpha1.FalconCRStatus, obj runtime.Object) error { + switch o := obj.(type) { + case client.Object: + name := o.GetName() + namespace := o.GetNamespace() + gvk := o.GetObjectKind().GroupVersionKind() + fgvk := falconObject.GetObjectKind().GroupVersionKind() + condType := fmt.Sprintf("%sReady", strings.ToUpper(gvk.Kind[:1])+gvk.Kind[1:]) + + log.Info(logMessage("Deleting", fgvk.Kind, gvk.Kind), oLogMessage(gvk.Kind, "Name"), name, oLogMessage(gvk.Kind, "Namespace"), namespace) + err := r.Delete(ctx, o) + if err != nil { + log.Error(err, logMessage("Failed to delete", fgvk.Kind, gvk.Kind), oLogMessage(gvk.Kind, "Name"), name, oLogMessage(gvk.Kind, "Namespace"), namespace) + + if err := ConditionsUpdate(r, ctx, req, log, falconObject, falconStatus, + metav1.Condition{ + Status: metav1.ConditionFalse, + Reason: falconv1alpha1.ReasonDeleteFailed, + Type: condType, + Message: fmt.Sprintf("%s %s deletion has failed", fgvk.Kind, gvk.Kind), + ObservedGeneration: falconObject.GetGeneration(), + }); err != nil { + return err + } + + return err + } + + if err := ConditionsUpdate(r, ctx, req, log, falconObject, falconStatus, + metav1.Condition{ + Status: metav1.ConditionTrue, + Reason: falconv1alpha1.ReasonDeleteSucceeded, + Type: condType, + Message: fmt.Sprintf("%s %s has been successfully deleted", fgvk.Kind, gvk.Kind), + ObservedGeneration: falconObject.GetGeneration(), + }); err != nil { + return err + } + + return nil + default: + return fmt.Errorf("Unrecognized kubernetes object type: %T", obj) + } +} + +// ConditionsUpdate updates the Falcon Object CR conditions +func ConditionsUpdate(r client.Client, ctx context.Context, req ctrl.Request, log logr.Logger, falconObject client.Object, falconStatus *v1alpha1.FalconCRStatus, falconCondition metav1.Condition) error { + if !meta.IsStatusConditionPresentAndEqual(falconStatus.Conditions, falconCondition.Type, falconCondition.Status) { + fgvk := falconObject.GetObjectKind().GroupVersionKind() + + // Re-fetch the memcached Custom Resource before update the status + // so that we have the latest state of the resource on the cluster and we will avoid + // raise the issue "the object has been modified, please apply + // your changes to the latest version and try again" which would re-trigger the reconciliation + err := r.Get(ctx, req.NamespacedName, falconObject) + if err != nil { + log.Error(err, fmt.Sprintf("Failed to re-fetch %s for status update", fgvk.Kind)) + return err + } + + // The following implementation will update the status + meta.SetStatusCondition(&falconStatus.Conditions, falconCondition) + err = r.Status().Update(ctx, falconObject) + if err != nil { + log.Error(err, fmt.Sprintf("Failed to update %s status", fgvk.Kind)) + return err + } + } + + return nil +} + func GetReadyPod(r client.Client, ctx context.Context, namespace string, matchingLabels client.MatchingLabels) (*corev1.Pod, error) { podList := &corev1.PodList{} listOpts := []client.ListOption{ @@ -29,3 +206,25 @@ func GetReadyPod(r client.Client, ctx context.Context, namespace string, matchin return &corev1.Pod{}, fmt.Errorf("No Injector pod found in a Ready state") } + +func GetDeployment(r client.Client, ctx context.Context, namespace string, matchingLabels client.MatchingLabels) (*appsv1.Deployment, error) { + depList := &appsv1.DeploymentList{} + listOpts := []client.ListOption{ + client.InNamespace(namespace), + matchingLabels, + } + + if err := r.List(ctx, depList, listOpts...); err != nil { + return nil, fmt.Errorf("unable to list deployments: %v", err) + } + + return &depList.Items[0], nil +} + +func oLogMessage(kind, obj string) string { + return fmt.Sprintf("%s.%s", kind, obj) +} + +func logMessage(msg, falconKind, kind string) string { + return fmt.Sprintf("%s %s %s", msg, falconKind, kind) +} diff --git a/internal/controller/common/utils_test.go b/internal/controller/common/utils_test.go index 805d0c79..fd13e042 100644 --- a/internal/controller/common/utils_test.go +++ b/internal/controller/common/utils_test.go @@ -1 +1,25 @@ package common + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestOLogMessage(t *testing.T) { + want := "test.test" + got := oLogMessage("test", "test") + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("oLogMessage() mismatch (-want +got):\n%s", diff) + } +} + +func TestLogMessage(t *testing.T) { + want := "test test test" + got := logMessage("test", "test", "test") + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("logMessage() mismatch (-want +got):\n%s", diff) + } +}