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

🐛 Support self-hosted GitLab instances where base URL has a path component #3819

Merged
merged 15 commits into from
Jan 31, 2024
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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,17 @@ scorecard --repo gitlab.com/<org>/<project>/<subproject>

For an example of using Scorecard in GitLab CI/CD, see [here](https://gitlab.com/ossf-test/scorecard-pipeline-example).

###### Self Hosted Editions
While we focus on GitLab.com support, Scorecard also works with self-hosted GitLab installations.
If your platform is hosted at a subdomain (e.g. `gitlab.foo.com`), Scorecard should work out of the box.
If your platform is hosted at some slug (e.g. `foo.com/bar/`), you will need to set the `GL_HOST` environment variable.

```bash
export GITLAB_AUTH_TOKEN=glpat-xxxx
export GL_HOST=foo.com/bar
scorecard --repo foo.com/bar/<org>/<project>
```

##### Using GitHub Enterprise Server (GHES) based Repository

To use a GitHub Enterprise host `github.corp.com`, use the `GH_HOST` environment variable.
Expand Down
49 changes: 37 additions & 12 deletions clients/gitlabrepo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
import (
"errors"
"fmt"
"net/http"
"net/url"
"os"
"strings"

"github.com/xanzy/go-gitlab"
Expand Down Expand Up @@ -61,14 +63,26 @@
t = input
}

// Allow skipping scheme for ease-of-use, default to https.
if !strings.Contains(t, "://") {
t = "https://" + t
u, err := url.Parse(withDefaultScheme(t))
if err != nil {
return sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("url.Parse: %v", err))

Check warning on line 68 in clients/gitlabrepo/repo.go

View check run for this annotation

Codecov / codecov/patch

clients/gitlabrepo/repo.go#L68

Added line #L68 was not covered by tests
}

u, e := url.Parse(t)
if e != nil {
return sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("url.Parse: %v", e))
// fixup the URL, for situations where GL_HOST contains part of the path
// https://github.com/ossf/scorecard/issues/3696
if h := os.Getenv("GL_HOST"); h != "" {
hostURL, err := url.Parse(withDefaultScheme(h))
if err != nil {
return sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("url.Parse GL_HOST: %v", err))
}

Check warning on line 77 in clients/gitlabrepo/repo.go

View check run for this annotation

Codecov / codecov/patch

clients/gitlabrepo/repo.go#L76-L77

Added lines #L76 - L77 were not covered by tests

// only modify behavior of repos which fall under GL_HOST
if hostURL.Host == u.Host {
// without the scheme and without trailing slashes
u.Host = hostURL.Host + strings.TrimRight(hostURL.Path, "/")
// remove any part of the path which belongs to the host
u.Path = strings.TrimPrefix(u.Path, hostURL.Path)
}
}

const splitLen = 2
Expand All @@ -81,6 +95,14 @@
return nil
}

// Allow skipping scheme for ease-of-use, default to https.
func withDefaultScheme(uri string) string {
if strings.Contains(uri, "://") {
return uri
}
return "https://" + uri
}

// URI implements Repo.URI().
func (r *repoURL) URI() string {
return fmt.Sprintf("%s/%s/%s", r.host, r.owner, r.project)
Expand All @@ -97,6 +119,10 @@

// IsValid implements Repo.IsValid.
func (r *repoURL) IsValid() error {
if strings.TrimSpace(r.owner) == "" || strings.TrimSpace(r.project) == "" {
return sce.WithMessage(sce.ErrorInvalidURL, "expected full project url: "+r.URI())
}

Check warning on line 124 in clients/gitlabrepo/repo.go

View check run for this annotation

Codecov / codecov/patch

clients/gitlabrepo/repo.go#L123-L124

Added lines #L123 - L124 were not covered by tests

if strings.Contains(r.host, "gitlab.") {
return nil
}
Expand All @@ -105,15 +131,18 @@
return fmt.Errorf("%w: %s", errInvalidGitlabRepoURL, r.host)
}

client, err := gitlab.NewClient("", gitlab.WithBaseURL(fmt.Sprintf("%s://%s", r.scheme, r.host)))
// intentionally pass empty token
// "When accessed without authentication, only public projects with simple fields are returned."
// https://docs.gitlab.com/ee/api/projects.html#list-all-projects
client, err := gitlab.NewClient("", gitlab.WithBaseURL(r.Host()))
if err != nil {
return sce.WithMessage(err,
fmt.Sprintf("couldn't create gitlab client for %s", r.host),
)
}

_, resp, err := client.Projects.ListProjects(&gitlab.ListProjectsOptions{})
if resp == nil || resp.StatusCode != 200 {
if resp == nil || resp.StatusCode != http.StatusOK {
return sce.WithMessage(sce.ErrRepoUnreachable,
fmt.Sprintf("couldn't reach gitlab instance at %s", r.host),
)
Expand All @@ -124,10 +153,6 @@
)
}

if strings.TrimSpace(r.owner) == "" || strings.TrimSpace(r.project) == "" {
return sce.WithMessage(sce.ErrorInvalidURL,
fmt.Sprintf("%v. Expected the full project url", r.URI()))
}
return nil
}

Expand Down
80 changes: 80 additions & 0 deletions clients/gitlabrepo/repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,83 @@ func TestRepoURL_MakeGitLabRepo(t *testing.T) {
}
}
}

//nolint:paralleltest // uses t.Setenv, can't be parallelized
func TestRepoURL_parse_GL_HOST(t *testing.T) {
tests := []struct {
name string
url string
host, owner, project string
glHost string
wantErr bool
}{
{
name: "GL_HOST ends with slash",
glHost: "https://foo.com/gitlab/",
url: "foo.com/gitlab/ssdlc/scorecard-scanner",
host: "foo.com/gitlab",
owner: "ssdlc",
project: "scorecard-scanner",
},
{
name: "GL_HOST doesn't end with slash",
glHost: "https://foo.com/gitlab",
url: "foo.com/gitlab/ssdlc/scorecard-scanner",
host: "foo.com/gitlab",
owner: "ssdlc",
project: "scorecard-scanner",
},
{
name: "GL_HOST doesn't match url",
glHost: "https://foo.com/gitlab",
url: "bar.com/gitlab/ssdlc/scorecard-scanner",
host: "bar.com",
owner: "gitlab",
project: "ssdlc/scorecard-scanner",
},
{
name: "GL_HOST has no path component",
glHost: "https://foo.com",
url: "foo.com/ssdlc/scorecard-scanner",
host: "foo.com",
owner: "ssdlc",
project: "scorecard-scanner",
},
{
name: "GL_HOST path has multiple slashes",
glHost: "https://foo.com/bar/baz/",
url: "foo.com/bar/baz/ssdlc/scorecard-scanner",
host: "foo.com/bar/baz",
owner: "ssdlc",
project: "scorecard-scanner",
},
{
name: "GL_HOST has no scheme",
glHost: "foo.com/bar/",
url: "foo.com/bar/ssdlc/scorecard-scanner",
host: "foo.com/bar",
owner: "ssdlc",
project: "scorecard-scanner",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("GL_HOST", tt.glHost)
var r repoURL
err := r.parse(tt.url)
if (err != nil) != tt.wantErr {
t.Fatalf("wanted err: %t, got: %v", tt.wantErr, err)
}
if r.host != tt.host {
t.Errorf("got host: %s, want: %s", r.host, tt.host)
}
if r.owner != tt.owner {
t.Errorf("got owner: %s, want: %s", r.owner, tt.owner)
}
if r.project != tt.project {
t.Errorf("got project: %s, want: %s", r.project, tt.project)
}
})
}
}
Loading