Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: pipe for Sonatype Nexus Raw repositories #1204

Closed
wants to merge 8 commits into from
148 changes: 148 additions & 0 deletions internal/pipe/nexusraw/nexusraw.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Package nexus_raw provides a Pipe that push artifacts to a Sonatype Nexus repository of type 'raw'
antonienko marked this conversation as resolved.
Show resolved Hide resolved
package nexusraw

import (
"bytes"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"os"
"strings"

"github.com/apex/log"
"github.com/goreleaser/goreleaser/internal/artifact"
"github.com/goreleaser/goreleaser/internal/pipe"
"github.com/goreleaser/goreleaser/internal/semerrgroup"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
)

type Pipe struct{}

func (Pipe) String() string {
return "Sonatype Nexus Raw Repositories"
}

func (Pipe) Publish(ctx *context.Context) error {
if len(ctx.Config.NexusRaws) == 0 {
return pipe.Skip("sonatype nexus section is not configured")
}
for _, instance := range ctx.Config.NexusRaws {
instance := instance
if err := checkConfig(&instance); err != nil {
return err
}
}

if ctx.SkipPublish {
return pipe.ErrSkipPublishEnabled
}

for _, nexus := range ctx.Config.NexusRaws {
nexus := nexus
if err := upload(ctx, &nexus); err != nil {
return err
}
}
return nil
}

func upload(ctx *context.Context, nexus *config.NexusRaw) error {
g := semerrgroup.New(ctx.Parallelism)
for _, art := range ctx.Artifacts.Filter(
artifact.Or(
artifact.ByType(artifact.UploadableArchive),
artifact.ByType(artifact.UploadableBinary),
artifact.ByType(artifact.Checksum),
artifact.ByType(artifact.Signature),
artifact.ByType(artifact.LinuxPackage),
),
).List() {
art := art
g.Go(func() error {
return uploadAsset(nexus, art)
})
}
return g.Wait()
}

func uploadAsset(nexus *config.NexusRaw, artif *artifact.Artifact) error {
var b bytes.Buffer
log.WithField("file", artif.Path).WithField("name", artif.Name).Info("uploading to nexus")
w := multipart.NewWriter(&b)
if err := createForm(w, artif.Path, artif.Name, nexus.Directory); err != nil {
return err
}

url := fmt.Sprintf("%s/service/rest/v1/components?repository=%s", nexus.URL, nexus.Repository)
req, err := http.NewRequest("POST", url, &b)
if err != nil {
return err
}
req.Header.Set("Content-Type", w.FormDataContentType())
if nexus.Username != "" {
req.SetBasicAuth(nexus.Username, nexus.Password)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
var bodyContents string
body, err := ioutil.ReadAll(res.Body)
if err == nil {
bodyContents = string(body)
}
return pipe.Skip(fmt.Sprintf("bad status: %s, body: %s", res.Status, bodyContents))
}
return nil
}

func createForm(w *multipart.Writer, artifPath, artifName, nexusDir string) error {
asset, err := os.Open(artifPath)
if err != nil {
return fmt.Errorf("cannot open artifact file: '%s'", err.Error())
}
defer asset.Close()
artifactField, err := w.CreateFormFile("raw.asset1", asset.Name())
if err != nil {
return fmt.Errorf("cannot create post field for artifact: '%s'", err.Error())
}
if _, err = io.Copy(artifactField, asset); err != nil {
return fmt.Errorf("cannot copy artifact contents to post field: '%s'", err.Error())
}
directoryField, err := w.CreateFormField("raw.directory")
if err != nil {
return fmt.Errorf("cannot create directory field: '%s'", err.Error())
}
if _, err = io.Copy(directoryField, strings.NewReader(nexusDir)); err != nil {
return fmt.Errorf("cannot fill directory field contents: '%s'", err.Error())
}
filenameField, err := w.CreateFormField("raw.asset1.filename")
if err != nil {
return fmt.Errorf("cannot create filename field: '%s'", err.Error())
}
if _, err = io.Copy(filenameField, strings.NewReader(artifName)); err != nil {
return fmt.Errorf("cannot fill filename field contents: '%s'", err.Error())
}
return w.Close()
antonienko marked this conversation as resolved.
Show resolved Hide resolved
}

func checkConfig(nexus *config.NexusRaw) error {
if nexus.URL == "" {
return pipe.Skip("nexus_raws section 'url' is not configured properly (missing url)")
}
if nexus.Repository == "" {
return pipe.Skip("nexus_raws section 'repository' is not configured properly (missing repository name)")
}
if nexus.Directory == "" {
return pipe.Skip("nexus_raws section 'directory' is not configured properly (missing directory)")
}
if (nexus.Username == "" || nexus.Password == "") && (nexus.Username != "" || nexus.Password != "") {
return pipe.Skip("nexus_raws sections 'username' and 'password' are not configured properly (they must be either both empty or both filled)")
}
return nil
}
210 changes: 210 additions & 0 deletions internal/pipe/nexusraw/nexusraw_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package nexusraw

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"testing"
"time"

"github.com/goreleaser/goreleaser/internal/artifact"
"github.com/goreleaser/goreleaser/internal/testlib"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

type components struct {
Items []item `json:"items"`
}

type item struct {
Name string `json:"name"`
}

func TestDescription(t *testing.T) {
assert.NotEmpty(t, Pipe{}.String())
}

func TestNoNexusRaw(t *testing.T) {
testlib.AssertSkipped(t, Pipe{}.Publish(context.New(config.Project{})))
}

func TestUpload(t *testing.T) {
artifactsFolder, err := ioutil.TempDir("", "goreleasertest")
assert.NoError(t, err)
artifactPath := filepath.Join(artifactsFolder, "artifact.tar.gz")
artifact2Path := filepath.Join(artifactsFolder, "artifact2.tar.gz")
directoryPath := filepath.Join(artifactsFolder, "directory")
artifact3Path := filepath.Join(directoryPath, "binaryArtifact")
assert.NoError(t, ioutil.WriteFile(artifactPath, []byte("fake\nartifact"), 0744))
assert.NoError(t, ioutil.WriteFile(artifact2Path, []byte("fake\nartifact\t2"), 0744))
assert.NoError(t, os.Mkdir(directoryPath, 0744))
assert.NoError(t, ioutil.WriteFile(artifact3Path, []byte("fake\nartifact\t3"), 0744))
listen := randomListen(t)
nexusPassword := startNexus(t, "nexusTestContainer", listen)
defer stopNexus(t, "nexusTestContainer")
repositoryName := "goreleaserrawrepo"
prepareEnv(t, listen, nexusPassword, repositoryName)
ctx := context.New(config.Project{
Dist: artifactsFolder,
ProjectName: "testupload",
NexusRaws: []config.NexusRaw{
{
URL: "http://" + listen,
Repository: repositoryName,
Directory: "testDirectory",
Username: "admin",
Password: nexusPassword,
},
},
})
ctx.Git = context.GitInfo{CurrentTag: "v1.0.0"}
ctx.Artifacts.Add(&artifact.Artifact{
Name: "artifact.tar.gz",
Path: artifactPath,
Type: artifact.UploadableArchive,
})
ctx.Artifacts.Add(&artifact.Artifact{
Name: "artifact2.tar.gz",
Path: artifact2Path,
Type: artifact.UploadableArchive,
})
ctx.Artifacts.Add(&artifact.Artifact{
Name: "binaryArtifact",
Path: artifact3Path,
Type: artifact.Binary,
})
assert.NoError(t, Pipe{}.Publish(ctx))

resp, err := http.Get(fmt.Sprintf("http://%s/service/rest/v1/components?repository=%s", listen, repositoryName))
assert.NoError(t, err)

var cs components
err = json.NewDecoder(resp.Body).Decode(&cs)
assert.NoError(t, err)
assert.Len(t, cs.Items, 2)
assert.ElementsMatch(t, cs.Items, []item{
{"testDirectory/artifact.tar.gz"},
{"testDirectory/artifact2.tar.gz"}},
)
}

func randomListen(t *testing.T) string {
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
listener.Close()
antonienko marked this conversation as resolved.
Show resolved Hide resolved
return listener.Addr().String()
}

func startNexus(t *testing.T, containerName, listen string) string {
volumeFolder, err := ioutil.TempDir("", "nexustestvolumefolder")
if err != nil {
t.Fatalf("cannot create folder for nexus volume: %s", err.Error())
}
err = os.Chmod(volumeFolder, 0777)
if err != nil {
t.Fatalf("cannot modify privileges of the nexus volume folder: %s", err.Error())
}
if out, err := exec.Command(
"docker", "run", "-d", "--rm", "--name", containerName,
"-p", listen+":8081",
"-v", fmt.Sprintf("%s:/nexus-data", volumeFolder),
"sonatype/nexus3",
).CombinedOutput(); err != nil {
t.Fatalf("failed to start nexus: %s", string(out))
}
var adminPass []byte
for range time.Tick(time.Second) {
adminPass, err = ioutil.ReadFile(filepath.Join(volumeFolder, "admin.password"))
if err == nil {
break
}
if !os.IsNotExist(err) {
t.Fatalf("cannot read nexus admin password file: %s", err.Error())
}
}
i := 0
var resp *http.Response
for range time.Tick(time.Second) {
url := fmt.Sprintf("http://%s/service/rest/v1/repositories", listen)
resp, err = http.Get(url)
if err == nil {
defer resp.Body.Close()
break
}

if i > 20 {
t.Fatalf("stopping: cannot list nexus repositories (url) '%s': %s", url, err.Error())
}
i++
}
j := 0
for range time.Tick(time.Second) {
if resp.StatusCode == http.StatusOK {
break
}
if j > 5 {
t.Fatalf("stopping: wrong status code: %s", resp.Status)
}
j++
}
return string(adminPass)
}

func prepareEnv(t *testing.T, listen, adminPass, repository string) {
scriptName := "addRepo"
uploadScriptToNexus(t, listen, adminPass, repository, scriptName)
executeScript(t, listen, adminPass, scriptName)
}

func uploadScriptToNexus(t *testing.T, listen, adminPass, repository, scriptName string) {
url := fmt.Sprintf("http://%s/service/rest/v1/script", listen)
script := "def rawStore = blobStore.createFileBlobStore('raw', 'raw')\\nrepository.createRawHosted('" + repository + "', rawStore.name)"
postData := []byte(`{"name":"` + scriptName + `","content":"` + script + `","type":"groovy"}"`)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(postData))
if err != nil {
t.Fatalf("cannot create post request to add script: %s", err.Error())
}
req.Header.Set("Content-Type", "application/json")
req.SetBasicAuth("admin", adminPass)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("cannot upload script to nexus: %s", err.Error())
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
t.Fatalf("wrong status after uploading script to nexus: %s", resp.Status)
}
}

func executeScript(t *testing.T, listen, adminPass, scriptName string) {
url := fmt.Sprintf("http://%s/service/rest/v1/script/%s/run", listen, scriptName)
req, err := http.NewRequest("POST", url, nil)
if err != nil {
t.Fatalf("cannot create post request to execute script: %s", err.Error())
}
req.Header.Set("Content-Type", "text/plain")
req.SetBasicAuth("admin", adminPass)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("cannot execute script: %s", err.Error())
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("wrong status after executing script: %s", resp.Status)
}
}

func stopNexus(t *testing.T, containerName string) {
if out, err := exec.Command("docker", "stop", containerName).CombinedOutput(); err != nil {
t.Fatalf("failed to stop nexus: %s", string(out))
}
}
2 changes: 2 additions & 0 deletions internal/pipe/publish/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/goreleaser/goreleaser/internal/pipe/blob"
"github.com/goreleaser/goreleaser/internal/pipe/brew"
"github.com/goreleaser/goreleaser/internal/pipe/docker"
"github.com/goreleaser/goreleaser/internal/pipe/nexusraw"
"github.com/goreleaser/goreleaser/internal/pipe/put"
"github.com/goreleaser/goreleaser/internal/pipe/release"
"github.com/goreleaser/goreleaser/internal/pipe/s3"
Expand Down Expand Up @@ -47,6 +48,7 @@ var publishers = []Publisher{
// brew and scoop use the release URL, so, they should be last
brew.Pipe{},
scoop.Pipe{},
nexusraw.Pipe{},
}

// Run the pipe
Expand Down
10 changes: 10 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,15 @@ type Put struct {
Signature bool `yaml:",omitempty"`
}

// Nexus Raw repository configuration
type NexusRaw struct {
antonienko marked this conversation as resolved.
Show resolved Hide resolved
URL string `yaml:",omitempty"`
Repository string `yaml:",omitempty"`
Directory string `yaml:",omitempty"`
Username string `yaml:",omitempty"`
Password string `yaml:",omitempty"`
}

// Project includes all project configuration
type Project struct {
ProjectName string `yaml:"project_name,omitempty"`
Expand All @@ -360,6 +369,7 @@ type Project struct {
Checksum Checksum `yaml:",omitempty"`
Dockers []Docker `yaml:",omitempty"`
Artifactories []Put `yaml:",omitempty"`
NexusRaws []NexusRaw `yaml:"nexus_raws,omitempty"`
Puts []Put `yaml:",omitempty"`
S3 []S3 `yaml:"s3,omitempty"`
Blob []Blob `yaml:"blob,omitempty"` // TODO: remove this
Expand Down