Skip to content

Commit

Permalink
Use local clusterctl overrides for determining latest version for pro…
Browse files Browse the repository at this point in the history
…vider (#751)

* Refactor auto-upgrade functionality

Signed-off-by: Danil-Grigorev <danil.grigorev@suse.com>

* Notify users about version update being available

Signed-off-by: Danil-Grigorev <danil.grigorev@suse.com>

* Add integration tests for custom providers

Signed-off-by: Danil-Grigorev <danil.grigorev@suse.com>

---------

Signed-off-by: Danil-Grigorev <danil.grigorev@suse.com>
  • Loading branch information
Danil-Grigorev authored Sep 25, 2024
1 parent 2e8c131 commit add8810
Show file tree
Hide file tree
Showing 14 changed files with 427 additions and 88 deletions.
11 changes: 6 additions & 5 deletions api/v1alpha1/clusterctl_config_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ const (
//nolint:lll
type ClusterctlConfigSpec struct {
// Images is a list of image overrided for specified providers
Images []Image `json:"images"`
// +optional
Images []Image `json:"images,omitempty"`

// Provider overrides
Providers ProviderList `json:"providers"`
// +optional
Providers ProviderList `json:"providers,omitempty"`
}

// Provider allows to define providers with known URLs to pull the components.
Expand All @@ -48,9 +50,8 @@ type Provider struct {

// Type is the type of the provider
// +required
// +kubebuilder:validation:Enum=infrastructure;core;controlPlane;bootstrap;addon;runtimeextension;ipam
// +kubebuilder:example=infrastructure
ProviderType Type `json:"type"`
// +kubebuilder:example=InfrastructureProvider
Type string `json:"type"`
}

// ProviderList is a list of providers.
Expand Down
8 changes: 8 additions & 0 deletions api/v1alpha1/conditions_consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,11 @@ const (
// CheckLatestVersionTime is set as a timestamp info of the last timestamp of the latest version being up-to-date for the CAPIProvider.
CheckLatestVersionTime = "CheckLatestVersionTime"
)

const (
// CheckLatestUpdateAvailableReason is a reason for a False condition, due to update being available.
CheckLatestUpdateAvailableReason = "UpdateAvailable"

// CheckLatestProviderUnknownReason is a reason for an Unknown condition, due to provider not being available.
CheckLatestProviderUnknownReason = "ProviderUnknown"
)
13 changes: 1 addition & 12 deletions charts/rancher-turtles/templates/rancher-turtles-components.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3170,15 +3170,7 @@ spec:
type: string
type:
description: Type is the type of the provider
enum:
- infrastructure
- core
- controlPlane
- bootstrap
- addon
- runtimeextension
- ipam
example: infrastructure
example: InfrastructureProvider
type: string
url:
description: URL of the provider components. Will be used unless
Expand All @@ -3190,9 +3182,6 @@ spec:
- url
type: object
type: array
required:
- images
- providers
type: object
type: object
x-kubernetes-validations:
Expand Down
13 changes: 1 addition & 12 deletions config/crd/bases/turtles-capi.cattle.io_clusterctlconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,7 @@ spec:
type: string
type:
description: Type is the type of the provider
enum:
- infrastructure
- core
- controlPlane
- bootstrap
- addon
- runtimeextension
- ipam
example: infrastructure
example: InfrastructureProvider
type: string
url:
description: URL of the provider components. Will be used unless
Expand All @@ -94,9 +86,6 @@ spec:
- url
type: object
type: array
required:
- images
- providers
type: object
type: object
x-kubernetes-validations:
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/rancher/turtles
go 1.22.0

require (
github.com/blang/semver/v4 v4.0.0
github.com/go-logr/logr v1.4.2
github.com/onsi/ginkgo/v2 v2.20.0
github.com/onsi/gomega v1.34.1
Expand All @@ -25,7 +26,6 @@ require (
require (
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.12.0 // indirect
Expand Down
124 changes: 123 additions & 1 deletion internal/controllers/clusterctl/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,22 @@ package clusterctl

import (
"cmp"
"context"
"fmt"
"os"
"slices"
"strings"

_ "embed"

"github.com/blang/semver/v4"
corev1 "k8s.io/api/core/v1"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/yaml"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"

turtlesv1 "github.com/rancher/turtles/api/v1alpha1"
)

var (
Expand All @@ -34,14 +43,127 @@ var (
config *corev1.ConfigMap
)

const (
latestVersionKey = "latest"
)

func init() {
utilruntime.Must(yaml.UnmarshalStrict(configDefault, &config))
}

// Config returns current set of turtles clusterctl overrides.
// ConfigRepository is a direct clusterctl config repository representation.
type ConfigRepository struct {
Providers turtlesv1.ProviderList `json:"providers"`
Images map[string]ConfigImage `json:"images"`
}

// ConfigImage is a direct clusterctl representation of image config value.
type ConfigImage struct {
// Repository sets the container registry override to pull images from.
Repository string `json:"repository,omitempty"`

// Tag allows to specify a tag for the images.
Tag string `json:"tag,omitempty"`
}

// Config returns current set of embedded turtles clusterctl overrides.
func Config() *corev1.ConfigMap {
configMap := config.DeepCopy()
configMap.Namespace = cmp.Or(os.Getenv("POD_NAMESPACE"), "rancher-turtles-system")

return configMap
}

// ClusterConfig collects overrides config from the local in-memory state
// and the user-specified ClusterctlConfig overrides layer.
func ClusterConfig(ctx context.Context, c client.Client) (*ConfigRepository, error) {
log := log.FromContext(ctx)

configMap := Config()

config := &turtlesv1.ClusterctlConfig{}
if err := c.Get(ctx, client.ObjectKeyFromObject(configMap), config); client.IgnoreNotFound(err) != nil {
log.Error(err, "Unable to collect ClusterctlConfig resource")

return nil, err
}

clusterctlConfig := &ConfigRepository{}
if err := yaml.UnmarshalStrict([]byte(configMap.Data["clusterctl.yaml"]), &clusterctlConfig); err != nil {
log.Error(err, "Unable to deserialize initial clusterctl config")

return nil, err
}

if clusterctlConfig.Images == nil {
clusterctlConfig.Images = map[string]ConfigImage{}
}

clusterctlConfig.Providers = append(clusterctlConfig.Providers, config.Spec.Providers...)

for _, image := range config.Spec.Images {
clusterctlConfig.Images[image.Name] = ConfigImage{
Tag: image.Tag,
Repository: image.Repository,
}
}

return clusterctlConfig, nil
}

// GetProviderVersion collects version of the collected provider overrides state.
// Returns latest if the version is not found.
func (r *ConfigRepository) GetProviderVersion(ctx context.Context, name, providerType string) (version string, providerKnown bool) {
for _, provider := range r.Providers {
if provider.Name == name && strings.EqualFold(provider.Type, providerType) {
return collectVersion(ctx, provider.URL), true
}
}

return latestVersionKey, false
}

func collectVersion(ctx context.Context, url string) string {
version := strings.Split(url, "/")
slices.Reverse(version)

if len(version) < 2 {
log.FromContext(ctx).Info("Provider url is invalid for version resolve, defaulting to latest", "url", url)

return latestVersionKey
}

return version[1]
}

// IsLatestVersion checks version against the expected max version, and returns false
// if the version given is newer then the latest in the clusterctlconfig override.
func (r *ConfigRepository) IsLatestVersion(providerVersion, expected string) (bool, error) {
// Return true for providers without version boundary or unknown providers
if providerVersion == latestVersionKey {
return true, nil
}

version, _ := strings.CutPrefix(providerVersion, "v")

maxVersion, err := semver.Parse(version)
if err != nil {
return false, fmt.Errorf("unable to parse default provider version %s: %w", providerVersion, err)
}

expected = cmp.Or(expected, latestVersionKey)
if expected == latestVersionKey {
// Latest should be reduced to the actual version set on the clusterctlprovider resource
return false, nil
}

version, _ = strings.CutPrefix(expected, "v")

desiredVersion, err := semver.Parse(version)
if err != nil {
return false, fmt.Errorf("unable to parse desired version %s: %w", expected, err)
}

// Disallow versions beyond current clusterctl.yaml override default
return maxVersion.LTE(desiredVersion), nil
}
37 changes: 8 additions & 29 deletions internal/controllers/clusterctlconfig_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,51 +87,30 @@ func (r *ClusterctlConfigReconciler) SetupWithManager(ctx context.Context, mgr c
return nil
}

//+kubebuilder:rbac:groups=turtles-capi.cattle.io,resources=clusterctlcofigs,verbs=get;list;watch;patch
//+kubebuilder:rbac:groups=turtles-capi.cattle.io,resources=clusterctlcofigs/status,verbs=get;list;watch;patch
//+kubebuilder:rbac:groups=turtles-capi.cattle.io,resources=clusterctlcofigs/finalizers,verbs=get;list;watch;patch;update
//+kubebuilder:rbac:groups=turtles-capi.cattle.io,resources=clusterctlconfigs,verbs=get;list;watch;patch
//+kubebuilder:rbac:groups=turtles-capi.cattle.io,resources=clusterctlconfigs/status,verbs=get;list;watch;patch
//+kubebuilder:rbac:groups=turtles-capi.cattle.io,resources=clusterctlconfigs/finalizers,verbs=get;list;watch;patch;update
//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;patch

// Reconcile reconciles the EtcdMachineSnapshot object.
func (r *ClusterctlConfigReconciler) Reconcile(ctx context.Context, req reconcile.Request) (ctrl.Result, error) {
func (r *ClusterctlConfigReconciler) Reconcile(ctx context.Context, _ reconcile.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)

configMap := clusterctl.Config()

config := &turtlesv1.ClusterctlConfig{}
if err := r.Client.Get(ctx, req.NamespacedName, config); client.IgnoreNotFound(err) != nil {
log.Error(err, "Unable to collect ClusterctlConfig resource")

return ctrl.Result{}, err
}

clusterctlConfig := &Config{}
if err := yaml.UnmarshalStrict([]byte(configMap.Data["clusterctl.yaml"]), &clusterctlConfig); err != nil {
log.Error(err, "Unable to deserialize initial clusterctl config")
clusterctlConfig, err := clusterctl.ClusterConfig(ctx, r.Client)
if err != nil {
log.Error(err, "Unable to serialize updated clusterctl config")

return ctrl.Result{}, err
}

if clusterctlConfig.Images == nil {
clusterctlConfig.Images = map[string]ConfigImage{}
}

clusterctlConfig.Providers = append(clusterctlConfig.Providers, config.Spec.Providers...)

for _, image := range config.Spec.Images {
clusterctlConfig.Images[image.Name] = ConfigImage{
Tag: image.Tag,
Repository: image.Repository,
}
}

clusterctlYaml, err := yaml.Marshal(clusterctlConfig)
if err != nil {
log.Error(err, "Unable to serialize updated clusterctl config")

return ctrl.Result{}, err
}

configMap := clusterctl.Config()
configMap.Data["clusterctl.yaml"] = string(clusterctlYaml)

if err := r.Client.Patch(ctx, configMap, client.Apply, []client.PatchOption{
Expand Down
Loading

0 comments on commit add8810

Please sign in to comment.