// Package artifactory provides a Pipe that push to artifactory package artifactory import ( "bytes" "encoding/json" "fmt" "html/template" "io" "io/ioutil" "net/http" "net/url" "os" "strings" "github.com/goreleaser/goreleaser/config" "github.com/goreleaser/goreleaser/context" "github.com/goreleaser/goreleaser/internal/buildtarget" "github.com/goreleaser/goreleaser/pipeline" "github.com/pkg/errors" "golang.org/x/sync/errgroup" "github.com/apex/log" ) // artifactoryResponse reflects the response after an upload request // to Artifactory. type artifactoryResponse struct { Repo string `json:"repo,omitempty"` Path string `json:"path,omitempty"` Created string `json:"created,omitempty"` CreatedBy string `json:"createdBy,omitempty"` DownloadURI string `json:"downloadUri,omitempty"` MimeType string `json:"mimeType,omitempty"` Size string `json:"size,omitempty"` Checksums artifactoryChecksums `json:"checksums,omitempty"` OriginalChecksums artifactoryChecksums `json:"originalChecksums,omitempty"` URI string `json:"uri,omitempty"` } // artifactoryChecksums reflects the checksums generated by // Artifactory type artifactoryChecksums struct { SHA1 string `json:"sha1,omitempty"` MD5 string `json:"md5,omitempty"` SHA256 string `json:"sha256,omitempty"` } // Pipe for Artifactory type Pipe struct{} // Description of the pipe func (Pipe) String() string { return "releasing to Artifactory" } // Run the pipe // // Docs: https://www.jfrog.com/confluence/display/RTF/Artifactory+REST+API#ArtifactoryRESTAPI-Example-DeployinganArtifact func (Pipe) Run(ctx *context.Context) error { if l := len(ctx.Config.Artifactories); l == 0 { return pipeline.Skip("artifactory section is not configured") } // Check requirements for every instance we have configured. // If not fulfilled, we can skip this pipeline for _, instance := range ctx.Config.Artifactories { if instance.Target == "" { return pipeline.Skip("artifactory section is not configured properly (missing target)") } if instance.Username == "" { return pipeline.Skip("artifactory section is not configured properly (missing username)") } if instance.Name == "" { return pipeline.Skip("artifactory section is not configured properly (missing name)") } envName := fmt.Sprintf("ARTIFACTORY_%s_SECRET", strings.ToUpper(instance.Name)) if os.Getenv(envName) == "" { return pipeline.Skip(fmt.Sprintf("missing secret for artifactory instance %s", instance.Name)) } } return doRun(ctx) } func doRun(ctx *context.Context) error { if !ctx.Publish { return pipeline.Skip("--skip-publish is set") } // Loop over all builds, because we want to publish // every build to Artifactory for _, build := range ctx.Config.Builds { if err := runPipeOnBuild(ctx, build); err != nil { return err } } return nil } // runPipeOnBuild runs the pipe for every configured build func runPipeOnBuild(ctx *context.Context, build config.Build) error { sem := make(chan bool, ctx.Parallelism) var g errgroup.Group // Lets generate the build matrix, because we want to publish // every target to Artifactory for _, target := range buildtarget.All(build) { sem <- true target := target build := build g.Go(func() error { defer func() { <-sem }() return upload(ctx, build, target) }) } return g.Wait() } // upload runs the pipe action of the current build and the current target. // This is where the real action take place. // The real action is // - Identify the binary // - Loop over all configured artifactories // - Resolve all variables in the target name // - Upload the binary to the artifactory func upload(ctx *context.Context, build config.Build, target buildtarget.Target) error { binary, err := getBinaryForUploadPerBuild(ctx, target) if err != nil { return err } // Loop over all configured Artifactory instances for _, instance := range ctx.Config.Artifactories { secret := os.Getenv(fmt.Sprintf("ARTIFACTORY_%s_SECRET", strings.ToUpper(instance.Name))) // Generate name of target uploadTarget, err := buildTargetName(ctx, instance, target) if err != nil { msg := "artifactory: error while building the target name" log.WithError(err).Error(msg) return errors.Wrap(err, msg) } // The upload url to Artifactory needs the binary name // Here we add the binary to the target url if !strings.HasSuffix(uploadTarget, "/") { uploadTarget += "/" } uploadTarget += binary.Name // Upload the binary to Artifactory file, err := os.Open(binary.Path) if err != nil { return err } defer file.Close() // nolint: errcheck artifact, _, err := uploadBinaryToArtifactory(ctx, uploadTarget, instance.Username, secret, file) if err != nil { msg := "artifactory: upload failed" log.WithError(err).WithFields(log.Fields{ "instance": instance.Name, "username": instance.Username, }).Error(msg) return errors.Wrap(err, msg) } log.WithFields(log.Fields{ "instance": instance.Name, "target": target.PrettyString(), "username": instance.Username, "uri": artifact.DownloadURI, }).Info("uploaded successful") } return nil } // getBinaryForUploadPerBuild determines the correct binary // for the upload func getBinaryForUploadPerBuild(ctx *context.Context, target buildtarget.Target) (*context.Binary, error) { var group = ctx.Binaries[target.String()] if group == nil { return nil, fmt.Errorf("binary for build target %s not found", target.String()) } var binary context.Binary for _, binaries := range group { for _, b := range binaries { binary = b break } break } return &binary, nil } // targetData is used as a template struct for // Artifactory.Target type targetData struct { Os string Arch string Arm string Version string Tag string ProjectName string } // buildTargetName returns the name resolved target name with replaced variables // Those variables can be replaced by the given context, goos, goarch, goarm and more func buildTargetName(ctx *context.Context, artifactory config.Artifactory, target buildtarget.Target) (string, error) { data := targetData{ Os: replace(ctx.Config.Archive.Replacements, target.OS), Arch: replace(ctx.Config.Archive.Replacements, target.Arch), Arm: replace(ctx.Config.Archive.Replacements, target.Arm), Version: ctx.Version, Tag: ctx.Git.CurrentTag, ProjectName: ctx.Config.ProjectName, } var out bytes.Buffer t, err := template.New(ctx.Config.ProjectName).Parse(artifactory.Target) if err != nil { return "", err } err = t.Execute(&out, data) return out.String(), err } func replace(replacements map[string]string, original string) string { result := replacements[original] if result == "" { return original } return result } // uploadBinaryToArtifactory uploads the binary file to target func uploadBinaryToArtifactory(ctx *context.Context, target, username, secret string, file *os.File) (*artifactoryResponse, *http.Response, error) { stat, err := file.Stat() if err != nil { return nil, nil, err } if stat.IsDir() { return nil, nil, errors.New("the asset to upload can't be a directory") } req, err := newUploadRequest(target, username, secret, file, stat.Size()) if err != nil { return nil, nil, err } asset := new(artifactoryResponse) resp, err := executeHTTPRequest(ctx, req, asset) if err != nil { return nil, resp, err } return asset, resp, nil } // newUploadRequest creates a new http.Request for uploading func newUploadRequest(target, username, secret string, reader io.Reader, size int64) (*http.Request, error) { u, err := url.Parse(target) if err != nil { return nil, err } req, err := http.NewRequest("PUT", u.String(), reader) if err != nil { return nil, err } req.ContentLength = size req.SetBasicAuth(username, secret) return req, err } // executeHTTPRequest processes the http call with respect of context ctx func executeHTTPRequest(ctx *context.Context, req *http.Request, v interface{}) (*http.Response, error) { resp, err := http.DefaultClient.Do(req) if err != nil { // If we got an error, and the context has been canceled, // the context's error is probably more useful. select { case <-ctx.Done(): return nil, ctx.Err() default: } return nil, err } defer resp.Body.Close() // nolint: errcheck err = checkResponse(resp) if err != nil { // even though there was an error, we still return the response // in case the caller wants to inspect it further return resp, err } err = json.NewDecoder(resp.Body).Decode(v) return resp, err } // 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. } func (e *Error) Error() string { return fmt.Sprintf("%v (%v)", e.Message, e.Status) } // 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 := ioutil.ReadAll(r.Body) if err == nil && data != nil { err := json.Unmarshal(data, errorResponse) if err != nil { return err } } return errorResponse }