Skip to content

Commit 2af64a3

Browse files
committed
fix: tighten airgap checksum handling
Signed-off-by: Kimmo Lehto <klehto@mirantis.com>
1 parent 39c388a commit 2af64a3

5 files changed

Lines changed: 93 additions & 11 deletions

File tree

pkg/airgap/airgap.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,10 +229,16 @@ func EnsureCached(ctx context.Context, k0sVersion *version.Version, artifact Art
229229
if _, err := os.Stat(dest); err == nil {
230230
if artifact.SHA256 != "" {
231231
if err := VerifySHA256(dest, artifact.SHA256); err != nil {
232-
return "", fmt.Errorf("verify cached airgap bundle: %w", err)
232+
log.Warnf("cached airgap bundle %s failed checksum verification, removing it: %v", dest, err)
233+
if removeErr := os.Remove(dest); removeErr != nil {
234+
return "", fmt.Errorf("remove invalid cached airgap bundle %s after checksum failure: %w", dest, removeErr)
235+
}
236+
} else {
237+
return dest, nil
233238
}
239+
} else {
240+
return dest, nil
234241
}
235-
return dest, nil
236242
} else if !errors.Is(err, os.ErrNotExist) {
237243
return "", fmt.Errorf("stat airgap cache path %s: %w", dest, err)
238244
}

pkg/airgap/airgap_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package airgap
22

33
import (
4+
"context"
45
"crypto/sha256"
56
"fmt"
7+
"net/http"
8+
"net/http/httptest"
69
"os"
710
"path/filepath"
811
"testing"
912

13+
"github.com/adrg/xdg"
1014
"github.com/k0sproject/k0sctl/configurer"
1115
linuxcfg "github.com/k0sproject/k0sctl/configurer/linux"
1216
"github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster"
@@ -90,6 +94,49 @@ func TestVerifySHA256(t *testing.T) {
9094
require.ErrorContains(t, VerifySHA256(file, "0000"), "sha256 mismatch")
9195
}
9296

97+
func TestEnsureCachedReplacesInvalidCachedBundle(t *testing.T) {
98+
k0sVersion := version.MustParse("v1.34.1+k0s.0")
99+
oldCacheHome, hadCacheHome := os.LookupEnv("XDG_CACHE_HOME")
100+
require.NoError(t, os.Setenv("XDG_CACHE_HOME", t.TempDir()))
101+
xdg.Reload()
102+
t.Cleanup(func() {
103+
if hadCacheHome {
104+
require.NoError(t, os.Setenv("XDG_CACHE_HOME", oldCacheHome))
105+
} else {
106+
require.NoError(t, os.Unsetenv("XDG_CACHE_HOME"))
107+
}
108+
xdg.Reload()
109+
})
110+
111+
content := []byte("good bundle")
112+
sum := sha256.Sum256(content)
113+
var requests int
114+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
115+
requests++
116+
_, err := w.Write(content)
117+
require.NoError(t, err)
118+
}))
119+
t.Cleanup(server.Close)
120+
121+
artifact := Artifact{
122+
Name: "bundle",
123+
URL: server.URL + "/bundle",
124+
OS: "linux",
125+
Arch: "amd64",
126+
SHA256: fmt.Sprintf("%x", sum),
127+
}
128+
cachePath, err := CacheFilePath(k0sVersion, artifact.OS, artifact.Arch, artifact.Name)
129+
require.NoError(t, err)
130+
require.NoError(t, os.MkdirAll(filepath.Dir(cachePath), 0o755))
131+
require.NoError(t, os.WriteFile(cachePath, []byte("bad bundle"), 0o644))
132+
133+
got, err := EnsureCached(context.Background(), k0sVersion, artifact)
134+
require.NoError(t, err)
135+
require.Equal(t, cachePath, got)
136+
require.Equal(t, 1, requests)
137+
require.NoError(t, VerifySHA256(cachePath, artifact.SHA256))
138+
}
139+
93140
func TestLocalPath(t *testing.T) {
94141
dir := t.TempDir()
95142
bundle := filepath.Join(dir, "bundle")

pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/airgap.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func (a *Airgap) Validate() error {
5252
validation.Field(&a.Mode, validation.Required, validation.In(AirgapModeUpload, AirgapModeRemoteDownload)),
5353
validation.Field(&a.Path, validation.Required.When(a.Source == AirgapSourceLocal)),
5454
validation.Field(&a.URL, validation.Required.When(a.Source == AirgapSourceURL)),
55-
validation.Field(&a.SHA256, validation.By(validateSHA256)),
55+
validation.Field(&a.SHA256, validation.By(validateSHA256), validation.By(validateSHA256Source(a.Source))),
5656
); err != nil {
5757
return err
5858
}
@@ -79,6 +79,19 @@ func validateSHA256(value any) error {
7979
return nil
8080
}
8181

82+
func validateSHA256Source(source string) validation.RuleFunc {
83+
return func(value any) error {
84+
checksum, ok := value.(string)
85+
if !ok {
86+
return fmt.Errorf("not a string")
87+
}
88+
if source == AirgapSourceAuto && checksum != "" {
89+
return fmt.Errorf("must be empty when source is %q", AirgapSourceAuto)
90+
}
91+
return nil
92+
}
93+
}
94+
8295
// Resolve prepares path-based airgap configuration after unmarshalling.
8396
func (a *Airgap) Resolve(baseDir string) {
8497
if a == nil || a.Path == "" || filepath.IsAbs(a.Path) || baseDir == "" {

pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/k0s.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,18 @@ func validateVersion(value any) error {
104104
}
105105

106106
func (k *K0s) Validate() error {
107-
return validation.ValidateStruct(k,
107+
if err := validation.ValidateStruct(k,
108108
validation.Field(&k.Version, validation.By(validateVersion)),
109109
validation.Field(&k.DynamicConfig, validation.By(k.validateMinDynamic())),
110110
validation.Field(&k.VersionChannel, validation.In("stable", "latest"), validation.When(k.VersionChannel != "")),
111111
validation.Field(&k.Airgap),
112-
)
112+
); err != nil {
113+
return err
114+
}
115+
if k.Airgap != nil && k.Airgap.Enabled && (k.Version == nil || k.Version.IsZero()) {
116+
return fmt.Errorf("version is required when airgap is enabled")
117+
}
118+
return nil
113119
}
114120

115121
func (k *K0s) validateMinDynamic() func(any) error {

pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/k0s_test.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func TestVersionDefaulting(t *testing.T) {
4545
}
4646

4747
func TestAirgapDefaults(t *testing.T) {
48-
k0s := &K0s{Airgap: &Airgap{Enabled: true}}
48+
k0s := &K0s{Version: version.MustParse("v1.34.1+k0s.0"), Airgap: &Airgap{Enabled: true}}
4949

5050
require.NoError(t, defaults.Set(k0s))
5151
require.NoError(t, k0s.Validate())
@@ -63,29 +63,39 @@ func TestAirgapValidateSetsDefaults(t *testing.T) {
6363

6464
func TestAirgapValidation(t *testing.T) {
6565
t.Run("local requires path", func(t *testing.T) {
66-
k0s := &K0s{Airgap: &Airgap{Enabled: true, Source: AirgapSourceLocal}}
66+
k0s := &K0s{Version: version.MustParse("v1.34.1+k0s.0"), Airgap: &Airgap{Enabled: true, Source: AirgapSourceLocal}}
6767
require.ErrorContains(t, k0s.Validate(), "Path: cannot be blank")
6868
})
6969

7070
t.Run("url requires url", func(t *testing.T) {
71-
k0s := &K0s{Airgap: &Airgap{Enabled: true, Source: AirgapSourceURL}}
71+
k0s := &K0s{Version: version.MustParse("v1.34.1+k0s.0"), Airgap: &Airgap{Enabled: true, Source: AirgapSourceURL}}
7272
require.ErrorContains(t, k0s.Validate(), "URL: cannot be blank")
7373
})
7474

7575
t.Run("invalid source", func(t *testing.T) {
76-
k0s := &K0s{Airgap: &Airgap{Enabled: true, Source: "other"}}
76+
k0s := &K0s{Version: version.MustParse("v1.34.1+k0s.0"), Airgap: &Airgap{Enabled: true, Source: "other"}}
7777
require.ErrorContains(t, k0s.Validate(), "Source: must be a valid value")
7878
})
7979

8080
t.Run("remote download deferred", func(t *testing.T) {
81-
k0s := &K0s{Airgap: &Airgap{Enabled: true, Mode: AirgapModeRemoteDownload}}
81+
k0s := &K0s{Version: version.MustParse("v1.34.1+k0s.0"), Airgap: &Airgap{Enabled: true, Mode: AirgapModeRemoteDownload}}
8282
require.ErrorContains(t, k0s.Validate(), `mode "remoteDownload" is not supported yet`)
8383
})
8484

8585
t.Run("invalid sha256", func(t *testing.T) {
86-
k0s := &K0s{Airgap: &Airgap{Enabled: true, SHA256: "abc123"}}
86+
k0s := &K0s{Version: version.MustParse("v1.34.1+k0s.0"), Airgap: &Airgap{Enabled: true, Source: AirgapSourceURL, URL: "https://example.invalid/bundle", SHA256: "abc123"}}
8787
require.ErrorContains(t, k0s.Validate(), "SHA256: must be 64 hex characters")
8888
})
89+
90+
t.Run("auto source rejects sha256", func(t *testing.T) {
91+
k0s := &K0s{Version: version.MustParse("v1.34.1+k0s.0"), Airgap: &Airgap{Enabled: true, SHA256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}}
92+
require.ErrorContains(t, k0s.Validate(), `SHA256: must be empty when source is "auto"`)
93+
})
94+
95+
t.Run("airgap requires version", func(t *testing.T) {
96+
k0s := &K0s{Airgap: &Airgap{Enabled: true}}
97+
require.ErrorContains(t, k0s.Validate(), "version is required when airgap is enabled")
98+
})
8999
}
90100

91101
func TestNodeConfigUsesLowercaseMetadataKey(t *testing.T) {

0 commit comments

Comments
 (0)