From a56d36fdaaf2bb566bc68e611228566e300094c7 Mon Sep 17 00:00:00 2001
From: Misty Stanley-Jones <misty@docker.com>
Date: Wed, 28 Sep 2016 14:33:25 -0700
Subject: [PATCH] Initial commit -f https://github.com/docker/orca

---
 docs/mock/registry.go |  42 +++++++
 docs/readme.md        |  18 +++
 docs/v1/error.go      |  15 +++
 docs/v1/registry.go   | 277 ++++++++++++++++++++++++++++++++++++++++++
 docs/v1/repository.go |  47 +++++++
 docs/v1/search.go     |  10 ++
 docs/v2/registry.go   | 149 +++++++++++++++++++++++
 7 files changed, 558 insertions(+)
 create mode 100644 docs/mock/registry.go
 create mode 100644 docs/readme.md
 create mode 100644 docs/v1/error.go
 create mode 100644 docs/v1/registry.go
 create mode 100644 docs/v1/repository.go
 create mode 100644 docs/v1/search.go
 create mode 100644 docs/v2/registry.go

diff --git a/docs/mock/registry.go b/docs/mock/registry.go
new file mode 100644
index 000000000..aad45e397
--- /dev/null
+++ b/docs/mock/registry.go
@@ -0,0 +1,42 @@
+package mock
+
+import (
+	"github.com/docker/orca"
+	"net/http"
+	"net/url"
+)
+
+type (
+	MockRegistry struct {
+		orca.RegistryConfig
+		client *orca.RegistryClient
+	}
+)
+
+func NewRegistry(reg *orca.RegistryConfig) (orca.Registry, error) {
+	u, err := url.Parse(reg.URL)
+	if err != nil {
+		return nil, err
+	}
+
+	rClient := &orca.RegistryClient{
+		URL: u,
+	}
+
+	return &MockRegistry{
+		RegistryConfig: *reg,
+		client:         rClient,
+	}, nil
+}
+
+func (r *MockRegistry) GetAuthToken(username, accessType, hostname, reponame string) (string, error) {
+	return "foo", nil
+}
+
+func (r *MockRegistry) GetConfig() *orca.RegistryConfig {
+	return &r.RegistryConfig
+}
+
+func (r *MockRegistry) GetTransport() http.RoundTripper {
+	return r.client.HttpClient.Transport
+}
diff --git a/docs/readme.md b/docs/readme.md
new file mode 100644
index 000000000..668ebf786
--- /dev/null
+++ b/docs/readme.md
@@ -0,0 +1,18 @@
+# Docker Registry Go lib
+This is a simple Go package to use with the Docker Registry v1.
+
+# Example
+
+```
+import registry "github.com/ehazlett/orca/registry/v1"
+
+// make sure to handle the err
+client, _ := registry.NewRegistryClient("http://localhost:5000", nil)
+
+res, _ := client.Search("busybox", 1, 100)
+
+fmt.Printf("Number of Repositories: %d\n", res.NumberOfResults)
+for _, r := range res.Results {
+	fmt.Printf(" -  Name: %s\n", r.Name)
+}
+```
diff --git a/docs/v1/error.go b/docs/v1/error.go
new file mode 100644
index 000000000..769671a8b
--- /dev/null
+++ b/docs/v1/error.go
@@ -0,0 +1,15 @@
+package v1
+
+import (
+	"fmt"
+)
+
+type Error struct {
+	StatusCode int
+	Status     string
+	msg        string
+}
+
+func (e Error) Error() string {
+	return fmt.Sprintf("%s: %s", e.Status, e.msg)
+}
diff --git a/docs/v1/registry.go b/docs/v1/registry.go
new file mode 100644
index 000000000..103faea67
--- /dev/null
+++ b/docs/v1/registry.go
@@ -0,0 +1,277 @@
+package v1
+
+import (
+	"bytes"
+	"crypto/tls"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"net"
+	"net/http"
+	"net/url"
+	"path"
+	"strings"
+	"time"
+
+	log "github.com/Sirupsen/logrus"
+)
+
+var (
+	ErrNotFound        = errors.New("Not found")
+	defaultHTTPTimeout = 30 * time.Second
+)
+
+type RegistryClient struct {
+	URL        *url.URL
+	tlsConfig  *tls.Config
+	httpClient *http.Client
+}
+
+type Repo struct {
+	Namespace  string
+	Repository string
+}
+
+func parseRepo(repo string) Repo {
+	namespace := "library"
+	r := repo
+
+	if strings.Index(repo, "/") != -1 {
+		parts := strings.Split(repo, "/")
+		namespace = parts[0]
+		r = path.Join(parts[1:]...)
+	}
+
+	return Repo{
+		Namespace:  namespace,
+		Repository: r,
+	}
+}
+
+func newHTTPClient(u *url.URL, tlsConfig *tls.Config, timeout time.Duration) *http.Client {
+	httpTransport := &http.Transport{
+		TLSClientConfig: tlsConfig,
+	}
+
+	httpTransport.Dial = func(proto, addr string) (net.Conn, error) {
+		return net.DialTimeout(proto, addr, timeout)
+	}
+	return &http.Client{Transport: httpTransport}
+}
+
+func NewRegistryClient(registryUrl string, tlsConfig *tls.Config) (*RegistryClient, error) {
+	u, err := url.Parse(registryUrl)
+	if err != nil {
+		return nil, err
+	}
+	httpClient := newHTTPClient(u, tlsConfig, defaultHTTPTimeout)
+	return &RegistryClient{
+		URL:        u,
+		httpClient: httpClient,
+		tlsConfig:  tlsConfig,
+	}, nil
+}
+
+func (client *RegistryClient) doRequest(method string, path string, body []byte, headers map[string]string) ([]byte, error) {
+	b := bytes.NewBuffer(body)
+
+	req, err := http.NewRequest(method, client.URL.String()+"/v1"+path, b)
+	if err != nil {
+		return nil, err
+	}
+
+	req.Header.Add("Content-Type", "application/json")
+	if headers != nil {
+		for header, value := range headers {
+			req.Header.Add(header, value)
+		}
+	}
+
+	resp, err := client.httpClient.Do(req)
+	if err != nil {
+		if !strings.Contains(err.Error(), "connection refused") && client.tlsConfig == nil {
+			return nil, fmt.Errorf("%v. Are you trying to connect to a TLS-enabled endpoint without TLS?", err)
+		}
+		return nil, err
+	}
+
+	defer resp.Body.Close()
+
+	data, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	if resp.StatusCode == 404 {
+		return nil, ErrNotFound
+	}
+
+	if resp.StatusCode >= 400 {
+		return nil, Error{StatusCode: resp.StatusCode, Status: resp.Status, msg: string(data)}
+	}
+
+	return data, nil
+}
+
+func (client *RegistryClient) Search(query string, page int, numResults int) (*SearchResult, error) {
+	if numResults < 1 {
+		numResults = 100
+	}
+	uri := fmt.Sprintf("/search?q=%s&n=%d&page=%d", query, numResults, page)
+	data, err := client.doRequest("GET", uri, nil, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	res := &SearchResult{}
+	if err := json.Unmarshal(data, &res); err != nil {
+		return nil, err
+	}
+
+	return res, nil
+}
+
+func (client *RegistryClient) DeleteRepository(repo string) error {
+	r := parseRepo(repo)
+	uri := fmt.Sprintf("/repositories/%s/%s/", r.Namespace, r.Repository)
+	if _, err := client.doRequest("DELETE", uri, nil, nil); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (client *RegistryClient) DeleteTag(repo string, tag string) error {
+	r := parseRepo(repo)
+	uri := fmt.Sprintf("/repositories/%s/%s/tags/%s", r.Namespace, r.Repository, tag)
+	if _, err := client.doRequest("DELETE", uri, nil, nil); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (client *RegistryClient) Layer(id string) (*Layer, error) {
+	uri := fmt.Sprintf("/images/%s/json", id)
+	data, err := client.doRequest("GET", uri, nil, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	layer := &Layer{}
+	if err := json.Unmarshal(data, &layer); err != nil {
+		return nil, err
+	}
+
+	return layer, nil
+}
+
+func (client *RegistryClient) loadLayer(name, id string) ([]Layer, []Tag, int64, error) {
+	uri := fmt.Sprintf("/images/%s/json", id)
+	layer := Layer{}
+	layers := []Layer{}
+	tags := []Tag{}
+	size := int64(0)
+
+	data, err := client.doRequest("GET", uri, nil, nil)
+	if err != nil {
+		return nil, nil, -1, err
+	}
+
+	if err := json.Unmarshal(data, &layer); err != nil {
+		return nil, nil, -1, err
+	}
+
+	uri = fmt.Sprintf("/images/%s/ancestry", id)
+
+	ancestry := []string{}
+
+	data, err = client.doRequest("GET", uri, nil, nil)
+	if err != nil {
+		return nil, nil, -1, err
+	}
+
+	if err = json.Unmarshal(data, &ancestry); err != nil {
+		return nil, nil, -1, err
+	}
+
+	tag := Tag{
+		ID:   id,
+		Name: name,
+	}
+
+	tags = append(tags, tag)
+	layer.Ancestry = ancestry
+
+	layers = append(layers, layer)
+	// parse ancestor layers
+	for _, i := range ancestry {
+		uri = fmt.Sprintf("/images/%s/json", i)
+		l := &Layer{}
+
+		data, err = client.doRequest("GET", uri, nil, nil)
+		if err != nil {
+			return nil, nil, -1, err
+		}
+
+		if err = json.Unmarshal(data, &l); err != nil {
+			return nil, nil, -1, err
+		}
+		size += l.Size
+		layers = append(layers, *l)
+	}
+
+	return layers, tags, size, nil
+}
+
+func (client *RegistryClient) Repository(name string) (*Repository, error) {
+	r := parseRepo(name)
+	uri := fmt.Sprintf("/repositories/%s/%s/tags", r.Namespace, r.Repository)
+
+	repository := &Repository{
+		Name:       path.Join(r.Namespace, r.Repository),
+		Namespace:  r.Namespace,
+		Repository: r.Repository,
+	}
+
+	// HACK: check for hub url and return
+	// used in orca catalog
+	baseURL := client.URL.String()
+	if strings.Contains(baseURL, "index.docker.io") {
+		return repository, nil
+	}
+
+	var repoTags map[string]string
+
+	data, err := client.doRequest("GET", uri, nil, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := json.Unmarshal(data, &repoTags); err != nil {
+		return nil, err
+	}
+
+	layers := []Layer{}
+	tags := []Tag{}
+	size := int64(0)
+
+	for n, id := range repoTags {
+		l, t, s, err := client.loadLayer(n, id)
+		if err != nil {
+			log.Warnf("error loading layer: id=%s", id)
+			continue
+		}
+
+		layers = append(layers, l...)
+		tags = append(tags, t...)
+		size += s
+	}
+
+	repository.Tags = tags
+	repository.Layers = layers
+	repository.Size = int64(size) / int64(len(tags))
+
+	return repository, nil
+}
diff --git a/docs/v1/repository.go b/docs/v1/repository.go
new file mode 100644
index 000000000..6f6ca4316
--- /dev/null
+++ b/docs/v1/repository.go
@@ -0,0 +1,47 @@
+package v1
+
+import (
+	"time"
+
+	"github.com/docker/engine-api/types"
+)
+
+type (
+	Tag struct {
+		ID   string
+		Name string
+	}
+
+	ContainerConfig struct {
+		types.ContainerJSON
+		Cmd []string `json:"Cmd,omitempty"`
+	}
+
+	Layer struct {
+		ID              string           `json:"id,omitempty"`
+		Parent          string           `json:"parent,omitempty"`
+		Created         *time.Time       `json:"created,omitempty"`
+		Container       string           `json:"container,omitempty"`
+		ContainerConfig *ContainerConfig `json:"container_config,omitempty"`
+		DockerVersion   string           `json:"docker_version,omitempty"`
+		Author          string           `json:"author,omitempty"`
+		Architecture    string           `json:"architecture,omitempty"`
+		OS              string           `json:"os,omitempty"`
+		Size            int64            `json:"size,omitempty"`
+		Ancestry        []string         `json:"ancestry,omitempty"`
+	}
+
+	Repository struct {
+		Description string  `json:"description,omitempty"`
+		Name        string  `json:"name,omitempty"`
+		Namespace   string  `json:"namespace,omitempty"`
+		Repository  string  `json:"repository,omitempty"`
+		Tags        []Tag   `json:"tags,omitempty"`
+		Layers      []Layer `json:"layers,omitempty"`
+		Size        int64   `json:"size,omitempty"`
+		// these are only for the official index
+		Trusted   bool `json:"is_trusted,omitempty"`
+		Official  bool `json:"is_official,omitempty"`
+		StarCount int  `json:"star_count,omitempty"`
+	}
+)
diff --git a/docs/v1/search.go b/docs/v1/search.go
new file mode 100644
index 000000000..084f8f98e
--- /dev/null
+++ b/docs/v1/search.go
@@ -0,0 +1,10 @@
+package v1
+
+type (
+	SearchResult struct {
+		NumberOfResults int           `json:"num_results,omitempty"`
+		NumberOfPages   int           `json:"num_pages,omitempty"`
+		Query           string        `json:"query,omitempty"`
+		Results         []*Repository `json:"results,omitempty"`
+	}
+)
diff --git a/docs/v2/registry.go b/docs/v2/registry.go
new file mode 100644
index 000000000..2188f019e
--- /dev/null
+++ b/docs/v2/registry.go
@@ -0,0 +1,149 @@
+package v2
+
+import (
+	"bytes"
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"time"
+
+	log "github.com/Sirupsen/logrus"
+	"github.com/docker/orca"
+	"github.com/docker/orca/auth"
+)
+
+var (
+	ErrNotFound        = errors.New("Not found")
+	defaultHTTPTimeout = 30 * time.Second
+)
+
+type (
+	AuthToken struct {
+		Token string `json:"token"`
+	}
+
+	V2Registry struct {
+		orca.RegistryConfig
+		client *orca.RegistryClient
+	}
+)
+
+func NewRegistry(reg *orca.RegistryConfig, swarmTLSConfig *tls.Config) (orca.Registry, error) {
+	// sanity check the registry settings
+	u, err := url.Parse(reg.URL)
+	if err != nil {
+		return nil, fmt.Errorf("The provided Docker Trusted Registry URL was malformed and could not be parsed")
+	}
+
+	// Create a new TLS config for the registry, based on swarm's
+	// This will allow us not to mess with the Swarm RootCAs
+	tlsConfig := *swarmTLSConfig
+	tlsConfig.InsecureSkipVerify = reg.Insecure
+	if reg.CACert != "" {
+		// If the user specified a CA, create a new RootCA pool containing only that CA cert.
+		log.Debugf("cert: %s", reg.CACert)
+		certPool := x509.NewCertPool()
+		certPool.AppendCertsFromPEM([]byte(reg.CACert))
+		tlsConfig.RootCAs = certPool
+		log.Debug("Connecting to Registry with user-provided CA")
+	} else {
+		// If the user did not specify a CA, fall back to the system's Root CAs
+		tlsConfig.RootCAs = nil
+		log.Debug("Connecting to Registry with system Root CAs")
+	}
+
+	httpClient := &http.Client{
+		Transport: &http.Transport{TLSClientConfig: &tlsConfig},
+		Timeout:   defaultHTTPTimeout,
+	}
+
+	rClient := &orca.RegistryClient{
+		URL:        u,
+		HttpClient: httpClient,
+	}
+
+	return &V2Registry{
+		RegistryConfig: *reg,
+		client:         rClient,
+	}, nil
+}
+
+func (r *V2Registry) doRequest(method string, path string, body []byte, headers map[string]string, username string) ([]byte, error) {
+	b := bytes.NewBuffer(body)
+
+	req, err := http.NewRequest(method, path, b)
+	if err != nil {
+		log.Errorf("couldn't create request: %s", err)
+		return nil, err
+	}
+
+	// The DTR Auth server will validate the UCP client cert and will grant access to whatever
+	// username is passed to it.
+	// However, DTR 1.4.3 rejects empty password strings under LDAP, in order to disallow anonymous users.
+	req.SetBasicAuth(username, "really?")
+
+	if headers != nil {
+		for header, value := range headers {
+			req.Header.Add(header, value)
+		}
+	}
+
+	resp, err := r.client.HttpClient.Do(req)
+	if err != nil {
+		if err == http.ErrHandlerTimeout {
+			log.Error("Login timed out to Docker Trusted Registry")
+			return nil, err
+		}
+		log.Errorf("There was an error while authenticating: %s", err)
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode == 401 {
+		// Unauthorized
+		log.Warnf("Unauthorized")
+		return nil, auth.ErrUnauthorized
+	} else if resp.StatusCode >= 400 {
+		log.Errorf("Docker Trusted Registry returned an unexpected status code while authenticating: %s", resp.Status)
+		return nil, auth.ErrUnknown
+	}
+
+	rBody, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		log.Errorf("couldn't read body: %s", err)
+		return nil, err
+	}
+
+	return rBody, nil
+}
+
+func (r *V2Registry) GetAuthToken(username, accessType, hostname, reponame string) (string, error) {
+	uri := fmt.Sprintf("%s/auth/token?scope=repository:%s:%s&service=%s", r.RegistryConfig.URL, reponame, accessType, hostname)
+
+	log.Debugf("contacting DTR for auth token: %s", uri)
+
+	data, err := r.doRequest("GET", uri, nil, nil, username)
+	if err != nil {
+		return "", err
+	}
+
+	var token AuthToken
+	if err := json.Unmarshal(data, &token); err != nil {
+		return "", err
+	}
+
+	return token.Token, nil
+}
+
+func (r *V2Registry) GetConfig() *orca.RegistryConfig {
+	return &r.RegistryConfig
+}
+
+func (r *V2Registry) GetTransport() http.RoundTripper {
+	return r.client.HttpClient.Transport
+}