diff --git a/api/v1/installation_types.go b/api/v1/installation_types.go index 7e319cdaa5..b0bde4360f 100644 --- a/api/v1/installation_types.go +++ b/api/v1/installation_types.go @@ -156,6 +156,23 @@ type InstallationSpec struct { // +optional FlexVolumePath string `json:"flexVolumePath,omitempty"` + // CalicoRunHostPath optionally specifies the host path mounted into calico-node containers at + // /var/run/calico. Environments such as microk8s place Calico runtime state under a non-standard + // host directory (for example /var/snap/microk8s/current/var/run/calico); set this field so the + // operator continues using that path after a manifest-to-operator migration. + // +optional + // +kubebuilder:default:="/var/run/calico" + // +kubebuilder:validation:MaxLength=1024 + CalicoRunHostPath string `json:"calicoRunHostPath,omitempty"` + + // CalicoLibHostPath optionally specifies the host path mounted into calico-node containers at + // /var/lib/calico. Pair this with CalicoRunHostPath when Calico data lives under a non-standard + // host directory (for example microk8s uses /var/snap/microk8s/current/var/lib/calico). + // +optional + // +kubebuilder:default:="/var/lib/calico" + // +kubebuilder:validation:MaxLength=1024 + CalicoLibHostPath string `json:"calicoLibHostPath,omitempty"` + // KubeletVolumePluginPath optionally specifies enablement of Calico CSI plugin. If not specified, // CSI will be enabled by default. If set to 'None', CSI will be disabled. // Default: /var/lib/kubelet diff --git a/pkg/controller/migration/convert/core.go b/pkg/controller/migration/convert/core.go index f56f46c26e..1b153f2f13 100644 --- a/pkg/controller/migration/convert/core.go +++ b/pkg/controller/migration/convert/core.go @@ -140,10 +140,12 @@ func handleCore(c *components, install *operatorv1.Installation) error { if err := checkNodeHostPathVolume(c.node.Spec.Template.Spec, "lib-modules", "/lib/modules"); err != nil { return err } - if err := checkNodeHostPathVolume(c.node.Spec.Template.Spec, "var-run-calico", "/var/run/calico"); err != nil { + // var-run-calico / var-lib-calico may use non-default hostPaths (e.g. microk8s snap paths). + // Capture them onto Installation so the operator continues rendering the same host paths. + if err := handleCalicoHostPathVolume(c.node.Spec.Template.Spec, "var-run-calico", "/var/run/calico", &install.Spec.CalicoRunHostPath); err != nil { return err } - if err := checkNodeHostPathVolume(c.node.Spec.Template.Spec, "var-lib-calico", "/var/lib/calico"); err != nil { + if err := handleCalicoHostPathVolume(c.node.Spec.Template.Spec, "var-lib-calico", "/var/lib/calico", &install.Spec.CalicoLibHostPath); err != nil { return err } if err := checkNodeHostPathVolume(c.node.Spec.Template.Spec, "xtables-lock", "/run/xtables.lock"); err != nil { @@ -221,6 +223,36 @@ func checkNodeHostPathVolume(spec corev1.PodSpec, name, path string) error { return nil } +// handleCalicoHostPathVolume verifies that a hostPath volume with the given name exists. +// When the host path matches defaultPath, dest is left empty so the operator uses its default. +// When the host path differs (for example microk8s snap-prefixed paths that still end with the +// standard suffix), dest is set so the operator continues using the existing path after migration. +func handleCalicoHostPathVolume(spec corev1.PodSpec, name, defaultPath string, dest *string) error { + v := getVolume(spec, name) + if v == nil || v.HostPath == nil || v.HostPath.Path == "" { + return ErrIncompatibleCluster{ + err: fmt.Sprintf("missing expected volume '%s' with hostPath '%s'", name, defaultPath), + component: ComponentCalicoNode, + fix: fmt.Sprintf("add the expected volume to %s", ComponentCalicoNode), + } + } + path := v.HostPath.Path + // Accept the standard path, or a path that ends with it (microk8s: /var/snap/.../var/run/calico). + if path != defaultPath && !strings.HasSuffix(path, defaultPath) { + return ErrIncompatibleCluster{ + err: fmt.Sprintf("volume '%s' has unsupported hostPath '%s' (expected '%s' or a path ending with it)", + name, path, defaultPath), + component: ComponentCalicoNode, + fix: fmt.Sprintf("set volume '%s' hostPath to '%s' (or a path ending with '%s'), or set Installation.spec accordingly", + name, defaultPath, defaultPath), + } + } + if path != defaultPath { + *dest = path + } + return nil +} + // addResources adds the rescReq resource for the specified component if none was previously set. If installation // already had a resource for compName then they are compared and if they are different then an error is returned. // If the Resource is added to installation or the existing one matches then nil is returned. diff --git a/pkg/controller/migration/convert/core_test.go b/pkg/controller/migration/convert/core_test.go index 049dba6b22..4277d1c761 100644 --- a/pkg/controller/migration/convert/core_test.go +++ b/pkg/controller/migration/convert/core_test.go @@ -564,6 +564,39 @@ var _ = Describe("core handler", func() { }) }) + Context("calico host paths", func() { + It("should accept default var-run/var-lib calico hostPaths without setting Installation fields", func() { + Expect(handleCore(&comps, i)).ToNot(HaveOccurred()) + Expect(i.Spec.CalicoRunHostPath).To(BeEmpty()) + Expect(i.Spec.CalicoLibHostPath).To(BeEmpty()) + }) + + It("should accept microk8s-style hostPaths and record them on Installation", func() { + for idx, vol := range comps.node.Spec.Template.Spec.Volumes { + switch vol.Name { + case "var-run-calico": + comps.node.Spec.Template.Spec.Volumes[idx].HostPath.Path = "/var/snap/microk8s/current/var/run/calico" + case "var-lib-calico": + comps.node.Spec.Template.Spec.Volumes[idx].HostPath.Path = "/var/snap/microk8s/current/var/lib/calico" + } + } + Expect(handleCore(&comps, i)).ToNot(HaveOccurred()) + Expect(i.Spec.CalicoRunHostPath).To(Equal("/var/snap/microk8s/current/var/run/calico")) + Expect(i.Spec.CalicoLibHostPath).To(Equal("/var/snap/microk8s/current/var/lib/calico")) + }) + + It("should reject hostPaths that do not end with the expected suffix", func() { + for idx, vol := range comps.node.Spec.Template.Spec.Volumes { + if vol.Name == "var-run-calico" { + comps.node.Spec.Template.Spec.Volumes[idx].HostPath.Path = "/opt/other/calico-run" + } + } + err := handleCore(&comps, i) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("var-run-calico")) + }) + }) + Context("cni", func() { It("should not raise an error if CNI_CONF_NAME is 10-calico.conflist", func() { comps.node.Spec.Template.Spec.InitContainers[0].Env = []v1.EnvVar{{ diff --git a/pkg/imports/crds/operator/operator.tigera.io_installations.yaml b/pkg/imports/crds/operator/operator.tigera.io_installations.yaml index 7d5eed4f43..d63613ba77 100644 --- a/pkg/imports/crds/operator/operator.tigera.io_installations.yaml +++ b/pkg/imports/crds/operator/operator.tigera.io_installations.yaml @@ -3039,6 +3039,23 @@ spec: type: object type: object type: object + calicoLibHostPath: + default: /var/lib/calico + description: |- + CalicoLibHostPath optionally specifies the host path mounted into calico-node containers at + /var/lib/calico. Pair this with CalicoRunHostPath when Calico data lives under a non-standard + host directory (for example microk8s uses /var/snap/microk8s/current/var/lib/calico). + maxLength: 1024 + type: string + calicoRunHostPath: + default: /var/run/calico + description: |- + CalicoRunHostPath optionally specifies the host path mounted into calico-node containers at + /var/run/calico. Environments such as microk8s place Calico runtime state under a non-standard + host directory (for example /var/snap/microk8s/current/var/run/calico); set this field so the + operator continues using that path after a manifest-to-operator migration. + maxLength: 1024 + type: string calicoNodeWindowsDaemonSet: description: CalicoNodeWindowsDaemonSet configures the calico-node-windows @@ -12425,6 +12442,23 @@ spec: type: object type: object type: object + calicoLibHostPath: + default: /var/lib/calico + description: |- + CalicoLibHostPath optionally specifies the host path mounted into calico-node containers at + /var/lib/calico. Pair this with CalicoRunHostPath when Calico data lives under a non-standard + host directory (for example microk8s uses /var/snap/microk8s/current/var/lib/calico). + maxLength: 1024 + type: string + calicoRunHostPath: + default: /var/run/calico + description: |- + CalicoRunHostPath optionally specifies the host path mounted into calico-node containers at + /var/run/calico. Environments such as microk8s place Calico runtime state under a non-standard + host directory (for example /var/snap/microk8s/current/var/run/calico); set this field so the + operator continues using that path after a manifest-to-operator migration. + maxLength: 1024 + type: string calicoNodeWindowsDaemonSet: description: CalicoNodeWindowsDaemonSet configures the calico-node-windows diff --git a/pkg/render/node.go b/pkg/render/node.go index c4fb001d08..d69376e636 100644 --- a/pkg/render/node.go +++ b/pkg/render/node.go @@ -1112,7 +1112,7 @@ func (c *nodeComponent) nodeVolumes() []corev1.Volume { c.cfg.TLS.TrustedBundle.Volume(), c.cfg.TLS.NodeSecret.Volume(), c.varRunCalicoVolume(), - corev1.Volume{Name: "var-lib-calico", VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: "/var/lib/calico", Type: &dirOrCreate}}}, + c.varLibCalicoVolume(), // Volume for the containing directory so that the init container can mount the child bpf directory if needed. corev1.Volume{Name: "sys-fs", VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: "/sys/fs", Type: &dirOrCreate}}}, // Volume for the bpffs itself, used by the main node container. @@ -1128,7 +1128,7 @@ func (c *nodeComponent) nodeVolumes() []corev1.Volume { if c.vppDataplaneEnabled() { volumes = append(volumes, // Volume that contains the felix dataplane binary - corev1.Volume{Name: "felix-plugins", VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: "/var/lib/calico/felix-plugins"}}}, + corev1.Volume{Name: "felix-plugins", VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: filepath.Join(c.calicoLibHostPath(), "felix-plugins")}}}, ) } @@ -1197,12 +1197,36 @@ func (c *nodeComponent) nodeVolumes() []corev1.Volume { return volumes } +func (c *nodeComponent) calicoRunHostPath() string { + if c.cfg.Installation != nil && c.cfg.Installation.CalicoRunHostPath != "" { + return c.cfg.Installation.CalicoRunHostPath + } + return "/var/run/calico" +} + +func (c *nodeComponent) calicoLibHostPath() string { + if c.cfg.Installation != nil && c.cfg.Installation.CalicoLibHostPath != "" { + return c.cfg.Installation.CalicoLibHostPath + } + return "/var/lib/calico" +} + func (c *nodeComponent) varRunCalicoVolume() corev1.Volume { dirOrCreate := corev1.HostPathDirectoryOrCreate return corev1.Volume{ Name: "var-run-calico", VolumeSource: corev1.VolumeSource{ - HostPath: &corev1.HostPathVolumeSource{Path: "/var/run/calico", Type: &dirOrCreate}, + HostPath: &corev1.HostPathVolumeSource{Path: c.calicoRunHostPath(), Type: &dirOrCreate}, + }, + } +} + +func (c *nodeComponent) varLibCalicoVolume() corev1.Volume { + dirOrCreate := corev1.HostPathDirectoryOrCreate + return corev1.Volume{ + Name: "var-lib-calico", + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{Path: c.calicoLibHostPath(), Type: &dirOrCreate}, }, } }