Writes and verify image
This commit is contained in:
@@ -3,13 +3,17 @@ module example.com/monok8s
|
|||||||
go 1.24.0
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/klauspost/compress v1.18.5
|
||||||
github.com/spf13/cobra v1.9.1
|
github.com/spf13/cobra v1.9.1
|
||||||
|
golang.org/x/sys v0.31.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
k8s.io/api v0.34.0
|
||||||
k8s.io/apiextensions-apiserver v0.34.0
|
k8s.io/apiextensions-apiserver v0.34.0
|
||||||
k8s.io/apimachinery v0.34.0
|
k8s.io/apimachinery v0.34.0
|
||||||
k8s.io/cli-runtime v0.34.0
|
k8s.io/cli-runtime v0.34.0
|
||||||
k8s.io/client-go v0.34.0
|
k8s.io/client-go v0.34.0
|
||||||
k8s.io/klog/v2 v2.130.1
|
k8s.io/klog/v2 v2.130.1
|
||||||
|
sigs.k8s.io/yaml v1.6.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -48,14 +52,12 @@ require (
|
|||||||
golang.org/x/net v0.38.0 // indirect
|
golang.org/x/net v0.38.0 // indirect
|
||||||
golang.org/x/oauth2 v0.27.0 // indirect
|
golang.org/x/oauth2 v0.27.0 // indirect
|
||||||
golang.org/x/sync v0.12.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/term v0.30.0 // indirect
|
||||||
golang.org/x/text v0.23.0 // indirect
|
golang.org/x/text v0.23.0 // indirect
|
||||||
golang.org/x/time v0.9.0 // indirect
|
golang.org/x/time v0.9.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.5 // indirect
|
google.golang.org/protobuf v1.36.5 // indirect
|
||||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||||
gopkg.in/inf.v0 v0.9.1 // 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/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
|
||||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
|
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
|
||||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // 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/kustomize/kyaml v0.20.1 // indirect
|
||||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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/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/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
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.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
VERSION ?= dev
|
VERSION ?= dev
|
||||||
|
|
||||||
# Target kube version
|
# Target kube version
|
||||||
KUBE_VERSION ?= v1.34.1
|
KUBE_VERSION ?= v1.35.0
|
||||||
|
|
||||||
GIT_REV := $(shell git rev-parse HEAD)
|
GIT_REV := $(shell git rev-parse HEAD)
|
||||||
|
|
||||||
@@ -41,8 +41,8 @@ build-local: .buildinfo
|
|||||||
mkdir -p $(BIN_DIR)
|
mkdir -p $(BIN_DIR)
|
||||||
go build -o $(BIN_DIR)/ctl-$(VERSION) ./cmd/ctl
|
go build -o $(BIN_DIR)/ctl-$(VERSION) ./cmd/ctl
|
||||||
|
|
||||||
run:
|
run-agent:
|
||||||
go run ./cmd/ctl
|
go run -tags dev ./cmd/ctl agent --env-file ./out/cluster.env
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
-docker image rm localhost/monok8s/control-agent:$(VERSION)
|
-docker image rm localhost/monok8s/control-agent:$(VERSION)
|
||||||
|
|||||||
30
clitools/pkg/catalog/checksums.go
Normal file
30
clitools/pkg/catalog/checksums.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -21,19 +21,17 @@ const (
|
|||||||
|
|
||||||
// ResolveCatalog resolves catalog using priority:
|
// ResolveCatalog resolves catalog using priority:
|
||||||
// Inline > ConfigMap > URL > cached
|
// Inline > ConfigMap > URL > cached
|
||||||
func ResolveCatalog(
|
func ResolveCatalog(ctx context.Context,
|
||||||
ctx context.Context,
|
|
||||||
kubeClient kubernetes.Interface,
|
kubeClient kubernetes.Interface,
|
||||||
namespace string,
|
namespace string, src *monov1alpha1.VersionCatalogSource,
|
||||||
src *monov1alpha1.VersionCatalogSource,
|
|
||||||
) (*VersionCatalog, error) {
|
) (*VersionCatalog, error) {
|
||||||
|
|
||||||
// 1. Inline
|
// Inline
|
||||||
if src != nil && src.Inline != "" {
|
if src != nil && src.Inline != "" {
|
||||||
return parseCatalog([]byte(src.Inline))
|
return parseCatalog([]byte(src.Inline))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. ConfigMap
|
// ConfigMap
|
||||||
if src != nil && src.ConfigMap != "" {
|
if src != nil && src.ConfigMap != "" {
|
||||||
cm, err := kubeClient.CoreV1().ConfigMaps(namespace).Get(ctx, src.ConfigMap, metav1.GetOptions{})
|
cm, err := kubeClient.CoreV1().ConfigMaps(namespace).Get(ctx, src.ConfigMap, metav1.GetOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -48,7 +46,7 @@ func ResolveCatalog(
|
|||||||
return parseCatalog([]byte(data))
|
return parseCatalog([]byte(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. URL
|
// URL
|
||||||
if src != nil && src.URL != "" {
|
if src != nil && src.URL != "" {
|
||||||
cat, err := fetchCatalog(src.URL)
|
cat, err := fetchCatalog(src.URL)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -64,7 +62,7 @@ func ResolveCatalog(
|
|||||||
return nil, fmt.Errorf("fetch catalog failed and no cache: %w", err)
|
return nil, fmt.Errorf("fetch catalog failed and no cache: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. cached fallback
|
// fallback cache
|
||||||
if cached, err := loadCached(); err == nil {
|
if cached, err := loadCached(); err == nil {
|
||||||
return cached, nil
|
return cached, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ type CatalogImage struct {
|
|||||||
Version string `json:"version" yaml:"version"`
|
Version string `json:"version" yaml:"version"`
|
||||||
URL string `json:"url" yaml:"url"`
|
URL string `json:"url" yaml:"url"`
|
||||||
Checksum string `json:"checksum,omitempty" yaml:"checksum,omitempty"`
|
Checksum string `json:"checksum,omitempty" yaml:"checksum,omitempty"`
|
||||||
|
Size int64 `json:"size,omitempty" yaml:"size,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,10 +190,7 @@ func matchesNode(osu *monov1alpha1.OSUpgrade, nodeName string, nodeLabels labels
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cheap fallback in case you temporarily target by explicit hostname-ish labels only.
|
return false
|
||||||
return nodeName != "" && selector.Matches(labels.Set{
|
|
||||||
"kubernetes.io/hostname": nodeName,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func statusPhase(st *monov1alpha1.OSUpgradeStatus) string {
|
func statusPhase(st *monov1alpha1.OSUpgradeStatus) string {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ Supported formats:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg *monov1alpha1.MonoKSConfig // or value, depending on your API
|
var cfg *monov1alpha1.MonoKSConfig
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case strings.TrimSpace(envFile) != "":
|
case strings.TrimSpace(envFile) != "":
|
||||||
|
|||||||
42
clitools/pkg/controller/osimage/apply.go
Normal file
42
clitools/pkg/controller/osimage/apply.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
15
clitools/pkg/controller/osimage/helpers.go
Normal file
15
clitools/pkg/controller/osimage/helpers.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
121
clitools/pkg/controller/osimage/progress.go
Normal file
121
clitools/pkg/controller/osimage/progress.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
50
clitools/pkg/controller/osimage/stream.go
Normal file
50
clitools/pkg/controller/osimage/stream.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
29
clitools/pkg/controller/osimage/types.go
Normal file
29
clitools/pkg/controller/osimage/types.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
104
clitools/pkg/controller/osimage/verify.go
Normal file
104
clitools/pkg/controller/osimage/verify.go
Normal file
@@ -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))
|
||||||
|
}
|
||||||
35
clitools/pkg/controller/osimage/verify_safe.go
Normal file
35
clitools/pkg/controller/osimage/verify_safe.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
7
clitools/pkg/controller/osimage/verify_unsafe.go
Normal file
7
clitools/pkg/controller/osimage/verify_unsafe.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build dev
|
||||||
|
|
||||||
|
package osimage
|
||||||
|
|
||||||
|
func CheckTargetSafe(targetPath string, expectedRawSize int64) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
82
clitools/pkg/controller/osimage/write.go
Normal file
82
clitools/pkg/controller/osimage/write.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,14 +10,12 @@ import (
|
|||||||
monov1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
|
monov1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
|
||||||
"example.com/monok8s/pkg/buildinfo"
|
"example.com/monok8s/pkg/buildinfo"
|
||||||
"example.com/monok8s/pkg/catalog"
|
"example.com/monok8s/pkg/catalog"
|
||||||
|
"example.com/monok8s/pkg/controller/osimage"
|
||||||
"example.com/monok8s/pkg/kube"
|
"example.com/monok8s/pkg/kube"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HandleOSUpgrade(
|
func HandleOSUpgrade(ctx context.Context, clients *kube.Clients,
|
||||||
ctx context.Context,
|
namespace string, nodeName string,
|
||||||
clients *kube.Clients,
|
|
||||||
namespace string,
|
|
||||||
nodeName string,
|
|
||||||
osu *monov1alpha1.OSUpgrade,
|
osu *monov1alpha1.OSUpgrade,
|
||||||
) error {
|
) error {
|
||||||
osup, err := ensureProgressHeartbeat(ctx, clients, namespace, nodeName, osu)
|
osup, err := ensureProgressHeartbeat(ctx, clients, namespace, nodeName, osu)
|
||||||
@@ -60,10 +58,12 @@ func HandleOSUpgrade(
|
|||||||
osup.Status.TargetVersion = plan.ResolvedTarget
|
osup.Status.TargetVersion = plan.ResolvedTarget
|
||||||
osup.Status.Phase = monov1alpha1.OSUpgradeProgressPhaseDownloading
|
osup.Status.Phase = monov1alpha1.OSUpgradeProgressPhaseDownloading
|
||||||
osup.Status.Message = fmt.Sprintf("downloading image: %s", first.URL)
|
osup.Status.Message = fmt.Sprintf("downloading image: %s", first.URL)
|
||||||
|
|
||||||
now := metav1.Now()
|
now := metav1.Now()
|
||||||
osup.Status.LastUpdatedAt = &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)
|
return fmt.Errorf("update progress status: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,9 +72,63 @@ func HandleOSUpgrade(
|
|||||||
"node", nodeName,
|
"node", nodeName,
|
||||||
"resolvedTarget", plan.ResolvedTarget,
|
"resolvedTarget", plan.ResolvedTarget,
|
||||||
"steps", len(plan.Path),
|
"steps", len(plan.Path),
|
||||||
"firstVersion", first.Version,
|
"currentVersion", buildinfo.KubeVersion,
|
||||||
|
"fSHA256irstVersion", first.Version,
|
||||||
"firstURL", first.URL,
|
"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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,11 +26,8 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func ensureProgressHeartbeat(
|
func ensureProgressHeartbeat(ctx context.Context, clients *kube.Clients,
|
||||||
ctx context.Context,
|
namespace string, nodeName string,
|
||||||
clients *kube.Clients,
|
|
||||||
namespace string,
|
|
||||||
nodeName string,
|
|
||||||
osu *monov1alpha1.OSUpgrade,
|
osu *monov1alpha1.OSUpgrade,
|
||||||
) (*monov1alpha1.OSUpgradeProgress, error) {
|
) (*monov1alpha1.OSUpgradeProgress, error) {
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/client-go/discovery"
|
"k8s.io/client-go/discovery"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/client-go/tools/clientcmd"
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
@@ -363,39 +362,6 @@ func isSupportedWorkerSkew(clusterVersion, nodeVersion string) bool {
|
|||||||
return false
|
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 {
|
func ValidateRequiredImagesPresent(ctx context.Context, n *NodeContext) error {
|
||||||
if n.Config.Spec.SkipImageCheck {
|
if n.Config.Spec.SkipImageCheck {
|
||||||
klog.Infof("skipping image check (skipImageCheck=true)")
|
klog.Infof("skipping image check (skipImageCheck=true)")
|
||||||
|
|||||||
Reference in New Issue
Block a user