Skip to content

Commit

Permalink
Validate available upgrades for managed clusters
Browse files Browse the repository at this point in the history
Closes #372
  • Loading branch information
eromanova committed Sep 26, 2024
1 parent c81b512 commit 9c8a9e3
Show file tree
Hide file tree
Showing 10 changed files with 308 additions and 111 deletions.
4 changes: 4 additions & 0 deletions api/v1alpha1/clustertemplatechain_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func (c *ClusterTemplateChain) GetSupportedTemplates() []SupportedTemplate {
return c.Spec.SupportedTemplates
}

// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Cluster

Expand Down
4 changes: 4 additions & 0 deletions api/v1alpha1/servicetemplatechain_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func (c *ServiceTemplateChain) GetSupportedTemplates() []SupportedTemplate {
return c.Spec.SupportedTemplates
}

// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Cluster

Expand Down
3 changes: 3 additions & 0 deletions api/v1alpha1/templates_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ const (
ChartAnnotationBootstrapProviders = "hmc.mirantis.com/bootstrap-providers"
// ChartAnnotationControlPlaneProviders is an annotation containing the CAPI control plane providers associated with Template.
ChartAnnotationControlPlaneProviders = "hmc.mirantis.com/control-plane-providers"

ClusterTemplateKind string = "ClusterTemplate"
ServiceTemplateKind string = "ServiceTemplate"
)

// TemplateSpecCommon is a Template configuration common for all Template types
Expand Down
22 changes: 6 additions & 16 deletions internal/controller/template_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (

hmc "github.com/Mirantis/hmc/api/v1alpha1"
"github.com/Mirantis/hmc/internal/helm"
"github.com/Mirantis/hmc/internal/templateutil"
)

const (
Expand Down Expand Up @@ -123,14 +124,7 @@ func (r *ProviderTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Req
return r.ReconcileTemplate(ctx, providerTemplate)
}

// Template is the interface defining a list of methods to interact with templates
type Template interface {
client.Object
GetSpec() *hmc.TemplateSpecCommon
GetStatus() *hmc.TemplateStatusCommon
}

func (r *TemplateReconciler) ReconcileTemplate(ctx context.Context, template Template) (ctrl.Result, error) {
func (r *TemplateReconciler) ReconcileTemplate(ctx context.Context, template templateutil.Template) (ctrl.Result, error) {
l := log.FromContext(ctx)

spec := template.GetSpec()
Expand All @@ -149,7 +143,7 @@ func (r *TemplateReconciler) ReconcileTemplate(ctx context.Context, template Tem
l.Error(err, "invalid helm chart reference")
return ctrl.Result{}, err
}
if template.GetNamespace() == r.SystemNamespace || !templateManagedByHMC(template) {
if template.GetNamespace() == r.SystemNamespace || !templateutil.IsManagedByHMC(template) {
err := r.reconcileDefaultHelmRepository(ctx, template.GetNamespace())
if err != nil {
l.Error(err, "Failed to reconcile default HelmRepository", "namespace", template.GetNamespace())
Expand Down Expand Up @@ -222,11 +216,7 @@ func (r *TemplateReconciler) ReconcileTemplate(ctx context.Context, template Tem
return ctrl.Result{}, r.updateStatus(ctx, template, "")
}

func templateManagedByHMC(template Template) bool {
return template.GetLabels()[hmc.HMCManagedLabelKey] == hmc.HMCManagedLabelValue
}

func parseChartMetadata(template Template, inChart *chart.Chart) error {
func parseChartMetadata(template templateutil.Template, inChart *chart.Chart) error {
if inChart.Metadata == nil {
return fmt.Errorf("chart metadata is empty")
}
Expand Down Expand Up @@ -261,7 +251,7 @@ func parseChartMetadata(template Template, inChart *chart.Chart) error {
return nil
}

func (r *TemplateReconciler) updateStatus(ctx context.Context, template Template, validationError string) error {
func (r *TemplateReconciler) updateStatus(ctx context.Context, template templateutil.Template, validationError string) error {
status := template.GetStatus()
status.ObservedGeneration = template.GetGeneration()
status.ValidationError = validationError
Expand Down Expand Up @@ -312,7 +302,7 @@ func (r *TemplateReconciler) reconcileDefaultHelmRepository(ctx context.Context,
return nil
}

func (r *TemplateReconciler) reconcileHelmChart(ctx context.Context, template Template) (*sourcev1.HelmChart, error) {
func (r *TemplateReconciler) reconcileHelmChart(ctx context.Context, template templateutil.Template) (*sourcev1.HelmChart, error) {
spec := template.GetSpec()
namespace := template.GetNamespace()
if namespace == "" {
Expand Down
14 changes: 7 additions & 7 deletions internal/controller/templatemanagement_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,11 @@ func (r *TemplateManagementReconciler) Reconcile(ctx context.Context, req ctrl.R
}

var errs error
err = r.distributeTemplates(ctx, expectedState.ClusterTemplatesState, templateutil.ClusterTemplateKind)
err = r.distributeTemplates(ctx, expectedState.ClusterTemplatesState, hmc.ClusterTemplateKind)
if err != nil {
errs = errors.Join(errs, err)
}
err = r.distributeTemplates(ctx, expectedState.ServiceTemplatesState, templateutil.ServiceTemplateKind)
err = r.distributeTemplates(ctx, expectedState.ServiceTemplatesState, hmc.ServiceTemplateKind)
if err != nil {
errs = errors.Join(errs, err)
}
Expand Down Expand Up @@ -125,7 +125,7 @@ func (r *TemplateManagementReconciler) applyTemplates(ctx context.Context, kind
chartName := ""

switch kind {
case templateutil.ClusterTemplateKind:
case hmc.ClusterTemplateKind:
source := &hmc.ClusterTemplate{}
err := r.Get(ctx, client.ObjectKey{
Namespace: r.SystemNamespace,
Expand All @@ -137,7 +137,7 @@ func (r *TemplateManagementReconciler) applyTemplates(ctx context.Context, kind
} else if !apierrors.IsNotFound(err) {
return err
}
case templateutil.ServiceTemplateKind:
case hmc.ServiceTemplateKind:
source := &hmc.ServiceTemplate{}
err := r.Get(ctx, client.ObjectKey{
Namespace: r.SystemNamespace,
Expand All @@ -150,7 +150,7 @@ func (r *TemplateManagementReconciler) applyTemplates(ctx context.Context, kind
return err
}
default:
return fmt.Errorf("invalid kind %s. Only %s or %s kinds are supported", kind, templateutil.ClusterTemplateKind, templateutil.ServiceTemplateKind)
return fmt.Errorf("invalid kind %s. Only %s or %s kinds are supported", kind, hmc.ClusterTemplateKind, hmc.ServiceTemplateKind)
}

spec := hmc.TemplateSpecCommon{
Expand All @@ -166,10 +166,10 @@ func (r *TemplateManagementReconciler) applyTemplates(ctx context.Context, kind
for ns, keep := range namespaces {
var target client.Object
meta.Namespace = ns
if kind == templateutil.ClusterTemplateKind {
if kind == hmc.ClusterTemplateKind {
target = &hmc.ClusterTemplate{ObjectMeta: meta, Spec: hmc.ClusterTemplateSpec{TemplateSpecCommon: spec}}
}
if kind == templateutil.ServiceTemplateKind {
if kind == hmc.ServiceTemplateKind {
target = &hmc.ServiceTemplate{ObjectMeta: meta, Spec: hmc.ServiceTemplateSpec{TemplateSpecCommon: spec}}
}
if keep {
Expand Down
32 changes: 32 additions & 0 deletions internal/templateutil/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2024
//
// 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 templateutil

import (
"sigs.k8s.io/controller-runtime/pkg/client"

hmc "github.com/Mirantis/hmc/api/v1alpha1"
)

// Template is the interface defining a list of methods to interact with templates
type Template interface {
client.Object
GetSpec() *hmc.TemplateSpecCommon
GetStatus() *hmc.TemplateStatusCommon
}

func IsManagedByHMC(template Template) bool {
return template.GetLabels()[hmc.HMCManagedLabelKey] == hmc.HMCManagedLabelValue
}
149 changes: 103 additions & 46 deletions internal/templateutil/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,18 @@ package templateutil

import (
"context"
"errors"
"fmt"
"slices"

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

hmc "github.com/Mirantis/hmc/api/v1alpha1"
)

const (
ClusterTemplateKind = "ClusterTemplate"
ServiceTemplateKind = "ServiceTemplate"
)

type State struct {
// ClusterTemplatesState is a map where keys are ClusterTemplate names and values is the map of namespaces
// where this ClusterTemplate should be distributed
Expand All @@ -50,7 +44,7 @@ func GetCurrentTemplatesState(ctx context.Context, cl client.Client, systemNames
}
clusterTemplatesList, serviceTemplatesList := &metav1.PartialObjectMetadataList{}, &metav1.PartialObjectMetadataList{}

for _, kind := range []string{ClusterTemplateKind, ServiceTemplateKind} {
for _, kind := range []string{hmc.ClusterTemplateKind, hmc.ServiceTemplateKind} {
partialList := &metav1.PartialObjectMetadataList{}
partialList.SetGroupVersionKind(schema.GroupVersionKind{
Group: hmc.GroupVersion.Group,
Expand All @@ -61,10 +55,10 @@ func GetCurrentTemplatesState(ctx context.Context, cl client.Client, systemNames
if err != nil {
return State{}, err
}
if kind == ClusterTemplateKind {
if kind == hmc.ClusterTemplateKind {
clusterTemplatesList = partialList
}
if kind == ServiceTemplateKind {
if kind == hmc.ServiceTemplateKind {
serviceTemplatesList = partialList
}
}
Expand Down Expand Up @@ -104,53 +98,33 @@ func ParseAccessRules(ctx context.Context, cl client.Client, rules []hmc.AccessR
if expectedState.ServiceTemplatesState == nil {
expectedState.ServiceTemplatesState = make(map[string]map[string]bool)
}
ctChains, err := getTemplateChains(ctx, cl, hmc.ClusterTemplateKind)
if err != nil {
return State{}, err
}
stChains, err := getTemplateChains(ctx, cl, hmc.ServiceTemplateKind)
if err != nil {
return State{}, err
}
for _, rule := range rules {
var clusterTemplates []string
var serviceTemplates []string
for _, ctChainName := range rule.ClusterTemplateChains {
ctChain := &hmc.ClusterTemplateChain{}
err := cl.Get(ctx, types.NamespacedName{
Name: ctChainName,
}, ctChain)
if err != nil {
errs = errors.Join(errs, err)
continue
}
for _, supportedTemplate := range ctChain.Spec.SupportedTemplates {
clusterTemplates = append(clusterTemplates, supportedTemplate.Name)
}
}
for _, stChainName := range rule.ServiceTemplateChains {
stChain := &hmc.ServiceTemplateChain{}
err := cl.Get(ctx, types.NamespacedName{
Name: stChainName,
}, stChain)
if err != nil {
errs = errors.Join(errs, err)
continue
}
for _, supportedTemplate := range stChain.Spec.SupportedTemplates {
serviceTemplates = append(serviceTemplates, supportedTemplate.Name)
}
}
namespaces, err := getTargetNamespaces(ctx, cl, rule.TargetNamespaces)
if err != nil {
return State{}, err
}
for _, ct := range clusterTemplates {
if expectedState.ClusterTemplatesState[ct] == nil {
expectedState.ClusterTemplatesState[ct] = make(map[string]bool)
for _, ct := range getSupportedTemplates(ctChains, rule.ClusterTemplateChains) {
if expectedState.ClusterTemplatesState[ct.Name] == nil {
expectedState.ClusterTemplatesState[ct.Name] = make(map[string]bool)
}
for _, ns := range namespaces {
expectedState.ClusterTemplatesState[ct][ns] = true
expectedState.ClusterTemplatesState[ct.Name][ns] = true
}
}
for _, st := range serviceTemplates {
if expectedState.ServiceTemplatesState[st] == nil {
expectedState.ServiceTemplatesState[st] = make(map[string]bool)
for _, st := range getSupportedTemplates(stChains, rule.ServiceTemplateChains) {
if expectedState.ServiceTemplatesState[st.Name] == nil {
expectedState.ServiceTemplatesState[st.Name] = make(map[string]bool)
}
for _, ns := range namespaces {
expectedState.ServiceTemplatesState[st][ns] = true
expectedState.ServiceTemplatesState[st.Name][ns] = true
}
}
}
Expand All @@ -160,6 +134,43 @@ func ParseAccessRules(ctx context.Context, cl client.Client, rules []hmc.AccessR
return expectedState, nil
}

func IsAvailableForUpgrade(ctx context.Context, cl client.Client, templateKind string, source, target string) (bool, error) {
tmList := &hmc.TemplateManagementList{}
listOpts := &client.ListOptions{Limit: 1}
err := cl.List(ctx, tmList, listOpts)
if err != nil {
return false, err
}
if len(tmList.Items) == 0 {
return false, fmt.Errorf("TemplateManagement is not found")
}
allChains, err := getTemplateChains(ctx, cl, templateKind)
if err != nil {
return false, err
}
for _, accessRule := range tmList.Items[0].Spec.AccessRules {
var accessRuleChains []string
switch templateKind {
case hmc.ClusterTemplateKind:
accessRuleChains = accessRule.ClusterTemplateChains
case hmc.ServiceTemplateKind:
accessRuleChains = accessRule.ServiceTemplateChains
default:
return false, fmt.Errorf("invalid template kind. Supported values: %s and %s", hmc.ClusterTemplateKind, hmc.ServiceTemplateKind)
}
for _, supportedTemplate := range getSupportedTemplates(allChains, accessRuleChains) {
if source == supportedTemplate.Name {
if slices.ContainsFunc(supportedTemplate.AvailableUpgrades, func(au hmc.AvailableUpgrade) bool {
return target == au.Name
}) {
return true, nil
}
}
}
}
return false, nil
}

func getTargetNamespaces(ctx context.Context, cl client.Client, targetNamespaces hmc.TargetNamespaces) ([]string, error) {
if len(targetNamespaces.List) > 0 {
return targetNamespaces.List, nil
Expand Down Expand Up @@ -193,3 +204,49 @@ func getTargetNamespaces(ctx context.Context, cl client.Client, targetNamespaces
}
return result, nil
}

func getSupportedTemplates(allChains []templateChain, accessRuleChains []string) []hmc.SupportedTemplate {
supportedTemplates := make(map[string][]hmc.SupportedTemplate)
for _, chain := range allChains {
supportedTemplates[chain.GetName()] = chain.GetSupportedTemplates()
}
var result []hmc.SupportedTemplate
for _, chainName := range accessRuleChains {
result = append(result, supportedTemplates[chainName]...)
}
return result
}

type templateChain interface {
client.Object
GetSupportedTemplates() []hmc.SupportedTemplate
}

func getTemplateChains(ctx context.Context, cl client.Client, kind string) ([]templateChain, error) {
switch kind {
case hmc.ClusterTemplateKind:
ctChains := &hmc.ClusterTemplateChainList{}
err := cl.List(ctx, ctChains)
if err != nil {
return nil, err
}
templateChains := make([]templateChain, len(ctChains.Items))
for i, chain := range ctChains.Items {
templateChains[i] = &chain
}
return templateChains, nil
case hmc.ServiceTemplateKind:
stChains := &hmc.ServiceTemplateChainList{}
err := cl.List(ctx, stChains)
if err != nil {
return nil, err
}
templateChains := make([]templateChain, len(stChains.Items))
for i, chain := range stChains.Items {
templateChains[i] = &chain
}
return templateChains, nil
default:
return nil, fmt.Errorf("invalid template kind. Supported values: %s and %s", hmc.ClusterTemplateKind, hmc.ServiceTemplateKind)
}
}
Loading

0 comments on commit 9c8a9e3

Please sign in to comment.