/
artifactory_uploader.go
219 lines (184 loc) · 5.34 KB
/
artifactory_uploader.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
package agent
import (
"context"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"hash"
"io"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"github.com/buildkite/agent/v3/api"
"github.com/buildkite/agent/v3/logger"
)
type ArtifactoryUploaderConfig struct {
// The destination which includes the Artifactory bucket name and the path.
// e.g artifactory://my-repo-name/foo/bar
Destination string
// Whether or not HTTP calls should be debugged
DebugHTTP bool
}
type ArtifactoryUploader struct {
// The artifactory bucket path set from the destination
Path string
// The artifactory bucket name set from the destination
Repository string
// URL of artifactory instance
iURL *url.URL
// The artifactory client to use
client *http.Client
// The configuration
conf ArtifactoryUploaderConfig
// The logger instance to use
logger logger.Logger
// Artifactory username
user string
// Artifactory password
password string
}
func NewArtifactoryUploader(l logger.Logger, c ArtifactoryUploaderConfig) (*ArtifactoryUploader, error) {
repo, path := ParseArtifactoryDestination(c.Destination)
stringURL := os.Getenv("BUILDKITE_ARTIFACTORY_URL")
username := os.Getenv("BUILDKITE_ARTIFACTORY_USER")
password := os.Getenv("BUILDKITE_ARTIFACTORY_PASSWORD")
// authentication is not set
if stringURL == "" || username == "" || password == "" {
return nil, errors.New("Must set BUILDKITE_ARTIFACTORY_URL, BUILDKITE_ARTIFACTORY_USER, BUILDKITE_ARTIFACTORY_PASSWORD when using rt:// path")
}
parsedURL, err := url.Parse(stringURL)
if err != nil {
return nil, err
}
return &ArtifactoryUploader{
logger: l,
conf: c,
client: &http.Client{},
iURL: parsedURL,
Path: path,
Repository: repo,
user: username,
password: password,
}, nil
}
func ParseArtifactoryDestination(destination string) (repo string, path string) {
parts := strings.Split(strings.TrimPrefix(string(destination), "rt://"), "/")
path = strings.Join(parts[1:], "/")
repo = parts[0]
return
}
func (u *ArtifactoryUploader) URL(artifact *api.Artifact) string {
url := *u.iURL
// ensure proper URL formatting for upload
url.Path = path.Join(
url.Path,
filepath.ToSlash(u.artifactPath(artifact)),
)
return url.String()
}
func (u *ArtifactoryUploader) Upload(_ context.Context, artifact *api.Artifact) error {
// Open file from filesystem
u.logger.Debug("Reading file \"%s\"", artifact.AbsolutePath)
f, err := os.Open(artifact.AbsolutePath)
if err != nil {
return fmt.Errorf("failed to open file %q (%v)", artifact.AbsolutePath, err)
}
// Upload the file to Artifactory.
u.logger.Debug("Uploading \"%s\" to `%s`", artifact.Path, u.URL(artifact))
req, err := http.NewRequest("PUT", u.URL(artifact), f)
req.SetBasicAuth(u.user, u.password)
if err != nil {
return err
}
md5Checksum, err := checksumFile(md5.New(), artifact.AbsolutePath)
if err != nil {
return err
}
req.Header.Add("X-Checksum-MD5", md5Checksum)
sha1Checksum, err := checksumFile(sha1.New(), artifact.AbsolutePath)
if err != nil {
return err
}
req.Header.Add("X-Checksum-SHA1", sha1Checksum)
sha256Checksum, err := checksumFile(sha256.New(), artifact.AbsolutePath)
if err != nil {
return err
}
req.Header.Add("X-Checksum-SHA256", sha256Checksum)
res, err := u.client.Do(req)
if err != nil {
return err
}
if err := checkResponse(res); err != nil {
return err
}
return nil
}
func checksumFile(hasher hash.Hash, path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
if _, err := io.Copy(hasher, f); err != nil {
return "", err
}
return fmt.Sprintf("%x", hasher.Sum(nil)), nil
}
func sha1File(path string) ([]byte, error) {
hasher := sha1.New()
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
if _, err := io.Copy(hasher, f); err != nil {
return nil, err
}
return hasher.Sum(nil), nil
}
func (u *ArtifactoryUploader) artifactPath(artifact *api.Artifact) string {
parts := []string{u.Repository, u.Path, artifact.Path}
return strings.Join(parts, "/")
}
// An ErrorResponse reports one or more errors caused by an API request.
type errorResponse struct {
Response *http.Response // HTTP response that caused this error
Errors []Error `json:"errors"` // more detail on individual errors
}
func (r *errorResponse) Error() string {
return fmt.Sprintf("%v %v: %d %+v",
r.Response.Request.Method, r.Response.Request.URL,
r.Response.StatusCode, r.Errors)
}
// An Error reports more details on an individual error in an ErrorResponse.
type Error struct {
Status int `json:"status"` // Error code
Message string `json:"message"` // Message describing the error.
}
// checkResponse checks the API response for errors, and returns them if
// present. A response is considered an error if it has a status code outside
// the 200 range.
// API error responses are expected to have either no response
// body, or a JSON response body that maps to ErrorResponse. Any other
// response body will be silently ignored.
func checkResponse(r *http.Response) error {
if c := r.StatusCode; 200 <= c && c <= 299 {
return nil
}
errorResponse := &errorResponse{Response: r}
data, err := io.ReadAll(r.Body)
if err == nil && data != nil {
err := json.Unmarshal(data, errorResponse)
if err != nil {
return err
}
}
return errorResponse
}