-
Notifications
You must be signed in to change notification settings - Fork 283
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
385 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package docker | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"net/http" | ||
"regexp" | ||
) | ||
|
||
var ErrNoMorePages = errors.New("no more pages") | ||
|
||
// Matches an RFC 5988 (https://tools.ietf.org/html/rfc5988#section-5) | ||
// Link header. For example, | ||
// | ||
// <http://registry.example.com/v2/_catalog?n=5&last=tag5>; type="application/json"; rel="next" | ||
// | ||
// The URL is _supposed_ to be wrapped by angle brackets `< ... >`, | ||
// but e.g., quay.io does not include them. Similarly, params like | ||
// `rel="next"` may not have quoted values in the wild. | ||
var nextLinkRE = regexp.MustCompile(`^ *<?([^;>]+)>? *(?:;[^;]*)*; *rel="?next"?(?:;.*)?`) | ||
|
||
func getNextLink(resp *http.Response) (string, error) { | ||
for _, link := range resp.Header[http.CanonicalHeaderKey("Link")] { | ||
parts := nextLinkRE.FindStringSubmatch(link) | ||
if parts != nil { | ||
return parts[1], nil | ||
} | ||
} | ||
return "", ErrNoMorePages | ||
} | ||
|
||
// getPaginatedJSON accepts a string and a pointer, and returns the | ||
// next page URL while updating pointed-to variable with a parsed JSON | ||
// value. When there are no more pages it returns `ErrNoMorePages`. | ||
func (registry *Registry) getPaginatedJSON(url string, response interface{}) (string, error) { | ||
resp, err := registry.Client.Get(registry.url(url)) | ||
if err != nil { | ||
return "", err | ||
} | ||
defer resp.Body.Close() | ||
|
||
decoder := json.NewDecoder(resp.Body) | ||
err = decoder.Decode(response) | ||
if err != nil { | ||
return "", err | ||
} | ||
return getNextLink(resp) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package docker | ||
|
||
import ( | ||
"io/ioutil" | ||
"net/http" | ||
"strings" | ||
|
||
manifestv2 "github.com/docker/distribution/manifest/schema2" | ||
"github.com/opencontainers/go-digest" | ||
oci "github.com/opencontainers/image-spec/specs-go/v1" | ||
) | ||
|
||
// ManifestDigest - get manifest digest | ||
func (registry *Registry) ManifestDigest(repository, reference string) (digest.Digest, error) { | ||
url := registry.url("/v2/%s/manifests/%s", repository, reference) | ||
registry.Logf("registry.manifest.head url=%s repository=%s reference=%s", url, repository, reference) | ||
|
||
req, err := http.NewRequest("GET", url, nil) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
req.Header.Set("Accept", manifestv2.MediaTypeManifest) | ||
resp, err := registry.Client.Do(req) | ||
if err != nil { | ||
// Try OCI headers if error relates to OCI | ||
if strings.Contains(err.Error(), "OCI index found, but accept header does not support OCI indexes") { | ||
req.Header.Set("Accept", oci.MediaTypeImageIndex) | ||
resp, err = registry.Client.Do(req) | ||
} | ||
if err != nil { | ||
return "", err | ||
} | ||
} | ||
defer resp.Body.Close() | ||
|
||
if hdr := resp.Header.Get("Docker-Content-Digest"); hdr != "" { | ||
return digest.Parse(hdr) | ||
} | ||
|
||
// Try to get digest from body instead, should be equal to what would be presented | ||
// in Docker-Content-Digest | ||
body, err := ioutil.ReadAll(resp.Body) | ||
if err != nil { | ||
return "", err | ||
} | ||
return digest.FromBytes(body), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package docker | ||
|
||
import ( | ||
"io" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
manifestV2 "github.com/docker/distribution/manifest/schema2" | ||
) | ||
|
||
func TestGetDigest(t *testing.T) { | ||
|
||
req, err := http.NewRequest("GET", "https://registry.opensource.zalan.do/v2/teapot/external-dns/manifests/v0.4.8", nil) | ||
if err != nil { | ||
t.Fatalf("failed to create request: %s", err) | ||
} | ||
req.Header.Set("Accept", manifestV2.MediaTypeManifest) | ||
|
||
client := &http.Client{} | ||
resp, err := client.Do(req) | ||
if err != nil { | ||
t.Fatalf("failed to request: %s", err) | ||
} | ||
defer resp.Body.Close() | ||
|
||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
w.Header().Add("content-type", "application/vnd.docker.distribution.manifest.v2+json; charset=ISO-8859-1") | ||
io.Copy(w, resp.Body) | ||
})) | ||
defer ts.Close() | ||
|
||
reg := New(ts.URL, "", "") | ||
|
||
digest, err := reg.ManifestDigest(ts.URL, "notimportant") | ||
if err != nil { | ||
t.Errorf("failed to get digest") | ||
} | ||
|
||
if digest.String() != "sha256:7aa5175f39a7e8a4172972524302c9a8196f681e40d6ee5d2f6bf0ab7d600fee" { | ||
t.Errorf("unexpected digest: %s", digest.String()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
package docker | ||
|
||
import ( | ||
"crypto/tls" | ||
"fmt" | ||
"log" | ||
"net" | ||
"net/http" | ||
"strings" | ||
"time" | ||
|
||
drc "github.com/rusenask/docker-registry-client/registry" | ||
) | ||
|
||
type LogfCallback func(format string, args ...interface{}) | ||
|
||
type Registry struct { | ||
URL string | ||
Client *http.Client | ||
Logf LogfCallback | ||
} | ||
|
||
/* | ||
* Pass log messages along to Go's "log" module. | ||
*/ | ||
func Log(format string, args ...interface{}) { | ||
log.Printf(format, args...) | ||
} | ||
|
||
/* | ||
* Create a new Registry with the given URL and credentials, then Ping()s it | ||
* before returning it to verify that the registry is available. | ||
* | ||
* You can, alternately, construct a Registry manually by populating the fields. | ||
* This passes http.DefaultTransport to WrapTransport when creating the | ||
* http.Client. | ||
*/ | ||
func New(registryURL, username, password string) *Registry { | ||
transport := &http.Transport{ | ||
Proxy: http.ProxyFromEnvironment, | ||
DialContext: (&net.Dialer{ | ||
Timeout: 10 * time.Second, | ||
KeepAlive: 30 * time.Second, | ||
DualStack: true, | ||
}).DialContext, | ||
MaxIdleConns: 10, | ||
IdleConnTimeout: 90 * time.Second, | ||
TLSHandshakeTimeout: 10 * time.Second, | ||
ExpectContinueTimeout: 1 * time.Second, | ||
} | ||
|
||
return newFromTransport(registryURL, username, password, transport, Log) | ||
} | ||
|
||
/* | ||
* Create a new Registry, as with New, using an http.Transport that disables | ||
* SSL certificate verification. | ||
*/ | ||
func NewInsecure(registryURL, username, password string) *Registry { | ||
transport := &http.Transport{ | ||
Proxy: http.ProxyFromEnvironment, | ||
DialContext: (&net.Dialer{ | ||
Timeout: 10 * time.Second, | ||
KeepAlive: 30 * time.Second, | ||
DualStack: true, | ||
}).DialContext, | ||
TLSClientConfig: &tls.Config{ | ||
InsecureSkipVerify: true, | ||
}, | ||
MaxIdleConns: 10, | ||
IdleConnTimeout: 90 * time.Second, | ||
TLSHandshakeTimeout: 10 * time.Second, | ||
ExpectContinueTimeout: 1 * time.Second, | ||
} | ||
|
||
return newFromTransport(registryURL, username, password, transport, Log) | ||
} | ||
|
||
func newFromTransport(registryURL, username, password string, transport *http.Transport, logf LogfCallback) *Registry { | ||
url := strings.TrimSuffix(registryURL, "/") | ||
registry := &Registry{ | ||
URL: url, | ||
Client: &http.Client{ | ||
Transport: drc.WrapTransport(transport, url, username, password), | ||
}, | ||
Logf: logf, | ||
} | ||
|
||
return registry | ||
} | ||
|
||
func (r *Registry) Ping() error { | ||
url := r.url("/v2/") | ||
r.Logf("registry.ping url=%s", url) | ||
resp, err := r.Client.Get(url) | ||
if resp != nil { | ||
defer resp.Body.Close() | ||
} | ||
return err | ||
} | ||
|
||
type tagsResponse struct { | ||
Tags []string `json:"tags"` | ||
} | ||
|
||
func (r *Registry) url(pathTemplate string, args ...interface{}) string { | ||
pathSuffix := fmt.Sprintf(pathTemplate, args...) | ||
if strings.HasPrefix(pathSuffix, r.URL) { | ||
return pathSuffix | ||
} | ||
|
||
url := fmt.Sprintf("%s%s", r.URL, pathSuffix) | ||
|
||
return url | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package docker | ||
|
||
func (registry *Registry) Tags(repository string) (tags []string, err error) { | ||
url := registry.url("/v2/%s/tags/list", repository) | ||
|
||
var response tagsResponse | ||
for { | ||
registry.Logf("registry.tags url=%s repository=%s", url, repository) | ||
url, err = registry.getPaginatedJSON(url, &response) | ||
switch err { | ||
case ErrNoMorePages: | ||
tags = append(tags, response.Tags...) | ||
return tags, nil | ||
case nil: | ||
tags = append(tags, response.Tags...) | ||
continue | ||
default: | ||
return nil, err | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package docker | ||
|
||
import ( | ||
"testing" | ||
) | ||
|
||
func TestGetDigestDockerHub(t *testing.T) { | ||
client := New("https://index.docker.io", "", "") | ||
|
||
tags, err := client.Tags("karolisr/keel") | ||
if err != nil { | ||
t.Errorf("failed to get tags, error: %s", err) | ||
} | ||
|
||
if len(tags) == 0 { | ||
t.Errorf("no tags?") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.