OSUpgrade: Version Planning
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
VERSION ?= dev
|
VERSION ?= dev
|
||||||
|
|
||||||
# Target kube version
|
# Target kube version
|
||||||
KUBE_VERSION ?= v1.35.1
|
KUBE_VERSION ?= v1.34.1
|
||||||
|
|
||||||
GIT_REV := $(shell git rev-parse HEAD)
|
GIT_REV := $(shell git rev-parse HEAD)
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ var (
|
|||||||
Label = "monok8s.io/label"
|
Label = "monok8s.io/label"
|
||||||
Annotation = "monok8s.io/annotation"
|
Annotation = "monok8s.io/annotation"
|
||||||
ControlAgentKey = "monok8s.io/control-agent"
|
ControlAgentKey = "monok8s.io/control-agent"
|
||||||
|
CatalogURL = "https://example.com/monok8s.io/v1alpha1/catalog.yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -56,12 +56,17 @@ type OSUpgradeSpec struct {
|
|||||||
// User request, can be "stable" or an explicit version like "v1.35.3".
|
// User request, can be "stable" or an explicit version like "v1.35.3".
|
||||||
DesiredVersion string `json:"desiredVersion,omitempty" yaml:"desiredVersion,omitempty"`
|
DesiredVersion string `json:"desiredVersion,omitempty" yaml:"desiredVersion,omitempty"`
|
||||||
|
|
||||||
ImageURL string `json:"imageURL,omitempty" yaml:"imageURL,omitempty"`
|
Catalog *VersionCatalogSource `json:"catalog,omitempty"`
|
||||||
Checksum string `json:"checksum,omitempty" yaml:"checksum,omitempty"`
|
|
||||||
|
|
||||||
NodeSelector *metav1.LabelSelector `json:"nodeSelector,omitempty" yaml:"nodeSelector,omitempty"`
|
NodeSelector *metav1.LabelSelector `json:"nodeSelector,omitempty" yaml:"nodeSelector,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VersionCatalogSource struct {
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
Inline string `json:"inline,omitempty"`
|
||||||
|
ConfigMap string `json:"configMapRef,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type OSUpgradeStatus struct {
|
type OSUpgradeStatus struct {
|
||||||
Phase OSUpgradePhase `json:"phase,omitempty" yaml:"phase,omitempty"`
|
Phase OSUpgradePhase `json:"phase,omitempty" yaml:"phase,omitempty"`
|
||||||
ResolvedVersion string `json:"resolvedVersion,omitempty" yaml:"resolvedVersion,omitempty"`
|
ResolvedVersion string `json:"resolvedVersion,omitempty" yaml:"resolvedVersion,omitempty"`
|
||||||
@@ -115,16 +120,22 @@ type OSUpgradeSourceRef struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OSUpgradeProgressStatus struct {
|
type OSUpgradeProgressStatus struct {
|
||||||
CurrentVersion string `json:"currentVersion,omitempty" yaml:"currentVersion,omitempty"`
|
CurrentVersion string `json:"currentVersion,omitempty" yaml:"currentVersion,omitempty"`
|
||||||
TargetVersion string `json:"targetVersion,omitempty" yaml:"targetVersion,omitempty"`
|
TargetVersion string `json:"targetVersion,omitempty" yaml:"targetVersion,omitempty"`
|
||||||
Phase OSUpgradeProgressPhase `json:"phase,omitempty" yaml:"phase,omitempty"`
|
Phase OSUpgradeProgressPhase `json:"phase,omitempty" yaml:"phase,omitempty"`
|
||||||
StartedAt *metav1.Time `json:"startedAt,omitempty" yaml:"startedAt,omitempty"`
|
|
||||||
CompletedAt *metav1.Time `json:"completedAt,omitempty" yaml:"completedAt,omitempty"`
|
StartedAt *metav1.Time `json:"startedAt,omitempty" yaml:"startedAt,omitempty"`
|
||||||
LastUpdatedAt *metav1.Time `json:"lastUpdatedAt,omitempty" yaml:"lastUpdatedAt,omitempty"`
|
CompletedAt *metav1.Time `json:"completedAt,omitempty" yaml:"completedAt,omitempty"`
|
||||||
RetryCount int32 `json:"retryCount,omitempty" yaml:"retryCount,omitempty"`
|
LastUpdatedAt *metav1.Time `json:"lastUpdatedAt,omitempty" yaml:"lastUpdatedAt,omitempty"`
|
||||||
InactivePartition string `json:"inactivePartition,omitempty" yaml:"inactivePartition,omitempty"`
|
RetryCount int32 `json:"retryCount,omitempty" yaml:"retryCount,omitempty"`
|
||||||
FailureReason string `json:"failureReason,omitempty" yaml:"failureReason,omitempty"`
|
InactivePartition string `json:"inactivePartition,omitempty" yaml:"inactivePartition,omitempty"`
|
||||||
Message string `json:"message,omitempty" yaml:"message,omitempty"`
|
FailureReason string `json:"failureReason,omitempty" yaml:"failureReason,omitempty"`
|
||||||
|
Message string `json:"message,omitempty" yaml:"message,omitempty"`
|
||||||
|
|
||||||
|
PlannedPath []string `json:"plannedPath,omitempty"`
|
||||||
|
CurrentStep int32 `json:"currentStep,omitempty"`
|
||||||
|
CurrentFrom string `json:"currentFrom,omitempty"`
|
||||||
|
CurrentTo string `json:"currentTo,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (in *OSUpgrade) DeepCopyObject() runtime.Object {
|
func (in *OSUpgrade) DeepCopyObject() runtime.Object {
|
||||||
|
|||||||
113
clitools/pkg/catalog/resolver.go
Normal file
113
clitools/pkg/catalog/resolver.go
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package catalog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"sigs.k8s.io/yaml"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
|
||||||
|
monov1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultCachePath = "/var/lib/monok8s/catalog.yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResolveCatalog resolves catalog using priority:
|
||||||
|
// Inline > ConfigMap > URL > cached
|
||||||
|
func ResolveCatalog(
|
||||||
|
ctx context.Context,
|
||||||
|
kubeClient kubernetes.Interface,
|
||||||
|
namespace string,
|
||||||
|
src *monov1alpha1.VersionCatalogSource,
|
||||||
|
) (*VersionCatalog, error) {
|
||||||
|
|
||||||
|
// 1. Inline
|
||||||
|
if src != nil && src.Inline != "" {
|
||||||
|
return parseCatalog([]byte(src.Inline))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. ConfigMap
|
||||||
|
if src != nil && src.ConfigMap != "" {
|
||||||
|
cm, err := kubeClient.CoreV1().ConfigMaps(namespace).Get(ctx, src.ConfigMap, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get catalog configmap: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, ok := cm.Data["catalog.yaml"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("configmap %s missing key catalog.yaml", src.ConfigMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseCatalog([]byte(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. URL
|
||||||
|
if src != nil && src.URL != "" {
|
||||||
|
cat, err := fetchCatalog(src.URL)
|
||||||
|
if err == nil {
|
||||||
|
_ = os.WriteFile(DefaultCachePath, mustMarshal(cat), 0644)
|
||||||
|
return cat, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback to cache
|
||||||
|
if cached, err2 := loadCached(); err2 == nil {
|
||||||
|
return cached, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("fetch catalog failed and no cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. cached fallback
|
||||||
|
if cached, err := loadCached(); err == nil {
|
||||||
|
return cached, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("no catalog source available")
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchCatalog(url string) (*VersionCatalog, error) {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return nil, fmt.Errorf("http %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseCatalog(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadCached() (*VersionCatalog, error) {
|
||||||
|
data, err := os.ReadFile(DefaultCachePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return parseCatalog(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCatalog(data []byte) (*VersionCatalog, error) {
|
||||||
|
var c VersionCatalog
|
||||||
|
if err := yaml.Unmarshal(data, &c); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse catalog: %w", err)
|
||||||
|
}
|
||||||
|
return &c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustMarshal(c *VersionCatalog) []byte {
|
||||||
|
b, _ := yaml.Marshal(c)
|
||||||
|
return b
|
||||||
|
}
|
||||||
13
clitools/pkg/catalog/types.go
Normal file
13
clitools/pkg/catalog/types.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package catalog
|
||||||
|
|
||||||
|
type VersionCatalog struct {
|
||||||
|
Stable string `json:"stable" yaml:"stable"`
|
||||||
|
Images []CatalogImage `json:"images" yaml:"images"`
|
||||||
|
Blocked []string `json:"blocked,omitempty" yaml:"blocked,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CatalogImage struct {
|
||||||
|
Version string `json:"version" yaml:"version"`
|
||||||
|
URL string `json:"url" yaml:"url"`
|
||||||
|
Checksum string `json:"checksum,omitempty" yaml:"checksum,omitempty"`
|
||||||
|
}
|
||||||
@@ -7,63 +7,198 @@ import (
|
|||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
|
|
||||||
monov1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
|
monov1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
|
||||||
mkscmd "example.com/monok8s/pkg/cmd"
|
mkscmd "example.com/monok8s/pkg/cmd"
|
||||||
|
osupgradeController "example.com/monok8s/pkg/controller/osupgrade"
|
||||||
"example.com/monok8s/pkg/kube"
|
"example.com/monok8s/pkg/kube"
|
||||||
"example.com/monok8s/pkg/templates"
|
"example.com/monok8s/pkg/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const defaultPollInterval = 15 * time.Second
|
||||||
|
|
||||||
|
var runtimeDefaultUnstructuredConverter = runtime.DefaultUnstructuredConverter
|
||||||
|
|
||||||
func NewCmdAgent(flags *genericclioptions.ConfigFlags) *cobra.Command {
|
func NewCmdAgent(flags *genericclioptions.ConfigFlags) *cobra.Command {
|
||||||
var namespace string
|
var namespace string
|
||||||
var envFile string
|
var envFile string
|
||||||
|
var pollInterval time.Duration
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "agent --env-file path",
|
Use: "agent --env-file path",
|
||||||
Short: "Watch OSUpgrade resources and do nothing for now",
|
Short: "Watch OSUpgrade resources and process matching upgrades for this node",
|
||||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if envFile == "" {
|
||||||
var cfg *monov1alpha1.MonoKSConfig // or value, depending on your API
|
return fmt.Errorf("--env-file is required")
|
||||||
|
}
|
||||||
|
|
||||||
if err := mkscmd.LoadEnvFile(envFile); err != nil {
|
if err := mkscmd.LoadEnvFile(envFile); err != nil {
|
||||||
return fmt.Errorf("load env file %q: %w", envFile, err)
|
return fmt.Errorf("load env file %q: %w", envFile, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
vals := templates.LoadTemplateValuesFromEnv()
|
vals := templates.LoadTemplateValuesFromEnv()
|
||||||
rendered := templates.DefaultMonoKSConfig(vals)
|
rendered := templates.DefaultMonoKSConfig(vals)
|
||||||
cfg = &rendered
|
cfg := &rendered
|
||||||
|
|
||||||
klog.InfoS("starting agent", "node", cfg.Spec.NodeName, "envFile", envFile)
|
if cfg.Spec.NodeName == "" {
|
||||||
|
return fmt.Errorf("node name is empty in rendered config")
|
||||||
|
}
|
||||||
|
|
||||||
|
klog.InfoS("starting agent",
|
||||||
|
"node", cfg.Spec.NodeName,
|
||||||
|
"namespace", namespace,
|
||||||
|
"envFile", envFile,
|
||||||
|
"pollInterval", pollInterval,
|
||||||
|
)
|
||||||
|
|
||||||
clients, err := kube.NewClients(flags)
|
clients, err := kube.NewClients(flags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("create kube clients: %w", err)
|
||||||
}
|
}
|
||||||
gvr := schema.GroupVersionResource{Group: monov1alpha1.Group, Version: monov1alpha1.Version, Resource: "osupgrades"}
|
|
||||||
ctx := cmd.Context()
|
ctx := cmd.Context()
|
||||||
for {
|
return runPollLoop(ctx, clients, namespace, cfg.Spec.NodeName, pollInterval)
|
||||||
list, err := clients.Dynamic.Resource(gvr).Namespace(namespace).List(ctx, metav1.ListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
klog.InfoS("agent tick", "namespace", namespace, "items", len(list.Items))
|
|
||||||
for _, item := range list.Items {
|
|
||||||
klog.InfoS("observed osupgrade", "name", item.GetName(), "resourceVersion", item.GetResourceVersion())
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
case <-time.After(15 * time.Second):
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringVar(&namespace, "namespace", "kube-system", "namespace to watch")
|
cmd.Flags().StringVar(&namespace, "namespace", "kube-system", "namespace to watch")
|
||||||
cmd.Flags().StringVar(&envFile, "env-file", "", "path to env file containing MKS_* variables")
|
cmd.Flags().StringVar(&envFile, "env-file", "", "path to env file containing MKS_* variables")
|
||||||
|
cmd.Flags().DurationVar(&pollInterval, "poll-interval", defaultPollInterval, "poll interval for OSUpgrade resources")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ = context.Background
|
func runPollLoop(ctx context.Context, clients *kube.Clients, namespace, nodeName string, interval time.Duration) error {
|
||||||
var _ = fmt.Sprintf
|
gvr := schema.GroupVersionResource{
|
||||||
|
Group: monov1alpha1.Group,
|
||||||
|
Version: monov1alpha1.Version,
|
||||||
|
Resource: "osupgrades",
|
||||||
|
}
|
||||||
|
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
if err := pollOnce(ctx, clients, gvr, namespace, nodeName); err != nil {
|
||||||
|
klog.ErrorS(err, "poll failed", "namespace", namespace, "node", nodeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-ticker.C:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pollOnce(
|
||||||
|
ctx context.Context,
|
||||||
|
clients *kube.Clients,
|
||||||
|
gvr schema.GroupVersionResource,
|
||||||
|
namespace string,
|
||||||
|
nodeName string,
|
||||||
|
) error {
|
||||||
|
list, err := clients.Dynamic.Resource(gvr).Namespace(namespace).List(ctx, metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list osupgrades: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
klog.InfoS("agent tick", "namespace", namespace, "items", len(list.Items), "node", nodeName)
|
||||||
|
|
||||||
|
nodeLabels := labels.Set{
|
||||||
|
"kubernetes.io/hostname": nodeName,
|
||||||
|
"monok8s.io/node-name": nodeName,
|
||||||
|
"monok8s.io/control-agent": "true",
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range list.Items {
|
||||||
|
item := &list.Items[i]
|
||||||
|
|
||||||
|
osu, err := decodeOSUpgrade(item)
|
||||||
|
if err != nil {
|
||||||
|
klog.ErrorS(err, "failed to decode osupgrade",
|
||||||
|
"name", item.GetName(),
|
||||||
|
"resourceVersion", item.GetResourceVersion(),
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !matchesNode(osu, nodeName, nodeLabels) {
|
||||||
|
klog.V(2).InfoS("skipping osupgrade; not targeted to this node",
|
||||||
|
"name", osu.Name,
|
||||||
|
"node", nodeName,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
klog.InfoS("matched osupgrade",
|
||||||
|
"name", osu.Name,
|
||||||
|
"node", nodeName,
|
||||||
|
"desiredVersion", osu.Spec.DesiredVersion,
|
||||||
|
"phase", statusPhase(osu.Status),
|
||||||
|
"resourceVersion", osu.ResourceVersion,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := osupgradeController.HandleOSUpgrade(ctx, clients, namespace, nodeName, osu); err != nil {
|
||||||
|
klog.ErrorS(err, "failed to handle osupgrade",
|
||||||
|
"name", osu.Name,
|
||||||
|
"node", nodeName,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeOSUpgrade(item *unstructured.Unstructured) (*monov1alpha1.OSUpgrade, error) {
|
||||||
|
var osu monov1alpha1.OSUpgrade
|
||||||
|
if err := runtimeDefaultUnstructuredConverter.FromUnstructured(item.Object, &osu); err != nil {
|
||||||
|
return nil, fmt.Errorf("convert unstructured to OSUpgrade: %w", err)
|
||||||
|
}
|
||||||
|
return &osu, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchesNode(osu *monov1alpha1.OSUpgrade, nodeName string, nodeLabels labels.Set) bool {
|
||||||
|
if osu == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
sel := osu.Spec.NodeSelector
|
||||||
|
if sel == nil {
|
||||||
|
// No selector means "match all nodes".
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
selector, err := metav1.LabelSelectorAsSelector(sel)
|
||||||
|
if err != nil {
|
||||||
|
klog.ErrorS(err, "invalid node selector on osupgrade", "name", osu.Name)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if selector.Empty() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if selector.Matches(nodeLabels) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cheap fallback in case you temporarily target by explicit hostname-ish labels only.
|
||||||
|
return nodeName != "" && selector.Matches(labels.Set{
|
||||||
|
"kubernetes.io/hostname": nodeName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusPhase(st *monov1alpha1.OSUpgradeStatus) string {
|
||||||
|
if st == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(st.Phase)
|
||||||
|
}
|
||||||
|
|||||||
80
clitools/pkg/controller/osupgrade/handler.go
Normal file
80
clitools/pkg/controller/osupgrade/handler.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package osupgrade
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
|
||||||
|
monov1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
|
||||||
|
"example.com/monok8s/pkg/buildinfo"
|
||||||
|
"example.com/monok8s/pkg/catalog"
|
||||||
|
"example.com/monok8s/pkg/kube"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleOSUpgrade(
|
||||||
|
ctx context.Context,
|
||||||
|
clients *kube.Clients,
|
||||||
|
namespace string,
|
||||||
|
nodeName string,
|
||||||
|
osu *monov1alpha1.OSUpgrade,
|
||||||
|
) error {
|
||||||
|
osup, err := ensureProgressHeartbeat(ctx, clients, namespace, nodeName, osu)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
klog.InfoS("handling osupgrade",
|
||||||
|
"name", osu.Name,
|
||||||
|
"node", nodeName,
|
||||||
|
"desiredVersion", osu.Spec.DesiredVersion,
|
||||||
|
)
|
||||||
|
|
||||||
|
kata, err := catalog.ResolveCatalog(ctx, clients.Kubernetes, namespace, osu.Spec.Catalog)
|
||||||
|
if err != nil {
|
||||||
|
return failProgress(ctx, clients, osup, "resolve catalog", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plan, err := PlanUpgrade(buildinfo.KubeVersion, osu, kata)
|
||||||
|
if err != nil {
|
||||||
|
return failProgress(ctx, clients, osup, "plan upgrade", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(plan.Path) == 0 {
|
||||||
|
osup.Status.CurrentVersion = buildinfo.KubeVersion
|
||||||
|
osup.Status.TargetVersion = buildinfo.KubeVersion
|
||||||
|
if err := markProgressCompleted(ctx, clients, osup, "already at target version"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
klog.InfoS("osupgrade already satisfied",
|
||||||
|
"name", osu.Name,
|
||||||
|
"node", nodeName,
|
||||||
|
"target", plan.ResolvedTarget,
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
first := plan.Path[0]
|
||||||
|
|
||||||
|
osup.Status.TargetVersion = plan.ResolvedTarget
|
||||||
|
osup.Status.Phase = monov1alpha1.OSUpgradeProgressPhaseDownloading
|
||||||
|
osup.Status.Message = fmt.Sprintf("downloading image: %s", first.URL)
|
||||||
|
now := metav1.Now()
|
||||||
|
osup.Status.LastUpdatedAt = &now
|
||||||
|
|
||||||
|
if _, err := updateProgressStatus(ctx, clients, osup_gvr, osup); err != nil {
|
||||||
|
return fmt.Errorf("update progress status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
klog.InfoS("planned osupgrade",
|
||||||
|
"name", osu.Name,
|
||||||
|
"node", nodeName,
|
||||||
|
"resolvedTarget", plan.ResolvedTarget,
|
||||||
|
"steps", len(plan.Path),
|
||||||
|
"firstVersion", first.Version,
|
||||||
|
"firstURL", first.URL,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
327
clitools/pkg/controller/osupgrade/planner.go
Normal file
327
clitools/pkg/controller/osupgrade/planner.go
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
package osupgrade
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
monov1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
|
||||||
|
"example.com/monok8s/pkg/catalog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Version struct {
|
||||||
|
Major int
|
||||||
|
Minor int
|
||||||
|
Patch int
|
||||||
|
Raw string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseVersion(s string) (Version, error) {
|
||||||
|
raw := strings.TrimSpace(s)
|
||||||
|
if raw == "" {
|
||||||
|
return Version{}, fmt.Errorf("empty version")
|
||||||
|
}
|
||||||
|
|
||||||
|
raw = strings.TrimPrefix(raw, "v")
|
||||||
|
parts := strings.Split(raw, ".")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return Version{}, fmt.Errorf("invalid version %q: expected vMAJOR.MINOR.PATCH", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
maj, err := strconv.Atoi(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return Version{}, fmt.Errorf("parse major from %q: %w", s, err)
|
||||||
|
}
|
||||||
|
min, err := strconv.Atoi(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return Version{}, fmt.Errorf("parse minor from %q: %w", s, err)
|
||||||
|
}
|
||||||
|
patch, err := strconv.Atoi(parts[2])
|
||||||
|
if err != nil {
|
||||||
|
return Version{}, fmt.Errorf("parse patch from %q: %w", s, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Version{
|
||||||
|
Major: maj,
|
||||||
|
Minor: min,
|
||||||
|
Patch: patch,
|
||||||
|
Raw: fmt.Sprintf("v%d.%d.%d", maj, min, patch),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) String() string {
|
||||||
|
return v.Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) Compare(o Version) int {
|
||||||
|
if v.Major != o.Major {
|
||||||
|
if v.Major < o.Major {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if v.Minor != o.Minor {
|
||||||
|
if v.Minor < o.Minor {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if v.Patch != o.Patch {
|
||||||
|
if v.Patch < o.Patch {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) SameMinor(o Version) bool {
|
||||||
|
return v.Major == o.Major && v.Minor == o.Minor
|
||||||
|
}
|
||||||
|
|
||||||
|
type Plan struct {
|
||||||
|
ResolvedTarget string
|
||||||
|
Path []catalog.CatalogImage
|
||||||
|
}
|
||||||
|
|
||||||
|
func PlanUpgrade(
|
||||||
|
current string,
|
||||||
|
osu *monov1alpha1.OSUpgrade,
|
||||||
|
cat *catalog.VersionCatalog,
|
||||||
|
) (*Plan, error) {
|
||||||
|
target, err := resolveTarget(osu.Spec.DesiredVersion, cat)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if isBlocked(target, cat.Blocked) {
|
||||||
|
return nil, fmt.Errorf("target %s is blocked", target)
|
||||||
|
}
|
||||||
|
|
||||||
|
imagesByVersion := make(map[string]catalog.CatalogImage, len(cat.Images))
|
||||||
|
installable := make([]string, 0, len(cat.Images))
|
||||||
|
|
||||||
|
for _, img := range cat.Images {
|
||||||
|
if img.Version == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isBlocked(img.Version, cat.Blocked) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := imagesByVersion[img.Version]; exists {
|
||||||
|
return nil, fmt.Errorf("duplicate image entry for version %s", img.Version)
|
||||||
|
}
|
||||||
|
imagesByVersion[img.Version] = img
|
||||||
|
installable = append(installable, img.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
versionPath, err := calculatePath(current, target, installable)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
path := make([]catalog.CatalogImage, 0, len(versionPath))
|
||||||
|
for _, v := range versionPath {
|
||||||
|
img, ok := imagesByVersion[v]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("internal error: no image for planned version %s", v)
|
||||||
|
}
|
||||||
|
path = append(path, img)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Plan{
|
||||||
|
ResolvedTarget: target,
|
||||||
|
Path: path,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func installableVersions(cat *catalog.VersionCatalog) []string {
|
||||||
|
out := make([]string, 0, len(cat.Images))
|
||||||
|
for _, img := range cat.Images {
|
||||||
|
if img.Version == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isBlocked(img.Version, cat.Blocked) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, img.Version)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveTarget(desired string, cat *catalog.VersionCatalog) (string, error) {
|
||||||
|
if desired == "stable" {
|
||||||
|
if cat.Stable == "" {
|
||||||
|
return "", fmt.Errorf("catalog missing stable")
|
||||||
|
}
|
||||||
|
return cat.Stable, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, img := range cat.Images {
|
||||||
|
if img.Version == desired {
|
||||||
|
return desired, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("desired version %s not in catalog", desired)
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculatePath(current, target string, available []string) ([]string, error) {
|
||||||
|
cur, err := ParseVersion(current)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse current version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tgt, err := ParseVersion(target)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse target version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cur.Compare(tgt) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if cur.Compare(tgt) > 0 {
|
||||||
|
return nil, fmt.Errorf("downgrade not supported: current=%s target=%s", cur, tgt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cur.Major != tgt.Major {
|
||||||
|
return nil, fmt.Errorf("cross-major upgrade not supported: %s -> %s", cur, tgt)
|
||||||
|
}
|
||||||
|
|
||||||
|
versions, err := parseAndSortVersions(available)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !containsVersion(versions, tgt) {
|
||||||
|
return nil, fmt.Errorf("target version %s not found in available versions", tgt)
|
||||||
|
}
|
||||||
|
|
||||||
|
var path []Version
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
|
||||||
|
add := func(v Version) {
|
||||||
|
if v.Compare(cur) <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := seen[v.String()]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen[v.String()] = struct{}{}
|
||||||
|
path = append(path, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same minor: jump directly to target patch.
|
||||||
|
if cur.SameMinor(tgt) {
|
||||||
|
add(tgt)
|
||||||
|
return versionsToStrings(path), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: finish current minor by moving to the latest patch available there.
|
||||||
|
if latestCurMinor, ok := latestPatchInMinor(versions, cur.Major, cur.Minor, cur); ok {
|
||||||
|
add(latestCurMinor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: walk each intermediate minor using the lowest available patch in that minor.
|
||||||
|
for minor := cur.Minor + 1; minor < tgt.Minor; minor++ {
|
||||||
|
bridge, ok := lowestPatchInMinor(versions, cur.Major, minor)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no available bridge version for v%d.%d.x", cur.Major, minor)
|
||||||
|
}
|
||||||
|
add(bridge)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: final target.
|
||||||
|
add(tgt)
|
||||||
|
|
||||||
|
return versionsToStrings(path), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAndSortVersions(raw []string) ([]Version, error) {
|
||||||
|
out := make([]Version, 0, len(raw))
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
|
||||||
|
for _, s := range raw {
|
||||||
|
v, err := ParseVersion(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse catalog version %q: %w", s, err)
|
||||||
|
}
|
||||||
|
if _, ok := seen[v.String()]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[v.String()] = struct{}{}
|
||||||
|
out = append(out, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(out, func(i, j int) bool {
|
||||||
|
return out[i].Compare(out[j]) < 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsRawVersion(versions []string, want string) bool {
|
||||||
|
for _, v := range versions {
|
||||||
|
if strings.TrimSpace(v) == strings.TrimSpace(want) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsVersion(versions []Version, want Version) bool {
|
||||||
|
for _, v := range versions {
|
||||||
|
if v.Compare(want) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isBlocked(version string, blocked []string) bool {
|
||||||
|
for _, v := range blocked {
|
||||||
|
if strings.TrimSpace(v) == strings.TrimSpace(version) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func latestPatchInMinor(versions []Version, major, minor int, gt Version) (Version, bool) {
|
||||||
|
var found Version
|
||||||
|
ok := false
|
||||||
|
|
||||||
|
for _, v := range versions {
|
||||||
|
if v.Major != major || v.Minor != minor {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if v.Compare(gt) <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !ok || found.Compare(v) < 0 {
|
||||||
|
found = v
|
||||||
|
ok = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return found, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func lowestPatchInMinor(versions []Version, major, minor int) (Version, bool) {
|
||||||
|
for _, v := range versions {
|
||||||
|
if v.Major == major && v.Minor == minor {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Version{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func versionsToStrings(vs []Version) []string {
|
||||||
|
out := make([]string, 0, len(vs))
|
||||||
|
for _, v := range vs {
|
||||||
|
out = append(out, v.String())
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
265
clitools/pkg/controller/osupgrade/progress.go
Normal file
265
clitools/pkg/controller/osupgrade/progress.go
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -77,12 +77,14 @@ func DefaultOSUpgrade(v TemplateValues) monov1alpha1.OSUpgrade {
|
|||||||
},
|
},
|
||||||
Spec: monov1alpha1.OSUpgradeSpec{
|
Spec: monov1alpha1.OSUpgradeSpec{
|
||||||
DesiredVersion: v.KubernetesVersion,
|
DesiredVersion: v.KubernetesVersion,
|
||||||
ImageURL: "https://example.invalid/images/monok8s-v0.0.1.img.zst",
|
|
||||||
NodeSelector: &metav1.LabelSelector{
|
NodeSelector: &metav1.LabelSelector{
|
||||||
MatchLabels: map[string]string{
|
MatchLabels: map[string]string{
|
||||||
"kubernetes.io/hostname": firstNonEmpty(v.NodeName, v.Hostname),
|
"kubernetes.io/hostname": firstNonEmpty(v.NodeName, v.Hostname),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Catalog: &monov1alpha1.VersionCatalogSource{
|
||||||
|
URL: monov1alpha1.CatalogURL,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user