From 252612a4e0f7a23ab02d0c7e4748c2f454c92877 Mon Sep 17 00:00:00 2001 From: David Collom Date: Fri, 21 Mar 2025 12:38:27 +0000 Subject: [PATCH 1/4] Adding support for GHCR custom domains for using Github Enterprise --- .dockerignore | 4 ++ cmd/app/options.go | 8 +++ deploy/charts/version-checker/README.md | 1 + .../version-checker/templates/secret.yaml | 6 ++- deploy/charts/version-checker/values.yaml | 2 + go.mod | 4 +- go.sum | 8 +-- pkg/client/docker/docker.go | 2 +- pkg/client/gcr/gcr.go | 2 +- pkg/client/ghcr/ghcr.go | 51 +++++++++++-------- pkg/client/ghcr/ghcr_test.go | 2 +- pkg/client/ghcr/path.go | 13 ++++- pkg/client/ghcr/path_test.go | 32 ++++++++++-- pkg/client/selfhosted/selfhosted.go | 2 +- 14 files changed, 102 insertions(+), 35 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..7ab01c66 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +bin +coverage.out +*.md +.git diff --git a/cmd/app/options.go b/cmd/app/options.go index 4c82ccdf..1ac24a0c 100644 --- a/cmd/app/options.go +++ b/cmd/app/options.go @@ -37,6 +37,7 @@ const ( envGCRAccessToken = "GCR_TOKEN" envGHCRAccessToken = "GHCR_TOKEN" + envGHCRHostname = "GHCR_HOSTNAME" envQuayToken = "QUAY_TOKEN" @@ -207,6 +208,12 @@ func (o *Options) addAuthFlags(fs *pflag.FlagSet) { "Personal Access token for read access to GHCR releases (%s_%s).", envPrefix, envGHCRAccessToken, )) + fs.StringVar(&o.Client.GHCR.Hostname, + "gchr-hostname", "", + fmt.Sprintf( + "Override hostname for Github Enterprise instances (%s_%s).", + envPrefix, envGHCRHostname, + )) /// /// Quay @@ -291,6 +298,7 @@ func (o *Options) complete() { {envGCRAccessToken, &o.Client.GCR.Token}, {envGHCRAccessToken, &o.Client.GHCR.Token}, + {envGHCRHostname, &o.Client.GHCR.Hostname}, {envQuayToken, &o.Client.Quay.Token}, } { diff --git a/deploy/charts/version-checker/README.md b/deploy/charts/version-checker/README.md index c6ddb80b..04ae1beb 100644 --- a/deploy/charts/version-checker/README.md +++ b/deploy/charts/version-checker/README.md @@ -34,6 +34,7 @@ A Helm chart for version-checker | extraVolumeMounts | list | `[]` | Allow for extra Volume Mounts to version-checkers container | | extraVolumes | list | `[]` | Allow for extra Volumes to be associated to the pod | | gcr.token | string | `nil` | Access token for read access to private GCR registries | +| ghcr.hostname | string | `nil` | Hostname for Github Enterprise to override the default ghcr domains. | | ghcr.token | string | `nil` | Personal Access token for read access to GHCR releases | | image.imagePullSecret | string | `nil` | Pull secrects - name of existing secret | | image.pullPolicy | string | `"IfNotPresent"` | Set the Image Pull Policy | diff --git a/deploy/charts/version-checker/templates/secret.yaml b/deploy/charts/version-checker/templates/secret.yaml index 6d80ed35..513858a4 100644 --- a/deploy/charts/version-checker/templates/secret.yaml +++ b/deploy/charts/version-checker/templates/secret.yaml @@ -1,4 +1,5 @@ -{{- if or .Values.acr.refreshToken .Values.acr.username .Values.acr.password .Values.docker.token .Values.ecr.accessKeyID .Values.ecr.secretAccessKey .Values.ecr.sessionToken .Values.docker.username .Values.docker.password .Values.gcr.token .Values.ghcr.token .Values.quay.token (not (eq (len .Values.selfhosted) 0)) }} +{{- if or .Values.acr.refreshToken .Values.acr.username .Values.acr.password .Values.docker.token .Values.ecr.accessKeyID .Values.ecr.secretAccessKey .Values.ecr.sessionToken .Values.docker.username .Values.docker.password .Values.gcr.token .Values.ghcr.token .Values.ghcr.hostname .Values.quay.token (not (eq (len .Values.selfhosted) 0)) }} +--- apiVersion: v1 data: # ACR @@ -43,6 +44,9 @@ data: {{- if .Values.ghcr.token }} ghcr.token: {{ .Values.ghcr.token | b64enc }} {{- end}} + {{- if .Values.ghcr.hostname }} + ghcr.hostname: {{ .Values.ghcr.hostname | b64enc }} + {{- end}} # Quay {{- if .Values.quay.token }} diff --git a/deploy/charts/version-checker/values.yaml b/deploy/charts/version-checker/values.yaml index d6ce2862..cdbc44c8 100644 --- a/deploy/charts/version-checker/values.yaml +++ b/deploy/charts/version-checker/values.yaml @@ -88,6 +88,8 @@ gcr: ghcr: # -- (string) Personal Access token for read access to GHCR releases token: + # -- (string) Hostname for Github Enterprise to override the default ghcr domains. + hostname: # Quay.io Registry Credentials Configuration quay: diff --git a/go.mod b/go.mod index 748f1cbd..b98eefca 100644 --- a/go.mod +++ b/go.mod @@ -32,9 +32,9 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.17.59 github.com/aws/aws-sdk-go-v2/service/ecr v1.41.0 github.com/gofri/go-github-ratelimit v1.1.0 - github.com/google/go-cmp v0.6.0 + github.com/google/go-cmp v0.7.0 github.com/google/go-containerregistry v0.20.3 - github.com/google/go-github/v62 v62.0.0 + github.com/google/go-github/v70 v70.0.0 github.com/jarcoal/httpmock v1.3.1 github.com/stretchr/testify v1.10.0 ) diff --git a/go.sum b/go.sum index b92527db..c4cfb01b 100644 --- a/go.sum +++ b/go.sum @@ -101,12 +101,12 @@ github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63Kqpo github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= -github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4= -github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4= +github.com/google/go-github/v70 v70.0.0 h1:/tqCp5KPrcvqCc7vIvYyFYTiCGrYvaWoYMGHSQbo55o= +github.com/google/go-github/v70 v70.0.0/go.mod h1:xBUZgo8MI3lUL/hwxl3hlceJW1U8MVnXP3zUyI+rhQY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= diff --git a/pkg/client/docker/docker.go b/pkg/client/docker/docker.go index 78e70d2c..0efb253e 100644 --- a/pkg/client/docker/docker.go +++ b/pkg/client/docker/docker.go @@ -138,7 +138,7 @@ func (c *Client) doRequest(ctx context.Context, url string) (*TagResponse, error resp, err := c.Do(req) if err != nil { - return nil, fmt.Errorf("failed to get docker image: %s", err) + return nil, fmt.Errorf("failed to get %q image: %s", c.Name(), err) } defer resp.Body.Close() diff --git a/pkg/client/gcr/gcr.go b/pkg/client/gcr/gcr.go index 05ec261e..f28d5207 100644 --- a/pkg/client/gcr/gcr.go +++ b/pkg/client/gcr/gcr.go @@ -58,7 +58,7 @@ func (c *Client) Tags(ctx context.Context, host, repo, image string) ([]api.Imag resp, err := c.Do(req) if err != nil { - return nil, fmt.Errorf("failed to get docker image: %w", err) + return nil, fmt.Errorf("failed to get %q image: %w", c.Name(), err) } defer resp.Body.Close() diff --git a/pkg/client/ghcr/ghcr.go b/pkg/client/ghcr/ghcr.go index 9133750d..718ae2a0 100644 --- a/pkg/client/ghcr/ghcr.go +++ b/pkg/client/ghcr/ghcr.go @@ -6,13 +6,15 @@ import ( "net/url" "strings" - "github.com/gofri/go-github-ratelimit/github_ratelimit" - "github.com/google/go-github/v62/github" "github.com/jetstack/version-checker/pkg/api" + + "github.com/gofri/go-github-ratelimit/github_ratelimit" + "github.com/google/go-github/v70/github" ) type Options struct { - Token string + Token string + Hostname string } type Client struct { @@ -32,6 +34,12 @@ func New(opts Options) *Client { panic(err) } client := github.NewClient(ghRateLimiter).WithAuthToken(opts.Token) + if opts.Hostname != "" { + client, err = client.WithEnterpriseURLs(fmt.Sprintf("https://%s/", opts.Hostname), fmt.Sprintf("https://%s/api/uploads/", opts.Hostname)) + if err != nil { + panic(fmt.Errorf("setting enterprise URLs: %w", err)) + } + } return &Client{ client: client, @@ -87,8 +95,8 @@ func (c *Client) determineGetAllVersionsFunc(ctx context.Context, owner, repo st func (c *Client) buildPackageListOptions() *github.PackageListOptions { return &github.PackageListOptions{ - PackageType: github.String("container"), - State: github.String("active"), + PackageType: github.Ptr("container"), + State: github.Ptr("active"), ListOptions: github.ListOptions{ PerPage: 100, }, @@ -98,25 +106,28 @@ func (c *Client) buildPackageListOptions() *github.PackageListOptions { func (c *Client) extractImageTags(versions []*github.PackageVersion) []api.ImageTag { var tags []api.ImageTag for _, ver := range versions { - if len(ver.Metadata.Container.Tags) == 0 { - continue - } + if meta, ok := ver.GetMetadata(); ok { - sha := "" - if strings.HasPrefix(*ver.Name, "sha") { - sha = *ver.Name - } - - for _, tag := range ver.Metadata.Container.Tags { - if c.shouldSkipTag(tag) { + if len(meta.Container.Tags) == 0 { continue } - tags = append(tags, api.ImageTag{ - Tag: tag, - SHA: sha, - Timestamp: ver.CreatedAt.Time, - }) + sha := "" + if strings.HasPrefix(*ver.Name, "sha") { + sha = *ver.Name + } + + for _, tag := range meta.Container.Tags { + if c.shouldSkipTag(tag) { + continue + } + + tags = append(tags, api.ImageTag{ + Tag: tag, + SHA: sha, + Timestamp: ver.CreatedAt.Time, + }) + } } } return tags diff --git a/pkg/client/ghcr/ghcr_test.go b/pkg/client/ghcr/ghcr_test.go index c8c687ae..39c2f6b6 100644 --- a/pkg/client/ghcr/ghcr_test.go +++ b/pkg/client/ghcr/ghcr_test.go @@ -5,7 +5,7 @@ import ( "net/http" "testing" - "github.com/google/go-github/v62/github" + "github.com/google/go-github/v70/github" "github.com/jarcoal/httpmock" "github.com/stretchr/testify/assert" ) diff --git a/pkg/client/ghcr/path.go b/pkg/client/ghcr/path.go index c65d7be6..270f93b2 100644 --- a/pkg/client/ghcr/path.go +++ b/pkg/client/ghcr/path.go @@ -1,16 +1,27 @@ package ghcr import ( + "regexp" "strings" ) +const ( + HostRegTempl = `^(containers\.[a-zA-Z0-9-]+\.ghe\.com|ghcr\.io)$` +) + +var HostReg = regexp.MustCompile(HostRegTempl) + func (c *Client) IsHost(host string) bool { // Package API requires Authentication // This forces the Client to use the fallback method if c.opts.Token == "" { return false } - return host == "ghcr.io" + // If we're using a custom hostname. + if c.opts.Hostname != "" && c.opts.Hostname == host { + return true + } + return HostReg.MatchString(host) } func (c *Client) RepoImageFromPath(path string) (string, string) { diff --git a/pkg/client/ghcr/path_test.go b/pkg/client/ghcr/path_test.go index 56f1bda4..640fc54f 100644 --- a/pkg/client/ghcr/path_test.go +++ b/pkg/client/ghcr/path_test.go @@ -4,9 +4,10 @@ import "testing" func TestIsHost(t *testing.T) { tests := map[string]struct { - token string - host string - expIs bool + token string + host string + customhost *string + expIs bool }{ "an empty token should be false": { token: "test-token", @@ -53,6 +54,24 @@ func TestIsHost(t *testing.T) { host: "ghcr.iofoo", expIs: false, }, + + // Support for GHE Cloud + "containers.yourdomain.ghe.com should be true": { + token: "test-token", + host: "containers.yourdomain.ghe.com", + expIs: true, + }, + "containers.jetstack.ghe.com should be true": { + token: "test-token", + host: "containers.jetstack.ghe.com", + expIs: true, + }, + "customhostname.ghe.internal should be true": { + token: "test-token", + host: "customhostname.ghe.internal", + customhost: strPtr("customhostname.ghe.internal"), + expIs: true, + }, } handler := new(Client) @@ -61,6 +80,9 @@ func TestIsHost(t *testing.T) { if test.token != "" { handler.opts.Token = test.token } + if test.customhost != nil { + handler.opts.Hostname = *test.customhost + } if isHost := handler.IsHost(test.host); isHost != test.expIs { t.Errorf("%s: unexpected IsHost, exp=%t got=%t", test.host, test.expIs, isHost) @@ -69,6 +91,10 @@ func TestIsHost(t *testing.T) { } } +func strPtr(str string) *string { + return &str +} + func TestRepoImage(t *testing.T) { tests := map[string]struct { path string diff --git a/pkg/client/selfhosted/selfhosted.go b/pkg/client/selfhosted/selfhosted.go index b8361919..0d0affa7 100644 --- a/pkg/client/selfhosted/selfhosted.go +++ b/pkg/client/selfhosted/selfhosted.go @@ -252,7 +252,7 @@ func (c *Client) doRequest(ctx context.Context, url, header string, obj interfac resp, err := c.Do(req) if err != nil { - return nil, fmt.Errorf("failed to get docker image: %s", err) + return nil, fmt.Errorf("failed to get %q image: %s", c.Name(), err) } defer resp.Body.Close() From 22abfa2324823fe8efcc06034a1a9eb3cc88aa86 Mon Sep 17 00:00:00 2001 From: David Collom Date: Fri, 21 Mar 2025 12:46:25 +0000 Subject: [PATCH 2/4] Run Unittests on all pr's --- .github/workflows/build-test.yaml | 114 +++++++++++++++--------------- 1 file changed, 56 insertions(+), 58 deletions(-) diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 1ab91461..1182877e 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -2,9 +2,7 @@ name: Test & Build on: pull_request: branches: - - 'main' - paths: - - "!README.md" + - "main" concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -13,9 +11,9 @@ concurrency: jobs: lint: permissions: - contents: read # for actions/checkout to fetch code - pull-requests: read # for golangci/golangci-lint-action to fetch pull requests - checks: write # for golangci/golangci-lint-action to annotate Pull Requests + contents: read # for actions/checkout to fetch code + pull-requests: read # for golangci/golangci-lint-action to fetch pull requests + checks: write # for golangci/golangci-lint-action to annotate Pull Requests name: Lint Go code runs-on: ubuntu-latest steps: @@ -26,7 +24,7 @@ jobs: with: go-version-file: go.mod - name: Run golangci-lint - uses: golangci/golangci-lint-action@2226d7cb06a077cd73e56eedd38eecad18e5d837 # v6.5.0 + uses: golangci/golangci-lint-action@2226d7cb06a077cd73e56eedd38eecad18e5d837 # v6.5.0 with: version: v1.54 args: --timeout 10m --exclude SA5011 --verbose --issues-exit-code=0 @@ -39,40 +37,40 @@ jobs: - id: govulncheck uses: golang/govulncheck-action@v1 with: - go-version-file: go.mod - go-package: ./... + go-version-file: go.mod + go-package: ./... test: name: Run unit tests for Go packages runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v4 # v3.5.3 - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version-file: go.mod + - name: Checkout code + uses: actions/checkout@v4 # v3.5.3 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod - - name: Download and required packages - run: | - make deps + - name: Download and required packages + run: | + make deps - - name: Run all unit tests - run: make test + - name: Run all unit tests + run: make test - - name: check test coverage - uses: vladopajic/go-test-coverage@v2 - with: - config: ./.testcoverage.yml + - name: check test coverage + uses: vladopajic/go-test-coverage@v2 + with: + config: ./.testcoverage.yml - - name: Trigger Coverage update - uses: ./coverage-badge.yaml + - name: Trigger Coverage update + uses: ./coverage-badge.yaml - - name: Generate code coverage artifacts - uses: actions/upload-artifact@v4 - with: - name: code-coverage - path: coverage.out + - name: Generate code coverage artifacts + uses: actions/upload-artifact@v4 + with: + name: code-coverage + path: coverage.out build: needs: @@ -87,33 +85,33 @@ jobs: - linux/arm64 name: Build Images steps: - - name: Checkout code - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + - name: Checkout code + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - with: - platforms: ${{ matrix.platform }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + platforms: ${{ matrix.platform }} - - name: Build Images - uses: docker/build-push-action@v6 - with: - context: . - platforms: ${{ matrix.platform }} - load: true - push: false - tags: quay.io/jetstack/version-checker:${{github.sha}} - cache-from: type=gha - cache-to: type=gha,mode=max + - name: Build Images + uses: docker/build-push-action@v6 + with: + context: . + platforms: ${{ matrix.platform }} + load: true + push: false + tags: quay.io/jetstack/version-checker:${{github.sha}} + cache-from: type=gha + cache-to: type=gha,mode=max - - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@0.29.0 - with: - image-ref: 'quay.io/jetstack/version-checker:${{github.sha}}' - format: 'table' - exit-code: '1' - ignore-unfixed: true - vuln-type: 'os,library' - severity: 'CRITICAL,HIGH' + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.29.0 + with: + image-ref: "quay.io/jetstack/version-checker:${{github.sha}}" + format: "table" + exit-code: "1" + ignore-unfixed: true + vuln-type: "os,library" + severity: "CRITICAL,HIGH" From e00494cb6e32c240ce94aa6d5a8988cb3c29a5d2 Mon Sep 17 00:00:00 2001 From: David Collom Date: Fri, 21 Mar 2025 13:14:12 +0000 Subject: [PATCH 3/4] Fix unittests coverage badge --- .github/workflows/build-test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 1182877e..8be95f97 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -64,7 +64,7 @@ jobs: config: ./.testcoverage.yml - name: Trigger Coverage update - uses: ./coverage-badge.yaml + uses: ./.github/workflows/coverage-badge.yaml - name: Generate code coverage artifacts uses: actions/upload-artifact@v4 From fb3cedd0d7c942c779fa173df3c816b21bf6a627 Mon Sep 17 00:00:00 2001 From: David Collom Date: Fri, 21 Mar 2025 13:18:22 +0000 Subject: [PATCH 4/4] Fix gh actions --- .github/workflows/build-test.yaml | 1 + .github/workflows/release.yaml | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 8be95f97..aefb257b 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -65,6 +65,7 @@ jobs: - name: Trigger Coverage update uses: ./.github/workflows/coverage-badge.yaml + continue-on-error: true - name: Generate code coverage artifacts uses: actions/upload-artifact@v4 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 6ab81b39..7816b123 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -12,7 +12,7 @@ concurrency: cancel-in-progress: true jobs: - prepair-release: + prepare-release: # Don't push back to a tag! if: ${{ !startsWith(github.ref, 'refs/tags/') }} name: Prepair release @@ -108,7 +108,6 @@ jobs: get_diff: false allow_no_diff: false - helm-release: runs-on: ubuntu-latest steps: @@ -158,7 +157,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: - platforms: ${{ matrix.platform }} + platforms: linux/amd64,linux/arm64 - name: Login to Docker Hub uses: docker/login-action@v3