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
145 changes: 145 additions & 0 deletions internal/pipe/nexus_raw/nexus_raw.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package nexus_raw
antonienko marked this conversation as resolved.
Show resolved Hide resolved

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 {
if skipErr := checkConfig(&instance); skipErr != nil {
return pipe.Skip(skipErr.Error())
}
antonienko marked this conversation as resolved.
Show resolved Hide resolved
}

if ctx.SkipPublish {
return pipe.ErrSkipPublishEnabled
}

for _, nexus := range ctx.Config.NexusRaws {
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 {
antonienko marked this conversation as resolved.
Show resolved Hide resolved
asset, err := os.Open(artifPath)
if err != nil {
return pipe.Skip(fmt.Sprintf("cannot open artifact file: '%s'", err.Error()))
}
defer asset.Close()
artifactField, err := w.CreateFormFile("raw.asset1", asset.Name())
if err != nil {
return pipe.Skip(fmt.Sprintf("cannot create post field for artifact: '%s'", err.Error()))
}
if _, err = io.Copy(artifactField, asset); err != nil {
return pipe.Skip(fmt.Sprintf("cannot copy artifact contents to post field: '%s'", err.Error()))
}
directoryField, err := w.CreateFormField("raw.directory")
if err != nil {
return pipe.Skip(fmt.Sprintf("cannot create directory field: '%s'", err.Error()))
}
if _, err = io.Copy(directoryField, strings.NewReader(nexusDir)); err != nil {
return pipe.Skip(fmt.Sprintf("cannot fill directory field contents: '%s'", err.Error()))
}
filenameField, err := w.CreateFormField("raw.asset1.filename")
if err != nil {
return pipe.Skip(fmt.Sprintf("cannot create filename field: '%s'", err.Error()))
}
if _, err = io.Copy(filenameField, strings.NewReader(artifName)); err != nil {
return pipe.Skip(fmt.Sprintf("cannot fill filename field contents: '%s'", err.Error()))
}
return w.Close()
}

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/nexus_raw/nexus_raw_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package nexus_raw

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()
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/nexus_raw"
"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{},
nexus_raw.Pipe{},
}

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

type NexusRaw struct {
antonienko marked this conversation as resolved.
Show resolved Hide resolved
Url string `yaml:",omitempty"`
antonienko marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -359,6 +367,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