Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ Note that `check` will skip tags that do not have associated releases.
If set, override default tag filter regular expression of `v?([^v].*)`.
If the filter includes a capture group, the capture group is used as the release version;
otherwise, the entire matching substring is used as the version.
* `download_auths`: *Optional.*
A list of credentials to use for external asset hosts when running `in`.
Each entry must define `host`, `username`, and `password`.

### Examples

Expand Down Expand Up @@ -89,6 +92,23 @@ To set a custom tag filter:
tag_filter: "version-(.*)"
```

To download release links from external hosts requiring basic authentication:

```yaml
- name: gl-release
type: gitlab-release
source:
repository: concourse
access_token: ((gitlab_access_token))
download_auths:
- host: artifacts.example.internal
username: ((artifacts_user))
password: ((artifacts_password))
- host: binaries.partner.example
username: ((partner_user))
password: ((partner_password))
```

## Behavior

### `check`: Check for released versions
Expand Down
32 changes: 30 additions & 2 deletions gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ type GitlabClient struct {

accessToken string
repository string
gitlabHost string
downloadAuths map[string]DownloadAuth
}

func NewGitLabClient(source Source) (*GitlabClient, error) {
Expand All @@ -66,24 +68,38 @@ func NewGitLabClient(source Source) (*GitlabClient, error) {
httpClientOpt := gitlab.WithHTTPClient(httpClient)

baseURLOpt := gitlab.WithBaseURL(defaultBaseURL)
baseUrl, err := url.Parse(defaultBaseURL)
if err != nil {
return nil, err
}
gitlabHost := strings.ToLower(baseUrl.Hostname())

if source.GitLabAPIURL != "" {
var err error
baseUrl, err := url.Parse(source.GitLabAPIURL)
if err != nil {
return nil, err
}
baseURLOpt = gitlab.WithBaseURL(baseUrl.String())
gitlabHost = strings.ToLower(baseUrl.Hostname())
}

client, err := gitlab.NewClient(source.AccessToken, httpClientOpt, baseURLOpt)
if err != nil {
return nil, err
}

auths := make(map[string]DownloadAuth)
for _, auth := range source.DownloadAuths {
auths[strings.ToLower(auth.Host)] = auth
}

return &GitlabClient{
client: client,
repository: source.Repository,
accessToken: source.AccessToken,
downloadAuths: auths,
gitlabHost: gitlabHost,
}, nil
}

Expand Down Expand Up @@ -386,13 +402,25 @@ func (g *GitlabClient) DownloadProjectFile(fileURL, destPath string) error {
if err != nil {
return err
}

client := &http.Client{}
req, err := http.NewRequest("GET", filePathRef.String(), nil)
if err != nil {
return err
}
req.Header.Add("Private-Token", g.accessToken)
downloadHost := strings.ToLower(filePathRef.Hostname())

switch {
case downloadHost == g.gitlabHost:
// C'est une URL GitLab, utiliser le token
if g.accessToken != "" {
req.Header.Set("Private-Token", g.accessToken)
}
case g.downloadAuths[downloadHost].Host != "":
// Use Basic Auth
auth := g.downloadAuths[downloadHost]
req.SetBasicAuth(auth.Username, auth.Password)
}

resp, err := client.Do(req)
if err != nil {
return err
Expand Down
146 changes: 145 additions & 1 deletion gitlab_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package resource_test

import (
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"

. "github.com/orange-cloudfoundry/gitlab-release-resource"

Expand All @@ -22,7 +26,9 @@ var _ = Describe("GitLab Client", func() {
})

JustBeforeEach(func() {
source.GitLabAPIURL = server.URL()
if source.GitLabAPIURL == "" {
source.GitLabAPIURL = server.URL()
}
var err error
client, err = NewGitLabClient(source)
Ω(err).ShouldNot(HaveOccurred())
Expand Down Expand Up @@ -170,4 +176,142 @@ var _ = Describe("GitLab Client", func() {
})
})
})

Describe("DownloadProjectFile", func() {
var (
tmpDir string
destPath string
)

BeforeEach(func() {
source = Source{
Repository: "concourse",
AccessToken: "abc123",
}

var err error
tmpDir, err = os.MkdirTemp("", "gitlab-download")
Ω(err).ShouldNot(HaveOccurred())
destPath = filepath.Join(tmpDir, "asset.bin")
})

AfterEach(func() {
Ω(os.RemoveAll(tmpDir)).Should(Succeed())
})

Context("when downloading from GitLab host", func() {
It("uses Private-Token and no basic auth", func() {
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", "/uploads/hash/asset.bin"),
ghttp.VerifyHeaderKV("Private-Token", "abc123"),
ghttp.VerifyHeader(http.Header{"Authorization": nil}),
ghttp.RespondWith(200, "downloaded-from-gitlab"),
),
)

err := client.DownloadProjectFile(server.URL()+"/api/v4/uploads/hash/asset.bin", destPath)
Ω(err).ShouldNot(HaveOccurred())

contents, err := os.ReadFile(destPath)
Ω(err).ShouldNot(HaveOccurred())
Ω(string(contents)).Should(Equal("downloaded-from-gitlab"))
})
})

Context("when downloading from external host with configured auth", func() {
var externalServer *ghttp.Server

BeforeEach(func() {
externalServer = ghttp.NewServer()
externalURL, err := url.Parse(externalServer.URL())
Ω(err).ShouldNot(HaveOccurred())
source.GitLabAPIURL = "https://gitlab.example.internal"
source.DownloadAuths = []DownloadAuth{{
Host: externalURL.Hostname(),
Username: "ext-user",
Password: "ext-pass",
}}

client, err = NewGitLabClient(source)
Ω(err).ShouldNot(HaveOccurred())
})

AfterEach(func() {
externalServer.Close()
})

It("uses basic auth and does not leak Private-Token", func() {
externalServer.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", "/files/asset.bin"),
ghttp.VerifyBasicAuth("ext-user", "ext-pass"),
ghttp.VerifyHeader(http.Header{"Private-Token": nil}),
ghttp.RespondWith(200, "downloaded-from-external"),
),
)



err := client.DownloadProjectFile(externalServer.URL()+"/files/asset.bin", destPath)
Ω(err).ShouldNot(HaveOccurred())
})
})

Context("when downloading from external host without configured auth", func() {
var externalServer *ghttp.Server

BeforeEach(func() {
externalServer = ghttp.NewServer()
source.GitLabAPIURL = "https://gitlab.example.internal"

var err error
client, err = NewGitLabClient(source)
Ω(err).ShouldNot(HaveOccurred())
})

AfterEach(func() {
externalServer.Close()
})

It("sends no authentication header", func() {
externalServer.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", "/files/public.bin"),
ghttp.VerifyHeader(http.Header{"Authorization": nil}),
ghttp.VerifyHeader(http.Header{"Private-Token": nil}),
ghttp.RespondWith(200, "public-file"),
),
)

err := client.DownloadProjectFile(externalServer.URL()+"/files/public.bin", destPath)
Ω(err).ShouldNot(HaveOccurred())
})
})

Context("when download returns a non-200 status", func() {
for _, tc := range []struct {
status int
label string
}{
{status: http.StatusUnauthorized, label: "401"},
{status: http.StatusNotFound, label: "404"},
{status: http.StatusInternalServerError, label: "500"},
} {
tc := tc
It(fmt.Sprintf("returns an error for HTTP %s", tc.label), func() {
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", "/uploads/hash/asset.bin"),
ghttp.VerifyHeaderKV("Private-Token", "abc123"),
ghttp.RespondWith(tc.status, ""),
),
)

err := client.DownloadProjectFile(server.URL()+"/api/v4/uploads/hash/asset.bin", destPath)
Ω(err).Should(MatchError(fmt.Sprintf("failed to download file `asset.bin`: HTTP status %d", tc.status)))
})
}
})
})
})
8 changes: 8 additions & 0 deletions resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ type Source struct {
Insecure bool `json:"insecure"`

TagFilter string `json:"tag_filter"`

DownloadAuths []DownloadAuth `json:"download_auths"`
}

type DownloadAuth struct {
Host string `json:"host"`
Username string `json:"username"`
Password string `json:"password"`
}

type CheckRequest struct {
Expand Down
Loading