refactor how the domain for the images is used

Signed-off-by: Jess Frazelle <acidburn@microsoft.com>
This commit is contained in:
Jess Frazelle 2018-06-17 15:41:58 -04:00
parent 246604b921
commit f3a9b00ec8
No known key found for this signature in database
GPG key ID: 18F3685C0022BFF3
20 changed files with 337 additions and 149 deletions

View file

@ -64,7 +64,6 @@ GLOBAL OPTIONS:
--force-non-ssl, -f force allow use of non-ssl
--username value, -u value username for the registry
--password value, -p value password for the registry
--registry value, -r value URL to the private registry (ex. r.j3ss.co) (default: "https://registry-1.docker.io") [$REG_REGISTRY]
--timeout value timeout for HTTP requests (default: "1m")
--skip-ping skip pinging the registry while establishing connection
--help, -h show help
@ -87,7 +86,7 @@ not present, you can pass through flags directly.
```console
# this command might take a while if you have hundreds of images like I do
$ reg -r r.j3ss.co ls
$ reg ls r.j3ss.co
Repositories for r.j3ss.co
REPO TAGS
awscli latest
@ -100,7 +99,7 @@ chrome beta, latest, stable
**Tags**
```console
$ reg tags tor-browser
$ reg tags r.j3ss.co/tor-browser
alpha
hardened
latest
@ -110,7 +109,7 @@ stable
### Get a Manifest
```console
$ reg manifest htop
$ reg manifest r.j3ss.co/htop
{
"schemaVersion": 1,
"name": "htop",
@ -130,30 +129,30 @@ $ reg manifest htop
### Get the Digest
```console
$ reg digest htop
$ reg digest r.j3ss.co/htop
sha256:791158756cc0f5b27ef8c5c546284568fc9b7f4cf1429fb736aff3ee2d2e340f
```
### Download a Layer
```console
$ reg layer -o chrome@sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
$ reg layer -o r.j3ss.co/chrome@sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
OR
$ reg layer chrome@sha256:a3ed95caeb0.. > layer.tar
$ reg layer r.j3ss.co/chrome@sha256:a3ed95caeb0.. > layer.tar
```
### Delete an Image
```console
$ reg rm chrome@sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
$ reg rm r.j3ss.co/chrome@sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
Deleted chrome@sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
```
### Vulnerability Reports
```console
$ reg vulns --clair https://clair.j3ss.co chrome
$ reg vulns --clair https://clair.j3ss.co r.j3ss.co/chrome
Found 32 vulnerabilities
CVE-2015-5180: [Low]

View file

@ -3,7 +3,7 @@ package main
import (
"fmt"
"github.com/genuinetools/reg/repoutils"
"github.com/genuinetools/reg/registry"
"github.com/urfave/cli"
)
@ -16,15 +16,32 @@ var deleteCommand = cli.Command{
return fmt.Errorf("pass the name of the repository")
}
repo, ref, err := repoutils.GetRepoAndRef(c.Args()[0])
image, err := registry.ParseImage(c.Args().First())
if err != nil {
return err
}
if err := r.Delete(repo, ref); err != nil {
return fmt.Errorf("Delete %s@%s failed: %v", repo, ref, err)
// Create the registry client.
r, err := createRegistryClient(c, image.Domain)
if err != nil {
return err
}
fmt.Printf("Deleted %s@%s\n", repo, ref)
// Get the digest.
digest, err := r.Digest(image)
if err != nil {
return err
}
if err := image.WithDigest(digest); err != nil {
return err
}
// Delete the reference.
if err := r.Delete(image.Path, digest); err != nil {
return err
}
fmt.Printf("Deleted %s\n", image.String())
return nil
},

View file

@ -1,37 +1,27 @@
package main
import (
"fmt"
"strings"
"testing"
)
func TestDelete(t *testing.T) {
// Make sure we have busybox in list.
out, err := run("ls")
out, err := run("ls", domain)
if err != nil {
t.Fatalf("output: %s, error: %v", string(out), err)
t.Fatalf("output: %s, error: %v", out, err)
}
expected := []string{"alpine latest", "busybox glibc, musl, latest"}
for _, e := range expected {
if !strings.Contains(out, e) {
t.Logf("expected to contain: %s\ngot: %s", e, out)
}
expected := `REPO TAGS
alpine 3.5, latest
busybox glibc, latest, musl`
if !strings.HasSuffix(strings.TrimSpace(out), expected) {
t.Fatalf("expected to contain: %s\ngot: %s", expected, out)
}
// Remove busybox image.
if out, err := run("rm", "busybox"); err != nil {
t.Fatalf("output: %s, error: %v", string(out), err)
if out, err := run("rm", fmt.Sprintf("%s/busybox:glibc", domain)); err != nil {
t.Fatalf("output: %s, error: %v", out, err)
}
// Make sure there is no busybox in list.
out, err = run("ls")
if err != nil {
t.Fatalf("output: %s, error: %v", string(out), err)
}
expected = []string{"alpine latest", "busybox glibc, musl\n"}
for _, e := range expected {
if !strings.Contains(out, e) {
t.Logf("expected to contain: %s\ngot: %s", e, out)
}
}
}

View file

@ -3,7 +3,7 @@ package main
import (
"fmt"
"github.com/genuinetools/reg/repoutils"
"github.com/genuinetools/reg/registry"
"github.com/urfave/cli"
)
@ -15,17 +15,24 @@ var digestCommand = cli.Command{
return fmt.Errorf("pass the name of the repository")
}
repo, ref, err := repoutils.GetRepoAndRef(c.Args()[0])
image, err := registry.ParseImage(c.Args().First())
if err != nil {
return err
}
digest, err := r.Digest(repo, ref)
// Create the registry client.
r, err := createRegistryClient(c, image.Domain)
if err != nil {
return err
}
fmt.Println(digest)
// Get the digest.
digest, err := r.Digest(image)
if err != nil {
return err
}
fmt.Println(digest.String())
return nil
},

View file

@ -5,8 +5,7 @@ import (
"io/ioutil"
"os"
"github.com/genuinetools/reg/repoutils"
digest "github.com/opencontainers/go-digest"
"github.com/genuinetools/reg/registry"
"github.com/urfave/cli"
)
@ -25,12 +24,25 @@ var layerCommand = cli.Command{
return fmt.Errorf("pass the name of the repository")
}
repo, ref, err := repoutils.GetRepoAndRef(c.Args()[0])
image, err := registry.ParseImage(c.Args().First())
if err != nil {
return err
}
layer, err := r.DownloadLayer(repo, digest.FromString(ref))
// Create the registry client.
r, err := createRegistryClient(c, image.Domain)
if err != nil {
return err
}
// Get the digest.
digest, err := r.Digest(image)
if err != nil {
return err
}
// Download the layer.
layer, err := r.DownloadLayer(image.Path, digest)
if err != nil {
return err
}

33
layer_test.go Normal file
View file

@ -0,0 +1,33 @@
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
)
func TestLayer(t *testing.T) {
// Get the digest.
out, err := run("digest", fmt.Sprintf("%s/busybox", domain))
if err != nil {
t.Fatalf("output: %s, error: %v", out, err)
}
tmpf := filepath.Join(os.TempDir(), "download-layer.tar")
defer os.RemoveAll(tmpf)
// Download the layer.
lines := strings.Split(strings.TrimSpace(out), "\n")
layer := fmt.Sprintf("%s/busybox@%s", domain, strings.TrimSpace(lines[len(lines)-1]))
out, err = run("layer", "-o", tmpf, layer)
if err != nil {
t.Fatalf("output: %s, error: %v", out, err)
}
// Make sure the file exists
if _, err := os.Stat(tmpf); os.IsNotExist(err) {
t.Fatalf("%s should exist after downloading the layer but it didn't", tmpf)
}
}

61
list.go
View file

@ -3,6 +3,7 @@ package main
import (
"fmt"
"os"
"sort"
"strings"
"sync"
"text/tabwriter"
@ -15,13 +16,46 @@ var listCommand = cli.Command{
Aliases: []string{"ls"},
Usage: "list all repositories",
Action: func(c *cli.Context) error {
if len(c.Args()) < 1 {
return fmt.Errorf("pass the domain of the registry")
}
// Create the registry client.
r, err := createRegistryClient(c, c.Args().First())
if err != nil {
return err
}
// Get the repositories via catalog.
repos, err := r.Catalog("")
if err != nil {
return err
}
sort.Strings(repos)
fmt.Printf("Repositories for %s\n", auth.ServerAddress)
fmt.Printf("Repositories for %s\n", r.Domain)
var (
wg sync.WaitGroup
repoTags = map[string][]string{}
)
wg.Add(len(repos))
for _, repo := range repos {
go func(repo string) {
// Get the tags.
tags, err := r.Tags(repo)
if err != nil {
fmt.Printf("Get tags of [%s] error: %s", repo, err)
}
// Sort the tags
sort.Strings(tags)
repoTags[repo] = tags
wg.Done()
}(repo)
}
wg.Wait()
// Setup the tab writer.
w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0)
@ -29,31 +63,10 @@ var listCommand = cli.Command{
// Print header.
fmt.Fprintln(w, "REPO\tTAGS")
var (
l sync.Mutex
wg sync.WaitGroup
)
wg.Add(len(repos))
// Sort the repos.
for _, repo := range repos {
go func(repo string) {
// Get the tags and print to stdout.
tags, err := r.Tags(repo)
if err != nil {
fmt.Printf("Get tags of [%s] error: %s", repo, err)
}
out := fmt.Sprintf("%s\t%s\n", repo, strings.Join(tags, ", "))
// Lock around the tabwriter to prevent garbled output.
// See: https://github.com/genuinetools/reg/issues/54
l.Lock()
w.Write([]byte(out))
l.Unlock()
wg.Done()
}(repo)
w.Write([]byte(fmt.Sprintf("%s\t%s\n", repo, strings.Join(repoTags[repo], ", "))))
}
wg.Wait()
w.Flush()

View file

@ -6,14 +6,15 @@ import (
)
func TestList(t *testing.T) {
out, err := run("ls")
out, err := run("ls", domain)
if err != nil {
t.Fatalf("output: %s, error: %v", string(out), err)
t.Fatalf("output: %s, error: %v", out, err)
}
expected := []string{"alpine latest", "busybox glibc, musl"}
for _, e := range expected {
if !strings.Contains(out, e) {
t.Logf("expected to contain: %s\ngot: %s", e, out)
}
expected := `REPO TAGS
alpine 3.5, latest
busybox glibc, latest, musl`
if !strings.HasSuffix(strings.TrimSpace(out), expected) {
t.Fatalf("expected to contain: %s\ngot: %s", expected, out)
}
}

63
main.go
View file

@ -6,7 +6,6 @@ import (
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/genuinetools/reg/registry"
"github.com/genuinetools/reg/repoutils"
"github.com/genuinetools/reg/version"
@ -14,11 +13,6 @@ import (
"github.com/urfave/cli"
)
var (
auth types.AuthConfig
r *registry.Registry
)
func main() {
app := cli.NewApp()
app.Name = "reg"
@ -48,12 +42,6 @@ func main() {
Name: "password, p",
Usage: "password for the registry",
},
cli.StringFlag{
Name: "registry, r",
Usage: "URL to the private registry (ex. r.j3ss.co)",
Value: repoutils.DefaultDockerRegistry,
EnvVar: "REG_REGISTRY",
},
cli.StringFlag{
Name: "timeout",
Value: "1m",
@ -90,33 +78,36 @@ func main() {
return
}
auth, err = repoutils.GetAuthConfig(c.GlobalString("username"), c.GlobalString("password"), c.GlobalString("registry"))
if err != nil {
return err
}
// Prevent non-ssl unless explicitly forced
if !c.GlobalBool("force-non-ssl") && strings.HasPrefix(auth.ServerAddress, "http:") {
return fmt.Errorf("Attempt to use insecure protocol! Use non-ssl option to force")
}
// Parse the timeout.
timeout, err := time.ParseDuration(c.GlobalString("timeout"))
if err != nil {
return fmt.Errorf("parsing %s as duration failed: %v", c.GlobalString("timeout"), err)
}
// Create the registry client.
r, err = registry.New(auth, registry.Opt{
Insecure: c.GlobalBool("insecure"),
Debug: c.GlobalBool("debug"),
SkipPing: c.GlobalBool("skip-ping"),
Timeout: timeout,
})
return err
return
}
if err := app.Run(os.Args); err != nil {
logrus.Fatal(err)
}
}
func createRegistryClient(c *cli.Context, domain string) (*registry.Registry, error) {
auth, err := repoutils.GetAuthConfig(c.GlobalString("username"), c.GlobalString("password"), domain)
if err != nil {
return nil, err
}
// Prevent non-ssl unless explicitly forced
if !c.GlobalBool("force-non-ssl") && strings.HasPrefix(auth.ServerAddress, "http:") {
return nil, fmt.Errorf("Attempt to use insecure protocol! Use non-ssl option to force")
}
// Parse the timeout.
timeout, err := time.ParseDuration(c.GlobalString("timeout"))
if err != nil {
return nil, fmt.Errorf("parsing %s as duration failed: %v", c.GlobalString("timeout"), err)
}
// Create the registry client.
return registry.New(auth, registry.Opt{
Insecure: c.GlobalBool("insecure"),
Debug: c.GlobalBool("debug"),
SkipPing: c.GlobalBool("skip-ping"),
Timeout: timeout,
})
}

View file

@ -13,6 +13,10 @@ import (
"github.com/genuinetools/reg/testutils"
)
const (
domain = "localhost:5000"
)
var (
exeSuffix string // ".exe" on Windows
@ -103,8 +107,9 @@ func TestMain(m *testing.M) {
func run(args ...string) (string, error) {
prog := "./testreg" + exeSuffix
// always add trust insecure, and the registry
newargs := append([]string{"-d", "-k", "-r", "localhost:5000"}, args...)
newargs := append([]string{"-d", "-k"}, args...)
cmd := exec.Command(prog, newargs...)
cmd.Env = []string{"REG_REGISTRY=localhost:5000"}
out, err := cmd.CombinedOutput()
return string(out), err
}

View file

@ -4,7 +4,7 @@ import (
"encoding/json"
"fmt"
"github.com/genuinetools/reg/repoutils"
"github.com/genuinetools/reg/registry"
"github.com/urfave/cli"
)
@ -22,7 +22,13 @@ var manifestCommand = cli.Command{
return fmt.Errorf("pass the name of the repository")
}
repo, ref, err := repoutils.GetRepoAndRef(c.Args()[0])
image, err := registry.ParseImage(c.Args().First())
if err != nil {
return err
}
// Create the registry client.
r, err := createRegistryClient(c, image.Domain)
if err != nil {
return err
}
@ -30,13 +36,13 @@ var manifestCommand = cli.Command{
var manifest interface{}
if c.Bool("v1") {
// Get the v1 manifest if it was explicitly asked for.
manifest, err = r.ManifestV1(repo, ref)
manifest, err = r.ManifestV1(image.Path, image.Reference())
if err != nil {
return err
}
} else {
// Get the v2 manifest.
manifest, err = r.Manifest(repo, ref)
manifest, err = r.Manifest(image.Path, image.Reference())
if err != nil {
return err
}

31
manifest_test.go Normal file
View file

@ -0,0 +1,31 @@
package main
import (
"fmt"
"strings"
"testing"
)
func TestManifestV2(t *testing.T) {
out, err := run("manifest", fmt.Sprintf("%s/busybox", domain))
if err != nil {
t.Fatalf("output: %s, error: %v", out, err)
}
expected := `"schemaVersion": 2,`
if !strings.Contains(out, expected) {
t.Fatalf("expected: %s\ngot: %s", expected, out)
}
}
func TestManifestV1(t *testing.T) {
out, err := run("manifest", "--v1", fmt.Sprintf("%s/busybox", domain))
if err != nil {
t.Fatalf("output: %s, error: %v", out, err)
}
expected := `"schemaVersion": 1,`
if !strings.Contains(out, expected) {
t.Fatalf("expected: %s\ngot: %s", expected, out)
}
}

View file

@ -5,24 +5,12 @@ import (
"net/http"
"github.com/docker/distribution/manifest/schema2"
ocd "github.com/opencontainers/go-digest"
digest "github.com/opencontainers/go-digest"
)
// Delete removes a repository digest or reference from the registry.
// Delete removes a repository digest from the registry.
// https://docs.docker.com/registry/spec/api/#deleting-an-image
func (r *Registry) Delete(repository, digest string) error {
// If digest is not valid try resolving it as a reference
if _, err := ocd.Parse(digest); err != nil {
digest, err = r.Digest(repository, digest)
if err != nil {
return err
}
if digest == "" {
return nil
}
}
// Delete the image.
func (r *Registry) Delete(repository string, digest digest.Digest) (err error) {
url := r.url("/v2/%s/manifests/%s", repository, digest)
r.Logf("registry.manifests.delete url=%s repository=%s digest=%s",
url, repository, digest)

View file

@ -5,20 +5,26 @@ import (
"net/http"
"github.com/docker/distribution/manifest/schema2"
digest "github.com/opencontainers/go-digest"
)
// Digest returns the digest for a repository and reference.
func (r *Registry) Digest(repository, ref string) (string, error) {
url := r.url("/v2/%s/manifests/%s", repository, ref)
// Digest returns the digest for an image.
func (r *Registry) Digest(image Image) (digest.Digest, error) {
if len(image.Digest) > 1 {
// return early if we already have an image digest.
return image.Digest, nil
}
url := r.url("/v2/%s/manifests/%s", image.Path, image.Tag)
r.Logf("registry.manifests.get url=%s repository=%s ref=%s",
url, repository, ref)
url, image.Path, image.Tag)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", err
}
req.Header.Set("Accept", schema2.MediaTypeManifest)
req.Header.Set("Accept", schema2.MediaTypeManifest)
resp, err := r.Client.Do(req)
if err != nil {
return "", err
@ -29,6 +35,5 @@ func (r *Registry) Digest(repository, ref string) (string, error) {
return "", fmt.Errorf("Got status code: %d", resp.StatusCode)
}
digest := resp.Header.Get("Docker-Content-Digest")
return digest, nil
return digest.FromString(resp.Header.Get("Docker-Content-Digest")), nil
}

67
registry/image.go Normal file
View file

@ -0,0 +1,67 @@
package registry
import (
"fmt"
"github.com/docker/distribution/reference"
digest "github.com/opencontainers/go-digest"
)
// Image holds information about an image.
type Image struct {
Domain string
Path string
Tag string
Digest digest.Digest
named reference.Named
}
// String returns the string representation of an image.
func (i Image) String() string {
return i.named.String()
}
// Reference returns either the digest if it is non-empty or the tag for the image.
func (i Image) Reference() string {
if len(i.Digest.String()) > 1 {
return i.Digest.String()
}
return i.Tag
}
// WithDigest sets the digest for an image.
func (i *Image) WithDigest(digest digest.Digest) (err error) {
i.Digest = digest
i.named, err = reference.WithDigest(i.named, digest)
return err
}
// ParseImage returns an Image struct with all the values filled in for a given image.
func ParseImage(image string) (Image, error) {
// Parse the image name and tag.
named, err := reference.ParseNormalizedNamed(image)
if err != nil {
return Image{}, fmt.Errorf("parsing image %q failed: %v", image, err)
}
// Add the latest lag if they did not provide one.
named = reference.TagNameOnly(named)
i := Image{
named: named,
Domain: reference.Domain(named),
Path: reference.Path(named),
}
// Add the tag if there was one.
if tagged, ok := named.(reference.Tagged); ok {
i.Tag = tagged.Tag()
}
// Add the digest if there was one.
if canonical, ok := named.(reference.Canonical); ok {
i.Digest = canonical.Digest()
}
return i, nil
}

View file

@ -87,17 +87,17 @@ func (r *Registry) ManifestV1(repository, ref string) (schema1.SignedManifest, e
return m, nil
}
// PutManifest calls a PUT for the specific manifest for an image.
func (r *Registry) PutManifest(repository, ref string, manifest distribution.Manifest) error {
url := r.url("/v2/%s/manifests/%s", repository, ref)
r.Logf("registry.manifest.put url=%s repository=%s reference=%s", url, repository, ref)
manifestJson, err := json.Marshal(manifest)
b, err := json.Marshal(manifest)
if err != nil {
return err
}
buffer := bytes.NewBuffer(manifestJson)
req, err := http.NewRequest("PUT", url, buffer)
req, err := http.NewRequest("PUT", url, bytes.NewBuffer(b))
if err != nil {
return err
}

16
tags.go
View file

@ -2,8 +2,10 @@ package main
import (
"fmt"
"sort"
"strings"
"github.com/genuinetools/reg/registry"
"github.com/urfave/cli"
)
@ -15,11 +17,23 @@ var tagsCommand = cli.Command{
return fmt.Errorf("pass the name of the repository")
}
tags, err := r.Tags(c.Args()[0])
image, err := registry.ParseImage(c.Args().First())
if err != nil {
return err
}
// Create the registry client.
r, err := createRegistryClient(c, image.Domain)
if err != nil {
return err
}
tags, err := r.Tags(image.Path)
if err != nil {
return err
}
sort.Strings(tags)
// Print the tags.
fmt.Println(strings.Join(tags, "\n"))

View file

@ -1,19 +1,21 @@
package main
import (
"fmt"
"strings"
"testing"
)
func TestTags(t *testing.T) {
out, err := run("tags", "busybox")
out, err := run("tags", fmt.Sprintf("%s/busybox", domain))
if err != nil {
t.Fatalf("output: %s, error: %v", string(out), err)
t.Fatalf("output: %s, error: %v", out, err)
}
expected := `glibc
latest
musl
`
if !strings.HasSuffix(out, expected) {
t.Logf("expected: %s\ngot: %s", expected, out)
t.Fatalf("expected: %s\ngot: %s", expected, out)
}
}

View file

@ -6,7 +6,7 @@ import (
"time"
"github.com/genuinetools/reg/clair"
"github.com/genuinetools/reg/repoutils"
"github.com/genuinetools/reg/registry"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -38,7 +38,13 @@ var vulnsCommand = cli.Command{
return fmt.Errorf("pass the name of the repository")
}
repo, ref, err := repoutils.GetRepoAndRef(c.Args()[0])
image, err := registry.ParseImage(c.Args().First())
if err != nil {
return err
}
// Create the registry client.
r, err := createRegistryClient(c, image.Domain)
if err != nil {
return err
}
@ -60,7 +66,7 @@ var vulnsCommand = cli.Command{
}
// Get the vulnerability report.
report, err := cr.Vulnerabilities(r, repo, ref)
report, err := cr.Vulnerabilities(r, image.Path, image.Reference())
if err != nil {
return err
}

View file

@ -1,18 +1,19 @@
package main
import (
"fmt"
"strings"
"testing"
)
func TestVulns(t *testing.T) {
out, err := run("vulns", "--clair", "http://localhost:6060", "alpine:3.5")
out, err := run("vulns", "--clair", "http://localhost:6060", fmt.Sprintf("%s/alpine:3.5", domain))
if err != nil {
t.Fatalf("output: %s, error: %v", string(out), err)
t.Fatalf("output: %s, error: %v", out, err)
}
expected := `clair.clair resp.Status=200 OK`
if !strings.HasSuffix(strings.TrimSpace(out), expected) {
t.Logf("expected: %s\ngot: %s", expected, out)
t.Fatalf("expected: %s\ngot: %s", expected, out)
}
}