Skip to content

Commit

Permalink
Refactor auto-upgrade functionality
Browse files Browse the repository at this point in the history
Signed-off-by: Danil-Grigorev <danil.grigorev@suse.com>
  • Loading branch information
Danil-Grigorev committed Sep 23, 2024
1 parent 0070e17 commit bc87424
Show file tree
Hide file tree
Showing 12 changed files with 254 additions and 82 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
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
122 changes: 121 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,125 @@ var (
config *corev1.ConfigMap
)

const (
latest = "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) string {
for _, provider := range r.Providers {
if provider.Name == name && strings.EqualFold(provider.Type, providerType) {
return collectVersion(ctx, provider.URL)
}
}

return latest
}

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 latest
}

return version[1]
}

// VerifyProviderVersion 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) VerifyProviderVersion(providerVersion, expected string) (bool, error) {
if providerVersion == latest {
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)
}

if expected == latest {
// Latest should be reduced to the actual version set on the clusterctlprovider resource
return false, nil
}

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

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

// Disallow versions beyond clusterctl.yaml default
return maxVersion.LTE(destinatonVersion), 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
48 changes: 32 additions & 16 deletions internal/sync/provider_sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,22 @@ limitations under the License.
package sync

import (
"cmp"
"context"
"fmt"
"strings"
"time"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"

operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2"
"sigs.k8s.io/cluster-api/util/conditions"

turtlesv1 "github.com/rancher/turtles/api/v1alpha1"
"github.com/rancher/turtles/internal/api"
"github.com/rancher/turtles/internal/controllers/clusterctl"
)

// AppliedSpecHashAnnotation is a spec hash annotation set by CAPI Operator,
Expand Down Expand Up @@ -91,9 +94,13 @@ func (ProviderSync) Template(capiProvider *turtlesv1.CAPIProvider) client.Object
// Spec -> down
// up <- Status.
func (s *ProviderSync) Sync(ctx context.Context) error {
if err := s.updateLatestVersion(ctx); err != nil {
return err
}

s.SyncObjects()

return s.updateLatestVersion(ctx)
return nil
}

// SyncObjects updates the Source CAPIProvider object and the destination provider object states.
Expand Down Expand Up @@ -153,30 +160,39 @@ func (s *ProviderSync) rolloutInfrastructure() {

func (s *ProviderSync) updateLatestVersion(ctx context.Context) error {
// Skip for user specified versions
// TODO: We may potentially need to verify if version specified is built in the override,
// and notify user with condition otherwise
if s.Source.Spec.Version != "" {
return nil
}

now := time.Now().UTC()
lastCheck := conditions.Get(s.Source, turtlesv1.CheckLatestVersionTime)
log := log.FromContext(ctx)

if lastCheck != nil && lastCheck.Status == corev1.ConditionTrue && lastCheck.LastTransitionTime.Add(24*time.Hour).After(now) {
return nil
config, err := clusterctl.ClusterConfig(ctx, s.client)
if err != nil {
return err
}

patchBase := client.MergeFrom(s.Destination)
providerVersion := config.GetProviderVersion(ctx, cmp.Or(s.Source.Spec.Name, s.Source.Name), s.Source.Spec.Type.ToKind())
expected := cmp.Or(s.Source.Spec.Version, "latest")

// Unsetting .spec.version to force latest version rollout
spec := s.Destination.GetSpec()
spec.Version = ""
s.Destination.SetSpec(spec)
if valid, err := config.VerifyProviderVersion(providerVersion, expected); err != nil {
return err
} else if !valid {
lastCheck := conditions.Get(s.Source, turtlesv1.CheckLatestVersionTime)
updatedMessage := fmt.Sprintf("Updated to latest %s version", providerVersion)

conditions.MarkTrue(s.Source, turtlesv1.CheckLatestVersionTime)
if lastCheck == nil || lastCheck.Message != updatedMessage {
log.Info(fmt.Sprintf("Version %s is beyound current latest, setting to %s", expected, providerVersion))

err := s.client.Patch(ctx, s.Destination, patchBase)
if err != nil {
conditions.MarkUnknown(s.Source, turtlesv1.CheckLatestVersionTime, "Requesting latest version rollout", "")
lastCheck = conditions.TrueCondition(turtlesv1.CheckLatestVersionTime)
lastCheck.Message = updatedMessage

conditions.Set(s.Source, lastCheck)
}

s.Source.Spec.Version = providerVersion
}

return client.IgnoreNotFound(err)
return nil
}
Loading

0 comments on commit bc87424

Please sign in to comment.