Skip to content

Commit

Permalink
Replace grab with internal download function
Browse files Browse the repository at this point in the history
This makes the download process use a temporary file and atomic file
replacements on success. Allows existing files to be re-downloaded,
which was not possible before. On the contrary, it will re-download files
even if they didn't change.

Signed-off-by: Tom Wieczorek <twieczorek@mirantis.com>
  • Loading branch information
twz123 committed Sep 23, 2024
1 parent 13bb8dd commit 461e29e
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 28 deletions.
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ require (
github.com/avast/retry-go v3.0.0+incompatible
github.com/bombsimon/logrusr/v4 v4.1.0
github.com/carlmjohnson/requests v0.24.2
github.com/cavaliergopher/grab/v3 v3.0.1
github.com/cilium/ebpf v0.16.0
github.com/cloudflare/cfssl v1.6.4
github.com/containerd/cgroups/v3 v3.0.3
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,6 @@ github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXe
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/carlmjohnson/requests v0.24.2 h1:JDakhAmTIKL/qL/1P7Kkc2INGBJIkIFP6xUeUmPzLso=
github.com/carlmjohnson/requests v0.24.2/go.mod h1:duYA/jDnyZ6f3xbcF5PpZ9N8clgopubP2nK5i6MVMhU=
github.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4=
github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
Expand Down
18 changes: 17 additions & 1 deletion internal/pkg/file/atomic.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,22 @@ func (f *Atomic) ReadFrom(r io.Reader) (int64, error) {
// Note that the atomicity aspects are only best-effort on Windows:
// https://github.com/golang/go/issues/22397#issuecomment-498856679
func (f *Atomic) Finish() (err error) {
return f.finish(f.target)
}

// Like Finish, but replaces the target base name with the given one.
// Note that the directory cannot be changed, just the file's base name.
func (f *Atomic) FinishWithBaseName(baseName string) (err error) {
dir := filepath.Dir(f.target)
target := filepath.Join(dir, baseName)
if filepath.Base(target) != baseName || filepath.Dir(target) != dir {
return errors.New("base name is invalid")
}

return f.finish(target)
}

func (f *Atomic) finish(target string) (err error) {
if f == nil {
return fs.ErrInvalid
}
Expand Down Expand Up @@ -244,7 +260,7 @@ func (f *Atomic) Finish() (err error) {
}
}

if err = os.Rename(f.fd.Name(), f.target); err != nil {
if err = os.Rename(f.fd.Name(), target); err != nil {
return err
}

Expand Down
61 changes: 37 additions & 24 deletions pkg/autopilot/download/downloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@
package download

import (
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"hash"
"io"
"path/filepath"

"github.com/cavaliergopher/grab/v3"
"github.com/k0sproject/k0s/pkg/build"
internalhttp "github.com/k0sproject/k0s/internal/http"
"github.com/k0sproject/k0s/internal/pkg/file"
)

type Downloader interface {
Expand All @@ -47,38 +51,47 @@ func NewDownloader(config Config) Downloader {
}
}

// Start begins the download process, starting the downloading functionality
// on a separate goroutine. Cancelling the context will abort this operation
// once started.
func (d *downloader) Download(ctx context.Context) error {
// Setup the library for downloading HTTP content ..
dlreq, err := grab.NewRequest(d.config.DownloadDir, d.config.URL)

if err != nil {
return fmt.Errorf("invalid download request: %w", err)
}
// Performs the download process.
func (d *downloader) Download(ctx context.Context) (err error) {
var targets []io.Writer

// If we've been provided a hash and actual value to compare with, use it.
var expectedHash []byte
if d.config.Hasher != nil && d.config.ExpectedHash != "" {
expectedHash, err := hex.DecodeString(d.config.ExpectedHash)
expectedHash, err = hex.DecodeString(d.config.ExpectedHash)
if err != nil {
return fmt.Errorf("invalid update hash: %w", err)
}
targets = append(targets, d.config.Hasher)
}

dlreq.SetChecksum(d.config.Hasher, expectedHash, true)
// Set up target file for download.
target, err := file.AtomicWithTarget(filepath.Join(d.config.DownloadDir, "download")).Open()
if err != nil {
return err
}
defer func() { err = errors.Join(err, target.Close()) }()
targets = append(targets, target)

client := grab.NewClient()
// Set user agent to mitigate 403 errors from GitHub
// See https://github.com/cavaliergopher/grab/issues/104
client.UserAgent = fmt.Sprintf("k0s/%s", build.Version)
httpResponse := client.Do(dlreq)
// Download from URL into targets.
var fileName string
if err = internalhttp.Download(ctx, d.config.URL, io.MultiWriter(targets...),
internalhttp.StoreSuggestedRemoteFileNameInto(&fileName),
); err != nil {
return fmt.Errorf("download failed: %w", err)
}

select {
case <-httpResponse.Done:
return httpResponse.Err()
// Check the hash of the downloaded data and fail if it doesn't match.
if expectedHash != nil {
if downloadedHash := d.config.Hasher.Sum(nil); !bytes.Equal(expectedHash, downloadedHash) {
return fmt.Errorf("hash mismatch: expected %x, got %x", expectedHash, downloadedHash)
}
}

case <-ctx.Done():
return fmt.Errorf("download cancelled")
// All is well. Finish the download.
if err := target.FinishWithBaseName(fileName); err != nil {
return fmt.Errorf("failed to finish download: %w", err)
}

return nil
}

0 comments on commit 461e29e

Please sign in to comment.