commit 4f490ab37e642b52c681edb18cd1b5933985247577e1555ee96e179470c61678
| author | 斟酌 鵬兄 <tgckpg@gmail.com> |
| date | 2026-04-03T18:45:46Z |
| subject | Writes and verify image |
commit 4f490ab37e642b52c681edb18cd1b5933985247577e1555ee96e179470c61678
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date: 2026-04-03T18:45:46Z
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 +++++++++++++++++++
clitools/pkg/controller/osimage/verify_safe.go | 35 +++++++
clitools/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(-)
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)")