Repository: rancher-sandbox/cluster-api-provider-rke2 (go)

Issue:
Feature parity of rke2 registry configuration
**Describe the solution you'd like:**
https://docs.rke2.io/install/containerd_registry_configuration < the provider should support the same functionality when configuring registries like here where you can configure it either with user/password, tls or a combo of both. 
 
**Why do you want this feature:**
Current implementation doesn't support either or.


Pull request description:
fix: Handling of registries config to work as rke2
<!--
Label the PR with the kind of change this for:

kind/feature
kind/bug
kind/documentation
kind/regression
kind/*
-->

**What this PR does / why we need it**:

<!-- Enter a description of the change and why this change is needed -->

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Fixes #253 

**Special notes for your reviewer**:

**Checklist**:
<!-- Put an "X" character inside the brackets of each completed task. Some may be optional depending on the PR in which case these can be deleted -->

- [x] squashed commits into logical changes
- [ ] includes documentation
- [x] adds unit tests
- [x] adds or updates e2e tests


Patch:
diff --git a/bootstrap/api/v1beta1/rke2config_types.go b/bootstrap/api/v1beta1/rke2config_types.go
index 53c5794..c3b503c 100644
--- a/bootstrap/api/v1beta1/rke2config_types.go
+++ b/bootstrap/api/v1beta1/rke2config_types.go
@@ -354,7 +354,7 @@ type Mirror struct {
 
 // RegistryConfig contains configuration used to communicate with the registry.
 type RegistryConfig struct {
-	// Auth si a reference to a Secret containing information to authenticate to the registry.
+	// Auth is a reference to a Secret containing information to authenticate to the registry.
 	// The Secret must provite a username and a password data entry.
 	//+optional
 	AuthSecret corev1.ObjectReference `json:"authSecret,omitempty"`
diff --git a/bootstrap/api/v1beta1/rke2config_webhook.go b/bootstrap/api/v1beta1/rke2config_webhook.go
index 116d740..194b7a5 100644
--- a/bootstrap/api/v1beta1/rke2config_webhook.go
+++ b/bootstrap/api/v1beta1/rke2config_webhook.go
@@ -21,6 +21,7 @@ import (
 
 	"github.com/coreos/butane/config/common"
 	fcos "github.com/coreos/butane/config/fcos/v1_4"
+	v1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/validation/field"
@@ -113,6 +114,25 @@ func (s *RKE2ConfigSpec) validate(pathPrefix *field.Path) field.ErrorList {
 	var allErrs field.ErrorList
 
 	allErrs = append(allErrs, s.validateIgnition(pathPrefix)...)
+	allErrs = append(allErrs, s.validateRegistries(pathPrefix)...)
+
+	return allErrs
+}
+
+func (s *RKE2ConfigSpec) validateRegistries(pathPrefix *field.Path) field.ErrorList {
+	var allErrs field.ErrorList
+
+	for regName, regConfig := range s.PrivateRegistriesConfig.Configs {
+		if regConfig.AuthSecret == (v1.ObjectReference{}) && regConfig.TLS == (TLSConfig{}) {
+			allErrs = append(
+				allErrs,
+				field.Invalid(
+					pathPrefix.Child(fmt.Sprintf("privateRegistriesConfig.configs.%s", regName)),
+					regConfig,
+					fmt.Sprintf("need either credentials, tls settings or both for registry: %s", regName)),
+			)
+		}
+	}
 
 	return allErrs
 }
diff --git a/bootstrap/config/crd/bases/bootstrap.cluster.x-k8s.io_rke2configs.yaml b/bootstrap/config/crd/bases/bootstrap.cluster.x-k8s.io_rke2configs.yaml
index f811f1b..e58b93e 100644
--- a/bootstrap/config/crd/bases/bootstrap.cluster.x-k8s.io_rke2configs.yaml
+++ b/bootstrap/config/crd/bases/bootstrap.cluster.x-k8s.io_rke2configs.yaml
@@ -931,7 +931,7 @@ spec:
                         with the registry.
                       properties:
                         authSecret:
-                          description: Auth si a reference to a Secret containing
+                          description: Auth is a reference to a Secret containing
                             information to authenticate to the registry. The Secret
                             must provite a username and a password data entry.
                           properties:
diff --git a/bootstrap/config/crd/bases/bootstrap.cluster.x-k8s.io_rke2configtemplates.yaml b/bootstrap/config/crd/bases/bootstrap.cluster.x-k8s.io_rke2configtemplates.yaml
index 7ea308f..cdb2515 100644
--- a/bootstrap/config/crd/bases/bootstrap.cluster.x-k8s.io_rke2configtemplates.yaml
+++ b/bootstrap/config/crd/bases/bootstrap.cluster.x-k8s.io_rke2configtemplates.yaml
@@ -952,7 +952,7 @@ spec:
                                 to communicate with the registry.
                               properties:
                                 authSecret:
-                                  description: Auth si a reference to a Secret containing
+                                  description: Auth is a reference to a Secret containing
                                     information to authenticate to the registry. The
                                     Secret must provite a username and a password
                                     data entry.
diff --git a/controlplane/config/crd/bases/controlplane.cluster.x-k8s.io_rke2controlplanes.yaml b/controlplane/config/crd/bases/controlplane.cluster.x-k8s.io_rke2controlplanes.yaml
index af1a5dd..140636a 100644
--- a/controlplane/config/crd/bases/controlplane.cluster.x-k8s.io_rke2controlplanes.yaml
+++ b/controlplane/config/crd/bases/controlplane.cluster.x-k8s.io_rke2controlplanes.yaml
@@ -1639,7 +1639,7 @@ spec:
                         with the registry.
                       properties:
                         authSecret:
-                          description: Auth si a reference to a Secret containing
+                          description: Auth is a reference to a Secret containing
                             information to authenticate to the registry. The Secret
                             must provite a username and a password data entry.
                           properties:
diff --git a/docs/release.md b/docs/release.md
deleted file mode 100644
index dbcfb46..0000000
--- a/docs/release.md
+++ /dev/null
@@ -1,99 +0,0 @@
-# CAPRKE2 Releases
-
-## Release Cadence
-
-- CAPRKE2 minor versions (v0.**2**.0 versus v0.**1**.0) are released every 3-4 months.
-- CAPRKE2 patch versions (v0.2.**2** versus v0.2.**1**) are released as often as weekly or once a month. 
-
-## Release Process
-
-1. Clone the repository locally: 
-
-```bash
-git clone git@github.com:rancher-sandbox/cluster-api-provider-rke2.git
-```
-
-2. Depending on whether you are cutting a minor or patch release, the process varies.
-
-    * If you are cutting a new minor release:
-
-        Create a new release branch (i.e release-X) and push it to the upstream repository.
-
-        ```bash
-            # Note: `upstream` must be the remote pointing to `github.com:rancher-sandbox/cluster-api-provider-rke2`.
-            git checkout -b release-0.2
-            git push -u upstream release-0.2
-            # Export the tag of the minor release to be cut, e.g.:
-            export RELEASE_TAG=v0.2.0
-        ```
-    * If you are cutting a patch release from an existing release branch:
-
-        Use existing release branch.
-
-        ```bash
-            git checkout upstream/release-0.2
-            # Export the tag of the patch release to be cut, e.g.:
-            export RELEASE_TAG=v0.2.1
-        ```
-3. Create a signed/annotated tag and push it:
-
-```bash
-# Create tags locally
-git tag -s -a ${RELEASE_TAG} -m ${RELEASE_TAG}
-
-# Push tags
-git push upstream ${RELEASE_TAG}
-```
-
-This will trigger a [release GitHub action](https://github.com/rancher-sandbox/cluster-api-provider-rke2/blob/main/.github/workflows/release.yml) that creates a release with RKE2 provider components.
-
-## Versioning
-
-Cluster API Provider RKE2 follows [semantic versioning](https://semver.org/) specification.
-
-Example versions:
-- Pre-release: `v0.2.0-alpha.1`
-- Minor release: `v0.2.0`
-- Patch release: `v0.2.1`
-- Major release: `v2.0.0`
-
-With the v0 release of our codebase, we provide the following guarantees:
-
-- A (*minor*) release CAN include:
-  - Introduction of new API versions, or new Kinds.
-  - Compatible API changes like field additions, deprecation notices, etc.
-  - Breaking API changes for deprecated APIs, fields, or code.
-  - Features, promotion or removal of feature gates.
-  - And more!
-
-- A (*patch*) release SHOULD only include backwards compatible set of bugfixes.
-
-### Backporting
-
-Any backport MUST not be breaking for either API or behavioral changes.
-
-It is generally not accepted to submit pull requests directly against release branches (release-X). However, backports of fixes or changes that have already been merged into the main branch may be accepted to all supported branches:
-
-- Critical bugs fixes, security issue fixes, or fixes for bugs without easy workarounds.
-- Dependency bumps for CVE (usually limited to CVE resolution; backports of non-CVE related version bumps are considered exceptions to be evaluated case by case)
-- Cert-manager version bumps (to avoid having releases with cert-manager versions that are out of support, when possible)
-- Changes required to support new Kubernetes versions, when possible. See supported Kubernetes versions for more details.
-- Changes to use the latest Go patch version to build controller images.
-- Improvements to existing docs (the latest supported branch hosts the current version of the book)
-
-**Note:** We generally do not accept backports to Cluster API Provider RKE2 release branches that are out of support.
-
-## Branches
-
-Cluster API Provider RKE2 has two types of branches: the main branch and release-X branches.
-
-The main branch is where development happens. All the latest and greatest code, including breaking changes, happens on main.
-
-The release-X branches contain stable, backwards compatible code. On every major or minor release, a new branch is created. It is from these branches that minor and patch releases are tagged. In some cases, it may be necessary to open PRs for bugfixes directly against stable branches, but this should generally not be the case.
-
-### Support and guarantees
-
-Cluster API Provider RKE2 maintains the most recent release/releases for all supported APIs. Support for this section refers to the ability to backport and release patch versions; [backport policy](#backporting) is defined above.
-
-- The API version is determined from the GroupVersion defined in the top-level `bootstrap/api/` and `controlplane/api/` packages.
-- For the current stable API version (v1beta1) we support the two most recent minor releases; older minor releases are immediately unsupported when a new major/minor release is available.
\ No newline at end of file
diff --git a/pkg/rke2/registries.go b/pkg/rke2/registries.go
index 44a5840..6b94974 100644
--- a/pkg/rke2/registries.go
+++ b/pkg/rke2/registries.go
@@ -17,6 +17,7 @@ package rke2
 
 import (
 	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/types"
 
 	bootstrapv1 "github.com/rancher-sandbox/cluster-api-provider-rke2/bootstrap/api/v1beta1"
@@ -45,85 +46,100 @@ func GenerateRegistries(rke2ConfigRegistry RegistryScope) (*Registry, []bootstra
 	}
 
 	for configName, regConfig := range rke2ConfigRegistry.Registry.Configs {
-		tlsSecret := corev1.Secret{}
-		authSecret := corev1.Secret{}
-
-		err := rke2ConfigRegistry.Client.Get(
-			rke2ConfigRegistry.Ctx,
-			types.NamespacedName{
-				Name:      regConfig.TLS.TLSConfigSecret.Name,
-				Namespace: regConfig.TLS.TLSConfigSecret.Namespace,
-			},
-			&tlsSecret,
-		)
-		if err != nil {
-			rke2ConfigRegistry.Logger.Error(err, "TLS Config Secret for the registry was not found!")
-
-			return &Registry{}, []bootstrapv1.File{}, err
-		}
-
-		for _, secretEntry := range []string{"tls.crt", "tls.key", "ca.crt"} {
-			if tlsSecret.Data[secretEntry] == nil {
-				rke2ConfigRegistry.Logger.Error(err, "TLS Config Secret for the registry is missing entries!", "secret-missing-entry", secretEntry)
+		registryConfig := RegistryConfig{}
+
+		if regConfig.TLS != (bootstrapv1.TLSConfig{}) {
+			tlsSecret := corev1.Secret{}
+
+			err := rke2ConfigRegistry.Client.Get(
+				rke2ConfigRegistry.Ctx,
+				types.NamespacedName{
+					Name:      regConfig.TLS.TLSConfigSecret.Name,
+					Namespace: regConfig.TLS.TLSConfigSecret.Namespace,
+				},
+				&tlsSecret,
+			)
+			if err != nil {
+				if apierrors.IsNotFound(err) {
+					rke2ConfigRegistry.Logger.Error(err, "TLS Secret for the registry was not found!")
+				} else {
+					rke2ConfigRegistry.Logger.Error(err, "Error fetching TLS Secret")
+				}
 
 				return &Registry{}, []bootstrapv1.File{}, err
 			}
 
-			files = append(files, bootstrapv1.File{
-				Path:    registryCertsPath + "/" + secretEntry,
-				Content: string(tlsSecret.Data[secretEntry]),
-			})
-		}
+			for _, secretEntry := range []string{"tls.crt", "tls.key", "ca.crt"} {
+				if tlsSecret.Data[secretEntry] == nil {
+					rke2ConfigRegistry.Logger.Error(err, "TLS Secret for the registry is missing entries!", "secret-missing-entry", secretEntry)
 
-		err = rke2ConfigRegistry.Client.Get(
-			rke2ConfigRegistry.Ctx,
-			types.NamespacedName{
-				Name:      regConfig.AuthSecret.Name,
-				Namespace: regConfig.AuthSecret.Namespace,
-			},
-			&authSecret,
-		)
+					return &Registry{}, []bootstrapv1.File{}, err
+				}
 
-		if err != nil {
-			rke2ConfigRegistry.Logger.Error(err, "Auth Config Secret for the registry was not found!")
+				files = append(files, bootstrapv1.File{
+					Path:    registryCertsPath + "/" + secretEntry,
+					Content: string(tlsSecret.Data[secretEntry]),
+				})
+			}
 
-			return &Registry{}, []bootstrapv1.File{}, err
+			registryConfig.TLS = &TLSConfig{
+				InsecureSkipVerify: regConfig.TLS.InsecureSkipVerify,
+				CAFile:             registryCertsPath + "/" + "ca.crt",
+				CertFile:           registryCertsPath + "/" + "tls.crt",
+				KeyFile:            registryCertsPath + "/" + "tls.key",
+			}
 		}
 
-		isBasicAuth := authSecret.Data["username"] != nil && authSecret.Data["password"] != nil
-		isTokenAuth := authSecret.Data["identity-token"] != nil
+		if regConfig.AuthSecret != (corev1.ObjectReference{}) {
+			authSecret := corev1.Secret{}
+
+			err := rke2ConfigRegistry.Client.Get(
+				rke2ConfigRegistry.Ctx,
+				types.NamespacedName{
+					Name:      regConfig.AuthSecret.Name,
+					Namespace: regConfig.AuthSecret.Namespace,
+				},
+				&authSecret,
+			)
+			if err != nil {
+				if apierrors.IsNotFound(err) {
+					rke2ConfigRegistry.Logger.Error(err, "AuthSecret for the registry was not found!")
+				} else {
+					rke2ConfigRegistry.Logger.Error(err, "Error fetching AuthSecret")
+				}
 
-		ok := isBasicAuth || isTokenAuth
+				return &Registry{}, []bootstrapv1.File{}, err
+			}
 
-		if !ok {
-			rke2ConfigRegistry.Logger.Error(
-				err,
-				"Auth Secret for the registry is missing entries! Possible entries are: (\"username\" AND \"password\") OR \"identity-token\" ",
-				"secret-entries", bsutil.GetMapKeysAsString(authSecret.Data))
+			isBasicAuth := authSecret.Data["username"] != nil && authSecret.Data["password"] != nil
+			isTokenAuth := authSecret.Data["identity-token"] != nil
 
-			return &Registry{}, []bootstrapv1.File{}, err
-		}
+			ok := isBasicAuth || isTokenAuth
 
-		authData := &AuthConfig{}
-		if isBasicAuth {
-			authData.Username = string(authSecret.Data["username"])
-			authData.Password = string(authSecret.Data["password"])
-		}
+			if !ok {
+				rke2ConfigRegistry.Logger.Error(
+					err,
+					"Auth Secret for the registry is missing entries! Possible entries are: (\"username\" AND \"password\") OR \"identity-token\" ",
+					"secret-entries", bsutil.GetMapKeysAsString(authSecret.Data))
 
-		if isTokenAuth {
-			authData.IdentityToken = string(authSecret.Data["identity-token"])
+				return &Registry{}, []bootstrapv1.File{}, err
+			}
+
+			authData := &AuthConfig{}
+			if isBasicAuth {
+				authData.Username = string(authSecret.Data["username"])
+				authData.Password = string(authSecret.Data["password"])
+			}
+
+			if isTokenAuth {
+				authData.IdentityToken = string(authSecret.Data["identity-token"])
+			}
+
+			registryConfig.Auth = authData
 		}
 
 		registry.Configs = make(map[string]RegistryConfig)
-		registry.Configs[configName] = RegistryConfig{
-			TLS: &TLSConfig{
-				InsecureSkipVerify: regConfig.TLS.InsecureSkipVerify,
-				CAFile:             registryCertsPath + "/" + "ca.crt",
-				CertFile:           registryCertsPath + "/" + "tls.crt",
-				KeyFile:            registryCertsPath + "/" + "tls.key",
-			},
-			Auth: authData,
-		}
+		registry.Configs[configName] = registryConfig
 	}
 
 	return registry, files, nil


Test patch:
diff --git a/bootstrap/api/v1beta1/rke2config_webhook_test.go b/bootstrap/api/v1beta1/rke2config_webhook_test.go
new file mode 100644
index 0000000..bdb612d
--- /dev/null
+++ b/bootstrap/api/v1beta1/rke2config_webhook_test.go
@@ -0,0 +1,88 @@
+/*
+Copyright 2024 SUSE LLC.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1beta1
+
+import (
+	"testing"
+
+	. "github.com/onsi/gomega"
+	v1 "k8s.io/api/core/v1"
+)
+
+func TestRKE2Config_ValidateCreate(t *testing.T) {
+	tests := []struct {
+		name      string
+		spec      *RKE2ConfigSpec
+		expectErr bool
+	}{
+		{
+			name: "private registry no tls or auth secret",
+			spec: &RKE2ConfigSpec{
+				PrivateRegistriesConfig: Registry{
+					Configs: map[string]RegistryConfig{
+						"testing.io": {},
+					},
+				},
+			},
+			expectErr: true,
+		},
+		{
+			name: "private registry empty tls",
+			spec: &RKE2ConfigSpec{
+				PrivateRegistriesConfig: Registry{
+					Configs: map[string]RegistryConfig{
+						"testing.io": {
+							TLS: TLSConfig{},
+						},
+					},
+				},
+			},
+			expectErr: true,
+		},
+		{
+			name: "private registry empty auth",
+			spec: &RKE2ConfigSpec{
+				PrivateRegistriesConfig: Registry{
+					Configs: map[string]RegistryConfig{
+						"testing.io": {
+							AuthSecret: v1.ObjectReference{},
+						},
+					},
+				},
+			},
+			expectErr: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			RegisterTestingT(t)
+
+			config := &RKE2Config{
+				Spec: *tt.spec.DeepCopy(),
+			}
+
+			_, err := config.ValidateCreate()
+
+			if tt.expectErr {
+				Expect(err).To(HaveOccurred())
+			} else {
+				Expect(err).NotTo(HaveOccurred())
+			}
+		})
+	}
+}