github.com/szyn/goreleaser@v0.76.1-0.20180517112710-333da09a1297/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.SkipPublish { 112 return pipeline.ErrSkipPublishEnabled 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 filter artifact.Filter 121 switch v := strings.ToLower(instance.Mode); v { 122 case modeArchive: 123 filter = artifact.Or( 124 artifact.ByType(artifact.UploadableArchive), 125 artifact.ByType(artifact.LinuxPackage), 126 ) 127 case modeBinary: 128 filter = artifact.ByType(artifact.UploadableBinary) 129 default: 130 err := fmt.Errorf("artifactory: mode \"%s\" not supported", v) 131 log.WithFields(log.Fields{ 132 "instance": instance.Name, 133 "mode": v, 134 }).Error(err.Error()) 135 return err 136 } 137 138 if err := runPipeByFilter(ctx, instance, filter); err != nil { 139 return err 140 } 141 } 142 143 return nil 144 } 145 146 func runPipeByFilter(ctx *context.Context, instance config.Artifactory, filter artifact.Filter) error { 147 sem := make(chan bool, ctx.Parallelism) 148 var g errgroup.Group 149 for _, artifact := range ctx.Artifacts.Filter(filter).List() { 150 sem <- true 151 artifact := artifact 152 g.Go(func() error { 153 defer func() { 154 <-sem 155 }() 156 return uploadAsset(ctx, instance, artifact) 157 }) 158 } 159 return g.Wait() 160 } 161 162 // uploadAsset uploads file to target and logs all actions 163 func uploadAsset(ctx *context.Context, instance config.Artifactory, artifact artifact.Artifact) error { 164 envName := fmt.Sprintf("ARTIFACTORY_%s_SECRET", strings.ToUpper(instance.Name)) 165 secret := ctx.Env[envName] 166 167 // Generate the target url 168 targetURL, err := resolveTargetTemplate(ctx, instance, artifact) 169 if err != nil { 170 msg := "artifactory: error while building the target url" 171 log.WithField("instance", instance.Name).WithError(err).Error(msg) 172 return errors.Wrap(err, msg) 173 } 174 175 // Handle the artifact 176 file, err := os.Open(artifact.Path) 177 if err != nil { 178 return err 179 } 180 defer file.Close() // nolint: errcheck 181 182 // The target url needs to contain the artifact name 183 if !strings.HasSuffix(targetURL, "/") { 184 targetURL += "/" 185 } 186 targetURL += artifact.Name 187 188 uploaded, _, err := uploadAssetToArtifactory(ctx, targetURL, instance.Username, secret, file) 189 if err != nil { 190 msg := "artifactory: upload failed" 191 log.WithError(err).WithFields(log.Fields{ 192 "instance": instance.Name, 193 "username": instance.Username, 194 }).Error(msg) 195 return errors.Wrap(err, msg) 196 } 197 198 log.WithFields(log.Fields{ 199 "instance": instance.Name, 200 "mode": instance.Mode, 201 "uri": uploaded.DownloadURI, 202 }).Info("uploaded successful") 203 204 return nil 205 } 206 207 // targetData is used as a template struct for 208 // Artifactory.Target 209 type targetData struct { 210 Version string 211 Tag string 212 ProjectName string 213 214 // Only supported in mode binary 215 Os string 216 Arch string 217 Arm string 218 } 219 220 // resolveTargetTemplate returns the resolved target template with replaced variables 221 // Those variables can be replaced by the given context, goos, goarch, goarm and more 222 func resolveTargetTemplate(ctx *context.Context, artifactory config.Artifactory, artifact artifact.Artifact) (string, error) { 223 data := targetData{ 224 Version: ctx.Version, 225 Tag: ctx.Git.CurrentTag, 226 ProjectName: ctx.Config.ProjectName, 227 } 228 229 if artifactory.Mode == modeBinary { 230 data.Os = replace(ctx.Config.Archive.Replacements, artifact.Goos) 231 data.Arch = replace(ctx.Config.Archive.Replacements, artifact.Goarch) 232 data.Arm = replace(ctx.Config.Archive.Replacements, artifact.Goarm) 233 } 234 235 var out bytes.Buffer 236 t, err := template.New(ctx.Config.ProjectName).Parse(artifactory.Target) 237 if err != nil { 238 return "", err 239 } 240 err = t.Execute(&out, data) 241 return out.String(), err 242 } 243 244 func replace(replacements map[string]string, original string) string { 245 result := replacements[original] 246 if result == "" { 247 return original 248 } 249 return result 250 } 251 252 // uploadAssetToArtifactory uploads the asset file to target 253 func uploadAssetToArtifactory(ctx *context.Context, target, username, secret string, file *os.File) (*artifactoryResponse, *http.Response, error) { 254 stat, err := file.Stat() 255 if err != nil { 256 return nil, nil, err 257 } 258 if stat.IsDir() { 259 return nil, nil, errors.New("the asset to upload can't be a directory") 260 } 261 262 req, err := newUploadRequest(target, username, secret, file, stat.Size()) 263 if err != nil { 264 return nil, nil, err 265 } 266 267 asset := new(artifactoryResponse) 268 resp, err := executeHTTPRequest(ctx, req, asset) 269 if err != nil { 270 return nil, resp, err 271 } 272 return asset, resp, nil 273 } 274 275 // newUploadRequest creates a new http.Request for uploading 276 func newUploadRequest(target, username, secret string, reader io.Reader, size int64) (*http.Request, error) { 277 u, err := url.Parse(target) 278 if err != nil { 279 return nil, err 280 } 281 req, err := http.NewRequest("PUT", u.String(), reader) 282 if err != nil { 283 return nil, err 284 } 285 286 req.ContentLength = size 287 req.SetBasicAuth(username, secret) 288 289 return req, err 290 } 291 292 // executeHTTPRequest processes the http call with respect of context ctx 293 func executeHTTPRequest(ctx *context.Context, req *http.Request, v interface{}) (*http.Response, error) { 294 resp, err := http.DefaultClient.Do(req) 295 if err != nil { 296 // If we got an error, and the context has been canceled, 297 // the context's error is probably more useful. 298 select { 299 case <-ctx.Done(): 300 return nil, ctx.Err() 301 default: 302 } 303 304 return nil, err 305 } 306 307 defer resp.Body.Close() // nolint: errcheck 308 309 err = checkResponse(resp) 310 if err != nil { 311 // even though there was an error, we still return the response 312 // in case the caller wants to inspect it further 313 return resp, err 314 } 315 316 err = json.NewDecoder(resp.Body).Decode(v) 317 return resp, err 318 } 319 320 // An ErrorResponse reports one or more errors caused by an API request. 321 type errorResponse struct { 322 Response *http.Response // HTTP response that caused this error 323 Errors []Error `json:"errors"` // more detail on individual errors 324 } 325 326 func (r *errorResponse) Error() string { 327 return fmt.Sprintf("%v %v: %d %+v", 328 r.Response.Request.Method, r.Response.Request.URL, 329 r.Response.StatusCode, r.Errors) 330 } 331 332 // An Error reports more details on an individual error in an ErrorResponse. 333 type Error struct { 334 Status int `json:"status"` // Error code 335 Message string `json:"message"` // Message describing the error. 336 } 337 338 // checkResponse checks the API response for errors, and returns them if 339 // present. A response is considered an error if it has a status code outside 340 // the 200 range. 341 // API error responses are expected to have either no response 342 // body, or a JSON response body that maps to ErrorResponse. Any other 343 // response body will be silently ignored. 344 func checkResponse(r *http.Response) error { 345 if c := r.StatusCode; 200 <= c && c <= 299 { 346 return nil 347 } 348 errorResponse := &errorResponse{Response: r} 349 data, err := ioutil.ReadAll(r.Body) 350 if err == nil && data != nil { 351 err := json.Unmarshal(data, errorResponse) 352 if err != nil { 353 return err 354 } 355 } 356 return errorResponse 357 }