Repository: rancher/fleet
Pull request #2659 (2024-07-22 13:19:20)

Add hostNetwork setting for agent deployment policy
<!-- Specify the issue ID that this pull request is solving -->
Refers to https://github.com/rancher-sandbox/cluster-api-addon-provider-fleet/issues/74
<!-- Make sure that the referenced issue provides steps to reproduce it -->

<!-- Describe the changes introduced by this pull request -->

<!--
  Please provide a unit, integration (`./integrationtests/`) or e2e (`./e2e/`) test if possible.
-->

Fleet agent can be used to deploy bundles during cluster bootstrap procedure. One common operation is to deploy CNI configuration. While it is possible, current fleet-agent config is not getting scheduled on the cluster due to dependency on the CNI itself.

To avoid it, a user may want to specify `spec.hostNetwork` setting on the `Cluster` resource. This allows the init pod to proceed further and register the agent on the management cluster, so it can proceed with applications rollout.

[![asciicast](https://asciinema.org/a/6x8WmsCXJQdDswAwfYHQlaVsj.svg)](https://asciinema.org/a/6x8WmsCXJQdDswAwfYHQlaVsj)


Patch:
diff --git a/charts/fleet-agent/templates/deployment.yaml b/charts/fleet-agent/templates/deployment.yaml
index a9033b28..a13d85ab 100644
--- a/charts/fleet-agent/templates/deployment.yaml
+++ b/charts/fleet-agent/templates/deployment.yaml
@@ -89,6 +89,9 @@ spec:
         - name: kube
           emptyDir: {}
       serviceAccountName: fleet-agent
+      {{- if .Values.fleetAgent.hostNetwork }}
+      hostNetwork: true
+      {{- end }}
       nodeSelector: {{ include "linux-node-selector" . | nindent 8 }}
 {{- if .Values.fleetAgent.nodeSelector }}
 {{ toYaml .Values.fleetAgent.nodeSelector | indent 8 }}
diff --git a/charts/fleet-agent/values.yaml b/charts/fleet-agent/values.yaml
index cca57641..201016ec 100644
--- a/charts/fleet-agent/values.yaml
+++ b/charts/fleet-agent/values.yaml
@@ -44,6 +44,9 @@ fleetAgent:
   nodeSelector: {}
   ## List of node taints to tolerate (requires Kubernetes >= 1.6)
   tolerations: []
+  ## HostNetwork setting for the agent deployment.
+  ## When set allows for provisioning of network related bundles (CNI configuration) in a cluster without CNI.
+  hostNetwork: false
 kubectl:
   ## Node labels for pod assignment
   ## Ref: https://kubernetes.io/docs/user-guide/node-selection/
diff --git a/charts/fleet-crd/templates/crds.yaml b/charts/fleet-crd/templates/crds.yaml
index 1f3776b1..f20173ca 100644
--- a/charts/fleet-crd/templates/crds.yaml
+++ b/charts/fleet-crd/templates/crds.yaml
@@ -5440,6 +5440,13 @@ spec:
                     either be predefined, or generated when importing the cluster.'
                   nullable: true
                   type: string
+                hostNetwork:
+                  description: 'HostNetwork sets the agent StatefulSet to use hostNetwork:
+                    true setting.
+
+                    Allows for provisioning of network related bundles (CNI configuration).'
+                  nullable: true
+                  type: boolean
                 kubeConfigSecret:
                   description: 'KubeConfigSecret is the name of the secret containing
                     the kubeconfig for the downstream cluster.
@@ -5522,6 +5529,11 @@ spec:
                     used to detect changes.
                   nullable: true
                   type: string
+                agentHostNetwork:
+                  description: AgentHostNetwork defines observed state of spec.hostNetwork
+                    setting that is currently used.
+                  nullable: true
+                  type: boolean
                 agentMigrated:
                   description: 'AgentMigrated is always set to true after importing
                     a cluster. If
diff --git a/internal/cmd/controller/agentmanagement/agent/manifest.go b/internal/cmd/controller/agentmanagement/agent/manifest.go
index 8de1c097..b8bb7282 100644
--- a/internal/cmd/controller/agentmanagement/agent/manifest.go
+++ b/internal/cmd/controller/agentmanagement/agent/manifest.go
@@ -37,6 +37,7 @@ type ManifestOptions struct {
 	SystemDefaultRegistry string
 	AgentAffinity         *corev1.Affinity
 	AgentResources        *corev1.ResourceRequirements
+	HostNetwork           bool
 }
 
 // Manifest builds and returns a deployment manifest for the fleet-agent with a
@@ -298,6 +299,9 @@ func agentApp(namespace string, agentScope string, opts ManifestOptions) *appsv1
 	// additional tolerations from cluster
 	app.Spec.Template.Spec.Tolerations = append(app.Spec.Template.Spec.Tolerations, opts.AgentTolerations...)
 
+	// Set hostNetwork
+	app.Spec.Template.Spec.HostNetwork = opts.HostNetwork
+
 	// overwrite affinity if present on cluster
 	if opts.AgentAffinity != nil {
 		app.Spec.Template.Spec.Affinity = opts.AgentAffinity
diff --git a/internal/cmd/controller/agentmanagement/controllers/cluster/import.go b/internal/cmd/controller/agentmanagement/controllers/cluster/import.go
index ecfe9078..4297fbb2 100644
--- a/internal/cmd/controller/agentmanagement/controllers/cluster/import.go
+++ b/internal/cmd/controller/agentmanagement/controllers/cluster/import.go
@@ -1,6 +1,7 @@
 package cluster
 
 import (
+	"cmp"
 	"context"
 	"crypto/sha256"
 	"encoding/json"
@@ -32,6 +33,7 @@ import (
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/rest"
 	"k8s.io/client-go/tools/clientcmd"
+	"k8s.io/utils/ptr"
 )
 
 var (
@@ -323,6 +325,7 @@ func (i *importHandler) importCluster(cluster *fleet.Cluster, status fleet.Clust
 				PrivateRepoURL:   cluster.Spec.PrivateRepoURL,
 				AgentAffinity:    cluster.Spec.AgentAffinity,
 				AgentResources:   cluster.Spec.AgentResources,
+				HostNetwork:      *cmp.Or(cluster.Spec.HostNetwork, ptr.To(false)),
 			},
 		})
 	if err != nil {
diff --git a/internal/cmd/controller/agentmanagement/controllers/manageagent/manageagent.go b/internal/cmd/controller/agentmanagement/controllers/manageagent/manageagent.go
index 095dbf28..e6c3b838 100644
--- a/internal/cmd/controller/agentmanagement/controllers/manageagent/manageagent.go
+++ b/internal/cmd/controller/agentmanagement/controllers/manageagent/manageagent.go
@@ -6,6 +6,7 @@
 package manageagent
 
 import (
+	"cmp"
 	"context"
 	"crypto/sha256"
 	"encoding/json"
@@ -28,6 +29,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/utils/ptr"
 )
 
 const (
@@ -169,6 +171,11 @@ func (h *handler) updateClusterStatus(cluster *fleet.Cluster, status fleet.Clust
 		changed = true
 	}
 
+	if hostNetwork := *cmp.Or(cluster.Spec.HostNetwork, ptr.To(false)); status.AgentHostNetwork != hostNetwork {
+		status.AgentHostNetwork = hostNetwork
+		changed = true
+	}
+
 	if c, hash, err := hashChanged(cluster.Spec.AgentAffinity, status.AgentAffinityHash); err != nil {
 		return status, changed, err
 	} else if c {
@@ -263,6 +270,7 @@ func (h *handler) newAgentBundle(ns string, cluster *fleet.Cluster) (runtime.Obj
 			SystemDefaultRegistry: cfg.SystemDefaultRegistry,
 			AgentAffinity:         cluster.Spec.AgentAffinity,
 			AgentResources:        cluster.Spec.AgentResources,
+			HostNetwork:           *cmp.Or(cluster.Spec.HostNetwork, ptr.To(false)),
 		},
 	)
 	agentYAML, err := yaml.Export(objs...)
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/cluster_types.go b/pkg/apis/fleet.cattle.io/v1alpha1/cluster_types.go
index d50d7ecf..dc1ed40c 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/cluster_types.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/cluster_types.go
@@ -125,6 +125,12 @@ type ClusterSpec struct {
 	// +nullable
 	// AgentResources sets the resources for the cluster's agent deployment.
 	AgentResources *corev1.ResourceRequirements `json:"agentResources,omitempty"`
+
+	// +nullable
+	// +optional
+	// HostNetwork sets the agent StatefulSet to use hostNetwork: true setting.
+	// Allows for provisioning of network related bundles (CNI configuration).
+	HostNetwork *bool `json:"hostNetwork,omitempty"`
 }
 
 type ClusterStatus struct {
@@ -154,6 +160,9 @@ type ClusterStatus struct {
 	// AgentPrivateRepoURL is the private repo URL for the agent that is currently used.
 	// +nullable
 	AgentPrivateRepoURL string `json:"agentPrivateRepoURL,omitempty"`
+	// AgentHostNetwork defines observed state of spec.hostNetwork setting that is currently used.
+	// +nullable
+	AgentHostNetwork bool `json:"agentHostNetwork,omitempty"`
 	// AgentDeployedGeneration is the generation of the agent that is currently deployed.
 	// +nullable
 	AgentDeployedGeneration *int64 `json:"agentDeployedGeneration,omitempty"`
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated.deepcopy.go
index 8ae8fbfb..e64aa508 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated.deepcopy.go
@@ -1059,6 +1059,11 @@ func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) {
 		*out = new(corev1.ResourceRequirements)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.HostNetwork != nil {
+		in, out := &in.HostNetwork, &out.HostNetwork
+		*out = new(bool)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterSpec.


Test patch:
diff --git a/internal/cmd/controller/agentmanagement/agent/manifest_test.go b/internal/cmd/controller/agentmanagement/agent/manifest_test.go
index 1aab9910..e4497c2f 100644
--- a/internal/cmd/controller/agentmanagement/agent/manifest_test.go
+++ b/internal/cmd/controller/agentmanagement/agent/manifest_test.go
@@ -104,6 +104,54 @@ func TestManifestAgentTolerations(t *testing.T) {
 	}
 }
 
+func TestManifestAgentHostNetwork(t *testing.T) {
+	const namespace = "fleet-system"
+	const scope = "test-scope"
+	baseOpts := ManifestOptions{
+		AgentEnvVars:          []corev1.EnvVar{},
+		AgentImage:            "rancher/fleet:1.2.3",
+		AgentImagePullPolicy:  "Always",
+		AgentTolerations:      []corev1.Toleration{},
+		CheckinInterval:       "1s",
+		PrivateRepoURL:        "private.rancher.com:5000",
+		SystemDefaultRegistry: "default.rancher.com",
+	}
+
+	for _, testCase := range []struct {
+		name            string
+		getOpts         func() ManifestOptions
+		expectedNetwork bool
+	}{
+		{
+			name: "DefaultSetting",
+			getOpts: func() ManifestOptions {
+				return baseOpts
+			},
+			expectedNetwork: false,
+		},
+		{
+			name: "With hostNetwork",
+			getOpts: func() ManifestOptions {
+				withHostNetwork := baseOpts
+				withHostNetwork.HostNetwork = true
+				return withHostNetwork
+			},
+			expectedNetwork: true,
+		},
+	} {
+		t.Run(testCase.name, func(t *testing.T) {
+			agent := getAgentFromManifests(namespace, scope, testCase.getOpts())
+			if agent == nil {
+				t.Fatal("there were no deployments returned from the manifests")
+			}
+
+			if !cmp.Equal(agent.Spec.Template.Spec.HostNetwork, testCase.expectedNetwork) {
+				t.Fatalf("hostNetwork is not as expected: %v", agent.Spec.Template.Spec.HostNetwork)
+			}
+		})
+	}
+}
+
 func TestManifestAgentAffinity(t *testing.T) {
 	const namespace = "fleet-system"
 
diff --git a/internal/cmd/controller/agentmanagement/controllers/manageagent/manageagent_test.go b/internal/cmd/controller/agentmanagement/controllers/manageagent/manageagent_test.go
index b865be9b..0bca795e 100644
--- a/internal/cmd/controller/agentmanagement/controllers/manageagent/manageagent_test.go
+++ b/internal/cmd/controller/agentmanagement/controllers/manageagent/manageagent_test.go
@@ -7,6 +7,7 @@ import (
 	"github.com/rancher/wrangler/v3/pkg/generic/fake"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
+	"k8s.io/utils/ptr"
 
 	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 )
@@ -271,3 +272,59 @@ func TestOnClusterChangeTolerations(t *testing.T) {
 		})
 	}
 }
+
+func TestOnClusterChangeHostNetwork(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	namespaces := fake.NewMockNonNamespacedControllerInterface[*corev1.Namespace, *corev1.NamespaceList](ctrl)
+	h := &handler{namespaces: namespaces}
+
+	for _, tt := range []struct {
+		name           string
+		cluster        *fleet.Cluster
+		status         fleet.ClusterStatus
+		expectedStatus fleet.ClusterStatus
+		enqueues       int
+	}{
+		{
+			name:           "Empty",
+			cluster:        &fleet.Cluster{},
+			status:         fleet.ClusterStatus{},
+			expectedStatus: fleet.ClusterStatus{},
+			enqueues:       0,
+		},
+		{
+			name:           "Equal HostNetwork",
+			cluster:        &fleet.Cluster{Spec: fleet.ClusterSpec{HostNetwork: ptr.To(true)}},
+			status:         fleet.ClusterStatus{AgentHostNetwork: true},
+			expectedStatus: fleet.ClusterStatus{AgentHostNetwork: true},
+			enqueues:       0,
+		},
+		{
+			name:           "Changed HostNetwork",
+			cluster:        &fleet.Cluster{Spec: fleet.ClusterSpec{HostNetwork: ptr.To(true)}},
+			status:         fleet.ClusterStatus{AgentHostNetwork: false},
+			expectedStatus: fleet.ClusterStatus{AgentHostNetwork: true},
+			enqueues:       1,
+		},
+		{
+			name:           "Removed Resources",
+			cluster:        &fleet.Cluster{Spec: fleet.ClusterSpec{}},
+			status:         fleet.ClusterStatus{AgentHostNetwork: true},
+			expectedStatus: fleet.ClusterStatus{AgentHostNetwork: false},
+			enqueues:       1,
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			namespaces.EXPECT().Enqueue(gomock.Any()).Times(tt.enqueues)
+
+			status, err := h.onClusterStatusChange(tt.cluster, tt.status)
+			if err != nil {
+				t.Error(err)
+			}
+
+			if status.AgentHostNetwork != tt.expectedStatus.AgentHostNetwork {
+				t.Fatalf("agent hostStatus is not equal: %v vs %v", status.AgentHostNetwork, tt.expectedStatus.AgentHostNetwork)
+			}
+		})
+	}
+}