Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use local clusterctl overrides for determining latest version for provider #751

Merged
merged 3 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 (
furkatgofurov7 marked this conversation as resolved.
Show resolved Hide resolved
// CheckLatestUpdateAvailableReason is a reason for a False condition, due to update being available.
CheckLatestUpdateAvailableReason = "UpdateAvailable"

// CheckLatestProviderUnknown is a reason for an Unknown condition, due to provider not being available.
CheckLatestProviderUnknown = "ProviderUnknown"
)
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