artifacts_uploader.go 3.92 KB
Newer Older
1
package helpers
2
3

import (
4
	"context"
5
	"errors"
6
7
	"io"
	"os"
8
	"path/filepath"
9

10
	"github.com/sirupsen/logrus"
11
	"github.com/urfave/cli"
12

13
	"gitlab.com/gitlab-org/gitlab-runner/commands/helpers/archive"
14
	"gitlab.com/gitlab-org/gitlab-runner/common"
15
	"gitlab.com/gitlab-org/gitlab-runner/helpers/retry"
16
	"gitlab.com/gitlab-org/gitlab-runner/log"
17
	"gitlab.com/gitlab-org/gitlab-runner/network"
18
19
)

20
21
22
23
24
25
26
27
28
29
const (
	DefaultUploadName       = "default"
	defaultTries            = 3
	serviceUnavailableTries = 6
)

var (
	errServiceUnavailable = errors.New("service unavailable")
	errTooLarge           = errors.New("too large")
)
30

31
type ArtifactsUploaderCommand struct {
32
	common.JobCredentials
Kamil Trzcinski's avatar
Kamil Trzcinski committed
33
34
	fileArchiver
	network common.Network
35

Kamil Trzciński's avatar
Kamil Trzciński committed
36
37
	Name     string                `long:"name" description:"The name of the archive"`
	ExpireIn string                `long:"expire-in" description:"When to expire artifacts"`
38
39
	Format   common.ArtifactFormat `long:"artifact-format" description:"Format of generated artifacts"`
	Type     string                `long:"artifact-type" description:"Type of generated artifacts"`
Kamil Trzciński's avatar
Kamil Trzciński committed
40
41
}

42
43
44
45
46
func (c *ArtifactsUploaderCommand) artifactFilename(name string, format common.ArtifactFormat) string {
	name = filepath.Base(name)
	if name == "" || name == "." {
		name = DefaultUploadName
	}
Kamil Trzciński's avatar
Kamil Trzciński committed
47

48
49
50
	switch format {
	case common.ArtifactFormatZip:
		return name + ".zip"
51

52
53
	case common.ArtifactFormatGzip:
		return name + ".gz"
Kamil Trzciński's avatar
Kamil Trzciński committed
54
	}
55
	return name
Kamil Trzciński's avatar
Kamil Trzciński committed
56
57
}

Kamil Trzciński's avatar
Kamil Trzciński committed
58
func (c *ArtifactsUploaderCommand) createReadStream() (string, io.ReadCloser, error) {
59
60
61
62
	if len(c.files) == 0 {
		return "", nil, nil
	}

63
64
65
	format := c.Format
	if format == common.ArtifactFormatDefault {
		format = common.ArtifactFormatZip
66
67
	}

68
69
	filename := c.artifactFilename(c.Name, format)
	pr, pw := io.Pipe()
70

71
72
73
74
75
	archiver, err := archive.NewArchiver(archive.Format(format), pw, c.wd, archive.DefaultCompression)
	if err != nil {
		_ = pr.CloseWithError(err)
		return filename, nil, err
	}
76

77
78
79
80
	go func() {
		err := archiver.Archive(context.Background(), c.files)
		_ = pw.CloseWithError(err)
	}()
Kamil Trzciński's avatar
Kamil Trzciński committed
81

82
	return filename, pr, nil
83
84
}

85
func (c *ArtifactsUploaderCommand) Run() error {
Kamil Trzciński's avatar
Kamil Trzciński committed
86
87
	artifactsName, stream, err := c.createReadStream()
	if err != nil {
88
		return err
Kamil Trzciński's avatar
Kamil Trzciński committed
89
	}
90
91
	if stream == nil {
		logrus.Errorln("No files to upload")
92
93

		return nil
94
	}
95
	defer func() { _ = stream.Close() }()
96
97

	// Create the archive
Kamil Trzciński's avatar
Kamil Trzciński committed
98
99
100
101
102
103
	options := common.ArtifactsOptions{
		BaseName: artifactsName,
		ExpireIn: c.ExpireIn,
		Format:   c.Format,
		Type:     c.Type,
	}
104

105
	// Upload the data
Kamil Trzciński's avatar
Kamil Trzciński committed
106
	switch c.network.UploadRawArtifacts(c.JobCredentials, stream, options) {
Kamil Trzcinski's avatar
Kamil Trzcinski committed
107
	case common.UploadSucceeded:
108
		return nil
Kamil Trzcinski's avatar
Kamil Trzcinski committed
109
	case common.UploadForbidden:
110
		return os.ErrPermission
Kamil Trzcinski's avatar
Kamil Trzcinski committed
111
	case common.UploadTooLarge:
112
		return errTooLarge
Kamil Trzcinski's avatar
Kamil Trzcinski committed
113
	case common.UploadFailed:
114
		return retryableErr{err: os.ErrInvalid}
115
116
	case common.UploadServiceUnavailable:
		return retryableErr{err: errServiceUnavailable}
Kamil Trzcinski's avatar
Kamil Trzcinski committed
117
	default:
118
		return os.ErrInvalid
Kamil Trzcinski's avatar
Kamil Trzcinski committed
119
	}
120
121
}

122
func (c *ArtifactsUploaderCommand) ShouldRetry(tries int, err error) bool {
123
124
	var errAs retryableErr
	if !errors.As(err, &errAs) {
125
126
127
128
		return false
	}

	maxTries := defaultTries
129
	if errors.Is(errAs, errServiceUnavailable) {
130
131
132
133
134
135
136
137
138
139
		maxTries = serviceUnavailableTries
	}

	if tries >= maxTries {
		return false
	}

	return true
}

140
func (c *ArtifactsUploaderCommand) Execute(*cli.Context) {
141
	log.SetRunnerFormatter()
142

143
	if c.URL == "" || c.Token == "" {
144
145
146
147
148
149
150
151
152
153
154
155
156
		logrus.Fatalln("Missing runner credentials")
	}
	if c.ID <= 0 {
		logrus.Fatalln("Missing build ID")
	}

	// Enumerate files
	err := c.enumerate()
	if err != nil {
		logrus.Fatalln(err)
	}

	// If the upload fails, exit with a non-zero exit code to indicate an issue?
157
158
159
	logger := logrus.WithField("context", "artifacts-uploader")
	retryable := retry.New(retry.WithLogrus(c, logger))
	err = retryable.Run()
Kamil Trzcinski's avatar
Kamil Trzcinski committed
160
161
	if err != nil {
		logrus.Fatalln(err)
162
163
164
165
	}
}

func init() {
Pedro Pombeiro's avatar
Pedro Pombeiro committed
166
167
168
169
170
171
172
173
	common.RegisterCommand2(
		"artifacts-uploader",
		"create and upload build artifacts (internal)",
		&ArtifactsUploaderCommand{
			network: network.NewGitLabClient(),
			Name:    "artifacts",
		},
	)
174
}