Repository: kubermatic/kubeone (Go)

Issue:
KubeOne mirroring with harbor is not usable
### Description of the feature you would like to add / User story

As a user
I would like the option to add additional parameters to the mirroring config of containerd with KubeOne
in order to make the mirroring work with harbors proxy cache projects.

### Solution details

- An option to add additional parameters to the mirroring section of containerd e.g. override_path = true

### Alternative approaches

Since the workers are a dynamic ressource we can't add this config to the vm template. KubeOne/machine-controller would overwrite the config we have there in place. So an alternative approach would be that we can deactivate the containerd config overwrite through KubeOne/machine-controller.


### Use cases

Harbors proxy cache projects have a different implementation than containerd. So we have to set the "/v2/" in the url which is possible right now. But we also need to set the parameter: 'override_path = true' for the mirroring to properly work. Otherwise we can't use the mirroring with harbor.


### Additional information

When the config doesn't look like this:
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
endpoint = ["https://registry/v2/project"]
override_path = true

We get this error when we try to pull images from e.g. Dockerhub:
Failed to pull image "nicolaka/netshoot": rpc error: code = NotFound desc = failed to pull and unpack image "docker.io/nicolaka/netshoot:latest": failed to unpack image on snapshotter overlayfs: unexpected media type text/html for sha256:ecd8a6c12adb298a513c0aa9ceba8d2c93263437d77b123c5ccfd97abaa49abb: not found
https://github.com/goharbor/harbor/issues/18530#issuecomment-1551959188





Discussion:
/label customer-request
Internal reference: 7864
We need to support this in OSM first, as it also needs to be propagated to worker nodes as well. We have a mechanism in place to propagate such features via MachineDeployment annotations.



Patch:
diff --git a/docs/api_reference/v1beta2.en.md b/docs/api_reference/v1beta2.en.md
index b81036ab1..89e17a698 100644
--- a/docs/api_reference/v1beta2.en.md
+++ b/docs/api_reference/v1beta2.en.md
@@ -1,6 +1,6 @@
 +++
 title = "v1beta2 API Reference"
-date = 2025-09-15T12:35:55+03:00
+date = 2025-10-08T17:27:45+03:00
 weight = 11
 +++
 ## v1beta2
@@ -262,6 +262,7 @@ ContainerdRegistry defines endpoints and security for given container registry
 | Field | Description | Scheme | Required |
 | ----- | ----------- | ------ | -------- |
 | mirrors | List of registry mirrors to use | []string | false |
+| overridePath | Configure override_path | bool | false |
 | tlsConfig | TLSConfig for the registry | *[ContainerdTLSConfig](#containerdtlsconfig) | false |
 | auth | Registry authentication | *[ContainerdRegistryAuthConfig](#containerdregistryauthconfig) | false |
 
diff --git a/docs/api_reference/v1beta3.en.md b/docs/api_reference/v1beta3.en.md
index 9afbc9adc..596172613 100644
--- a/docs/api_reference/v1beta3.en.md
+++ b/docs/api_reference/v1beta3.en.md
@@ -1,6 +1,6 @@
 +++
 title = "v1beta3 API Reference"
-date = 2025-09-15T12:35:55+03:00
+date = 2025-10-08T17:27:45+03:00
 weight = 11
 +++
 ## v1beta3
@@ -264,6 +264,7 @@ ContainerdRegistry defines endpoints and security for given container registry
 | Field | Description | Scheme | Required |
 | ----- | ----------- | ------ | -------- |
 | mirrors | List of registry mirrors to use | []string | false |
+| overridePath | Configure override_path | bool | false |
 | tlsConfig | TLSConfig for the registry | *[ContainerdTLSConfig](#containerdtlsconfig) | false |
 | auth | Registry authentication | *[ContainerdRegistryAuthConfig](#containerdregistryauthconfig) | false |
 
diff --git a/pkg/apis/kubeone/helpers.go b/pkg/apis/kubeone/helpers.go
index b348f8a94..9080034c9 100644
--- a/pkg/apis/kubeone/helpers.go
+++ b/pkg/apis/kubeone/helpers.go
@@ -20,6 +20,7 @@ import (
 	"bytes"
 	"fmt"
 	"math/rand"
+	"net/url"
 	"path/filepath"
 	"sort"
 	"strings"
@@ -150,6 +151,16 @@ func (crc ContainerRuntimeConfig) MachineControllerFlags() []string {
 		}
 
 		for _, mirror := range containerdRegistry.Mirrors {
+			if containerdRegistry.OverridePath {
+				mirrorURL, _ := url.Parse(mirror)
+				mirrorQS := mirrorURL.Query()
+				kubermaticValues := url.Values{}
+				kubermaticValues.Add("override_path", "true")
+				mirrorQS.Add("kubermatic", kubermaticValues.Encode())
+				mirrorURL.RawPath = mirrorQS.Encode()
+				mirror = mirrorURL.String()
+			}
+
 			mcFlags = append(mcFlags,
 				fmt.Sprintf("-node-containerd-registry-mirrors=%s=%s", registryName, mirror),
 			)
diff --git a/pkg/apis/kubeone/types.go b/pkg/apis/kubeone/types.go
index 8d274983c..d9ebf08ae 100644
--- a/pkg/apis/kubeone/types.go
+++ b/pkg/apis/kubeone/types.go
@@ -225,6 +225,9 @@ type ContainerdRegistry struct {
 	// List of registry mirrors to use
 	Mirrors []string `json:"mirrors,omitempty"`
 
+	// Configure override_path
+	OverridePath bool `json:"overridePath,omitempty"`
+
 	// TLSConfig for the registry
 	TLSConfig *ContainerdTLSConfig `json:"tlsConfig,omitempty"`
 
diff --git a/pkg/apis/kubeone/v1beta2/types.go b/pkg/apis/kubeone/v1beta2/types.go
index 53773cb8c..2f63246cb 100644
--- a/pkg/apis/kubeone/v1beta2/types.go
+++ b/pkg/apis/kubeone/v1beta2/types.go
@@ -235,6 +235,9 @@ type ContainerdRegistry struct {
 	// List of registry mirrors to use
 	Mirrors []string `json:"mirrors,omitempty"`
 
+	// Configure override_path
+	OverridePath bool `json:"overridePath,omitempty"`
+
 	// TLSConfig for the registry
 	TLSConfig *ContainerdTLSConfig `json:"tlsConfig,omitempty"`
 
diff --git a/pkg/apis/kubeone/v1beta2/zz_generated.conversion.go b/pkg/apis/kubeone/v1beta2/zz_generated.conversion.go
index b70151cd0..28a662953 100644
--- a/pkg/apis/kubeone/v1beta2/zz_generated.conversion.go
+++ b/pkg/apis/kubeone/v1beta2/zz_generated.conversion.go
@@ -1078,6 +1078,7 @@ func autoConvert_kubeone_ContainerRuntimeContainerd_To_v1beta2_ContainerRuntimeC
 
 func autoConvert_v1beta2_ContainerdRegistry_To_kubeone_ContainerdRegistry(in *ContainerdRegistry, out *kubeone.ContainerdRegistry, s conversion.Scope) error {
 	out.Mirrors = *(*[]string)(unsafe.Pointer(&in.Mirrors))
+	out.OverridePath = in.OverridePath
 	out.TLSConfig = (*kubeone.ContainerdTLSConfig)(unsafe.Pointer(in.TLSConfig))
 	out.Auth = (*kubeone.ContainerdRegistryAuthConfig)(unsafe.Pointer(in.Auth))
 	return nil
@@ -1090,6 +1091,7 @@ func Convert_v1beta2_ContainerdRegistry_To_kubeone_ContainerdRegistry(in *Contai
 
 func autoConvert_kubeone_ContainerdRegistry_To_v1beta2_ContainerdRegistry(in *kubeone.ContainerdRegistry, out *ContainerdRegistry, s conversion.Scope) error {
 	out.Mirrors = *(*[]string)(unsafe.Pointer(&in.Mirrors))
+	out.OverridePath = in.OverridePath
 	out.TLSConfig = (*ContainerdTLSConfig)(unsafe.Pointer(in.TLSConfig))
 	out.Auth = (*ContainerdRegistryAuthConfig)(unsafe.Pointer(in.Auth))
 	return nil
diff --git a/pkg/apis/kubeone/v1beta3/types.go b/pkg/apis/kubeone/v1beta3/types.go
index 38b561570..c66a0b019 100644
--- a/pkg/apis/kubeone/v1beta3/types.go
+++ b/pkg/apis/kubeone/v1beta3/types.go
@@ -222,6 +222,9 @@ type ContainerdRegistry struct {
 	// List of registry mirrors to use
 	Mirrors []string `json:"mirrors,omitempty"`
 
+	// Configure override_path
+	OverridePath bool `json:"overridePath,omitempty"`
+
 	// TLSConfig for the registry
 	TLSConfig *ContainerdTLSConfig `json:"tlsConfig,omitempty"`
 
diff --git a/pkg/apis/kubeone/v1beta3/zz_generated.conversion.go b/pkg/apis/kubeone/v1beta3/zz_generated.conversion.go
index cec3d78c5..317b0061d 100644
--- a/pkg/apis/kubeone/v1beta3/zz_generated.conversion.go
+++ b/pkg/apis/kubeone/v1beta3/zz_generated.conversion.go
@@ -1121,6 +1121,7 @@ func Convert_kubeone_ContainerRuntimeContainerd_To_v1beta3_ContainerRuntimeConta
 
 func autoConvert_v1beta3_ContainerdRegistry_To_kubeone_ContainerdRegistry(in *ContainerdRegistry, out *kubeone.ContainerdRegistry, s conversion.Scope) error {
 	out.Mirrors = *(*[]string)(unsafe.Pointer(&in.Mirrors))
+	out.OverridePath = in.OverridePath
 	out.TLSConfig = (*kubeone.ContainerdTLSConfig)(unsafe.Pointer(in.TLSConfig))
 	out.Auth = (*kubeone.ContainerdRegistryAuthConfig)(unsafe.Pointer(in.Auth))
 	return nil
@@ -1133,6 +1134,7 @@ func Convert_v1beta3_ContainerdRegistry_To_kubeone_ContainerdRegistry(in *Contai
 
 func autoConvert_kubeone_ContainerdRegistry_To_v1beta3_ContainerdRegistry(in *kubeone.ContainerdRegistry, out *ContainerdRegistry, s conversion.Scope) error {
 	out.Mirrors = *(*[]string)(unsafe.Pointer(&in.Mirrors))
+	out.OverridePath = in.OverridePath
 	out.TLSConfig = (*ContainerdTLSConfig)(unsafe.Pointer(in.TLSConfig))
 	out.Auth = (*ContainerdRegistryAuthConfig)(unsafe.Pointer(in.Auth))
 	return nil
diff --git a/pkg/containerruntime/containerd_config.go b/pkg/containerruntime/containerd_config.go
index cce6f5ee5..a1ba910c9 100644
--- a/pkg/containerruntime/containerd_config.go
+++ b/pkg/containerruntime/containerd_config.go
@@ -50,8 +50,8 @@ type containerdCRISettings struct {
 }
 
 type containerdCRIRuntime struct {
-	RuntimeType string      `toml:"runtime_type"`
-	Options     interface{} `toml:"options"`
+	RuntimeType string `toml:"runtime_type"`
+	Options     any    `toml:"options"`
 }
 
 type containerdCRIRuncOptions struct {
@@ -64,7 +64,8 @@ type containerdCRIRegistry struct {
 }
 
 type containerdRegistryMirror struct {
-	Endpoint []string `toml:"endpoint"`
+	Endpoint     []string `toml:"endpoint"`
+	OverridePath bool     `toml:"override_path,omitempty"`
 }
 
 type containerdRegistryConfig struct {
@@ -135,7 +136,8 @@ func marshalContainerdConfig(cluster *kubeoneapi.KubeOneCluster) (string, error)
 
 		for registryName, registry := range regs {
 			criPlugin.Registry.Mirrors[registryName] = containerdRegistryMirror{
-				Endpoint: registry.Mirrors,
+				Endpoint:     registry.Mirrors,
+				OverridePath: registry.OverridePath,
 			}
 
 			if registry.TLSConfig != nil {
diff --git a/pkg/templates/images/images.go b/pkg/templates/images/images.go
index f9abbebc1..dd35e14d6 100644
--- a/pkg/templates/images/images.go
+++ b/pkg/templates/images/images.go
@@ -230,7 +230,7 @@ func baseResources() map[Resource]map[string]string {
 		Flannel:                {"*": "docker.io/flannel/flannel:v0.24.4"},
 		MachineController:      {"*": "quay.io/kubermatic/machine-controller:1b7f31166cf4827955e52acb8581a7aa48eebba4"},
 		MetricsServer:          {"*": "registry.k8s.io/metrics-server/metrics-server:v0.8.0"},
-		OperatingSystemManager: {"*": "quay.io/kubermatic/operating-system-manager:3f18bc13f185b35562f79e2766b6f148c2ba4257"},
+		OperatingSystemManager: {"*": "quay.io/kubermatic/operating-system-manager:6ee458b6ff4783518bd77b80b0bef859b2e7c44f"},
 	}
 }
 


Test patch:
diff --git a/pkg/containerruntime/containerd_config_test.go b/pkg/containerruntime/containerd_config_test.go
index 623936682..952b35981 100644
--- a/pkg/containerruntime/containerd_config_test.go
+++ b/pkg/containerruntime/containerd_config_test.go
@@ -24,9 +24,7 @@ import (
 	"k8c.io/kubeone/pkg/testhelper"
 )
 
-var (
-	updateFlag = flag.Bool("update", false, "update testdata files")
-)
+var updateFlag = flag.Bool("update", false, "update testdata files")
 
 func Test_marshalContainerdConfig(t *testing.T) {
 	tests := []struct {
@@ -52,6 +50,15 @@ func Test_marshalContainerdConfig(t *testing.T) {
 				},
 			})),
 		},
+		{
+			name: "mirror registry with override_path",
+			cluster: genCluster(withContainerdRegistry(map[string]kubeoneapi.ContainerdRegistry{
+				"docker.io": {
+					Mirrors:      []string{"https://custom.secure.registry/v2/someproject"},
+					OverridePath: true,
+				},
+			})),
+		},
 		{
 			name: "multi registry mirrors",
 			cluster: genCluster(withContainerdRegistry(map[string]kubeoneapi.ContainerdRegistry{
diff --git a/pkg/containerruntime/testdata/Test_marshalContainerdConfig-mirror_registry_with_override_path.golden b/pkg/containerruntime/testdata/Test_marshalContainerdConfig-mirror_registry_with_override_path.golden
new file mode 100644
index 000000000..c9f1c92e1
--- /dev/null
+++ b/pkg/containerruntime/testdata/Test_marshalContainerdConfig-mirror_registry_with_override_path.golden
@@ -0,0 +1,21 @@
+version = 2
+
+[metrics]
+address = "127.0.0.1:1338"
+
+[plugins]
+[plugins."io.containerd.grpc.v1.cri"]
+sandbox_image = "registry.k8s.io/pause:3.10"
+device_ownership_from_security_context = true
+[plugins."io.containerd.grpc.v1.cri".containerd]
+[plugins."io.containerd.grpc.v1.cri".containerd.runtimes]
+[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
+runtime_type = "io.containerd.runc.v2"
+[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
+SystemdCgroup = true
+[plugins."io.containerd.grpc.v1.cri".registry]
+[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
+[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
+endpoint = ["https://custom.secure.registry/v2/someproject"]
+override_path = true
+[plugins."io.containerd.grpc.v1.cri".registry.configs]