blob: 374177adcea5e1909f1c4de12e92532b70ea9722 [file] [log] [blame]
// Client for interacting with the Google Container Registry.
//
// The Docker v2 API is protected by OAuth2, but it doesn't look
// exactly like Google OAuth2, so we have to create our own token
// source.
//
// Go implementation of the bash commands from:
//
// https://stackoverflow.com/questions/34037256/does-google-container-registry-support-docker-remote-api-v2/34046435#34046435
//
// I.e.:
// $ export NAME=project-id/image
// $ export BEARER=$(curl -u _token:$(gcloud auth print-access-token) https://gcr.io/v2/token?scope=repository:$NAME:pull | cut -d'"' -f 10)
// $ curl -H "Authorization: Bearer $BEARER" https://gcr.io/v2/$NAME/tags/list
package gcr
import (
"encoding/json"
"fmt"
"net/http"
"time"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/util"
"golang.org/x/oauth2"
)
const (
SERVER = "gcr.io"
)
// gcrTokenSource it an oauth2.TokenSource that works with the Google Container Registry API.
type gcrTokenSource struct {
// client is an authorized client that has access to the GCS bucket where gcr stores docker images.
client *http.Client
// projectId - The Google Cloud project name, e.g. 'skia-public'.
projectId string
// imageName - The name of the image.
imageName string
}
func (g *gcrTokenSource) Token() (*oauth2.Token, error) {
// Use the authorized client to get a gcr.io specific oauth token.
resp, err := g.client.Get(fmt.Sprintf("https://%s/v2/token?scope=repository:%s/%s:pull", SERVER, g.projectId, g.imageName))
if err != nil {
return nil, err
}
defer util.Close(resp.Body)
if resp.StatusCode != 200 {
return nil, fmt.Errorf("Got unexpected status: %s", resp.Status)
}
var res struct {
AccessToken string `json:"token"`
ExpiresInSec int `json:"expires_in"`
}
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
return nil, fmt.Errorf("Invalid token JSON from metadata: %v", err)
}
if res.ExpiresInSec == 0 || res.AccessToken == "" {
return nil, fmt.Errorf("Incomplete token received from metadata: %#v", res)
}
return &oauth2.Token{
AccessToken: res.AccessToken,
TokenType: "Bearer",
Expiry: time.Now().Add(time.Duration(res.ExpiresInSec) * time.Second),
}, nil
}
// Client talks to the Google Cloud Registry that supports the v2 Docker API.
type Client struct {
client *http.Client
// projectId - The Google Cloud project name, e.g. 'skia-public'.
projectId string
// imageName - The name of the image.
imageName string
}
// NewClient creates a Client that retrieves information about the docker images store under 'projectID'/'image'.
//
// tokenSource - An oauth2.TokenSource that Has read access to the bucket that the docker images are stored in.
// projectId - The Google Cloud project name, e.g. 'skia-public'.
// imageName - The name of the image, e.g. docserver.
func NewClient(tokenSource oauth2.TokenSource, projectId, imageName string) *Client {
gcrTokenSource := &gcrTokenSource{
client: httputils.DefaultClientConfig().WithTokenSource(tokenSource).With2xxOnly().Client(),
projectId: projectId,
imageName: imageName,
}
return &Client{
client: httputils.DefaultClientConfig().WithTokenSource(gcrTokenSource).With2xxOnly().Client(),
projectId: projectId,
imageName: imageName,
}
}
// Tags returns all of the tags for all versions of the image.
func (c *Client) Tags() ([]string, error) {
// TODO(jcgregorio) Look for link rel=next header to do pagination. https://docs.docker.com/registry/spec/api/#listing-image-tags
resp, err := c.client.Get(fmt.Sprintf("https://%s/v2/%s/%s/tags/list", SERVER, c.projectId, c.imageName))
if err != nil {
return nil, fmt.Errorf("Failed to request tags: %s", err)
}
defer util.Close(resp.Body)
if resp.StatusCode != 200 {
return nil, fmt.Errorf("Got unexpected response: %s", resp.Status)
}
type Response struct {
Tags []string `json:"tags"`
}
var response Response
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("Could not decode response: %s", err)
}
return response.Tags, nil
}