commands/helpers/artifacts_downloader.go (138 lines of code) (raw):
package helpers
import (
"bytes"
"context"
"fmt"
"io"
"os"
"time"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"gitlab.com/gitlab-org/gitlab-runner/commands/helpers/archive"
"gitlab.com/gitlab-org/gitlab-runner/commands/helpers/meter"
"gitlab.com/gitlab-org/gitlab-runner/common"
"gitlab.com/gitlab-org/gitlab-runner/log"
"gitlab.com/gitlab-org/gitlab-runner/network"
)
type ArtifactsDownloaderCommand struct {
common.JobCredentials
retryHelper
network common.Network
meter.TransferMeterCommand
DirectDownload bool `long:"direct-download" env:"FF_USE_DIRECT_DOWNLOAD" description:"Support direct download for data stored externally to GitLab"`
StagingDir string `long:"archiver-staging-dir" env:"ARCHIVER_STAGING_DIR" description:"Directory to stage artifact archives"`
}
func (c *ArtifactsDownloaderCommand) directDownloadFlag(retry int) *bool {
// We want to send `?direct_download=true`
// Use direct download only on a first attempt
if c.DirectDownload && retry == 0 {
return &c.DirectDownload
}
// We don't want to send `?direct_download=false`
return nil
}
func (c *ArtifactsDownloaderCommand) download(file string, retry int) error {
artifactsFile, err := os.Create(file)
if err != nil {
return fmt.Errorf("creating target file: %w", err)
}
// Close() is checked properly inside of DownloadArtifacts() call
defer func() { _ = artifactsFile.Close() }()
writer := meter.NewWriter(
artifactsFile,
c.TransferMeterFrequency,
meter.LabelledRateFormat(os.Stdout, "Downloading artifacts", meter.UnknownTotalSize),
)
// Close() is checked properly inside of DownloadArtifacts() call
defer func() { _ = writer.Close() }()
switch c.network.DownloadArtifacts(c.JobCredentials, writer, c.directDownloadFlag(retry)) {
case common.DownloadSucceeded:
return nil
case common.DownloadNotFound:
return os.ErrNotExist
case common.DownloadForbidden, common.DownloadUnauthorized:
return os.ErrPermission
case common.DownloadFailed:
return retryableErr{err: os.ErrInvalid}
default:
return os.ErrInvalid
}
}
func (c *ArtifactsDownloaderCommand) Execute(cliContext *cli.Context) {
log.SetRunnerFormatter()
wd, err := os.Getwd()
if err != nil {
logrus.Fatalln("Unable to get working directory")
}
if c.URL == "" {
logrus.Warningln("Missing URL (--url)")
}
if c.Token == "" {
logrus.Warningln("Missing runner credentials (--token)")
}
if c.ID <= 0 {
logrus.Warningln("Missing build ID (--id)")
}
if c.ID <= 0 || c.Token == "" || c.URL == "" {
logrus.Fatalln("Incomplete arguments")
}
// Create temporary file
file, err := os.CreateTemp(c.StagingDir, "artifacts")
if err != nil {
logrus.Fatalln(err)
}
_ = file.Close()
defer func() { _ = os.Remove(file.Name()) }()
// Download artifacts file
err = c.doRetry(func(retry int) error {
return c.download(file.Name(), retry)
})
if err != nil {
logrus.Fatalln(err)
}
f, size, format, err := openArchive(file.Name())
if err != nil {
logrus.Fatalln(err)
}
defer f.Close()
extractor, err := archive.NewExtractor(format, f, size, wd)
if err != nil {
logrus.Fatalln(err)
}
// Extract artifacts file
err = extractor.Extract(context.Background())
if err != nil {
logrus.Fatalln(err)
}
}
var (
zstMagic = []byte{0x28, 0xB5, 0x2F, 0xFD}
gzipMagic = []byte{0x1F, 0x8B}
)
func openArchive(filename string) (*os.File, int64, archive.Format, error) {
format := archive.Zip
f, err := os.Open(filename)
if err != nil {
return nil, 0, format, err
}
var magic [4]byte
_, _ = f.Read(magic[:])
_, _ = f.Seek(0, io.SeekStart)
switch {
case bytes.HasPrefix(magic[:], zstMagic):
format = archive.TarZstd
case bytes.HasPrefix(magic[:], gzipMagic):
format = archive.Gzip
}
fi, err := f.Stat()
if err != nil {
f.Close()
return nil, 0, format, err
}
return f, fi.Size(), format, nil
}
func init() {
common.RegisterCommand2(
"artifacts-downloader",
"download and extract build artifacts (internal)",
&ArtifactsDownloaderCommand{
network: network.NewGitLabClient(),
retryHelper: retryHelper{
Retry: 2,
RetryTime: time.Second,
},
},
)
}