gitee.com/mirrors_opencollective/goreleaser@v0.45.0/pipeline/artifactory/artifactory.go (about) 1 // Package artifactory provides a Pipe that push to artifactory 2 package artifactory 3 4 import ( 5 "bytes" 6 "encoding/json" 7 "fmt" 8 "html/template" 9 "io" 10 "io/ioutil" 11 "net/http" 12 "net/url" 13 "os" 14 "strings" 15 16 "github.com/apex/log" 17 "github.com/pkg/errors" 18 "golang.org/x/sync/errgroup" 19 20 "github.com/goreleaser/goreleaser/config" 21 "github.com/goreleaser/goreleaser/context" 22 "github.com/goreleaser/goreleaser/internal/artifact" 23 "github.com/goreleaser/goreleaser/pipeline" 24 ) 25 26 // artifactoryResponse reflects the response after an upload request 27 // to Artifactory. 28 type artifactoryResponse struct { 29 Repo string `json:"repo,omitempty"` 30 Path string `json:"path,omitempty"` 31 Created string `json:"created,omitempty"` 32 CreatedBy string `json:"createdBy,omitempty"` 33 DownloadURI string `json:"downloadUri,omitempty"` 34 MimeType string `json:"mimeType,omitempty"` 35 Size string `json:"size,omitempty"` 36 Checksums artifactoryChecksums `json:"checksums,omitempty"` 37 OriginalChecksums artifactoryChecksums `json:"originalChecksums,omitempty"` 38 URI string `json:"uri,omitempty"` 39 } 40 41 // artifactoryChecksums reflects the checksums generated by 42 // Artifactory 43 type artifactoryChecksums struct { 44 SHA1 string `json:"sha1,omitempty"` 45 MD5 string `json:"md5,omitempty"` 46 SHA256 string `json:"sha256,omitempty"` 47 } 48 49 const ( 50 modeBinary = "binary" 51 modeArchive = "archive" 52 ) 53 54 // Pipe for Artifactory 55 type Pipe struct{} 56 57 // String returns the description of the pipe 58 func (Pipe) String() string { 59 return "releasing to Artifactory" 60 } 61 62 // Default sets the pipe defaults 63 func (Pipe) Default(ctx *context.Context) error { 64 if len(ctx.Config.Artifactories) == 0 { 65 return nil 66 } 67 68 // Check if a mode was set 69 for i := range ctx.Config.Artifactories { 70 if ctx.Config.Artifactories[i].Mode == "" { 71 ctx.Config.Artifactories[i].Mode = modeArchive 72 } 73 } 74 75 return nil 76 } 77 78 // Run the pipe 79 // 80 // Docs: https://www.jfrog.com/confluence/display/RTF/Artifactory+REST+API#ArtifactoryRESTAPI-Example-DeployinganArtifact 81 func (Pipe) Run(ctx *context.Context) error { 82 if len(ctx.Config.Artifactories) == 0 { 83 return pipeline.Skip("artifactory section is not configured") 84 } 85 86 // Check requirements for every instance we have configured. 87 // If not fulfilled, we can skip this pipeline 88 for _, instance := range ctx.Config.Artifactories { 89 if instance.Target == "" { 90 return pipeline.Skip("artifactory section is not configured properly (missing target)") 91 } 92 93 if instance.Username == "" { 94 return pipeline.Skip("artifactory section is not configured properly (missing username)") 95 } 96 97 if instance.Name == "" { 98 return pipeline.Skip("artifactory section is not configured properly (missing name)") 99 } 100 101 envName := fmt.Sprintf("ARTIFACTORY_%s_SECRET", strings.ToUpper(instance.Name)) 102 if _, ok := ctx.Env[envName]; !ok { 103 return pipeline.Skip(fmt.Sprintf("missing secret for artifactory instance %s", instance.Name)) 104 } 105 } 106 107 return doRun(ctx) 108 } 109 110 func doRun(ctx *context.Context) error { 111 if !ctx.Publish { 112 return pipeline.ErrSkipPublish 113 } 114 115 // Handle every configured artifactory instance 116 for _, instance := range ctx.Config.Artifactories { 117 // We support two different modes 118 // - "archive": Upload all artifacts 119 // - "binary": Upload only the raw binaries 120 var err error 121 switch v := strings.ToLower(instance.Mode); v { 122 case modeArchive: 123 err = runPipeByFilter(ctx, instance, artifact.ByType(artifact.UploadableArchive)) 124 125 case modeBinary: 126 err = runPipeByFilter(ctx, instance, artifact.ByType(artifact.UploadableBinary)) 127 128 default: 129 err = fmt.Errorf("artifactory: mode \"%s\" not supported", v) 130 log.WithFields(log.Fields{ 131 "instance": instance.Name, 132 "mode": v, 133 }).Error(err.Error()) 134 } 135 136 if err != nil { 137 return err 138 } 139 } 140 141 return nil 142 } 143 144 func runPipeByFilter(ctx *context.Context, instance config.Artifactory, filter artifact.Filter) error { 145 sem := make(chan bool, ctx.Parallelism) 146 var g errgroup.Group 147 for _, artifact := range ctx.Artifacts.Filter(filter).List() { 148 sem <- true 149 artifact := artifact 150 g.Go(func() error { 151 defer func() { 152 <-sem 153 }() 154 return uploadAsset(ctx, instance, artifact) 155 }) 156 } 157 return g.Wait() 158 } 159 160 // uploadAsset uploads file to target and logs all actions 161 func uploadAsset(ctx *context.Context, instance config.Artifactory, artifact artifact.Artifact) error { 162 envName := fmt.Sprintf("ARTIFACTORY_%s_SECRET", strings.ToUpper(instance.Name)) 163 secret := ctx.Env[envName] 164 165 // Generate the target url 166 targetURL, err := resolveTargetTemplate(ctx, instance, artifact) 167 if err != nil { 168 msg := "artifactory: error while building the target url" 169 log.WithField("instance", instance.Name).WithError(err).Error(msg) 170 return errors.Wrap(err, msg) 171 } 172 173 // Handle the artifact 174 file, err := os.Open(artifact.Path) 175 if err != nil { 176 return err 177 } 178 defer file.Close() // nolint: errcheck 179 180 // The target url needs to contain the artifact name 181 if !strings.HasSuffix(targetURL, "/") { 182 targetURL += "/" 183 } 184 targetURL += artifact.Name 185 186 uploaded, _, err := uploadAssetToArtifactory(ctx, targetURL, instance.Username, secret, file) 187 if err != nil { 188 msg := "artifactory: upload failed" 189 log.WithError(err).WithFields(log.Fields{ 190 "instance": instance.Name, 191 "username": instance.Username, 192 }).Error(msg) 193 return errors.Wrap(err, msg) 194 } 195 196 log.WithFields(log.Fields{ 197 "instance": instance.Name, 198 "mode": instance.Mode, 199 "uri": uploaded.DownloadURI, 200 }).Info("uploaded successful") 201 202 return nil 203 } 204 205 // targetData is used as a template struct for 206 // Artifactory.Target 207 type targetData struct { 208 Version string 209 Tag string 210 ProjectName string 211 212 // Only supported in mode binary 213 Os string 214 Arch string 215 Arm string 216 } 217 218 // resolveTargetTemplate returns the resolved target template with replaced variables 219 // Those variables can be replaced by the given context, goos, goarch, goarm and more 220 func resolveTargetTemplate(ctx *context.Context, artifactory config.Artifactory, artifact artifact.Artifact) (string, error) { 221 data := targetData{ 222 Version: ctx.Version, 223 Tag: ctx.Git.CurrentTag, 224 ProjectName: ctx.Config.ProjectName, 225 } 226 227 if artifactory.Mode == modeBinary { 228 data.Os = replace(ctx.Config.Archive.Replacements, artifact.Goos) 229 data.Arch = replace(ctx.Config.Archive.Replacements, artifact.Goarch) 230 data.Arm = replace(ctx.Config.Archive.Replacements, artifact.Goarm) 231 } 232 233 var out bytes.Buffer 234 t, err := template.New(ctx.Config.ProjectName).Parse(artifactory.Target) 235 if err != nil { 236 return "", err 237 } 238 err = t.Execute(&out, data) 239 return out.String(), err 240 } 241 242 func replace(replacements map[string]string, original string) string { 243 result := replacements[original] 244 if result == "" { 245 return original 246 } 247 return result 248 } 249 250 // uploadAssetToArtifactory uploads the asset file to target 251 func uploadAssetToArtifactory(ctx *context.Context, target, username, secret string, file *os.File) (*artifactoryResponse, *http.Response, error) { 252 stat, err := file.Stat() 253 if err != nil { 254 return nil, nil, err 255 } 256 if stat.IsDir() { 257 return nil, nil, errors.New("the asset to upload can't be a directory") 258 } 259 260 req, err := newUploadRequest(target, username, secret, file, stat.Size()) 261 if err != nil { 262 return nil, nil, err 263 } 264 265 asset := new(artifactoryResponse) 266 resp, err := executeHTTPRequest(ctx, req, asset) 267 if err != nil { 268 return nil, resp, err 269 } 270 return asset, resp, nil 271 } 272 273 // newUploadRequest creates a new http.Request for uploading 274 func newUploadRequest(target, username, secret string, reader io.Reader, size int64) (*http.Request, error) { 275 u, err := url.Parse(target) 276 if err != nil { 277 return nil, err 278 } 279 req, err := http.NewRequest("PUT", u.String(), reader) 280 if err != nil { 281 return nil, err 282 } 283 284 req.ContentLength = size 285 req.SetBasicAuth(username, secret) 286 287 return req, err 288 } 289 290 // executeHTTPRequest processes the http call with respect of context ctx 291 func executeHTTPRequest(ctx *context.Context, req *http.Request, v interface{}) (*http.Response, error) { 292 resp, err := http.DefaultClient.Do(req) 293 if err != nil { 294 // If we got an error, and the context has been canceled, 295 // the context's error is probably more useful. 296 select { 297 case <-ctx.Done(): 298 return nil, ctx.Err() 299 default: 300 } 301 302 return nil, err 303 } 304 305 defer resp.Body.Close() // nolint: errcheck 306 307 err = checkResponse(resp) 308 if err != nil { 309 // even though there was an error, we still return the response 310 // in case the caller wants to inspect it further 311 return resp, err 312 } 313 314 err = json.NewDecoder(resp.Body).Decode(v) 315 return resp, err 316 } 317 318 // An ErrorResponse reports one or more errors caused by an API request. 319 type errorResponse struct { 320 Response *http.Response // HTTP response that caused this error 321 Errors []Error `json:"errors"` // more detail on individual errors 322 } 323 324 func (r *errorResponse) Error() string { 325 return fmt.Sprintf("%v %v: %d %+v", 326 r.Response.Request.Method, r.Response.Request.URL, 327 r.Response.StatusCode, r.Errors) 328 } 329 330 // An Error reports more details on an individual error in an ErrorResponse. 331 type Error struct { 332 Status int `json:"status"` // Error code 333 Message string `json:"message"` // Message describing the error. 334 } 335 336 // checkResponse checks the API response for errors, and returns them if 337 // present. A response is considered an error if it has a status code outside 338 // the 200 range. 339 // API error responses are expected to have either no response 340 // body, or a JSON response body that maps to ErrorResponse. Any other 341 // response body will be silently ignored. 342 func checkResponse(r *http.Response) error { 343 if c := r.StatusCode; 200 <= c && c <= 299 { 344 return nil 345 } 346 errorResponse := &errorResponse{Response: r} 347 data, err := ioutil.ReadAll(r.Body) 348 if err == nil && data != nil { 349 err := json.Unmarshal(data, errorResponse) 350 if err != nil { 351 return err 352 } 353 } 354 return errorResponse 355 }