From d298fc7cf1336276adcc0da05d0073d79757e760 Mon Sep 17 00:00:00 2001 From: Dean Chen <862469039@qq.com> Date: Wed, 22 Jul 2026 15:50:18 +0500 Subject: [PATCH 1/2] operator: allow custom calico-node run/lib hostPaths for microk8s migration Manifest installs such as microk8s mount Calico state under snap-prefixed host paths (e.g. /var/snap/microk8s/current/var/run/calico). Migration previously required the exact default hostPaths and failed. Add Installation.spec.calicoNodeRunPath and calicoNodeLibPath, capture compatible non-default paths during migration, and render calico-node volumes using those paths. Refs: projectcalico/calico#10733 Signed-off-by: Dean Chen <862469039@qq.com> --- api/v1/installation_types.go | 17 +++++++++ pkg/controller/migration/convert/core.go | 36 +++++++++++++++++-- pkg/controller/migration/convert/core_test.go | 33 +++++++++++++++++ .../operator.tigera.io_installations.yaml | 34 ++++++++++++++++++ pkg/render/node.go | 30 ++++++++++++++-- 5 files changed, 145 insertions(+), 5 deletions(-) diff --git a/api/v1/installation_types.go b/api/v1/installation_types.go index 7e319cdaa5..1260e67b27 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"` + // CalicoNodeRunPath 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. + // Default: /var/run/calico + // +optional + // +kubebuilder:validation:MaxLength=1024 + CalicoNodeRunPath string `json:"calicoNodeRunPath,omitempty"` + + // CalicoNodeLibPath optionally specifies the host path mounted into calico-node containers at + // /var/lib/calico. Pair this with CalicoNodeRunPath when Calico data lives under a non-standard + // host directory (for example microk8s uses /var/snap/microk8s/current/var/lib/calico). + // Default: /var/lib/calico + // +optional + // +kubebuilder:validation:MaxLength=1024 + CalicoNodeLibPath string `json:"calicoNodeLibPath,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..2bfcc9f6c2 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.CalicoNodeRunPath); 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.CalicoNodeLibPath); 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..905bbf3f6f 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.CalicoNodeRunPath).To(BeEmpty()) + Expect(i.Spec.CalicoNodeLibPath).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.CalicoNodeRunPath).To(Equal("/var/snap/microk8s/current/var/run/calico")) + Expect(i.Spec.CalicoNodeLibPath).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..18c4150784 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 + calicoNodeLibPath: + description: |- + CalicoNodeLibPath optionally specifies the host path mounted into calico-node containers at + /var/lib/calico. Pair this with CalicoNodeRunPath when Calico data lives under a non-standard + host directory (for example microk8s uses /var/snap/microk8s/current/var/lib/calico). + Default: /var/lib/calico + maxLength: 1024 + type: string + calicoNodeRunPath: + description: |- + CalicoNodeRunPath 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. + Default: /var/run/calico + maxLength: 1024 + type: string calicoNodeWindowsDaemonSet: description: CalicoNodeWindowsDaemonSet configures the calico-node-windows @@ -12425,6 +12442,23 @@ spec: type: object type: object type: object + calicoNodeLibPath: + description: |- + CalicoNodeLibPath optionally specifies the host path mounted into calico-node containers at + /var/lib/calico. Pair this with CalicoNodeRunPath when Calico data lives under a non-standard + host directory (for example microk8s uses /var/snap/microk8s/current/var/lib/calico). + Default: /var/lib/calico + maxLength: 1024 + type: string + calicoNodeRunPath: + description: |- + CalicoNodeRunPath 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. + Default: /var/run/calico + 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..557c312525 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.CalicoNodeRunPath != "" { + return c.cfg.Installation.CalicoNodeRunPath + } + return "/var/run/calico" +} + +func (c *nodeComponent) calicoLibHostPath() string { + if c.cfg.Installation != nil && c.cfg.Installation.CalicoNodeLibPath != "" { + return c.cfg.Installation.CalicoNodeLibPath + } + 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}, }, } } From 0d2ffcd6e2246f41038aeedcc6560c94df8c6764 Mon Sep 17 00:00:00 2001 From: Dean Chen <862469039@qq.com> Date: Fri, 24 Jul 2026 17:01:39 +0500 Subject: [PATCH 2/2] operator: rename host path fields to CalicoRunHostPath/CalicoLibHostPath Address review feedback on the microk8s migration host path knobs: - rename CalicoNodeRunPath/CalicoNodeLibPath to CalicoRunHostPath/ CalicoLibHostPath for a clearer, future-proof API name - encode defaults with kubebuilder markers and OpenAPI defaults Signed-off-by: Dean Chen <862469039@qq.com> --- api/v1/installation_types.go | 14 +++++----- pkg/controller/migration/convert/core.go | 4 +-- pkg/controller/migration/convert/core_test.go | 8 +++--- .../operator.tigera.io_installations.yaml | 28 +++++++++---------- pkg/render/node.go | 8 +++--- 5 files changed, 31 insertions(+), 31 deletions(-) diff --git a/api/v1/installation_types.go b/api/v1/installation_types.go index 1260e67b27..b0bde4360f 100644 --- a/api/v1/installation_types.go +++ b/api/v1/installation_types.go @@ -156,22 +156,22 @@ type InstallationSpec struct { // +optional FlexVolumePath string `json:"flexVolumePath,omitempty"` - // CalicoNodeRunPath optionally specifies the host path mounted into calico-node containers at + // 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. - // Default: /var/run/calico // +optional + // +kubebuilder:default:="/var/run/calico" // +kubebuilder:validation:MaxLength=1024 - CalicoNodeRunPath string `json:"calicoNodeRunPath,omitempty"` + CalicoRunHostPath string `json:"calicoRunHostPath,omitempty"` - // CalicoNodeLibPath optionally specifies the host path mounted into calico-node containers at - // /var/lib/calico. Pair this with CalicoNodeRunPath when Calico data lives under a non-standard + // 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). - // Default: /var/lib/calico // +optional + // +kubebuilder:default:="/var/lib/calico" // +kubebuilder:validation:MaxLength=1024 - CalicoNodeLibPath string `json:"calicoNodeLibPath,omitempty"` + 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. diff --git a/pkg/controller/migration/convert/core.go b/pkg/controller/migration/convert/core.go index 2bfcc9f6c2..1b153f2f13 100644 --- a/pkg/controller/migration/convert/core.go +++ b/pkg/controller/migration/convert/core.go @@ -142,10 +142,10 @@ func handleCore(c *components, install *operatorv1.Installation) error { } // 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.CalicoNodeRunPath); err != nil { + if err := handleCalicoHostPathVolume(c.node.Spec.Template.Spec, "var-run-calico", "/var/run/calico", &install.Spec.CalicoRunHostPath); err != nil { return err } - if err := handleCalicoHostPathVolume(c.node.Spec.Template.Spec, "var-lib-calico", "/var/lib/calico", &install.Spec.CalicoNodeLibPath); 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 { diff --git a/pkg/controller/migration/convert/core_test.go b/pkg/controller/migration/convert/core_test.go index 905bbf3f6f..4277d1c761 100644 --- a/pkg/controller/migration/convert/core_test.go +++ b/pkg/controller/migration/convert/core_test.go @@ -567,8 +567,8 @@ 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.CalicoNodeRunPath).To(BeEmpty()) - Expect(i.Spec.CalicoNodeLibPath).To(BeEmpty()) + Expect(i.Spec.CalicoRunHostPath).To(BeEmpty()) + Expect(i.Spec.CalicoLibHostPath).To(BeEmpty()) }) It("should accept microk8s-style hostPaths and record them on Installation", func() { @@ -581,8 +581,8 @@ var _ = Describe("core handler", func() { } } Expect(handleCore(&comps, i)).ToNot(HaveOccurred()) - Expect(i.Spec.CalicoNodeRunPath).To(Equal("/var/snap/microk8s/current/var/run/calico")) - Expect(i.Spec.CalicoNodeLibPath).To(Equal("/var/snap/microk8s/current/var/lib/calico")) + 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() { diff --git a/pkg/imports/crds/operator/operator.tigera.io_installations.yaml b/pkg/imports/crds/operator/operator.tigera.io_installations.yaml index 18c4150784..d63613ba77 100644 --- a/pkg/imports/crds/operator/operator.tigera.io_installations.yaml +++ b/pkg/imports/crds/operator/operator.tigera.io_installations.yaml @@ -3039,21 +3039,21 @@ spec: type: object type: object type: object - calicoNodeLibPath: + calicoLibHostPath: + default: /var/lib/calico description: |- - CalicoNodeLibPath optionally specifies the host path mounted into calico-node containers at - /var/lib/calico. Pair this with CalicoNodeRunPath when Calico data lives under a non-standard + 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). - Default: /var/lib/calico maxLength: 1024 type: string - calicoNodeRunPath: + calicoRunHostPath: + default: /var/run/calico description: |- - CalicoNodeRunPath optionally specifies the host path mounted into calico-node containers at + 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. - Default: /var/run/calico maxLength: 1024 type: string calicoNodeWindowsDaemonSet: @@ -12442,21 +12442,21 @@ spec: type: object type: object type: object - calicoNodeLibPath: + calicoLibHostPath: + default: /var/lib/calico description: |- - CalicoNodeLibPath optionally specifies the host path mounted into calico-node containers at - /var/lib/calico. Pair this with CalicoNodeRunPath when Calico data lives under a non-standard + 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). - Default: /var/lib/calico maxLength: 1024 type: string - calicoNodeRunPath: + calicoRunHostPath: + default: /var/run/calico description: |- - CalicoNodeRunPath optionally specifies the host path mounted into calico-node containers at + 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. - Default: /var/run/calico maxLength: 1024 type: string calicoNodeWindowsDaemonSet: diff --git a/pkg/render/node.go b/pkg/render/node.go index 557c312525..d69376e636 100644 --- a/pkg/render/node.go +++ b/pkg/render/node.go @@ -1198,15 +1198,15 @@ func (c *nodeComponent) nodeVolumes() []corev1.Volume { } func (c *nodeComponent) calicoRunHostPath() string { - if c.cfg.Installation != nil && c.cfg.Installation.CalicoNodeRunPath != "" { - return c.cfg.Installation.CalicoNodeRunPath + 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.CalicoNodeLibPath != "" { - return c.cfg.Installation.CalicoNodeLibPath + if c.cfg.Installation != nil && c.cfg.Installation.CalicoLibHostPath != "" { + return c.cfg.Installation.CalicoLibHostPath } return "/var/lib/calico" }