penguin/monok8s

k8s image for Mono Gateway Dev Kit

clitools/pkg/controller/osupgrade/progress.go

raw ยท 6990 bytes

package osupgrade

import (
	"context"
	"fmt"

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/klog/v2"

	monov1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
	"example.com/monok8s/pkg/buildinfo"
	"example.com/monok8s/pkg/kube"
)

var (
	unstructuredConverter = runtime.DefaultUnstructuredConverter

	osup_gvr = schema.GroupVersionResource{
		Group:    monov1alpha1.Group,
		Version:  monov1alpha1.Version,
		Resource: "osupgradeprogresses",
	}
)

func ensureProgressHeartbeat(
	ctx context.Context,
	clients *kube.Clients,
	namespace string,
	nodeName string,
	osu *monov1alpha1.OSUpgrade,
) (*monov1alpha1.OSUpgradeProgress, error) {

	name := fmt.Sprintf("%s-%s", osu.Name, nodeName)
	now := metav1.Now()

	currentVersion := buildinfo.KubeVersion
	targetVersion := ""

	if osu.Status != nil {
		targetVersion = osu.Status.ResolvedVersion
	}

	progress := &monov1alpha1.OSUpgradeProgress{
		TypeMeta: metav1.TypeMeta{
			APIVersion: monov1alpha1.APIVersion,
			Kind:       "OSUpgradeProgress",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
		},
		Spec: monov1alpha1.OSUpgradeProgressSpec{
			NodeName: nodeName,
			SourceRef: monov1alpha1.OSUpgradeSourceRef{
				Name: osu.Name,
			},
		},
		Status: &monov1alpha1.OSUpgradeProgressStatus{
			CurrentVersion: currentVersion,
			TargetVersion:  targetVersion,
			Phase:          monov1alpha1.OSUpgradeProgressPhasePending,
			LastUpdatedAt:  &now,
			Message:        "acknowledged",
		},
	}

	created, err := createProgress(ctx, clients, osup_gvr, progress)
	if err == nil {
		klog.InfoS("created osupgradeprogress", "name", created.Name, "namespace", created.Namespace)
		return created, nil
	}
	if !apierrors.IsAlreadyExists(err) {
		return nil, fmt.Errorf("create OSUpgradeProgress %s/%s: %w", namespace, name, err)
	}

	existing, err := getProgress(ctx, clients, osup_gvr, namespace, name)
	if err != nil {
		return nil, fmt.Errorf("get existing OSUpgradeProgress %s/%s: %w", namespace, name, err)
	}

	// Spec should remain aligned with the source and node.
	existing.Spec.NodeName = nodeName
	existing.Spec.SourceRef.Name = osu.Name

	if existing, err = updateProgressSpec(ctx, clients, osup_gvr, existing); err != nil {
		return nil, fmt.Errorf("update OSUpgradeProgress spec %s/%s: %w", namespace, name, err)
	}

	if existing.Status == nil {
		existing.Status = &monov1alpha1.OSUpgradeProgressStatus{}
	}

	existing.Status.CurrentVersion = currentVersion
	existing.Status.TargetVersion = targetVersion
	existing.Status.LastUpdatedAt = &now

	// Only set phase/message if they are still empty, so later real state machine
	// updates are not clobbered by the heartbeat.
	if existing.Status.Phase == "" {
		existing.Status.Phase = monov1alpha1.OSUpgradeProgressPhasePending
	}

	if existing, err = updateProgressStatus(ctx, clients, osup_gvr, existing); err != nil {
		return nil, fmt.Errorf("update OSUpgradeProgress status %s/%s: %w", namespace, name, err)
	}

	klog.InfoS("updated osupgradeprogress", "name", existing.Name, "namespace", existing.Namespace)
	return existing, nil
}

func createProgress(
	ctx context.Context,
	clients *kube.Clients,
	gvr schema.GroupVersionResource,
	progress *monov1alpha1.OSUpgradeProgress,
) (*monov1alpha1.OSUpgradeProgress, error) {
	obj, err := toUnstructured(progress)
	if err != nil {
		return nil, err
	}

	created, err := clients.Dynamic.
		Resource(gvr).
		Namespace(progress.Namespace).
		Create(ctx, obj, metav1.CreateOptions{})
	if err != nil {
		return nil, err
	}

	return fromUnstructuredProgress(created)
}

func getProgress(
	ctx context.Context,
	clients *kube.Clients,
	gvr schema.GroupVersionResource,
	namespace, name string,
) (*monov1alpha1.OSUpgradeProgress, error) {
	got, err := clients.Dynamic.
		Resource(gvr).
		Namespace(namespace).
		Get(ctx, name, metav1.GetOptions{})
	if err != nil {
		return nil, err
	}

	return fromUnstructuredProgress(got)
}

func updateProgressSpec(
	ctx context.Context,
	clients *kube.Clients,
	gvr schema.GroupVersionResource,
	progress *monov1alpha1.OSUpgradeProgress,
) (*monov1alpha1.OSUpgradeProgress, error) {
	obj, err := toUnstructured(progress)
	if err != nil {
		return nil, err
	}

	updated, err := clients.Dynamic.
		Resource(gvr).
		Namespace(progress.Namespace).
		Update(ctx, obj, metav1.UpdateOptions{})
	if err != nil {
		return nil, err
	}

	return fromUnstructuredProgress(updated)
}

func updateProgressStatus(
	ctx context.Context,
	clients *kube.Clients,
	gvr schema.GroupVersionResource,
	progress *monov1alpha1.OSUpgradeProgress,
) (*monov1alpha1.OSUpgradeProgress, error) {
	obj, err := toUnstructured(progress)
	if err != nil {
		return nil, err
	}

	updated, err := clients.Dynamic.
		Resource(gvr).
		Namespace(progress.Namespace).
		UpdateStatus(ctx, obj, metav1.UpdateOptions{})
	if err != nil {
		return nil, err
	}

	return fromUnstructuredProgress(updated)
}

func failProgress(
	ctx context.Context,
	clients *kube.Clients,
	osup *monov1alpha1.OSUpgradeProgress,
	action string,
	cause error,
) error {
	now := metav1.Now()

	if osup.Status == nil {
		osup.Status = &monov1alpha1.OSUpgradeProgressStatus{}
	}

	osup.Status.LastUpdatedAt = &now
	osup.Status.Message = fmt.Sprintf("%s: %v", action, cause)
	osup.Status.Phase = monov1alpha1.OSUpgradeProgressPhaseFailed

	if _, err := updateProgressStatus(ctx, clients, osup_gvr, osup); err != nil {
		klog.ErrorS(err, "failed to update osupgradeprogress status after error",
			"action", action,
			"name", osup.Name,
			"namespace", osup.Namespace,
		)
	}

	return fmt.Errorf("%s: %w", action, cause)
}

func markProgressCompleted(
	ctx context.Context,
	clients *kube.Clients,
	osup *monov1alpha1.OSUpgradeProgress,
	message string,
) error {
	now := metav1.Now()

	if osup.Status == nil {
		osup.Status = &monov1alpha1.OSUpgradeProgressStatus{}
	}

	osup.Status.Phase = monov1alpha1.OSUpgradeProgressPhaseCompleted
	osup.Status.Message = message
	osup.Status.LastUpdatedAt = &now
	osup.Status.CompletedAt = &now

	_, err := updateProgressStatus(ctx, clients, osup_gvr, osup)
	if err != nil {
		return fmt.Errorf("mark progress completed: %w", err)
	}

	return nil
}

func toUnstructured(progress *monov1alpha1.OSUpgradeProgress) (*unstructured.Unstructured, error) {
	m, err := unstructuredConverter.ToUnstructured(progress)
	if err != nil {
		return nil, fmt.Errorf("convert OSUpgradeProgress to unstructured: %w", err)
	}
	return &unstructured.Unstructured{Object: m}, nil
}

func fromUnstructuredProgress(obj *unstructured.Unstructured) (*monov1alpha1.OSUpgradeProgress, error) {
	var progress monov1alpha1.OSUpgradeProgress
	if err := unstructuredConverter.FromUnstructured(obj.Object, &progress); err != nil {
		return nil, fmt.Errorf("convert unstructured to OSUpgradeProgress: %w", err)
	}
	return &progress, nil
}