From 1d8b9a6229874f3e6c4591d83942ecb95bec38d7 Mon Sep 17 00:00:00 2001 From: Atanas Dinov Date: Wed, 24 Jul 2024 21:04:43 +0300 Subject: [PATCH] Upgrade Rancher Signed-off-by: Atanas Dinov --- api/v1alpha1/upgradeplan_types.go | 1 + cmd/main.go | 2 + config/rbac/role.yaml | 29 +++++++ go.mod | 11 ++- go.sum | 26 ++++-- internal/controller/reconcile_rancher.go | 80 +++++++++++++++++++ internal/controller/upgradeplan_controller.go | 74 +++++++++++++++-- internal/upgrade/base.go | 13 ++- internal/upgrade/helm.go | 14 ++++ internal/upgrade/os.go | 2 +- manifests/release-3.0.1.yaml | 6 +- pkg/release/release.go | 7 ++ 12 files changed, 243 insertions(+), 22 deletions(-) create mode 100644 internal/controller/reconcile_rancher.go create mode 100644 internal/upgrade/helm.go diff --git a/api/v1alpha1/upgradeplan_types.go b/api/v1alpha1/upgradeplan_types.go index 65a6afc..9202a4c 100644 --- a/api/v1alpha1/upgradeplan_types.go +++ b/api/v1alpha1/upgradeplan_types.go @@ -22,6 +22,7 @@ import ( const ( KubernetesUpgradedCondition = "KubernetesUpgraded" + RancherUpgradedCondition = "RancherUpgraded" // UpgradePending indicates that the upgrade process has not begun. UpgradePending = "Pending" diff --git a/cmd/main.go b/cmd/main.go index 3d0648b..484495d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -38,6 +38,7 @@ import ( metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" + helmcattlev1 "github.com/k3s-io/helm-controller/pkg/apis/helm.cattle.io/v1" upgradecattlev1 "github.com/rancher/system-upgrade-controller/pkg/apis/upgrade.cattle.io/v1" lifecyclev1alpha1 "github.com/suse-edge/upgrade-controller/api/v1alpha1" "github.com/suse-edge/upgrade-controller/internal/controller" @@ -55,6 +56,7 @@ func init() { utilruntime.Must(lifecyclev1alpha1.AddToScheme(scheme)) utilruntime.Must(upgradecattlev1.AddToScheme(scheme)) + utilruntime.Must(helmcattlev1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index c5212c6..71093fa 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -20,6 +20,20 @@ rules: - delete - get - list +- apiGroups: + - batch + resources: + - jobs + verbs: + - get + - list + - watch +- apiGroups: + - batch + resources: + - jobs/status + verbs: + - get - apiGroups: - "" resources: @@ -27,6 +41,21 @@ rules: verbs: - create - patch +- apiGroups: + - helm.cattle.io + resources: + - helmcharts + verbs: + - get + - list + - update + - watch +- apiGroups: + - helm.cattle.io + resources: + - helmcharts/status + verbs: + - get - apiGroups: - lifecycle.suse.com resources: diff --git a/go.mod b/go.mod index ddda564..dc3b229 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22.0 toolchain go1.22.5 require ( + github.com/k3s-io/helm-controller v0.16.1 github.com/onsi/ginkgo/v2 v2.19.0 github.com/onsi/gomega v1.33.1 github.com/rancher/system-upgrade-controller/pkg/apis v0.0.0-20240612205712-57605e3390c0 @@ -12,6 +13,7 @@ require ( k8s.io/api v0.30.3 k8s.io/apimachinery v0.30.3 k8s.io/client-go v0.30.3 + k8s.io/utils v0.0.0-20230726121419-3b25d923346b sigs.k8s.io/controller-runtime v0.18.4 ) @@ -20,6 +22,7 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-logr/logr v1.4.1 // indirect @@ -49,14 +52,17 @@ require ( github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect + github.com/rancher/lasso v0.0.0-20240430201833-6f3def65ffc5 // indirect github.com/rancher/wrangler v1.1.1-0.20230425173236-39a4707f0689 // indirect - github.com/sirupsen/logrus v1.9.0 // indirect + github.com/rancher/wrangler/v3 v3.0.0-rc2 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect golang.org/x/net v0.25.0 // indirect - golang.org/x/oauth2 v0.12.0 // indirect + golang.org/x/oauth2 v0.16.0 // indirect + golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/term v0.20.0 // indirect golang.org/x/text v0.15.0 // indirect @@ -70,7 +76,6 @@ require ( k8s.io/apiextensions-apiserver v0.30.1 // indirect k8s.io/klog/v2 v2.120.1 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect - k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.3.0 // indirect diff --git a/go.sum b/go.sum index 0d76e8e..7e09cd1 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= -github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -30,6 +30,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -52,6 +54,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/k3s-io/helm-controller v0.16.1 h1:4sdJSYdAeTvMjjq3Pt1ZcyenRTJIAvKojTWRg/i8Ne4= +github.com/k3s-io/helm-controller v0.16.1/go.mod h1:AcSxEhOIUgeVvBTnJOAwcezBZXtYew/RhKwO5xp3RlM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -88,14 +92,18 @@ github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdO github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rancher/lasso v0.0.0-20240430201833-6f3def65ffc5 h1:6K4RhfmCy7uxaw9OzCljNLfFcgD/q7SeF+/2gCQ3Tvw= +github.com/rancher/lasso v0.0.0-20240430201833-6f3def65ffc5/go.mod h1:7WkdfPEvWAdnHVioMUkhpZkshJzjDY62ocHVhcbw89M= github.com/rancher/system-upgrade-controller/pkg/apis v0.0.0-20240612205712-57605e3390c0 h1:Pl1tXKFK32WTfr4vNtC1FIq2ATdhmpejFfvPkHHuX1M= github.com/rancher/system-upgrade-controller/pkg/apis v0.0.0-20240612205712-57605e3390c0/go.mod h1:04o7UUy7ZFiMDEtHEjO1yS7IkO8TcsgjBl93Fcjq7Gg= github.com/rancher/wrangler v1.1.1-0.20230425173236-39a4707f0689 h1:otb4OjgXH2b8a4C9g76jCDuTF3opjaYffZ55SiVe7KU= github.com/rancher/wrangler v1.1.1-0.20230425173236-39a4707f0689/go.mod h1:D6Tu6oVX8aGtCHsMCtYaysgVK3ad920MTSeAu7rzb5U= +github.com/rancher/wrangler/v3 v3.0.0-rc2 h1:XGSPPp6GXELqlLvwJp5MsdqyCPu6SCA4UKJ7rQJzE40= +github.com/rancher/wrangler/v3 v3.0.0-rc2/go.mod h1:f54hh7gFkwwbjsieT2b63FowzTU8FvrBonPe//0CIXo= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -106,8 +114,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -130,12 +138,14 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= -golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/controller/reconcile_rancher.go b/internal/controller/reconcile_rancher.go new file mode 100644 index 0000000..ceebeb9 --- /dev/null +++ b/internal/controller/reconcile_rancher.go @@ -0,0 +1,80 @@ +package controller + +import ( + "context" + "fmt" + + helmcattlev1 "github.com/k3s-io/helm-controller/pkg/apis/helm.cattle.io/v1" + lifecyclev1alpha1 "github.com/suse-edge/upgrade-controller/api/v1alpha1" + "github.com/suse-edge/upgrade-controller/internal/upgrade" + "github.com/suse-edge/upgrade-controller/pkg/release" + "golang.org/x/exp/slices" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func (r *UpgradePlanReconciler) reconcileRancher(ctx context.Context, upgradePlan *lifecyclev1alpha1.UpgradePlan, rancher *release.HelmChart) (ctrl.Result, error) { + chart := &helmcattlev1.HelmChart{} + + if err := r.Get(ctx, upgrade.ChartNamespacedName(rancher.Name), chart); err != nil { + if !errors.IsNotFound(err) { + return ctrl.Result{}, err + } + + setSkippedCondition(upgradePlan, lifecyclev1alpha1.RancherUpgradedCondition, "Rancher installation is not found") + return ctrl.Result{Requeue: true}, nil + } + + if chart.Spec.Version != rancher.Version { + setInProgressCondition(upgradePlan, lifecyclev1alpha1.RancherUpgradedCondition, "Rancher is being upgraded") + + return ctrl.Result{}, r.updateHelmChart(ctx, upgradePlan, chart, rancher) + } + + job := &batchv1.Job{} + if err := r.Get(ctx, types.NamespacedName{Name: chart.Status.JobName, Namespace: upgrade.ChartNamespace}, job); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + idx := slices.IndexFunc(job.Status.Conditions, func(condition batchv1.JobCondition) bool { + return condition.Status == corev1.ConditionTrue && + (condition.Type == batchv1.JobComplete || condition.Type == batchv1.JobFailed) + }) + + if idx == -1 { + // Upgrade job is still ongoing. + return ctrl.Result{}, nil + } + + condition := job.Status.Conditions[idx] + + switch condition.Type { + case batchv1.JobComplete: + setSuccessfulCondition(upgradePlan, lifecyclev1alpha1.RancherUpgradedCondition, "Rancher is upgraded") + case batchv1.JobFailed: + setFailedCondition(upgradePlan, lifecyclev1alpha1.RancherUpgradedCondition, fmt.Sprintf("Error occurred: %s", condition.Message)) + } + + return ctrl.Result{Requeue: true}, nil +} + +func (r *UpgradePlanReconciler) updateHelmChart(ctx context.Context, upgradePlan *lifecyclev1alpha1.UpgradePlan, chart *helmcattlev1.HelmChart, releaseChart *release.HelmChart) error { + const backoffLimit = 6 + + if chart.Annotations == nil { + chart.Annotations = map[string]string{} + } + chart.Annotations[upgrade.PlanAnnotation] = upgradePlan.Name + chart.Spec.ChartContent = "" + chart.Spec.Chart = releaseChart.Name + chart.Spec.Version = releaseChart.Version + chart.Spec.Repo = releaseChart.Repository + chart.Spec.BackOffLimit = ptr.To[int32](backoffLimit) + + return r.Update(ctx, chart) +} diff --git a/internal/controller/upgradeplan_controller.go b/internal/controller/upgradeplan_controller.go index 003b818..08e457e 100644 --- a/internal/controller/upgradeplan_controller.go +++ b/internal/controller/upgradeplan_controller.go @@ -20,10 +20,15 @@ import ( "context" "errors" "fmt" + "slices" + helmcattlev1 "github.com/k3s-io/helm-controller/pkg/apis/helm.cattle.io/v1" + "github.com/k3s-io/helm-controller/pkg/controllers/chart" upgradecattlev1 "github.com/rancher/system-upgrade-controller/pkg/apis/upgrade.cattle.io/v1" lifecyclev1alpha1 "github.com/suse-edge/upgrade-controller/api/v1alpha1" + "github.com/suse-edge/upgrade-controller/internal/upgrade" "github.com/suse-edge/upgrade-controller/pkg/release" + batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -33,8 +38,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) // UpgradePlanReconciler reconciles a UpgradePlan object @@ -53,6 +60,10 @@ type UpgradePlanReconciler struct { // +kubebuilder:rbac:groups="",resources=nodes,verbs=watch;list // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;delete;create // +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch +// +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch +// +kubebuilder:rbac:groups=batch,resources=jobs/status,verbs=get +// +kubebuilder:rbac:groups=helm.cattle.io,resources=helmcharts,verbs=get;update;list;watch +// +kubebuilder:rbac:groups=helm.cattle.io,resources=helmcharts/status,verbs=get // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -80,21 +91,20 @@ func (r *UpgradePlanReconciler) executePlan(ctx context.Context, upgradePlan *li if len(upgradePlan.Status.Conditions) == 0 { setPendingCondition(upgradePlan, lifecyclev1alpha1.KubernetesUpgradedCondition, "Kubernetes upgrade is not yet started") + setPendingCondition(upgradePlan, lifecyclev1alpha1.RancherUpgradedCondition, "Rancher upgrade is not yet started") - // Append OS and other components conditions here... return ctrl.Result{Requeue: true}, nil } - // Upgrade OS here... - - if !meta.IsStatusConditionTrue(upgradePlan.Status.Conditions, lifecyclev1alpha1.KubernetesUpgradedCondition) { + switch { + case !meta.IsStatusConditionTrue(upgradePlan.Status.Conditions, lifecyclev1alpha1.KubernetesUpgradedCondition): return r.reconcileKubernetes(ctx, upgradePlan, &release.Components.Kubernetes) + case !isHelmUpgradeFinished(upgradePlan, lifecyclev1alpha1.RancherUpgradedCondition): + return r.reconcileRancher(ctx, upgradePlan, &release.Components.Rancher) } - // Upgrade rest of the components here... - logger := log.FromContext(ctx) - logger.Info("Upgrade completed successfully") + logger.Info("Upgrade completed") return ctrl.Result{}, nil } @@ -159,6 +169,33 @@ func setSkippedCondition(plan *lifecyclev1alpha1.UpgradePlan, conditionType, mes meta.SetStatusCondition(&plan.Status.Conditions, condition) } +func (r *UpgradePlanReconciler) findUpgradePlanFromJob(ctx context.Context, job client.Object) []reconcile.Request { + jobLabels := job.GetLabels() + chartName, ok := jobLabels[chart.Label] + if !ok || chartName == "" { + // Job is not scheduled by the Helm controller. + return []reconcile.Request{} + } + + helmChart := &helmcattlev1.HelmChart{} + if err := r.Get(ctx, upgrade.ChartNamespacedName(chartName), helmChart); err != nil { + logger := log.FromContext(ctx) + logger.Error(err, "failed to get helm chart") + + return []reconcile.Request{} + } + + planName, ok := helmChart.Annotations[upgrade.PlanAnnotation] + if !ok || planName == "" { + // Helm chart is not managed by the Upgrade controller. + return []reconcile.Request{} + } + + return []reconcile.Request{ + {NamespacedName: upgrade.PlanNamespacedName(planName)}, + } +} + // SetupWithManager sets up the controller with the Manager. func (r *UpgradePlanReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). @@ -172,5 +209,28 @@ func (r *UpgradePlanReconciler) SetupWithManager(mgr ctrl.Manager) error { len(e.ObjectOld.(*upgradecattlev1.Plan).Status.Applying) != 0 }, })). + Watches(&batchv1.Job{}, handler.EnqueueRequestsFromMapFunc(r.findUpgradePlanFromJob), builder.WithPredicates(predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return false + }, + UpdateFunc: func(e event.UpdateEvent) bool { + // Only requeue an upgrade plan when a respective job finishes. + isJobFinished := func(conditions []batchv1.JobCondition) bool { + return slices.ContainsFunc(conditions, func(condition batchv1.JobCondition) bool { + return condition.Status == corev1.ConditionTrue && + (condition.Type == batchv1.JobComplete || condition.Type == batchv1.JobFailed) + }) + } + + return isJobFinished(e.ObjectNew.(*batchv1.Job).Status.Conditions) && + !isJobFinished(e.ObjectOld.(*batchv1.Job).Status.Conditions) + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return false + }, + GenericFunc: func(e event.GenericEvent) bool { + return false + }, + })). Complete(r) } diff --git a/internal/upgrade/base.go b/internal/upgrade/base.go index 321cee0..66cee69 100644 --- a/internal/upgrade/base.go +++ b/internal/upgrade/base.go @@ -3,10 +3,12 @@ package upgrade import ( upgradecattlev1 "github.com/rancher/system-upgrade-controller/pkg/apis/upgrade.cattle.io/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" ) const ( - upgradeNamespace = "cattle-system" + planNamespace = "cattle-system" + PlanAnnotation = "lifecycle.suse.com/upgrade-plan" ControlPlaneLabel = "node-role.kubernetes.io/control-plane" ) @@ -25,7 +27,7 @@ func baseUpgradePlan(name string) *upgradecattlev1.Plan { }, ObjectMeta: metav1.ObjectMeta{ Name: name, - Namespace: upgradeNamespace, + Namespace: planNamespace, }, Spec: upgradecattlev1.PlanSpec{ ServiceAccountName: serviceAccountName, @@ -34,3 +36,10 @@ func baseUpgradePlan(name string) *upgradecattlev1.Plan { return plan } + +func PlanNamespacedName(plan string) types.NamespacedName { + return types.NamespacedName{ + Name: plan, + Namespace: planNamespace, + } +} diff --git a/internal/upgrade/helm.go b/internal/upgrade/helm.go new file mode 100644 index 0000000..5c1169e --- /dev/null +++ b/internal/upgrade/helm.go @@ -0,0 +1,14 @@ +package upgrade + +import "k8s.io/apimachinery/pkg/types" + +const ( + ChartNamespace = "kube-system" +) + +func ChartNamespacedName(chart string) types.NamespacedName { + return types.NamespacedName{ + Name: chart, + Namespace: ChartNamespace, + } +} diff --git a/internal/upgrade/os.go b/internal/upgrade/os.go index 4ae9b38..36f5878 100644 --- a/internal/upgrade/os.go +++ b/internal/upgrade/os.go @@ -47,7 +47,7 @@ func OSUpgradeSecret(releaseOS *release.OperatingSystem) (*corev1.Secret, error) secret := &corev1.Secret{ ObjectMeta: v1.ObjectMeta{ Name: secretName, - Namespace: upgradeNamespace, + Namespace: planNamespace, }, Type: corev1.SecretTypeOpaque, StringData: map[string]string{ diff --git a/manifests/release-3.0.1.yaml b/manifests/release-3.0.1.yaml index 8e237dc..056b799 100644 --- a/manifests/release-3.0.1.yaml +++ b/manifests/release-3.0.1.yaml @@ -6,11 +6,15 @@ components: version: v1.28.9+k3s1 rke2: version: v1.28.9+rke2r1 + rancher: + chart: rancher + version: v2.8.5 + repository: https://charts.rancher.com/server-charts/prime operatingSystem: version: 6.0 zypperID: SL-Micro cpeScheme: cpe:/o:suse:sl-micro:6.0 repoGPGPath: /usr/lib/rpm/gnupg/keys/gpg-pubkey-09d9ea69-645b99ce.asc supportedArchs: - - x86_64 + - x86_64 # - aarch64 TODO: add when we start supporting it diff --git a/pkg/release/release.go b/pkg/release/release.go index 713dd05..7a0b519 100644 --- a/pkg/release/release.go +++ b/pkg/release/release.go @@ -9,6 +9,7 @@ type Release struct { type Components struct { Kubernetes Kubernetes `yaml:"kubernetes"` OperatingSystem OperatingSystem `yaml:"operatingSystem"` + Rancher HelmChart `yaml:"rancher"` } type Kubernetes struct { @@ -27,3 +28,9 @@ type OperatingSystem struct { RepoGPGPath string `yaml:"repoGPGPath"` SupportedArchs []string `yaml:"supportedArchs"` } + +type HelmChart struct { + Name string `yaml:"chart"` + Repository string `yaml:"repository"` + Version string `yaml:"version"` +}