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

support deployment type #33

Merged
merged 3 commits into from
Feb 17, 2025
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
7 changes: 4 additions & 3 deletions api/v1alpha1/falco_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ import (
type FalcoSpec struct {
// Type specifies the type of Kubernetes resource to deploy Falco.
// Allowed values: "daemonset" or "deployment". Default value is daemonset.
// +kubebuilder:default=daemonset
// +kubebuilder:validation:Enum=daemonset;deployment
// +kubebuilder:default=DaemonSet
// +kubebuilder:validation:Enum=DaemonSet;Deployment
Type string `json:"type,omitempty"`

// Replicas defines the number of replicas for the Deployment.
// Required only when 'type' is "deployment".
// Required only when 'type' is "Deployment".
// Default is 1.
// +kubebuilder:default=1
// +kubebuilder:validation:Minimum=1
// +optional
Replicas *int32 `json:"replicas,omitempty"`
Expand Down
9 changes: 5 additions & 4 deletions config/crd/bases/instance.falcosecurity.dev_falcos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8098,21 +8098,22 @@ spec:
type: object
type: object
replicas:
default: 1
description: |-
Replicas defines the number of replicas for the Deployment.
Required only when 'type' is "deployment".
Required only when 'type' is "Deployment".
Default is 1.
format: int32
minimum: 1
type: integer
type:
default: daemonset
default: DaemonSet
description: |-
Type specifies the type of Kubernetes resource to deploy Falco.
Allowed values: "daemonset" or "deployment". Default value is daemonset.
enum:
- daemonset
- deployment
- DaemonSet
- Deployment
type: string
version:
description: |-
Expand Down
2 changes: 1 addition & 1 deletion internal/controllers/falco/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
}

logger.Info("Generating apply configuration from user input")
applyConfig, err := GenerateDaemonsetApplyConfiguration(ctx, r.Client, falco)
applyConfig, err := generateApplyConfiguration(ctx, r.Client, falco)
if err != nil {
logger.Error(err, "unable to generate apply configuration")
return ctrl.Result{}, err
Expand Down
8 changes: 4 additions & 4 deletions internal/controllers/falco/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,20 +122,20 @@ var _ = Describe("Falco Controller", Ordered, func() {
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
})

It("type should be set to daemonset", func() {
It("type should be set to DaemonSet", func() {
resource := &instancev1alpha1.Falco{}
By("Getting the created resource")
err := k8sClient.Get(ctx, typeNamespacedName, resource)
Expect(err).NotTo(HaveOccurred())
Expect(resource.Spec.Type).To(BeEquivalentTo("daemonset"))
Expect(resource.Spec.Type).To(BeEquivalentTo("DaemonSet"))
})

It("replicas should be set to nil", func() {
It("replicas should be set to 1", func() {
resource := &instancev1alpha1.Falco{}
By("Getting the created resource")
err := k8sClient.Get(ctx, typeNamespacedName, resource)
Expect(err).NotTo(HaveOccurred())
Expect(resource.Spec.Replicas).To(BeNil())
Expect(*resource.Spec.Replicas).To(BeEquivalentTo(1))
})

It("version should be set to empty string", func() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,64 +34,132 @@ import (
"github.com/alacuku/falco-operator/internal/pkg/scheme"
)

func GenerateDaemonsetApplyConfiguration(ctx context.Context, cl client.Client, falco *v1alpha1.Falco) (*unstructured.Unstructured, error) {
// Build the default daemonset.
baseDs := baseDaemonset(falco)
const (
resourceTypeDeployment = "Deployment"
resourceTypeDaemonSet = "DaemonSet"
)

// generateApplyConfiguration generates the apply configuration for the given Kubernetes resource.
func generateApplyConfiguration(ctx context.Context, cl client.Client, falco *v1alpha1.Falco) (*unstructured.Unstructured, error) {
// Determine the resource type from the Falco object.
resourceType := falco.Spec.Type

// Build the default resource.
var baseResource interface{}
switch resourceType {
case resourceTypeDeployment:
baseResource = baseDeployment(falco)
case resourceTypeDaemonSet:
baseResource = baseDaemonSet(falco)
default:
// Should never happen, since the type is validated by the CRD.
return nil, fmt.Errorf("unsupported resource type: %s", resourceType)
}

// Create a parser to merge the base daemonset with the user defined one.
// Create a parser to merge the base resource with the user defined one.
parser := scheme.Parser()

// Parse the base daemonset.
baseTyped, err := parser.Type("io.k8s.api.apps.v1.DaemonSet").FromStructured(baseDs)
// Parse the base resource.
baseTyped, err := parser.Type("io.k8s.api.apps.v1." + resourceType).FromStructured(baseResource)
if err != nil {
return nil, err
}

// Generate the user defined daemonset.
userUnstructured, err := generateUserDefinedDaemonset(falco)
// Generate the user defined resource.
userUnstructured, err := generateUserDefinedResource(falco)
if err != nil {
return nil, err
}

// Parse the user defined daemonset.
userTyped, err := parser.Type("io.k8s.api.apps.v1.DaemonSet").FromUnstructured(userUnstructured.Object)
// Parse the user defined resource.
userTyped, err := parser.Type("io.k8s.api.apps.v1." + resourceType).FromUnstructured(userUnstructured.Object)
if err != nil {
return nil, err
}

// Merge the base and user defined daemonsets.
// Merge the base and user defined resources.
desiredTyped, err := baseTyped.Merge(userTyped)
if err != nil {
return nil, err
}

mergedUnstructured := (desiredTyped.AsValue().Unstructured()).(map[string]interface{})

desiredDsUnstructured := &unstructured.Unstructured{
desiredResourceUnstructured := &unstructured.Unstructured{
Object: mergedUnstructured,
}

if err := setDefaultValues(ctx, cl, desiredDsUnstructured, schema.GroupVersionKind{
if err := setDefaultValues(ctx, cl, desiredResourceUnstructured, schema.GroupVersionKind{
Group: appsv1.GroupName,
Version: "v1",
Kind: "DaemonSet",
Version: appsv1.SchemeGroupVersion.Version,
Kind: resourceType,
}); err != nil {
return nil, err
}

// Set the name of the daemonset to the name of the falco CR.
if err := unstructured.SetNestedField(desiredDsUnstructured.Object, falco.Name, "metadata", "name"); err != nil {
// Set the name of the resource to the name of the falco CR.
if err := unstructured.SetNestedField(desiredResourceUnstructured.Object, falco.Name, "metadata", "name"); err != nil {
return nil, fmt.Errorf("failed to set name field: %w", err)
}

// Remove unwanted fields.
removeUnwantedFields(desiredDsUnstructured)
removeUnwantedFields(desiredResourceUnstructured)

return desiredDsUnstructured, nil
return desiredResourceUnstructured, nil
}

// baseDaemonset returns the base daemonset for Falco with default values + metadata coming from the Falco CR.
func baseDaemonset(falco *v1alpha1.Falco) *appsv1.DaemonSet {
// baseDeployment returns the base deployment for Falco with default values + metadata coming from the Falco CR.
func baseDeployment(falco *v1alpha1.Falco) *appsv1.Deployment {
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: falco.Name,
Namespace: falco.Namespace,
Labels: falco.Labels,
},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app.kubernetes.io/name": falco.Name,
"app.kubernetes.io/instance": falco.Name,
},
},
Replicas: falco.Spec.Replicas,
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: podTemplateSpecLabels(falco.Name, falco.Labels),
},
Spec: corev1.PodSpec{
Tolerations: []corev1.Toleration{
{Key: "node-role.kubernetes.io/master", Effect: corev1.TaintEffectNoSchedule},
{Key: "node-role.kubernetes.io/control-plane", Effect: corev1.TaintEffectNoSchedule},
},
Volumes: DefaultFalcoVolumes,
Containers: []corev1.Container{
{
Name: "falco",
Image: image.BuildFalcoImageStringFromVersion(falco.Spec.Version),
ImagePullPolicy: DefaultFalcoImagePullPolicy,
Resources: DefaultFalcoResources,
Ports: DefaultFalcoPorts,
Args: DefaultFalcoArgs,
Env: DefaultFalcoEnv,
VolumeMounts: DefaultFalcoVolumeMounts,
LivenessProbe: DefaultFalcoLivenessProbe,
ReadinessProbe: DefaultFalcoReadinessProbe,
SecurityContext: DefaultFalcoSecurityContext,
},
},
},
},
Strategy: appsv1.DeploymentStrategy{
Type: appsv1.RollingUpdateDeploymentStrategyType,
},
},
}
}

// baseDaemonSet returns the base daemonset for Falco with default values + metadata coming from the Falco CR.
func baseDaemonSet(falco *v1alpha1.Falco) *appsv1.DaemonSet {
return &appsv1.DaemonSet{
ObjectMeta: metav1.ObjectMeta{
Name: falco.Name,
Expand Down Expand Up @@ -132,45 +200,62 @@ func baseDaemonset(falco *v1alpha1.Falco) *appsv1.DaemonSet {
},
},
},
UpdateStrategy: appsv1.DaemonSetUpdateStrategy{
Type: appsv1.RollingUpdateDaemonSetStrategyType,
},
},
}
}

// generateUserDefinedDaemonset generates a user defined daemonset from the falco CR.
func generateUserDefinedDaemonset(falco *v1alpha1.Falco) (*unstructured.Unstructured, error) {
// Build the default daemonset from the base one.
// generateUserDefinedResource generates a user defined resource from the falco CR.
func generateUserDefinedResource(falco *v1alpha1.Falco) (*unstructured.Unstructured, error) {
// Build the default resource from the base one.
// We use the base one as a starting point to have the same structure and, then we override the user defined fields.
userDs := baseDaemonset(falco)
var userResource interface{}
// Determine the resource type from the Falco object.
resourceType := falco.Spec.Type

switch resourceType {
case resourceTypeDeployment:
userResource = baseDeployment(falco)
case resourceTypeDaemonSet:
userResource = baseDaemonSet(falco)
default:
return nil, fmt.Errorf("unsupported resource type: %s", resourceType)
}

// Set the PodTemplateSpec to the user define one if present, otherwise set it to an empty one.
if falco.Spec.PodTemplateSpec != nil {
userDs.Spec.Template = *falco.Spec.PodTemplateSpec
} else {
userDs.Spec.Template = corev1.PodTemplateSpec{}
// Set the PodTemplateSpec to the user defined one if present, otherwise set it to an empty one.
switch res := userResource.(type) {
case *appsv1.Deployment:
if falco.Spec.PodTemplateSpec != nil {
res.Spec.Template = *falco.Spec.PodTemplateSpec
} else {
res.Spec.Template = corev1.PodTemplateSpec{}
}
case *appsv1.DaemonSet:
if falco.Spec.PodTemplateSpec != nil {
res.Spec.Template = *falco.Spec.PodTemplateSpec
} else {
res.Spec.Template = corev1.PodTemplateSpec{}
}
}

// Convert to unstructured and remove the fields we don't want to compare.
unUserDs, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&userDs)
unUserResource, err := runtime.DefaultUnstructuredConverter.ToUnstructured(userResource)
if err != nil {
return nil, err
}

ds := &unstructured.Unstructured{
Object: unUserDs,
resource := &unstructured.Unstructured{
Object: unUserResource,
}

// Remove the empty containers field if it exists.
if removeEmptyContainers(ds) != nil {
if removeEmptyContainers(resource) != nil {
return nil, err
}

// Remove unwanted fields.
removeUnwantedFields(ds)
removeUnwantedFields(resource)

return ds, nil
return resource, nil
}

// removeEmptyContainers removes the empty containers field from the unstructured DaemonSet if it exists.
Expand Down
Loading