From 4f490ab37e642b52c681edb18cd1b5933985247577e1555ee96e179470c61678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=9F=E9=85=8C=20=E9=B5=AC=E5=85=84?= Date: Sat, 4 Apr 2026 02:45:46 +0800 Subject: [PATCH] Writes and verify image --- clitools/go.mod | 7 +- clitools/go.sum | 2 + clitools/makefile | 6 +- clitools/pkg/catalog/checksums.go | 30 +++++ clitools/pkg/catalog/resolver.go | 14 +- clitools/pkg/catalog/types.go | 1 + clitools/pkg/cmd/agent/agent.go | 5 +- clitools/pkg/cmd/initcmd/init.go | 2 +- clitools/pkg/controller/osimage/apply.go | 42 ++++++ clitools/pkg/controller/osimage/helpers.go | 15 +++ clitools/pkg/controller/osimage/progress.go | 121 ++++++++++++++++++ clitools/pkg/controller/osimage/stream.go | 50 ++++++++ clitools/pkg/controller/osimage/types.go | 29 +++++ clitools/pkg/controller/osimage/verify.go | 104 +++++++++++++++ .../pkg/controller/osimage/verify_safe.go | 35 +++++ .../pkg/controller/osimage/verify_unsafe.go | 7 + clitools/pkg/controller/osimage/write.go | 82 ++++++++++++ clitools/pkg/controller/osupgrade/handler.go | 68 +++++++++- clitools/pkg/controller/osupgrade/progress.go | 7 +- clitools/pkg/node/kubeadm.go | 34 ----- 20 files changed, 596 insertions(+), 65 deletions(-) create mode 100644 clitools/pkg/catalog/checksums.go create mode 100644 clitools/pkg/controller/osimage/apply.go create mode 100644 clitools/pkg/controller/osimage/helpers.go create mode 100644 clitools/pkg/controller/osimage/progress.go create mode 100644 clitools/pkg/controller/osimage/stream.go create mode 100644 clitools/pkg/controller/osimage/types.go create mode 100644 clitools/pkg/controller/osimage/verify.go create mode 100644 clitools/pkg/controller/osimage/verify_safe.go create mode 100644 clitools/pkg/controller/osimage/verify_unsafe.go create mode 100644 clitools/pkg/controller/osimage/write.go diff --git a/clitools/go.mod b/clitools/go.mod index 389f4cf..aec328a 100644 --- a/clitools/go.mod +++ b/clitools/go.mod @@ -3,13 +3,17 @@ module example.com/monok8s go 1.24.0 require ( + github.com/klauspost/compress v1.18.5 github.com/spf13/cobra v1.9.1 + golang.org/x/sys v0.31.0 gopkg.in/yaml.v3 v3.0.1 + k8s.io/api v0.34.0 k8s.io/apiextensions-apiserver v0.34.0 k8s.io/apimachinery v0.34.0 k8s.io/cli-runtime v0.34.0 k8s.io/client-go v0.34.0 k8s.io/klog/v2 v2.130.1 + sigs.k8s.io/yaml v1.6.0 ) require ( @@ -48,14 +52,12 @@ require ( golang.org/x/net v0.38.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect golang.org/x/term v0.30.0 // indirect golang.org/x/text v0.23.0 // indirect golang.org/x/time v0.9.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - k8s.io/api v0.34.0 // indirect k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect @@ -63,5 +65,4 @@ require ( sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect - sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/clitools/go.sum b/clitools/go.sum index 801d9e3..5f0d86e 100644 --- a/clitools/go.sum +++ b/clitools/go.sum @@ -50,6 +50,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= diff --git a/clitools/makefile b/clitools/makefile index 10e2719..d55fce5 100644 --- a/clitools/makefile +++ b/clitools/makefile @@ -2,7 +2,7 @@ VERSION ?= dev # Target kube version -KUBE_VERSION ?= v1.34.1 +KUBE_VERSION ?= v1.35.0 GIT_REV := $(shell git rev-parse HEAD) @@ -41,8 +41,8 @@ build-local: .buildinfo mkdir -p $(BIN_DIR) go build -o $(BIN_DIR)/ctl-$(VERSION) ./cmd/ctl -run: - go run ./cmd/ctl +run-agent: + go run -tags dev ./cmd/ctl agent --env-file ./out/cluster.env clean: -docker image rm localhost/monok8s/control-agent:$(VERSION) diff --git a/clitools/pkg/catalog/checksums.go b/clitools/pkg/catalog/checksums.go new file mode 100644 index 0000000..ca19221 --- /dev/null +++ b/clitools/pkg/catalog/checksums.go @@ -0,0 +1,30 @@ +package catalog + +import ( + "encoding/hex" + "fmt" + "strings" +) + +func (c *CatalogImage) SHA256() (string, error) { + if c.Checksum == "" { + return "", fmt.Errorf("checksum is empty") + } + + const prefix = "sha256:" + if !strings.HasPrefix(c.Checksum, prefix) { + return "", fmt.Errorf("unsupported checksum format (expected sha256:...)") + } + + hash := strings.TrimPrefix(c.Checksum, prefix) + + if len(hash) != 64 { + return "", fmt.Errorf("invalid sha256 length: got %d, want 64", len(hash)) + } + + if _, err := hex.DecodeString(hash); err != nil { + return "", fmt.Errorf("invalid sha256 hex: %w", err) + } + + return hash, nil +} diff --git a/clitools/pkg/catalog/resolver.go b/clitools/pkg/catalog/resolver.go index f9e347d..f5fca21 100644 --- a/clitools/pkg/catalog/resolver.go +++ b/clitools/pkg/catalog/resolver.go @@ -21,19 +21,17 @@ const ( // ResolveCatalog resolves catalog using priority: // Inline > ConfigMap > URL > cached -func ResolveCatalog( - ctx context.Context, +func ResolveCatalog(ctx context.Context, kubeClient kubernetes.Interface, - namespace string, - src *monov1alpha1.VersionCatalogSource, + namespace string, src *monov1alpha1.VersionCatalogSource, ) (*VersionCatalog, error) { - // 1. Inline + // Inline if src != nil && src.Inline != "" { return parseCatalog([]byte(src.Inline)) } - // 2. ConfigMap + // ConfigMap if src != nil && src.ConfigMap != "" { cm, err := kubeClient.CoreV1().ConfigMaps(namespace).Get(ctx, src.ConfigMap, metav1.GetOptions{}) if err != nil { @@ -48,7 +46,7 @@ func ResolveCatalog( return parseCatalog([]byte(data)) } - // 3. URL + // URL if src != nil && src.URL != "" { cat, err := fetchCatalog(src.URL) if err == nil { @@ -64,7 +62,7 @@ func ResolveCatalog( return nil, fmt.Errorf("fetch catalog failed and no cache: %w", err) } - // 4. cached fallback + // fallback cache if cached, err := loadCached(); err == nil { return cached, nil } diff --git a/clitools/pkg/catalog/types.go b/clitools/pkg/catalog/types.go index e78f0a9..ada01f2 100644 --- a/clitools/pkg/catalog/types.go +++ b/clitools/pkg/catalog/types.go @@ -10,4 +10,5 @@ type CatalogImage struct { Version string `json:"version" yaml:"version"` URL string `json:"url" yaml:"url"` Checksum string `json:"checksum,omitempty" yaml:"checksum,omitempty"` + Size int64 `json:"size,omitempty" yaml:"size,omitempty"` } diff --git a/clitools/pkg/cmd/agent/agent.go b/clitools/pkg/cmd/agent/agent.go index edd050c..cc5e676 100644 --- a/clitools/pkg/cmd/agent/agent.go +++ b/clitools/pkg/cmd/agent/agent.go @@ -190,10 +190,7 @@ func matchesNode(osu *monov1alpha1.OSUpgrade, nodeName string, nodeLabels labels 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, - }) + return false } func statusPhase(st *monov1alpha1.OSUpgradeStatus) string { diff --git a/clitools/pkg/cmd/initcmd/init.go b/clitools/pkg/cmd/initcmd/init.go index 991ade0..4bab9a8 100644 --- a/clitools/pkg/cmd/initcmd/init.go +++ b/clitools/pkg/cmd/initcmd/init.go @@ -69,7 +69,7 @@ Supported formats: } } - var cfg *monov1alpha1.MonoKSConfig // or value, depending on your API + var cfg *monov1alpha1.MonoKSConfig switch { case strings.TrimSpace(envFile) != "": diff --git a/clitools/pkg/controller/osimage/apply.go b/clitools/pkg/controller/osimage/apply.go new file mode 100644 index 0000000..7136f25 --- /dev/null +++ b/clitools/pkg/controller/osimage/apply.go @@ -0,0 +1,42 @@ +package osimage + +import ( + "context" + "fmt" +) + +func ApplyImageStreamed(ctx context.Context, opts ApplyOptions) (*ApplyResult, error) { + if err := ValidateApplyOptions(opts); err != nil { + return nil, err + } + + if err := CheckTargetSafe(opts.TargetPath, opts.ExpectedRawSize); err != nil { + return nil, fmt.Errorf("unsafe target %q: %w", opts.TargetPath, err) + } + + src, closeFn, err := OpenDecompressedHTTPStream(ctx, opts.URL, opts.HTTPTimeout) + if err != nil { + return nil, fmt.Errorf("open source stream: %w", err) + } + defer closeFn() + + written, err := WriteStreamToTarget(ctx, src, opts.TargetPath, opts.ExpectedRawSize, opts.BufferSize, opts.Progress) + if err != nil { + return nil, fmt.Errorf("write target: %w", err) + } + + sum, err := VerifyTargetSHA256(ctx, opts.TargetPath, opts.ExpectedRawSize, opts.BufferSize, opts.Progress) + if err != nil { + return nil, fmt.Errorf("verify target: %w", err) + } + + if err := VerifySHA256(sum, opts.ExpectedRawSHA256); err != nil { + return nil, fmt.Errorf("final disk checksum mismatch: %w", err) + } + + return &ApplyResult{ + BytesWritten: written, + VerifiedSHA256: sum, + VerificationOK: true, + }, nil +} diff --git a/clitools/pkg/controller/osimage/helpers.go b/clitools/pkg/controller/osimage/helpers.go new file mode 100644 index 0000000..505c573 --- /dev/null +++ b/clitools/pkg/controller/osimage/helpers.go @@ -0,0 +1,15 @@ +package osimage + +func PercentOf(done, total int64) int64 { + if total <= 0 { + return 0 + } + p := (done * 100) / total + if p < 0 { + return 0 + } + if p > 100 { + return 100 + } + return p +} diff --git a/clitools/pkg/controller/osimage/progress.go b/clitools/pkg/controller/osimage/progress.go new file mode 100644 index 0000000..e393f20 --- /dev/null +++ b/clitools/pkg/controller/osimage/progress.go @@ -0,0 +1,121 @@ +package osimage + +import ( + "k8s.io/klog/v2" + "time" +) + +type progressState struct { + lastTime time.Time + lastPercent int64 + lastBucket int64 +} + +type ProgressLogger struct { + minInterval time.Duration + bucketSize int64 + states map[string]*progressState +} + +func NewProgressLogger(minSeconds int, bucketSize int64) *ProgressLogger { + if minSeconds < 0 { + minSeconds = 0 + } + if bucketSize <= 0 { + bucketSize = 10 + } + + return &ProgressLogger{ + minInterval: time.Duration(minSeconds) * time.Second, + bucketSize: bucketSize, + states: make(map[string]*progressState), + } +} + +func (l *ProgressLogger) state(stage string) *progressState { + s, ok := l.states[stage] + if ok { + return s + } + s = &progressState{ + lastPercent: -1, + lastBucket: -1, + } + l.states[stage] = s + return s +} + +func (l *ProgressLogger) Log(p Progress) { + if p.BytesTotal <= 0 { + return + } + + percent := PercentOf(p.BytesComplete, p.BytesTotal) + + now := time.Now() + bucket := percent / l.bucketSize + s := l.state(p.Stage) + + // Always log first visible progress + if s.lastPercent == -1 { + s.lastPercent = percent + s.lastBucket = bucket + s.lastTime = now + klog.V(4).InfoS(p.Stage, "progress", percent) + return + } + + // Always log completion once + if percent == 100 && s.lastPercent < 100 { + s.lastPercent = 100 + s.lastBucket = 100 / l.bucketSize + s.lastTime = now + klog.V(4).InfoS(p.Stage, "progress", 100) + return + } + + // Log if we crossed a new milestone bucket + if bucket > s.lastBucket { + s.lastPercent = percent + s.lastBucket = bucket + s.lastTime = now + klog.V(4).InfoS(p.Stage, "progress", percent) + return + } + + // Otherwise allow a timed refresh if progress moved + if now.Sub(s.lastTime) >= l.minInterval && percent > s.lastPercent { + s.lastPercent = percent + s.lastTime = now + klog.V(4).InfoS(p.Stage, "progress", percent) + } +} + +type TimeBasedUpdater struct { + interval time.Duration + lastRun time.Time +} + +func NewTimeBasedUpdater(seconds int) *TimeBasedUpdater { + if seconds <= 0 { + seconds = 15 + } + return &TimeBasedUpdater{ + interval: time.Duration(seconds) * time.Second, + } +} + +func (u *TimeBasedUpdater) Run(fn func() error) error { + now := time.Now() + + if !u.lastRun.IsZero() && now.Sub(u.lastRun) < u.interval { + return nil + } + + if err := fn(); err != nil { + return err + } + + u.lastRun = now + return nil +} diff --git a/clitools/pkg/controller/osimage/stream.go b/clitools/pkg/controller/osimage/stream.go new file mode 100644 index 0000000..c1921ce --- /dev/null +++ b/clitools/pkg/controller/osimage/stream.go @@ -0,0 +1,50 @@ +package osimage + +import ( + "context" + "fmt" + "io" + "net/http" + "time" + + "github.com/klauspost/compress/zstd" +) + +func OpenDecompressedHTTPStream(ctx context.Context, url string, timeout time.Duration) (io.Reader, func() error, error) { + if url == "" { + return nil, nil, fmt.Errorf("url is required") + } + if timeout <= 0 { + timeout = 30 * time.Minute + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, nil, fmt.Errorf("build request: %w", err) + } + + client := &http.Client{Timeout: timeout} + + resp, err := client.Do(req) + if err != nil { + return nil, nil, fmt.Errorf("http get %q: %w", url, err) + } + + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, nil, fmt.Errorf("unexpected status: %s", resp.Status) + } + + dec, err := zstd.NewReader(resp.Body) + if err != nil { + resp.Body.Close() + return nil, nil, fmt.Errorf("create zstd decoder: %w", err) + } + + closeFn := func() error { + dec.Close() + return resp.Body.Close() + } + + return dec, closeFn, nil +} diff --git a/clitools/pkg/controller/osimage/types.go b/clitools/pkg/controller/osimage/types.go new file mode 100644 index 0000000..8474cc8 --- /dev/null +++ b/clitools/pkg/controller/osimage/types.go @@ -0,0 +1,29 @@ +package osimage + +import "time" + +type ApplyOptions struct { + URL string + TargetPath string + ExpectedRawSHA256 string + ExpectedRawSize int64 + + HTTPTimeout time.Duration + BufferSize int + + Progress ProgressFunc +} + +type Progress struct { + Stage string + BytesComplete int64 + BytesTotal int64 +} + +type ProgressFunc func(Progress) + +type ApplyResult struct { + BytesWritten int64 + VerifiedSHA256 string + VerificationOK bool +} diff --git a/clitools/pkg/controller/osimage/verify.go b/clitools/pkg/controller/osimage/verify.go new file mode 100644 index 0000000..5b30318 --- /dev/null +++ b/clitools/pkg/controller/osimage/verify.go @@ -0,0 +1,104 @@ +package osimage + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "os" + "strings" +) + +func VerifyTargetSHA256(ctx context.Context, targetPath string, expectedSize int64, + bufferSize int, progress ProgressFunc) (string, error) { + if targetPath == "" { + return "", fmt.Errorf("target path is required") + } + if expectedSize <= 0 { + return "", fmt.Errorf("expected raw size is required for verification") + } + if bufferSize <= 0 { + bufferSize = 4 * 1024 * 1024 + } + + f, err := os.Open(targetPath) + if err != nil { + return "", fmt.Errorf("open target for verify: %w", err) + } + defer f.Close() + + h := sha256.New() + buf := make([]byte, bufferSize) + + var readTotal int64 + limited := io.LimitReader(f, expectedSize) + + for { + select { + case <-ctx.Done(): + return "", ctx.Err() + default: + } + + n, err := limited.Read(buf) + if n > 0 { + if _, werr := h.Write(buf[:n]); werr != nil { + return "", fmt.Errorf("hash target: %w", werr) + } + readTotal += int64(n) + if progress != nil { + progress(Progress{ + Stage: "verify", + BytesComplete: readTotal, + BytesTotal: expectedSize, + }) + } + } + + if err == io.EOF { + break + } + if err != nil { + return "", fmt.Errorf("read target: %w", err) + } + } + + if readTotal != expectedSize { + return "", fmt.Errorf("verify size mismatch: got %d want %d", readTotal, expectedSize) + } + + return hex.EncodeToString(h.Sum(nil)), nil +} + +func ValidateApplyOptions(opts ApplyOptions) error { + if opts.URL == "" { + return fmt.Errorf("url is required") + } + if opts.TargetPath == "" { + return fmt.Errorf("target path is required") + } + if opts.ExpectedRawSHA256 == "" { + return fmt.Errorf("expected raw sha256 is required") + } + if opts.ExpectedRawSize <= 0 { + return fmt.Errorf("expected raw size must be > 0") + } + return nil +} + +func VerifySHA256(got, expected string) error { + expected = NormalizeSHA256(expected) + if expected == "" { + return nil + } + got = NormalizeSHA256(got) + if got != expected { + return fmt.Errorf("sha256 mismatch: got %s want %s", got, expected) + } + return nil +} + +func NormalizeSHA256(s string) string { + return strings.ToLower(strings.TrimSpace(s)) +} diff --git a/clitools/pkg/controller/osimage/verify_safe.go b/clitools/pkg/controller/osimage/verify_safe.go new file mode 100644 index 0000000..3a2e91a --- /dev/null +++ b/clitools/pkg/controller/osimage/verify_safe.go @@ -0,0 +1,35 @@ +//go:build !dev + +package osimage + +import ( + "fmt" + "os" + "strings" +) + +func CheckTargetSafe(targetPath string, expectedRawSize int64) error { + + if !strings.HasPrefix(targetPath, "/dev/") { + return fmt.Errorf("target must be a device path under /dev") + } + + st, err := os.Stat(targetPath) + if err != nil { + return fmt.Errorf("stat target: %w", err) + } + + mode := st.Mode() + if mode&os.ModeDevice == 0 { + return fmt.Errorf("target is not a device") + } + + // TODO: Add stronger checks + // - EnsureNotMounted(targetPath) + // - EnsureNotCurrentRoot(targetPath) + // - EnsurePartitionNotWholeDisk(targetPath) + // - EnsureCapacity(targetPath, expectedRawSize) + + _ = expectedRawSize + return nil +} diff --git a/clitools/pkg/controller/osimage/verify_unsafe.go b/clitools/pkg/controller/osimage/verify_unsafe.go new file mode 100644 index 0000000..1111262 --- /dev/null +++ b/clitools/pkg/controller/osimage/verify_unsafe.go @@ -0,0 +1,7 @@ +//go:build dev + +package osimage + +func CheckTargetSafe(targetPath string, expectedRawSize int64) error { + return nil +} diff --git a/clitools/pkg/controller/osimage/write.go b/clitools/pkg/controller/osimage/write.go new file mode 100644 index 0000000..b7e93f9 --- /dev/null +++ b/clitools/pkg/controller/osimage/write.go @@ -0,0 +1,82 @@ +package osimage + +import ( + "context" + "fmt" + "io" + "os" +) + +func WriteStreamToTarget(ctx context.Context, + src io.Reader, + targetPath string, + expectedSize int64, bufferSize int, + progress ProgressFunc, +) (int64, error) { + if targetPath == "" { + return 0, fmt.Errorf("target path is required") + } + if bufferSize <= 0 { + bufferSize = 4 * 1024 * 1024 + } + + f, err := os.OpenFile(targetPath, os.O_WRONLY, 0) + if err != nil { + return 0, fmt.Errorf("open target: %w", err) + } + defer f.Close() + + written, err := copyWithProgressBuffer(ctx, f, src, expectedSize, "flash", progress, make([]byte, bufferSize)) + if err != nil { + return written, err + } + + if expectedSize > 0 && written != expectedSize { + return written, fmt.Errorf("written size mismatch: got %d want %d", written, expectedSize) + } + + if err := f.Sync(); err != nil { + return written, fmt.Errorf("sync target: %w", err) + } + + return written, nil +} + +func copyWithProgressBuffer(ctx context.Context, dst io.Writer, src io.Reader, total int64, stage string, progress ProgressFunc, buf []byte) (int64, error) { + var written int64 + + for { + select { + case <-ctx.Done(): + return written, ctx.Err() + default: + } + + nr, er := src.Read(buf) + if nr > 0 { + nw, ew := dst.Write(buf[:nr]) + if nw > 0 { + written += int64(nw) + if progress != nil { + progress(Progress{ + Stage: stage, + BytesComplete: written, + BytesTotal: total, + }) + } + } + if ew != nil { + return written, ew + } + if nw != nr { + return written, io.ErrShortWrite + } + } + if er != nil { + if er == io.EOF { + return written, nil + } + return written, fmt.Errorf("copy %s: %w", stage, er) + } + } +} diff --git a/clitools/pkg/controller/osupgrade/handler.go b/clitools/pkg/controller/osupgrade/handler.go index 85378c5..57f587e 100644 --- a/clitools/pkg/controller/osupgrade/handler.go +++ b/clitools/pkg/controller/osupgrade/handler.go @@ -10,14 +10,12 @@ import ( monov1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1" "example.com/monok8s/pkg/buildinfo" "example.com/monok8s/pkg/catalog" + "example.com/monok8s/pkg/controller/osimage" "example.com/monok8s/pkg/kube" ) -func HandleOSUpgrade( - ctx context.Context, - clients *kube.Clients, - namespace string, - nodeName string, +func HandleOSUpgrade(ctx context.Context, clients *kube.Clients, + namespace string, nodeName string, osu *monov1alpha1.OSUpgrade, ) error { osup, err := ensureProgressHeartbeat(ctx, clients, namespace, nodeName, osu) @@ -60,10 +58,12 @@ func HandleOSUpgrade( 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 + osup, err = updateProgressStatus(ctx, clients, osup_gvr, osup) - if _, err := updateProgressStatus(ctx, clients, osup_gvr, osup); err != nil { + if err != nil { return fmt.Errorf("update progress status: %w", err) } @@ -72,9 +72,63 @@ func HandleOSUpgrade( "node", nodeName, "resolvedTarget", plan.ResolvedTarget, "steps", len(plan.Path), - "firstVersion", first.Version, + "currentVersion", buildinfo.KubeVersion, + "fSHA256irstVersion", first.Version, "firstURL", first.URL, + "size", first.Size, ) + imageSHA, err := first.SHA256() + if err != nil { + now = metav1.Now() + return failProgress(ctx, clients, osup, "apply image", err) + } + + pLogger := osimage.NewProgressLogger(2, 25) + statusUpdater := osimage.NewTimeBasedUpdater(15) + + imageOptions := osimage.ApplyOptions{ + URL: first.URL, + TargetPath: "./out/flash.img", + ExpectedRawSHA256: imageSHA, + ExpectedRawSize: first.Size, + BufferSize: 6 * 1024 * 1024, + Progress: func(p osimage.Progress) { + pLogger.Log(p) + if err := statusUpdater.Run(func() error { + + now := metav1.Now() + switch p.Stage { + case "flash": + osup.Status.Phase = monov1alpha1.OSUpgradeProgressPhaseWriting + case "verify": + osup.Status.Phase = monov1alpha1.OSUpgradeProgressPhaseVerifying + } + osup.Status.LastUpdatedAt = &now + osup.Status.Message = fmt.Sprintf("%s: %d%%", p.Stage, osimage.PercentOf(p.BytesComplete, p.BytesTotal)) + + updated, err := updateProgressStatus(ctx, clients, osup_gvr, osup) + if err != nil { + klog.ErrorS(err, "update progress status") + return err + } + + osup = updated + return nil + }); err != nil { + klog.ErrorS(err, "throttled progress update failed") + } + }, + } + + result, err := osimage.ApplyImageStreamed(ctx, imageOptions) + if err != nil { + now = metav1.Now() + return failProgress(ctx, clients, osup, "apply image", err) + } + + klog.Info(result) + // TODO: fw_setenv + return nil } diff --git a/clitools/pkg/controller/osupgrade/progress.go b/clitools/pkg/controller/osupgrade/progress.go index 93f4588..a7c501d 100644 --- a/clitools/pkg/controller/osupgrade/progress.go +++ b/clitools/pkg/controller/osupgrade/progress.go @@ -26,11 +26,8 @@ var ( } ) -func ensureProgressHeartbeat( - ctx context.Context, - clients *kube.Clients, - namespace string, - nodeName string, +func ensureProgressHeartbeat(ctx context.Context, clients *kube.Clients, + namespace string, nodeName string, osu *monov1alpha1.OSUpgrade, ) (*monov1alpha1.OSUpgradeProgress, error) { diff --git a/clitools/pkg/node/kubeadm.go b/clitools/pkg/node/kubeadm.go index f0f3d11..cc01781 100644 --- a/clitools/pkg/node/kubeadm.go +++ b/clitools/pkg/node/kubeadm.go @@ -11,7 +11,6 @@ import ( "time" "gopkg.in/yaml.v3" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/discovery" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" @@ -363,39 +362,6 @@ func isSupportedWorkerSkew(clusterVersion, nodeVersion string) bool { return false } -// This should not try to taint the node directly here. -// Just record intent and let a later reconcile step apply the taint. -func markUnsupportedWorkerVersionSkew(nctx *NodeContext, clusterVersion, nodeVersion string) { - // Replace this with whatever state carrier you already use. - // - // Example: - // nctx.Metadata.UnsupportedWorkerVersionSkew = true - // nctx.Metadata.UnsupportedWorkerVersionSkewReason = - // fmt.Sprintf("unsupported worker version skew: cluster=%s node=%s", clusterVersion, nodeVersion) - - _ = nctx - _ = clusterVersion - _ = nodeVersion -} - -// Optional helper if you want to probe readiness later through the API. -// Keeping this here in case you want a very cheap liveness call elsewhere. -func apiServerReady(ctx context.Context, kubeconfigPath string) error { - restCfg, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath) - if err != nil { - return err - } - restCfg.Timeout = 5 * time.Second - - clientset, err := kubernetes.NewForConfig(restCfg) - if err != nil { - return err - } - - _, err = clientset.CoreV1().Namespaces().Get(ctx, metav1.NamespaceSystem, metav1.GetOptions{}) - return err -} - func ValidateRequiredImagesPresent(ctx context.Context, n *NodeContext) error { if n.Config.Spec.SkipImageCheck { klog.Infof("skipping image check (skipImageCheck=true)")