Skip to content

Commit

Permalink
Manage release manifests (#56)
Browse files Browse the repository at this point in the history
* Create release manifest via a job

Signed-off-by: Atanas Dinov <[email protected]>

* Extract release manifest image source

Signed-off-by: Atanas Dinov <[email protected]>

* Extract service account name

Signed-off-by: Atanas Dinov <[email protected]>

---------

Signed-off-by: Atanas Dinov <[email protected]>
  • Loading branch information
atanasdinov authored Aug 22, 2024
1 parent 72ae0e0 commit 8db0eb2
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 28 deletions.
21 changes: 18 additions & 3 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,17 @@ func init() {
// +kubebuilder:scaffold:scheme
}

const defaultReleaseManifestImage = "registry.opensuse.org/isv/suse/edge/lifecycle/containerfile/suse/release-manifest"

func main() {
var metricsAddr string
var enableLeaderElection bool
var probeAddr string
var secureMetrics bool
var enableHTTP2 bool
var watchNamespace string
var releaseManifestImage string
var serviceAccountName string

flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metric endpoint binds to. "+
"Use the port :8080. If not set, it will be 0 in order to disable the metrics server")
Expand All @@ -77,6 +81,11 @@ func main() {
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
flag.StringVar(&watchNamespace, "namespace", os.Getenv("WATCH_NAMESPACE"),
"Namespace that the controller watches to reconcile resources.")
flag.StringVar(&releaseManifestImage, "release-manifest-image", os.Getenv("RELEASE_MANIFEST_IMAGE"),
"Source of release manifest container images")
flag.StringVar(&serviceAccountName, "service-account-name", os.Getenv("SERVICE_ACCOUNT_NAME"),
"Service account of the controller")

opts := zap.Options{
Development: true,
}
Expand Down Expand Up @@ -114,6 +123,10 @@ func main() {
}
}

if releaseManifestImage == "" {
releaseManifestImage = defaultReleaseManifestImage
}

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
Metrics: metricsserver.Options{
Expand Down Expand Up @@ -146,9 +159,11 @@ func main() {
}

if err = (&controller.UpgradePlanReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("upgrade-plan-controller"),
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("upgrade-plan-controller"),
ServiceAccount: serviceAccountName,
ReleaseManifestImage: releaseManifestImage,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "UpgradePlan")
os.Exit(1)
Expand Down
6 changes: 6 additions & 0 deletions config/manager/manager.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: RELEASE_MANIFEST_IMAGE
value: registry.opensuse.org/isv/suse/edge/lifecycle/containerfile/suse/release-manifest
- name: SERVICE_ACCOUNT_NAME
valueFrom:
fieldRef:
fieldPath: spec.serviceAccountName
securityContext:
allowPrivilegeEscalation: false
capabilities:
Expand Down
2 changes: 2 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ rules:
resources:
- jobs
verbs:
- create
- get
- list
- watch
Expand Down Expand Up @@ -69,6 +70,7 @@ rules:
resources:
- releasemanifests
verbs:
- create
- get
- list
- watch
Expand Down
43 changes: 43 additions & 0 deletions internal/controller/release_manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package controller

import (
"context"
"fmt"
"time"

lifecyclev1alpha1 "github.com/suse-edge/upgrade-controller/api/v1alpha1"
"github.com/suse-edge/upgrade-controller/internal/upgrade"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"
)

var errReleaseManifestNotFound = fmt.Errorf("release manifest not found")

func (r *UpgradePlanReconciler) retrieveReleaseManifest(ctx context.Context, upgradePlan *lifecyclev1alpha1.UpgradePlan) (*lifecyclev1alpha1.ReleaseManifest, error) {
manifests := &lifecyclev1alpha1.ReleaseManifestList{}
listOpts := &client.ListOptions{
Namespace: upgradePlan.Namespace,
}
if err := r.List(ctx, manifests, listOpts); err != nil {
return nil, fmt.Errorf("listing release manifests in cluster: %w", err)
}

for _, manifest := range manifests.Items {
if manifest.Spec.ReleaseVersion == upgradePlan.Spec.ReleaseVersion {
return &manifest, nil
}
}

return nil, errReleaseManifestNotFound
}

func (r *UpgradePlanReconciler) createReleaseManifest(ctx context.Context, upgradePlan *lifecyclev1alpha1.UpgradePlan) error {
annotations := upgrade.PlanIdentifierAnnotations(upgradePlan.Name, upgradePlan.Namespace)
job := upgrade.ReleaseManifestInstallJob(r.ReleaseManifestImage, upgradePlan.Spec.ReleaseVersion, r.ServiceAccount, upgradePlan.Namespace, annotations)

// Retry the creation since a previously failed job could be in the process of deletion due to TTL
return retry.OnError(wait.Backoff{Steps: 5, Duration: 500 * time.Millisecond},
func(err error) bool { return true },
func() error { return r.createObject(ctx, upgradePlan, job) })
}
44 changes: 19 additions & 25 deletions internal/controller/upgradeplan_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@ import (
// UpgradePlanReconciler reconciles a UpgradePlan object
type UpgradePlanReconciler struct {
client.Client
Scheme *runtime.Scheme
Recorder record.EventRecorder
Scheme *runtime.Scheme
Recorder record.EventRecorder
ServiceAccount string
ReleaseManifestImage string
}

// +kubebuilder:rbac:groups=lifecycle.suse.com,resources=upgradeplans,verbs=get;list;watch;create;update;patch;delete
Expand All @@ -59,12 +61,12 @@ type UpgradePlanReconciler struct {
// +kubebuilder:rbac:groups="",resources=nodes,verbs=watch;list
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;delete;create;watch
// +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,verbs=get;list;watch;create
// +kubebuilder:rbac:groups=batch,resources=jobs/status,verbs=get
// +kubebuilder:rbac:groups=helm.cattle.io,resources=helmcharts,verbs=get;update;list;watch;create
// +kubebuilder:rbac:groups=helm.cattle.io,resources=helmcharts/status,verbs=get
// +kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=get
// +kubebuilder:rbac:groups=lifecycle.suse.com,resources=releasemanifests,verbs=get;list;watch
// +kubebuilder:rbac:groups=lifecycle.suse.com,resources=releasemanifests,verbs=get;list;watch;create

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
Expand All @@ -84,28 +86,14 @@ func (r *UpgradePlanReconciler) Reconcile(ctx context.Context, req ctrl.Request)
return result, errors.Join(err, r.Status().Update(ctx, plan))
}

func (r *UpgradePlanReconciler) getReleaseManifest(ctx context.Context, upgradePlan *lifecyclev1alpha1.UpgradePlan) (*lifecyclev1alpha1.ReleaseManifest, error) {
manifests := &lifecyclev1alpha1.ReleaseManifestList{}
listOpts := &client.ListOptions{
Namespace: upgradePlan.Namespace,
}
if err := r.List(ctx, manifests, listOpts); err != nil {
return nil, fmt.Errorf("listing release manifests in cluster: %w", err)
}

for _, manifest := range manifests.Items {
if manifest.Spec.ReleaseVersion == upgradePlan.Spec.ReleaseVersion {
return &manifest, nil
}
}

return nil, fmt.Errorf("release manifest with version %s not found", upgradePlan.Spec.ReleaseVersion)
}

func (r *UpgradePlanReconciler) executePlan(ctx context.Context, upgradePlan *lifecyclev1alpha1.UpgradePlan) (ctrl.Result, error) {
release, err := r.getReleaseManifest(ctx, upgradePlan)
release, err := r.retrieveReleaseManifest(ctx, upgradePlan)
if err != nil {
return ctrl.Result{}, fmt.Errorf("retrieving release manifest: %w", err)
if !errors.Is(err, errReleaseManifestNotFound) {
return ctrl.Result{}, fmt.Errorf("retrieving release manifest: %w", err)
}

return ctrl.Result{}, r.createReleaseManifest(ctx, upgradePlan)
}

if len(upgradePlan.Status.Conditions) == 0 {
Expand Down Expand Up @@ -223,10 +211,16 @@ func setSkippedCondition(plan *lifecyclev1alpha1.UpgradePlan, conditionType, mes
}

func (r *UpgradePlanReconciler) findUpgradePlanFromJob(ctx context.Context, job client.Object) []reconcile.Request {
// Check whether the Job was created by the Upgrade Controller first
requests := r.findUpgradePlanFromAnnotations(ctx, job)
if len(requests) != 0 {
return requests
}

// Check whether the Job was created by the Helm Controller
jobLabels := job.GetLabels()
chartName, ok := jobLabels[chart.Label]
if !ok || chartName == "" {
// Job is not scheduled by the Helm controller.
return []reconcile.Request{}
}

Expand Down
49 changes: 49 additions & 0 deletions internal/upgrade/release_manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package upgrade

import (
"fmt"
"strings"

batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func ReleaseManifestInstallJob(image, version, serviceAccount, namespace string, annotations map[string]string) *batchv1.Job {
version = strings.TrimPrefix(version, "v")
workloadName := fmt.Sprintf("apply-release-manifest-%s", strings.ReplaceAll(version, ".", "-"))
image = fmt.Sprintf("%s:%s", image, version)
ttl := int32(0)

return &batchv1.Job{
TypeMeta: metav1.TypeMeta{
APIVersion: "batch/v1",
Kind: "Job",
},
ObjectMeta: metav1.ObjectMeta{
Name: workloadName,
Namespace: namespace,
Annotations: annotations,
},
Spec: batchv1.JobSpec{
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Name: workloadName,
Namespace: namespace,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: workloadName,
Image: image,
Args: []string{"apply", "-f", "release_manifest.yaml"},
},
},
RestartPolicy: "OnFailure",
ServiceAccountName: serviceAccount,
},
},
TTLSecondsAfterFinished: &ttl,
},
}
}

0 comments on commit 8db0eb2

Please sign in to comment.