Skip to content

Commit

Permalink
Add GitHub Container Registry provider
Browse files Browse the repository at this point in the history
  • Loading branch information
msom authored Jun 21, 2024
1 parent aff152c commit dd7b831
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 2 deletions.
4 changes: 4 additions & 0 deletions actions/test/run-tests
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,9 @@ echo "🌲 Testing GCR pull"
go run roots.go pull gcr.io/google-containers/etcd:3.3.10 /tmp/etcd
echo ""

echo "🌲 Testing GHCR pull"
go run roots.go pull ghcr.io/github/issue_metrics:latest /tmp/issue_metrics
echo ""

echo "🌲 Testing Cache Purge"
go run roots.go purge
4 changes: 2 additions & 2 deletions pkg/image/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,13 @@ func (r *Remote) Digest() (string, error) {
//
// we could be cleverer here by picking the platform or we could let
// the user know that he should pick one
if r.platform == nil && lst != nil {
if r.platform == nil && lst != nil && len(lst.Manifests) != 0 {
return lst.Manifests[0].Digest, nil
}

// if there's no list and no platform, fall back to whatever the server
// gives us through the docker-content-digest header
if r.platform == nil && lst == nil {
if r.platform == nil && (lst == nil || len(lst.Manifests) == 0) {
res, err := r.request("HEAD", ManifestMimeType, "manifests", r.url.Reference())

if err != nil {
Expand Down
86 changes: 86 additions & 0 deletions pkg/provider/github.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package provider

import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"sync"

"github.com/seantis/roots/pkg/image"
)

// GHProvider does not authenticate at the moment
type GHProvider struct {
clients map[string]*http.Client
mu sync.Mutex
}

func init() {
image.RegisterProvider("gh", &GHProvider{
clients: make(map[string]*http.Client),
})
}

var ghhosts = regexp.MustCompile(`ghcr\.io`)

// Supports returns true if the URLs host is one of the GitHub Container
// Registry hosts
func (p *GHProvider) Supports(url image.URL) bool {
return ghhosts.MatchString(url.Host)
}

// GetClient returns a client for the GitHub Container Registry. Currently
// there's no support for private repositories and 'auth' is ignored.
func (p *GHProvider) GetClient(url image.URL, auth string) (*http.Client, error) {

p.mu.Lock()
defer p.mu.Unlock()

// The client for Docker is bound to the repository
if p.clients[url.Repository] == nil {
client, err := p.newClient(url.Repository, url.Name, auth)

if err != nil {
return nil, err
}

p.clients[url.Repository] = client
}

return p.clients[url.Repository], nil
}

// newClient spawns a new unauthenticated http client for GitHub Container
// Repository
func (p *GHProvider) newClient(repository string, name string, auth string) (*http.Client, error) {
// even public api connections need an authorization token
t := "https://ghcr.io/token?scope=repository:%s/%s:pull"
u := fmt.Sprintf(t, repository, name)

res, err := http.Get(u)
if err != nil {
return nil, fmt.Errorf("error getting access-token via %s: %v", u, err)
}

if res.StatusCode != 200 {
return nil, fmt.Errorf("GET %s failed with %s", u, res.Status)
}

// we'll get it from the json response
tr := &dockerTokenResponse{}
err = json.NewDecoder(res.Body).Decode(&tr)

if err != nil {
return nil, fmt.Errorf("error parsing response: %e", err)
}

if len(tr.Token) == 0 {
return nil, fmt.Errorf("%s did not return a token", u)
}

// we then use it to create a client with a proper bearer token set
return clientWithHeaders(map[string]string{
"Authorization": fmt.Sprintf("Bearer %s", tr.Token),
}), err
}

0 comments on commit dd7b831

Please sign in to comment.