diff --git a/Makefile b/Makefile index f94be255..f00dfb13 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ # To re-generate a bundle for another specific version without changing the standard setup, you can: # - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2) # - use environment variables to overwrite this value (e.g export VERSION=0.0.2) -VERSION ?= 1.0.0 +VERSION ?= 1.1.0 # CHANNELS define the bundle channels used in the bundle. # Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") diff --git a/UNSAFE.md b/UNSAFE.md new file mode 100644 index 00000000..8158489a --- /dev/null +++ b/UNSAFE.md @@ -0,0 +1,15 @@ +# Unsafe Settings + +Need to flesh this out, but here are the highlights: + +* A K8s cluster should always match the configuration it's given and never surprise you +* Configurations should be functional, in the mathematic sense: + * The same config should produce the same cluster no matter where or when it's used +* Image SHAs are good + * They can never refer to anything other than one particular image +* Image tags and version numbers are bad + * What they refer to can change unknowingly +* Version policies are worse + * Just like above, but to a further degree +* Self-adjusting resources are worse still + * Just like above, but to a wider degree diff --git a/api/falcon/v1alpha1/falconadmission_types.go b/api/falcon/v1alpha1/falconadmission_types.go index cfa3ac3c..685c40d5 100644 --- a/api/falcon/v1alpha1/falconadmission_types.go +++ b/api/falcon/v1alpha1/falconadmission_types.go @@ -54,6 +54,12 @@ type FalconAdmissionSpec struct { // Falcon Admission Controller Version. The latest version will be selected when version specifier is missing. Example: 6.31, 6.31.0, 6.31.0-1409, etc. // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Falcon Admission Controller Version",order=8 Version *string `json:"version,omitempty"` + + // FalconUnsafe configures various options that go against industry practices or are otherwise not recommended for use. + // Adjusting these settings may result in incorrect or undesirable behavior. Proceed at your own risk. + // For more information, please see https://github.com/CrowdStrike/falcon-operator/blob/main/UNSAFE.md. + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Falcon Admission Controller Unsafe Settings" + Unsafe FalconUnsafe `json:"unsafe,omitempty"` } type FalconAdmissionRQSpec struct { diff --git a/api/falcon/v1alpha1/falconcontainer_types.go b/api/falcon/v1alpha1/falconcontainer_types.go index 94133e1f..9bd6ae7d 100644 --- a/api/falcon/v1alpha1/falconcontainer_types.go +++ b/api/falcon/v1alpha1/falconcontainer_types.go @@ -43,6 +43,12 @@ type FalconContainerSpec struct { // Falcon Container Version. The latest version will be selected when version specifier is missing; ignored when Image is set. // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Falcon Container Image Version",order=6 Version *string `json:"version,omitempty"` + + // FalconUnsafe configures various options that go against industry practices or are otherwise not recommended for use. + // Adjusting these settings may result in incorrect or undesirable behavior. Proceed at your own risk. + // For more information, please see https://github.com/CrowdStrike/falcon-operator/blob/main/UNSAFE.md. + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Falcon Admission Controller Unsafe Settings" + Unsafe FalconUnsafe `json:"unsafe,omitempty"` } type FalconContainerInjectorSpec struct { diff --git a/api/falcon/v1alpha1/falconnodesensor_types.go b/api/falcon/v1alpha1/falconnodesensor_types.go index 09086fc4..0f9d89cc 100644 --- a/api/falcon/v1alpha1/falconnodesensor_types.go +++ b/api/falcon/v1alpha1/falconnodesensor_types.go @@ -103,6 +103,12 @@ type FalconNodeSensorConfig struct { // Version of the sensor to be installed. The latest version will be selected when this version specifier is missing. Version *string `json:"version,omitempty"` + + // FalconUnsafe configures various options that go against industry practices or are otherwise not recommended for use. + // Adjusting these settings may result in incorrect or undesirable behavior. Proceed at your own risk. + // For more information, please see https://github.com/CrowdStrike/falcon-operator/blob/main/UNSAFE.md. + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Falcon Admission Controller Unsafe Settings" + Unsafe FalconUnsafe `json:"unsafe,omitempty"` } type PriorityClassConfig struct { diff --git a/api/falcon/v1alpha1/unsafe.go b/api/falcon/v1alpha1/unsafe.go new file mode 100644 index 00000000..b5c3031c --- /dev/null +++ b/api/falcon/v1alpha1/unsafe.go @@ -0,0 +1,10 @@ +package v1alpha1 + +// FalconUnsafe configures various options that go against industry practices or are otherwise not recommended for use. +// Adjusting these settings may result in incorrect or undesirable behavior. Proceed at your own risk. +// For more information, please see https://github.com/CrowdStrike/falcon-operator/blob/main/UNSAFE.md. +type FalconUnsafe struct { + // UpdatePolicy is the name of a sensor update policy configured and enabled in Falcon UI. It is ignored when Image and/or Version are set. + // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Falcon Admission Controller Update Policy",order=1 + UpdatePolicy *string `json:"updatePolicy,omitempty"` +} diff --git a/api/falcon/v1alpha1/zz_generated.deepcopy.go b/api/falcon/v1alpha1/zz_generated.deepcopy.go index 8375a772..d9cb0bf7 100644 --- a/api/falcon/v1alpha1/zz_generated.deepcopy.go +++ b/api/falcon/v1alpha1/zz_generated.deepcopy.go @@ -462,6 +462,7 @@ func (in *FalconAdmissionSpec) DeepCopyInto(out *FalconAdmissionSpec) { *out = new(string) **out = **in } + in.Unsafe.DeepCopyInto(&out.Unsafe) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FalconAdmissionSpec. @@ -739,6 +740,7 @@ func (in *FalconContainerSpec) DeepCopyInto(out *FalconContainerSpec) { *out = new(string) **out = **in } + in.Unsafe.DeepCopyInto(&out.Unsafe) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FalconContainerSpec. @@ -1032,6 +1034,7 @@ func (in *FalconNodeSensorConfig) DeepCopyInto(out *FalconNodeSensorConfig) { *out = new(string) **out = **in } + in.Unsafe.DeepCopyInto(&out.Unsafe) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FalconNodeSensorConfig. @@ -1198,6 +1201,26 @@ func (in *FalconSensor) DeepCopy() *FalconSensor { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FalconUnsafe) DeepCopyInto(out *FalconUnsafe) { + *out = *in + if in.UpdatePolicy != nil { + in, out := &in.UpdatePolicy, &out.UpdatePolicy + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FalconUnsafe. +func (in *FalconUnsafe) DeepCopy() *FalconUnsafe { + if in == nil { + return nil + } + out := new(FalconUnsafe) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PriorityClassConfig) DeepCopyInto(out *PriorityClassConfig) { *out = *in diff --git a/config/crd/bases/falcon.crowdstrike.com_falconadmissions.yaml b/config/crd/bases/falcon.crowdstrike.com_falconadmissions.yaml index 89161a80..b64d795c 100644 --- a/config/crd/bases/falcon.crowdstrike.com_falconadmissions.yaml +++ b/config/crd/bases/falcon.crowdstrike.com_falconadmissions.yaml @@ -450,6 +450,18 @@ spec: can be created in the namespace. type: string type: object + unsafe: + description: FalconUnsafe configures various options that go against + industry practices or are otherwise not recommended for use. Adjusting + these settings may result in incorrect or undesirable behavior. + Proceed at your own risk. For more information, please see https://github.com/CrowdStrike/falcon-operator/blob/main/UNSAFE.md. + properties: + updatePolicy: + description: UpdatePolicy is the name of a sensor update policy + configured and enabled in Falcon UI. It is ignored when Image + and/or Version are set. + type: string + type: object version: description: 'Falcon Admission Controller Version. The latest version will be selected when version specifier is missing. Example: 6.31, diff --git a/config/crd/bases/falcon.crowdstrike.com_falconcontainers.yaml b/config/crd/bases/falcon.crowdstrike.com_falconcontainers.yaml index 27b849ee..344fe4d1 100644 --- a/config/crd/bases/falcon.crowdstrike.com_falconcontainers.yaml +++ b/config/crd/bases/falcon.crowdstrike.com_falconcontainers.yaml @@ -1924,6 +1924,18 @@ spec: required: - type type: object + unsafe: + description: FalconUnsafe configures various options that go against + industry practices or are otherwise not recommended for use. Adjusting + these settings may result in incorrect or undesirable behavior. + Proceed at your own risk. For more information, please see https://github.com/CrowdStrike/falcon-operator/blob/main/UNSAFE.md. + properties: + updatePolicy: + description: UpdatePolicy is the name of a sensor update policy + configured and enabled in Falcon UI. It is ignored when Image + and/or Version are set. + type: string + type: object version: description: Falcon Container Version. The latest version will be selected when version specifier is missing; ignored when Image is diff --git a/config/crd/bases/falcon.crowdstrike.com_falconnodesensors.yaml b/config/crd/bases/falcon.crowdstrike.com_falconnodesensors.yaml index 5488d3b2..f6b69fe6 100644 --- a/config/crd/bases/falcon.crowdstrike.com_falconnodesensors.yaml +++ b/config/crd/bases/falcon.crowdstrike.com_falconnodesensors.yaml @@ -516,6 +516,19 @@ spec: type: string type: object type: array + unsafe: + description: FalconUnsafe configures various options that go against + industry practices or are otherwise not recommended for use. + Adjusting these settings may result in incorrect or undesirable + behavior. Proceed at your own risk. For more information, please + see https://github.com/CrowdStrike/falcon-operator/blob/main/UNSAFE.md. + properties: + updatePolicy: + description: UpdatePolicy is the name of a sensor update policy + configured and enabled in Falcon UI. It is ignored when + Image and/or Version are set. + type: string + type: object updateStrategy: description: Type of DaemonSet update. Can be "RollingUpdate" or "OnDelete". Default is RollingUpdate. diff --git a/deploy/falcon-operator.yaml b/deploy/falcon-operator.yaml index 94635ba1..217e1407 100644 --- a/deploy/falcon-operator.yaml +++ b/deploy/falcon-operator.yaml @@ -464,6 +464,18 @@ spec: can be created in the namespace. type: string type: object + unsafe: + description: FalconUnsafe configures various options that go against + industry practices or are otherwise not recommended for use. Adjusting + these settings may result in incorrect or undesirable behavior. + Proceed at your own risk. For more information, please see https://github.com/CrowdStrike/falcon-operator/blob/main/UNSAFE.md. + properties: + updatePolicy: + description: UpdatePolicy is the name of a sensor update policy + configured and enabled in Falcon UI. It is ignored when Image + and/or Version are set. + type: string + type: object version: description: 'Falcon Admission Controller Version. The latest version will be selected when version specifier is missing. Example: 6.31, @@ -2479,6 +2491,18 @@ spec: required: - type type: object + unsafe: + description: FalconUnsafe configures various options that go against + industry practices or are otherwise not recommended for use. Adjusting + these settings may result in incorrect or undesirable behavior. + Proceed at your own risk. For more information, please see https://github.com/CrowdStrike/falcon-operator/blob/main/UNSAFE.md. + properties: + updatePolicy: + description: UpdatePolicy is the name of a sensor update policy + configured and enabled in Falcon UI. It is ignored when Image + and/or Version are set. + type: string + type: object version: description: Falcon Container Version. The latest version will be selected when version specifier is missing; ignored when Image is @@ -3503,6 +3527,19 @@ spec: type: string type: object type: array + unsafe: + description: FalconUnsafe configures various options that go against + industry practices or are otherwise not recommended for use. + Adjusting these settings may result in incorrect or undesirable + behavior. Proceed at your own risk. For more information, please + see https://github.com/CrowdStrike/falcon-operator/blob/main/UNSAFE.md. + properties: + updatePolicy: + description: UpdatePolicy is the name of a sensor update policy + configured and enabled in Falcon UI. It is ignored when + Image and/or Version are set. + type: string + type: object updateStrategy: description: Type of DaemonSet update. Can be "RollingUpdate" or "OnDelete". Default is RollingUpdate. diff --git a/go.mod b/go.mod index 25b71c8e..7095d1d4 100644 --- a/go.mod +++ b/go.mod @@ -11,11 +11,13 @@ require ( github.com/containers/image/v5 v5.31.1 github.com/crowdstrike/gofalcon v0.6.0 github.com/go-logr/logr v1.4.1 + github.com/go-openapi/swag v0.23.0 github.com/google/go-cmp v0.6.0 github.com/onsi/ginkgo/v2 v2.9.5 github.com/onsi/gomega v1.27.7 github.com/openshift/api v0.0.0-20220630121623-32f1d77b9f50 github.com/operator-framework/operator-lib v0.11.0 + github.com/stretchr/testify v1.9.0 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 k8s.io/api v0.27.2 k8s.io/apimachinery v0.27.2 @@ -78,7 +80,6 @@ require ( github.com/go-openapi/runtime v0.28.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/strfmt v0.23.0 // indirect - github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/validate v0.24.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -123,6 +124,7 @@ require ( github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/proglottis/gpgme v0.1.3 // indirect github.com/prometheus/client_golang v1.19.0 // indirect github.com/prometheus/client_model v0.6.0 // indirect @@ -138,6 +140,7 @@ require ( github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/sylabs/sif/v2 v2.16.0 // indirect github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect github.com/tchap/go-patricia/v2 v2.3.1 // indirect diff --git a/go.sum b/go.sum index c426b223..47a6ac87 100644 --- a/go.sum +++ b/go.sum @@ -426,6 +426,8 @@ github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= diff --git a/internal/apitest/apitest.go b/internal/apitest/apitest.go new file mode 100644 index 00000000..ad6f443c --- /dev/null +++ b/internal/apitest/apitest.go @@ -0,0 +1,73 @@ +package apitest + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +type Test struct { + expectedOutputs []any + goTest *testing.T + inputs []any + m *mock.Mock + mockCalls []*mock.Call + name string +} + +func NewTest(name string) *Test { + test := Test{ + name: name, + } + return &test +} + +func (test Test) AssertExpectations(outputs ...any) { + test.m.AssertExpectations(test.goTest) + + for i, expectedValue := range test.expectedOutputs { + assert.Equal(test.goTest, expectedValue, outputs[i], fmt.Sprintf("wrong value in output %d", i)) + } +} + +func (test *Test) ExpectOutputs(outputs ...any) *Test { + test.expectedOutputs = outputs + return test +} + +func (test Test) GetInput(index int) any { + return test.inputs[index] +} + +func (test Test) GetStringPointerInput(index int) *string { + return test.GetInput(index).(*string) +} + +func (test Test) GetMock() *mock.Mock { + return test.m +} + +func (test Test) Run(goTest *testing.T, runner func(Test)) { + test.m = &mock.Mock{} + for _, call := range test.mockCalls { + call.Parent = test.m + } + test.m.ExpectedCalls = test.mockCalls + + goTest.Run(test.name, func(goTest *testing.T) { + test.goTest = goTest + runner(test) + }) +} + +func (test *Test) WithMockCall(call *mock.Mock) *Test { + test.mockCalls = append(test.mockCalls, call.ExpectedCalls...) + return test +} + +func (test *Test) WithInputs(inputs ...any) *Test { + test.inputs = inputs + return test +} diff --git a/internal/controller/admission/falconadmission_controller.go b/internal/controller/admission/falconadmission_controller.go index c1cd55a8..085c63cd 100644 --- a/internal/controller/admission/falconadmission_controller.go +++ b/internal/controller/admission/falconadmission_controller.go @@ -23,7 +23,6 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" - "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/resource" @@ -96,7 +95,7 @@ func (r *FalconAdmissionReconciler) Reconcile(ctx context.Context, req ctrl.Requ falconAdmission := &falconv1alpha1.FalconAdmission{} err := r.Get(ctx, req.NamespacedName, falconAdmission) if err != nil { - if errors.IsNotFound(err) { + if apierrors.IsNotFound(err) { // If the custom resource is not found then, it usually means that it was deleted or not created // In this way, we will stop the reconciliation log.Info("FalconAdmission resource not found. Ignoring since object must be deleted") @@ -123,11 +122,11 @@ func (r *FalconAdmissionReconciler) Reconcile(ctx context.Context, req ctrl.Requ return ctrl.Result{}, err } log.Error(nil, "FalconAdmission is attempting to install in a namespace with existing pods. Please update the CR configuration to a namespace that does not have workoads already running.") - return ctrl.Result{}, err + return ctrl.Result{}, nil } // Let's just set the status as Unknown when no status is available - if falconAdmission.Status.Conditions == nil || len(falconAdmission.Status.Conditions) == 0 { + if len(falconAdmission.Status.Conditions) == 0 { err := retry.RetryOnConflict(retry.DefaultRetry, func() error { meta.SetStatusCondition(&falconAdmission.Status.Conditions, metav1.Condition{Type: falconv1alpha1.ConditionPending, Status: metav1.ConditionUnknown, Reason: "Reconciling", Message: "Starting reconciliation"}) return r.Status().Update(ctx, falconAdmission) @@ -256,7 +255,7 @@ func (r *FalconAdmissionReconciler) Reconcile(ctx context.Context, req ctrl.Requ } pod, err := k8sutils.GetReadyPod(r.Client, ctx, falconAdmission.Spec.InstallNamespace, map[string]string{common.FalconComponentKey: common.FalconAdmissionController}) - if err != nil && err.Error() != "No webhook service pod found in a Ready state" { + if err != nil && err != k8sutils.ErrNoWebhookServicePodReady { log.Error(err, "Failed to find Ready admission controller pod") return ctrl.Result{}, err } diff --git a/internal/controller/admission/falconadmission_controller_test.go b/internal/controller/admission/falconadmission_controller_test.go index 101b766e..636fe796 100644 --- a/internal/controller/admission/falconadmission_controller_test.go +++ b/internal/controller/admission/falconadmission_controller_test.go @@ -143,7 +143,7 @@ var _ = Describe("FalconAdmission controller", func() { By("Checking if pods were successfully created in the reconciliation") Eventually(func() error { pod, err := k8sutils.GetReadyPod(k8sClient, ctx, AdmissionControllerNamespace, map[string]string{common.FalconComponentKey: common.FalconAdmissionController}) - if err != nil && err.Error() != "No webhook service pod found in a Ready state" { + if err != nil && err != k8sutils.ErrNoWebhookServicePodReady { return err } if pod.Name == "" { @@ -156,7 +156,7 @@ var _ = Describe("FalconAdmission controller", func() { By("Checking the latest Status Condition added to the FalconAdmission instance") Eventually(func() error { - if falconAdmission.Status.Conditions != nil && len(falconAdmission.Status.Conditions) != 0 { + if len(falconAdmission.Status.Conditions) != 0 { latestStatusCondition := falconAdmission.Status.Conditions[len(falconAdmission.Status.Conditions)-1] expectedLatestStatusCondition := metav1.Condition{Type: falconv1alpha1.ConditionDeploymentReady, Status: metav1.ConditionTrue, Reason: falconv1alpha1.ReasonInstallSucceeded, diff --git a/internal/controller/admission/image_push.go b/internal/controller/admission/image_push.go index 22c7a11d..e65e0416 100644 --- a/internal/controller/admission/image_push.go +++ b/internal/controller/admission/image_push.go @@ -10,13 +10,13 @@ import ( "k8s.io/apimachinery/pkg/types" falconv1alpha1 "github.com/crowdstrike/falcon-operator/api/falcon/v1alpha1" + "github.com/crowdstrike/falcon-operator/internal/controller/common/sensor" "github.com/crowdstrike/falcon-operator/internal/controller/image" "github.com/crowdstrike/falcon-operator/pkg/aws" "github.com/crowdstrike/falcon-operator/pkg/common" "github.com/crowdstrike/falcon-operator/pkg/gcp" "github.com/crowdstrike/falcon-operator/pkg/k8s_utils" "github.com/crowdstrike/falcon-operator/pkg/registry/auth" - "github.com/crowdstrike/falcon-operator/pkg/registry/falcon_registry" "github.com/crowdstrike/falcon-operator/pkg/registry/pushtoken" "github.com/crowdstrike/gofalcon/falcon" "github.com/go-logr/logr" @@ -193,13 +193,13 @@ func (r *FalconAdmissionReconciler) setImageTag(ctx context.Context, falconAdmis return *falconAdmission.Status.Sensor, r.Client.Status().Update(ctx, falconAdmission) } - // Otherwise, get the newest version matching the requested version string - registry, err := falcon_registry.NewFalconRegistry(ctx, r.falconApiConfig(ctx, falconAdmission)) + apiConfig := r.falconApiConfig(ctx, falconAdmission) + imageRepo, err := sensor.NewImageRepository(ctx, apiConfig) if err != nil { return "", err } - tag, err := registry.LastContainerTag(ctx, falcon.KacSensor, falconAdmission.Spec.Version) + tag, err := imageRepo.GetPreferredImage(ctx, falcon.KacSensor, falconAdmission.Spec.Version, falconAdmission.Spec.Unsafe.UpdatePolicy) if err == nil { falconAdmission.Status.Sensor = common.ImageVersion(tag) } diff --git a/internal/controller/common/sensor/images.go b/internal/controller/common/sensor/images.go new file mode 100644 index 00000000..84c84f3e --- /dev/null +++ b/internal/controller/common/sensor/images.go @@ -0,0 +1,177 @@ +package sensor + +import ( + "context" + "fmt" + "strings" + + "github.com/crowdstrike/falcon-operator/pkg/registry/falcon_registry" + "github.com/crowdstrike/gofalcon/falcon" + "github.com/crowdstrike/gofalcon/falcon/client/sensor_update_policies" + "github.com/go-logr/logr" + "github.com/go-openapi/swag" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +type ImageRepository struct { + api sensorUpdatePoliciesAPI + tags tagRegistry +} + +func NewImageRepository(ctx context.Context, apiConfig *falcon.ApiConfig) (ImageRepository, error) { + apiClient, err := falcon.NewClient(apiConfig) + if err != nil { + return ImageRepository{}, err + } + + registry, err := falcon_registry.NewFalconRegistry(ctx, apiConfig) + if err != nil { + return ImageRepository{}, err + } + + return ImageRepository{ + api: apiClient.SensorUpdatePolicies, + tags: registry, + }, nil +} + +func (images ImageRepository) GetPreferredImage(ctx context.Context, sensorType falcon.SensorType, versionSpec *string, updatePolicySpec *string) (string, error) { + logger := log.FromContext(ctx). + WithValues("sensorType", sensorType) + + version, err := images.getPreferredSensorVersion(versionSpec, updatePolicySpec, logger) + if err != nil { + return "", err + } + + tag, err := images.getImageTagForSensorVersion(ctx, sensorType, version) + if err != nil { + return "", err + } + + logger.Info("selected sensor image", "tag", tag) + return tag, nil +} + +func (images ImageRepository) findPolicy(policyName string) (string, error) { + filter := falconFilter{}. + addClause("platform_name", "Linux"). + addClause("name.raw", policyName). + encode() + + params := sensor_update_policies.NewQuerySensorUpdatePoliciesParams().WithFilter(&filter) + response, err := images.api.QuerySensorUpdatePolicies(params) + if err != nil { + return "", err + } + + ids := getNonZeroValuesInSlice(response.Payload.Resources) + if len(ids) == 0 { + return "", fmt.Errorf("update-policy %s not found", policyName) + } + + return ids[0], nil +} + +func (images ImageRepository) findSensorVersionByUpdatePolicy(updatePolicy string) (string, error) { + policyID, err := images.findPolicy(updatePolicy) + if err != nil { + return "", err + } + + version, err := images.getSensorVersionForPolicy(policyID) + if err != nil { + return "", err + } + + return version, nil +} + +func (images ImageRepository) getImageTagForSensorVersion(ctx context.Context, sensorType falcon.SensorType, version *string) (string, error) { + if sensorType == falcon.NodeSensor { + return images.tags.LastNodeTag(ctx, version) + } + + return images.tags.LastContainerTag(ctx, sensorType, version) +} + +func (images ImageRepository) getPreferredSensorVersion(versionSpec *string, updatePolicySpec *string, logger logr.Logger) (*string, error) { + if versionSpec != nil && *versionSpec != "" { + logger.Info("requested specific sensor version", "version", *versionSpec) + return versionSpec, nil + } + + if updatePolicySpec != nil && *updatePolicySpec != "" { + logger.Info("requested sensor update policy", "policyName", *updatePolicySpec) + version, err := images.findSensorVersionByUpdatePolicy(*updatePolicySpec) + if err != nil { + return nil, err + } + + logger.Info("version selected by sensor update policy", "policyName", *updatePolicySpec, "version", version) + return &version, nil + } + + logger.Info("requested latest sensor version") + return nil, nil +} + +func (images ImageRepository) getSensorVersionForPolicy(policyID string) (string, error) { + params := sensor_update_policies.NewGetSensorUpdatePoliciesV2Params().WithIds([]string{policyID}) + response, err := images.api.GetSensorUpdatePoliciesV2(params) + if err != nil { + return "", err + } + + policies := getNonZeroValuesInSlice(response.Payload.Resources) + if len(policies) == 0 { + return "", fmt.Errorf("update-policy with ID %s not found", policyID) + } + + policy := policies[0] + if !*policy.Enabled { + return "", fmt.Errorf("update-policy with ID %s is disabled", policyID) + } + + parts := strings.Split(*policy.Settings.SensorVersion, ".") + if len(parts) != 3 { + return "", fmt.Errorf("update-policy with ID %s has an invalid sensor version", policyID) + } + + return strings.Join(parts[0:2], "."), nil +} + +func getNonZeroValuesInSlice[T any](input []T) []T { + output := make([]T, 0) + + for _, value := range input { + if !swag.IsZero(value) { + output = append(output, value) + } + } + + return output +} + +type falconFilter struct { + clauses []string +} + +func (filter falconFilter) addClause(name string, value string) falconFilter { + filter.clauses = append(filter.clauses, fmt.Sprintf(`%s:"%s"`, name, value)) + return filter +} + +func (filter falconFilter) encode() string { + return strings.Join(filter.clauses, "+") +} + +type sensorUpdatePoliciesAPI interface { + GetSensorUpdatePoliciesV2(params *sensor_update_policies.GetSensorUpdatePoliciesV2Params, opts ...sensor_update_policies.ClientOption) (*sensor_update_policies.GetSensorUpdatePoliciesV2OK, error) + QuerySensorUpdatePolicies(params *sensor_update_policies.QuerySensorUpdatePoliciesParams, opts ...sensor_update_policies.ClientOption) (*sensor_update_policies.QuerySensorUpdatePoliciesOK, error) +} + +type tagRegistry interface { + LastContainerTag(ctx context.Context, sensorType falcon.SensorType, versionRequested *string) (string, error) + LastNodeTag(ctx context.Context, versionRequested *string) (string, error) +} diff --git a/internal/controller/common/sensor/images_test.go b/internal/controller/common/sensor/images_test.go new file mode 100644 index 00000000..d59ad6a5 --- /dev/null +++ b/internal/controller/common/sensor/images_test.go @@ -0,0 +1,199 @@ +package sensor + +import ( + "context" + "errors" + "testing" + + "github.com/crowdstrike/falcon-operator/internal/apitest" + "github.com/crowdstrike/gofalcon/falcon" + "github.com/crowdstrike/gofalcon/falcon/client/sensor_update_policies" + "github.com/crowdstrike/gofalcon/falcon/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetPreferredImage(t *testing.T) { + ctx := context.Background() + + runner := func(t apitest.Test) { + m := &mockFalcon{Mock: *t.GetMock()} + images := ImageRepository{ + api: m, + tags: m, + } + + image, err := images.GetPreferredImage( + ctx, + t.GetInput(0).(falcon.SensorType), + t.GetStringPointerInput(1), + t.GetStringPointerInput(2), + ) + t.AssertExpectations(image, err) + } + + noError := error(nil) + noUpdatePolicyRequested := (*string)(nil) + noVersionRequested := (*string)(nil) + + const policyDisabled = false + const policyEnabled = true + + apitest.NewTest("latestVersion"). + WithInputs(falcon.SidecarSensor, noVersionRequested, noUpdatePolicyRequested). + ExpectOutputs("someImageTag", noError). + WithMockCall(newLastContainerTagCall(ctx, falcon.SidecarSensor, noVersionRequested, "someImageTag", noError)). + Run(t, runner) + + apitest.NewTest("latestNodeSensorVersion"). + WithInputs(falcon.NodeSensor, noVersionRequested, noUpdatePolicyRequested). + ExpectOutputs("someNodeImageTag", noError). + WithMockCall(newLastNodeTagCall(ctx, noVersionRequested, "someNodeImageTag", noError)). + Run(t, runner) + + apitest.NewTest("specificVersion"). + WithInputs(falcon.SidecarSensor, stringPointer("someSpecificVersion"), noUpdatePolicyRequested). + ExpectOutputs("imageByVersion", noError). + WithMockCall(newLastContainerTagCall(ctx, falcon.SidecarSensor, stringPointer("someSpecificVersion"), "imageByVersion", noError)). + Run(t, runner) + + apitest.NewTest("versionByPolicy"). + WithInputs(falcon.SidecarSensor, noVersionRequested, stringPointer("somePolicyName")). + ExpectOutputs("imageByPolicy", noError). + WithMockCall(newQuerySensorUpdatePoliciesCall("somePolicyName", "somePolicyID", noError)). + WithMockCall(newGetSensorUpdatePoliciesCall("somePolicyID", "1.2.3", policyEnabled, noError)). + WithMockCall(newLastContainerTagCall(ctx, falcon.SidecarSensor, stringPointer("1.2"), "imageByPolicy", noError)). + Run(t, runner) + + apitest.NewTest("querySensorUpdatePoliciesFails"). + WithInputs(falcon.SidecarSensor, noVersionRequested, stringPointer("somePolicyName")). + ExpectOutputs("", assert.AnError). + WithMockCall(newQuerySensorUpdatePoliciesCall("somePolicyName", "", assert.AnError)). + Run(t, runner) + + apitest.NewTest("getSensorUpdatePoliciesFails"). + WithInputs(falcon.SidecarSensor, noVersionRequested, stringPointer("somePolicyName")). + ExpectOutputs("", assert.AnError). + WithMockCall(newQuerySensorUpdatePoliciesCall("somePolicyName", "somePolicyID", noError)). + WithMockCall(newGetSensorUpdatePoliciesCall("somePolicyID", "", policyDisabled, assert.AnError)). + Run(t, runner) + + apitest.NewTest("policyNameNotFound"). + WithInputs(falcon.SidecarSensor, noVersionRequested, stringPointer("somePolicyName")). + ExpectOutputs("", errors.New("update-policy somePolicyName not found")). + WithMockCall(newQuerySensorUpdatePoliciesCall("somePolicyName", "", noError)). + Run(t, runner) + + apitest.NewTest("policyIDNotFound"). + WithInputs(falcon.SidecarSensor, noVersionRequested, stringPointer("somePolicyName")). + ExpectOutputs("", errors.New("update-policy with ID somePolicyID not found")). + WithMockCall(newQuerySensorUpdatePoliciesCall("somePolicyName", "somePolicyID", noError)). + WithMockCall(newGetSensorUpdatePoliciesCall("somePolicyID", "", policyDisabled, noError)). + Run(t, runner) + + apitest.NewTest("policyDisabled"). + WithInputs(falcon.SidecarSensor, noVersionRequested, stringPointer("somePolicyName")). + ExpectOutputs("", errors.New("update-policy with ID somePolicyID is disabled")). + WithMockCall(newQuerySensorUpdatePoliciesCall("somePolicyName", "somePolicyID", noError)). + WithMockCall(newGetSensorUpdatePoliciesCall("somePolicyID", "1.2.3", policyDisabled, noError)). + Run(t, runner) + + apitest.NewTest("invalidSensorVersion"). + WithInputs(falcon.SidecarSensor, noVersionRequested, stringPointer("somePolicyName")). + ExpectOutputs("", errors.New("update-policy with ID somePolicyID has an invalid sensor version")). + WithMockCall(newQuerySensorUpdatePoliciesCall("somePolicyName", "somePolicyID", noError)). + WithMockCall(newGetSensorUpdatePoliciesCall("somePolicyID", "1.2", policyEnabled, noError)). + Run(t, runner) + + apitest.NewTest("lastContainerTagFails"). + WithInputs(falcon.SidecarSensor, noVersionRequested, noUpdatePolicyRequested). + ExpectOutputs("", assert.AnError). + WithMockCall(newLastContainerTagCall(ctx, falcon.SidecarSensor, noVersionRequested, "", assert.AnError)). + Run(t, runner) + + apitest.NewTest("lastNodeTagFails"). + WithInputs(falcon.NodeSensor, noVersionRequested, noUpdatePolicyRequested). + ExpectOutputs("", assert.AnError). + WithMockCall(newLastNodeTagCall(ctx, noVersionRequested, "", assert.AnError)). + Run(t, runner) +} + +type mockFalcon struct { + mock.Mock +} + +func (m *mockFalcon) GetSensorUpdatePoliciesV2(params *sensor_update_policies.GetSensorUpdatePoliciesV2Params, opts ...sensor_update_policies.ClientOption) (*sensor_update_policies.GetSensorUpdatePoliciesV2OK, error) { + args := m.Called(params, opts) + return args.Get(0).(*sensor_update_policies.GetSensorUpdatePoliciesV2OK), args.Error(1) +} + +func (m *mockFalcon) LastContainerTag(ctx context.Context, sensorType falcon.SensorType, versionRequested *string) (string, error) { + args := m.Called(ctx, sensorType, versionRequested) + return args.String(0), args.Error(1) +} + +func (m *mockFalcon) LastNodeTag(ctx context.Context, versionRequested *string) (string, error) { + args := m.Called(ctx, versionRequested) + return args.String(0), args.Error(1) +} + +func (m *mockFalcon) QuerySensorUpdatePolicies(params *sensor_update_policies.QuerySensorUpdatePoliciesParams, opts ...sensor_update_policies.ClientOption) (*sensor_update_policies.QuerySensorUpdatePoliciesOK, error) { + args := m.Called(params, opts) + return args.Get(0).(*sensor_update_policies.QuerySensorUpdatePoliciesOK), args.Error(1) +} + +func newGetSensorUpdatePoliciesCall(policyID string, expectedVersion string, expectedStatus bool, expectedError error) *mock.Mock { + params := sensor_update_policies.NewGetSensorUpdatePoliciesV2Params().WithIds([]string{policyID}) + + payload := &models.SensorUpdateRespV2{} + if expectedVersion != "" { + payload.Resources = []*models.SensorUpdatePolicyV2{ + { + Enabled: &expectedStatus, + Settings: &models.SensorUpdateSettingsRespV2{ + SensorVersion: stringPointer(expectedVersion), + }, + }, + } + } + + m := &mock.Mock{} + m.On("GetSensorUpdatePoliciesV2", params, []sensor_update_policies.ClientOption(nil)). + Return(&sensor_update_policies.GetSensorUpdatePoliciesV2OK{Payload: payload}, expectedError) + return m +} + +func newLastContainerTagCall(ctx context.Context, sensorType falcon.SensorType, versionRequested *string, expectedImage string, expectedError error) *mock.Mock { + m := &mock.Mock{} + m.On("LastContainerTag", ctx, sensorType, versionRequested).Return(expectedImage, expectedError) + return m +} + +func newLastNodeTagCall(ctx context.Context, versionRequested *string, expectedImage string, expectedError error) *mock.Mock { + m := &mock.Mock{} + m.On("LastNodeTag", ctx, versionRequested).Return(expectedImage, expectedError) + return m +} + +func newQuerySensorUpdatePoliciesCall(updatePolicyRequested string, expectedPolicyID string, expectedError error) *mock.Mock { + filter := falconFilter{}. + addClause("platform_name", "Linux"). + addClause("name.raw", updatePolicyRequested). + encode() + + params := sensor_update_policies.NewQuerySensorUpdatePoliciesParams().WithFilter(&filter) + + payload := &models.MsaQueryResponse{} + if expectedPolicyID != "" { + payload.Resources = []string{expectedPolicyID} + } + + m := &mock.Mock{} + m.On("QuerySensorUpdatePolicies", params, []sensor_update_policies.ClientOption(nil)). + Return(&sensor_update_policies.QuerySensorUpdatePoliciesOK{Payload: payload}, expectedError) + return m +} + +func stringPointer(s string) *string { + return &s +} diff --git a/internal/controller/common/utils.go b/internal/controller/common/utils.go index 91f112e4..29f189d8 100644 --- a/internal/controller/common/utils.go +++ b/internal/controller/common/utils.go @@ -2,11 +2,11 @@ package common import ( "context" + "errors" "fmt" "sort" "strings" - "github.com/crowdstrike/falcon-operator/api/falcon/v1alpha1" falconv1alpha1 "github.com/crowdstrike/falcon-operator/api/falcon/v1alpha1" "github.com/go-logr/logr" "golang.org/x/exp/slices" @@ -20,7 +20,9 @@ import ( "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 { +var ErrNoWebhookServicePodReady = errors.New("no webhook service pod found in a Ready state") + +func Create(r client.Client, sch *runtime.Scheme, ctx context.Context, req ctrl.Request, log logr.Logger, falconObject client.Object, falconStatus *falconv1alpha1.FalconCRStatus, obj runtime.Object) error { switch o := obj.(type) { case client.Object: name := o.GetName() @@ -67,11 +69,11 @@ func Create(r client.Client, sch *runtime.Scheme, ctx context.Context, req ctrl. return nil default: - return fmt.Errorf("Unrecognized kubernetes object type: %T", obj) + 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 { +func Update(r client.Client, ctx context.Context, req ctrl.Request, log logr.Logger, falconObject client.Object, falconStatus *falconv1alpha1.FalconCRStatus, obj runtime.Object) error { switch o := obj.(type) { case client.Object: name := o.GetName() @@ -112,11 +114,11 @@ func Update(r client.Client, ctx context.Context, req ctrl.Request, log logr.Log return nil default: - return fmt.Errorf("Unrecognized kubernetes object type: %T", obj) + 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 { +func Delete(r client.Client, ctx context.Context, req ctrl.Request, log logr.Logger, falconObject client.Object, falconStatus *falconv1alpha1.FalconCRStatus, obj runtime.Object) error { switch o := obj.(type) { case client.Object: name := o.GetName() @@ -157,12 +159,12 @@ func Delete(r client.Client, ctx context.Context, req ctrl.Request, log logr.Log return nil default: - return fmt.Errorf("Unrecognized kubernetes object type: %T", obj) + 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 { +func ConditionsUpdate(r client.Client, ctx context.Context, req ctrl.Request, log logr.Logger, falconObject client.Object, falconStatus *falconv1alpha1.FalconCRStatus, falconCondition metav1.Condition) error { if !meta.IsStatusConditionPresentAndEqual(falconStatus.Conditions, falconCondition.Type, falconCondition.Status) { fgvk := falconObject.GetObjectKind().GroupVersionKind() @@ -233,7 +235,7 @@ func GetReadyPod(r client.Client, ctx context.Context, namespace string, matchin } } - return &corev1.Pod{}, fmt.Errorf("No webhook service pod found in a Ready state") + return &corev1.Pod{}, ErrNoWebhookServicePodReady } func GetDeployment(r client.Client, ctx context.Context, namespace string, matchingLabels client.MatchingLabels) (*appsv1.Deployment, error) { diff --git a/internal/controller/common/utils_test.go b/internal/controller/common/utils_test.go index 984d3d2c..073dbab0 100644 --- a/internal/controller/common/utils_test.go +++ b/internal/controller/common/utils_test.go @@ -80,9 +80,8 @@ func TestGetReadyPod(t *testing.T) { testLabel := map[string]string{"testLabel": "testPod"} - wantErr := "No webhook service pod found in a Ready state" _, gotErr := GetReadyPod(fakeClient, ctx, "test-namespace", testLabel) - if diff := cmp.Diff(wantErr, gotErr.Error()); diff != "" { + if diff := cmp.Diff(ErrNoWebhookServicePodReady.Error(), gotErr.Error()); diff != "" { t.Errorf("GetReadyPod() mismatch (-want +got):\n%s", diff) } diff --git a/internal/controller/falcon_container/falconcontainer_controller.go b/internal/controller/falcon_container/falconcontainer_controller.go index 8b4417dd..2f73c16d 100644 --- a/internal/controller/falcon_container/falconcontainer_controller.go +++ b/internal/controller/falcon_container/falconcontainer_controller.go @@ -86,7 +86,7 @@ func (r *FalconContainerReconciler) Reconcile(ctx context.Context, req ctrl.Requ return ctrl.Result{}, err } - if falconContainer.Status.Conditions == nil || len(falconContainer.Status.Conditions) == 0 { + if len(falconContainer.Status.Conditions) == 0 { err := r.StatusUpdate(ctx, req, log, falconContainer, falconv1alpha1.ConditionPending, metav1.ConditionFalse, falconv1alpha1.ReasonReqNotMet, @@ -269,7 +269,7 @@ func (r *FalconContainerReconciler) Reconcile(ctx context.Context, req ctrl.Requ } pod, err := k8sutils.GetReadyPod(r.Client, ctx, falconContainer.Spec.InstallNamespace, map[string]string{common.FalconComponentKey: common.FalconSidecarSensor}) - if err != nil && err.Error() != "No webhook service pod found in a Ready state" { + if err != nil && err != k8sutils.ErrNoWebhookServicePodReady { err = r.StatusUpdate(ctx, req, log, falconContainer, falconv1alpha1.ConditionFailed, metav1.ConditionFalse, "Reconciling", fmt.Sprintf("failed to find Ready injector pod: %v", err)) if err != nil { return ctrl.Result{}, err diff --git a/internal/controller/falcon_container/falconcontainer_controller_test.go b/internal/controller/falcon_container/falconcontainer_controller_test.go index c1be0322..70344af2 100644 --- a/internal/controller/falcon_container/falconcontainer_controller_test.go +++ b/internal/controller/falcon_container/falconcontainer_controller_test.go @@ -149,7 +149,7 @@ var _ = Describe("FalconContainer controller", func() { By("Checking if pods were successfully created in the reconciliation") Eventually(func() error { pod, err := k8sutils.GetReadyPod(k8sClient, ctx, SidecarSensorNamespace, map[string]string{common.FalconComponentKey: common.FalconSidecarSensor}) - if err != nil && err.Error() != "No webhook service pod found in a Ready state" { + if err != nil && err != k8sutils.ErrNoWebhookServicePodReady { return err } if pod.Name == "" { @@ -162,7 +162,7 @@ var _ = Describe("FalconContainer controller", func() { By("Checking the latest Status Condition added to the FalconContainer instance") Eventually(func() error { - if falconContainer.Status.Conditions != nil && len(falconContainer.Status.Conditions) != 0 { + if len(falconContainer.Status.Conditions) != 0 { latestStatusCondition := falconContainer.Status.Conditions[len(falconContainer.Status.Conditions)-1] expectedLatestStatusCondition := metav1.Condition{Type: falconv1alpha1.ConditionDeploymentReady, Status: metav1.ConditionTrue, Reason: falconv1alpha1.ReasonInstallSucceeded, diff --git a/internal/controller/falcon_container/image_push.go b/internal/controller/falcon_container/image_push.go index 42f8cfad..e7f62138 100644 --- a/internal/controller/falcon_container/image_push.go +++ b/internal/controller/falcon_container/image_push.go @@ -10,13 +10,13 @@ import ( "k8s.io/client-go/util/retry" falconv1alpha1 "github.com/crowdstrike/falcon-operator/api/falcon/v1alpha1" + "github.com/crowdstrike/falcon-operator/internal/controller/common/sensor" "github.com/crowdstrike/falcon-operator/internal/controller/image" "github.com/crowdstrike/falcon-operator/pkg/aws" "github.com/crowdstrike/falcon-operator/pkg/common" "github.com/crowdstrike/falcon-operator/pkg/gcp" "github.com/crowdstrike/falcon-operator/pkg/k8s_utils" "github.com/crowdstrike/falcon-operator/pkg/registry/auth" - "github.com/crowdstrike/falcon-operator/pkg/registry/falcon_registry" "github.com/crowdstrike/falcon-operator/pkg/registry/pushtoken" "github.com/crowdstrike/gofalcon/falcon" "github.com/go-logr/logr" @@ -202,13 +202,13 @@ func (r *FalconContainerReconciler) setImageTag(ctx context.Context, falconConta return *falconContainer.Status.Sensor, r.Client.Status().Update(ctx, falconContainer) } - // Otherwise, get the newest version matching the requested version string - registry, err := falcon_registry.NewFalconRegistry(ctx, r.falconApiConfig(ctx, falconContainer)) + apiConfig := r.falconApiConfig(ctx, falconContainer) + imageRepo, err := sensor.NewImageRepository(ctx, apiConfig) if err != nil { return "", err } - tag, err := registry.LastContainerTag(ctx, falcon.SidecarSensor, falconContainer.Spec.Version) + tag, err := imageRepo.GetPreferredImage(ctx, falcon.SidecarSensor, falconContainer.Spec.Version, falconContainer.Spec.Unsafe.UpdatePolicy) if err == nil { falconContainer.Status.Sensor = common.ImageVersion(tag) } diff --git a/internal/controller/falcon_image_analyzer/falconimage_controller.go b/internal/controller/falcon_image_analyzer/falconimage_controller.go index 93bedfaf..37356358 100644 --- a/internal/controller/falcon_image_analyzer/falconimage_controller.go +++ b/internal/controller/falcon_image_analyzer/falconimage_controller.go @@ -115,7 +115,7 @@ func (r *FalconImageAnalyzerReconciler) Reconcile(ctx context.Context, req ctrl. } // Let's just set the status as Unknown when no status is available - if falconImageAnalyzer.Status.Conditions == nil || len(falconImageAnalyzer.Status.Conditions) == 0 { + if len(falconImageAnalyzer.Status.Conditions) == 0 { meta.SetStatusCondition(&falconImageAnalyzer.Status.Conditions, metav1.Condition{Type: falconv1alpha1.ConditionPending, Status: metav1.ConditionUnknown, Reason: "Reconciling", Message: "Starting reconciliation"}) if err = r.Status().Update(ctx, falconImageAnalyzer); err != nil { log.Error(err, "Failed to update FalconImageAnalyzer status") diff --git a/internal/controller/falcon_node/falconnodesensor_controller.go b/internal/controller/falcon_node/falconnodesensor_controller.go index 2b3d3cce..6effd25c 100644 --- a/internal/controller/falcon_node/falconnodesensor_controller.go +++ b/internal/controller/falcon_node/falconnodesensor_controller.go @@ -103,7 +103,7 @@ func (r *FalconNodeSensorReconciler) Reconcile(ctx context.Context, req ctrl.Req return ctrl.Result{}, err } logger.Error(nil, "FalconNodeSensor is attempting to install in a namespace with existing pods. Please update the CR configuration to a namespace that does not have workoads already running.") - return ctrl.Result{}, err + return ctrl.Result{}, nil } dsCondition := meta.FindStatusCondition(nodesensor.Status.Conditions, falconv1alpha1.ConditionSuccess) @@ -181,7 +181,7 @@ func (r *FalconNodeSensorReconciler) Reconcile(ctx context.Context, req ctrl.Req } logger.Error(err, "error handling configmap") - return ctrl.Result{}, err + return ctrl.Result{}, nil } if sensorConf == nil { err = r.conditionsUpdate(falconv1alpha1.ConditionConfigMapReady, diff --git a/internal/controller/falcon_node/falconnodesensor_controller_test.go b/internal/controller/falcon_node/falconnodesensor_controller_test.go index f7ee00b8..9685469c 100644 --- a/internal/controller/falcon_node/falconnodesensor_controller_test.go +++ b/internal/controller/falcon_node/falconnodesensor_controller_test.go @@ -127,7 +127,7 @@ var _ = Describe("FalconNodeSensor controller", func() { By("Checking the latest Status Condition added to the FalconNodeSensor instance") Eventually(func() error { - if falconNode.Status.Conditions != nil && len(falconNode.Status.Conditions) != 0 { + if len(falconNode.Status.Conditions) != 0 { latestStatusCondition := falconNode.Status.Conditions[len(falconNode.Status.Conditions)-1] expectedLatestStatusCondition := metav1.Condition{Type: falconv1alpha1.ConditionDaemonSetReady, Status: metav1.ConditionTrue, Reason: falconv1alpha1.ReasonInstallSucceeded, diff --git a/pkg/node/config_cache.go b/pkg/node/config_cache.go index 933db7a5..b90129df 100644 --- a/pkg/node/config_cache.go +++ b/pkg/node/config_cache.go @@ -2,11 +2,13 @@ package node import ( "context" + "errors" "fmt" "os" "strings" falconv1alpha1 "github.com/crowdstrike/falcon-operator/api/falcon/v1alpha1" + "github.com/crowdstrike/falcon-operator/internal/controller/common/sensor" "github.com/crowdstrike/falcon-operator/pkg/common" "github.com/crowdstrike/falcon-operator/pkg/falcon_api" "github.com/crowdstrike/falcon-operator/pkg/registry/falcon_registry" @@ -15,6 +17,8 @@ import ( "github.com/go-logr/logr" ) +var ErrFalconAPINotConfigured = errors.New("missing falcon_api configuration") + // ConfigCache holds config values for node sensor. Those values are either provided by user or fetched dynamically. That happens transparently to the caller. type ConfigCache struct { cid string @@ -46,7 +50,7 @@ func (cc *ConfigCache) GetImageURI(ctx context.Context, logger logr.Logger) (str func (cc *ConfigCache) GetPullToken(ctx context.Context) ([]byte, error) { if cc.nodesensor.Spec.FalconAPI == nil { - return nil, fmt.Errorf("Missing falcon_api configuration") + return nil, ErrFalconAPINotConfigured } return pulltoken.CrowdStrike(ctx, cc.nodesensor.Spec.FalconAPI.ApiConfig()) } @@ -97,7 +101,7 @@ func getFalconImage(ctx context.Context, nodesensor *falconv1alpha1.FalconNodeSe } if nodesensor.Spec.FalconAPI == nil { - return "", fmt.Errorf("Missing falcon_api configuration") + return "", ErrFalconAPINotConfigured } cloud, err := nodesensor.Spec.FalconAPI.FalconCloud(ctx) @@ -110,11 +114,14 @@ func getFalconImage(ctx context.Context, nodesensor *falconv1alpha1.FalconNodeSe return fmt.Sprintf("%s:%s", imageUri, *nodesensor.Status.Sensor), nil } - registry, err := falcon_registry.NewFalconRegistry(ctx, nodesensor.Spec.FalconAPI.ApiConfig()) + apiConfig := nodesensor.Spec.FalconAPI.ApiConfig() + apiConfig.Context = ctx + imageRepo, err := sensor.NewImageRepository(ctx, apiConfig) if err != nil { return "", err } - imageTag, err := registry.LastNodeTag(ctx, nodesensor.Spec.Node.Version) + + imageTag, err := imageRepo.GetPreferredImage(ctx, falcon.NodeSensor, nodesensor.Spec.Node.Version, nodesensor.Spec.Node.Unsafe.UpdatePolicy) if err != nil { return "", err } diff --git a/pkg/node/config_cache_test.go b/pkg/node/config_cache_test.go index 76c546d0..498381f3 100644 --- a/pkg/node/config_cache_test.go +++ b/pkg/node/config_cache_test.go @@ -50,7 +50,7 @@ func TestGetImageURI(t *testing.T) { config.imageUri = "" got, err := config.GetImageURI(context.Background(), logger) if err != nil { - if err.Error() != "Missing falcon_api configuration" { + if err != ErrFalconAPINotConfigured { t.Errorf("GetImageURI() error: %v", err) } } @@ -75,7 +75,7 @@ func TestGetImageURI(t *testing.T) { func TestGetPullToken(t *testing.T) { got, err := config.GetPullToken(context.Background()) if err != nil { - if err.Error() != "Missing falcon_api configuration" { + if err != ErrFalconAPINotConfigured { t.Errorf("GetPullToken() error: %v", err) } } @@ -168,7 +168,7 @@ func TestGetFalconImage(t *testing.T) { falconNode.Spec.FalconAPI = nil _, err = getFalconImage(context.Background(), &falconNode) if err != nil { - if err.Error() != "Missing falcon_api configuration" { + if err != ErrFalconAPINotConfigured { t.Errorf("getFalconImage() error: %v", err) } }