Skip to content

Commit

Permalink
Use docker:// scheme for installing docker images (#3)
Browse files Browse the repository at this point in the history
* Use docker:// as base for provider urls

* Remove type checker and add TODO for windows

Signed-off-by: Matias Pan <matias.pan26@gmail.com>

* Add TODO for file validation

Signed-off-by: Matias Pan <matias.pan26@gmail.com>
  • Loading branch information
matipan committed Jun 22, 2020
1 parent 778bb53 commit cae6a22
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 63 deletions.
12 changes: 6 additions & 6 deletions cmd/install.go
Expand Up @@ -137,24 +137,24 @@ func getFinalPath(path, fileName string) (string, error) {
//TODO check if other binary has the same hash and warn about it.
//TODO if the file is zipped, tared, whatever then extract it
func saveToDisk(f *providers.File, path string, overwrite bool) error {

defer f.Data.Close()

var buf bytes.Buffer
tee := io.TeeReader(f.Data, &buf)

t, err := filetype.MatchReader(tee)

if err != nil {
return err
}

if t != matchers.TypeElf && t != matchers.TypeGz {
return fmt.Errorf("File type [%v] not supported", t)
}

var outputFile = io.MultiReader(&buf, f.Data)

// TODO: validating the type of the file will eventually be
// handled by each provider
// if t != matchers.TypeElf && t != matchers.TypeGz {
// return fmt.Errorf("File type [%v] not supported", t)
// }

if t == matchers.TypeGz {
fileName, file, err := processTarGz(outputFile)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Expand Up @@ -6,7 +6,7 @@ require (
github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/WeiZhang555/tabwriter v0.0.0-20200115015932-e5c45f4da38d
github.com/apex/log v1.1.4
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/distribution v2.7.1+incompatible
github.com/docker/docker v1.13.1
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
Expand Down
84 changes: 51 additions & 33 deletions pkg/providers/docker.go
Expand Up @@ -6,25 +6,29 @@ import (
"fmt"
"io"
"io/ioutil"
"net/url"
"strings"

distreference "github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/reference"
"github.com/moby/moby/client"
)

const (
sh = `docker run --rm -i -t -v ${PWD}:/tmp/cmd -w /tmp/cmd %s/%s:%s "$@"`
// TODO: this probably won't work on windows so we might need how we mount
// TODO: there might be a way were users can configure a template for the
// actual execution since some CLIs require some other folders to be mounted
// or networks to be shared
sh = `docker run --rm -i -t -v ${PWD}:/tmp/cmd -w /tmp/cmd %s:%s "$@"`
)

type docker struct {
client *client.Client
url *url.URL
client *client.Client
repo, tag string
}

func (d *docker) Fetch() (*File, error) {
owner, name, version := getImageDesc(d.url.Path)
out, err := d.client.ImagePull(context.Background(), fmt.Sprintf("docker.io/%s/%s:%s", owner, name, version), types.ImagePullOptions{})
out, err := d.client.ImagePull(context.Background(), fmt.Sprintf("%s:%s", d.repo, d.tag), types.ImagePullOptions{})
if err != nil {
return nil, err
}
Expand All @@ -35,49 +39,63 @@ func (d *docker) Fetch() (*File, error) {
}

return &File{
Data: ioutil.NopCloser(strings.NewReader(fmt.Sprintf(sh, owner, name, version))),
Name: name,
Version: version,
Data: ioutil.NopCloser(strings.NewReader(fmt.Sprintf(sh, d.repo, d.tag))),
Name: getImageName(d.repo),
Version: d.tag,
Hash: sha256.New(),
}, nil
}

// getImageName gets the name of the image from the image repo.
func getImageName(repo string) string {
image := strings.Split(repo, "/")
return image[len(image)-1]
}

// TODO: implement
func (d *docker) GetLatestVersion(name string) (string, string, error) {
return "", "", nil
}

// getImageDesc gest the image owner, name and version from the path.
func getImageDesc(path string) (string, string, string) {
path = strings.TrimPrefix(path, "/")
path = strings.TrimPrefix(path, "r/")
var (
owner, image, version string
imageDesc = strings.Split(path, "/")
)
if len(imageDesc) == 1 {
owner, image = "library", imageDesc[0]
} else {
owner, image = imageDesc[0], imageDesc[1]
func newDocker(imageURL string) (Provider, error) {
imageURL = strings.TrimPrefix(imageURL, "docker://")

repo, tag, err := parseImage(imageURL)
if err != nil {
return nil, err
}

version = "latest"
imageVersion := strings.Split(image, ":")
if len(imageVersion) > 1 {
image, version = imageVersion[0], imageVersion[1]
client, err := client.NewEnvClient()
if err != nil {
return nil, err
}

return owner, image, version
return &docker{repo: repo, tag: tag, client: client}, nil
}

func newDocker(u *url.URL) (Provider, error) {
if u.Path == "" || len(strings.Split(strings.TrimPrefix("/r", u.Path), "/")) > 3 {
return nil, fmt.Errorf("Error parsing registry URL. %s is not a valid image name and version", u.Path)
// parseImage parses the image returning the repository, tag
// and an error if it fails. ParseImage handles non-canonical
// URLs like `hashicorp/terraform`.
func parseImage(imageURL string) (string, string, error) {
repo, tag, err := reference.Parse(imageURL)
if err == nil {
return repo, tag, nil
}
client, err := client.NewEnvClient()
if err != nil {
return nil, err

if err != distreference.ErrNameNotCanonical {
return "", "", fmt.Errorf("image %s is invalid: %w", imageURL, err)
}

image := imageURL
tag = "latest"
if i := strings.LastIndex(imageURL, ":"); i > -1 {
image = imageURL[:i]
tag = imageURL[i+1:]
}

if strings.Count(imageURL, "/") == 0 {
image = "library/" + image
}

return &docker{url: u, client: client}, nil
return fmt.Sprintf("docker.io/%s", image), tag, nil
}
38 changes: 20 additions & 18 deletions pkg/providers/docker_test.go
@@ -1,31 +1,33 @@
package providers

import "testing"
import (
"testing"
)

func TestGetImageDesc(t *testing.T) {
func TestParseImage(t *testing.T) {
cases := []struct {
name string
path string
expectedOwner string
expectedName string
expectedVersion string
name string
imageURL string
expectedRepo, expectedTag string
withErr bool
}{
{"no owner no version", "/alpine", "library", "alpine", "latest"},
{"no owner with version", "/alpine:3.0.9", "library", "alpine", "3.0.9"},
{"with owner and no version", "/hashicorp/terraform", "hashicorp", "terraform", "latest"},
{"with owner with version", "/hashicorp/terraform:light", "hashicorp", "terraform", "light"},
{name: "no host, no version", imageURL: "postgres", expectedRepo: "docker.io/library/postgres", expectedTag: "latest"},
{name: "no host, with version", imageURL: "postgres:1.2.3", expectedRepo: "docker.io/library/postgres", expectedTag: "1.2.3"},
{name: "with host, no version", imageURL: "quay.io/calico/node", expectedRepo: "quay.io/calico/node", expectedTag: "latest"},
{name: "with host, with version", imageURL: "quay.io/calico/node:1.2.3", expectedRepo: "quay.io/calico/node", expectedTag: "1.2.3"},
{name: "no host, with version and owner", imageURL: "hashicorp/terraform:1.2.3", expectedRepo: "docker.io/hashicorp/terraform", expectedTag: "1.2.3"},
}

for _, test := range cases {
t.Run(test.name, func(t *testing.T) {
owner, name, version := getImageDesc(test.path)
repo, tag, err := parseImage(test.imageURL)
switch {
case test.expectedOwner != owner:
t.Errorf("expected owner was %s got %s", test.expectedOwner, owner)
case test.expectedName != name:
t.Errorf("expected name was %s got %s", test.expectedName, name)
case test.expectedVersion != version:
t.Errorf("expected version was %s got %s", test.expectedVersion, version)
case test.expectedRepo != repo:
t.Errorf("expected repo was %s, got %s", test.expectedRepo, repo)
case test.expectedTag != tag:
t.Errorf("expected tag was %s, got %s", test.expectedTag, tag)
case test.withErr != (err != nil):
t.Errorf("expected err != nil to be %v", test.withErr)
}
})
}
Expand Down
12 changes: 7 additions & 5 deletions pkg/providers/providers.go
Expand Up @@ -24,9 +24,15 @@ type Provider interface {
GetLatestVersion(string) (string, string, error)
}

var httpUrlPrefix = regexp.MustCompile("^https?://")
var (
httpUrlPrefix = regexp.MustCompile("^https?://")
dockerUrlPrefix = regexp.MustCompile("^docker://")
)

func New(u string) (Provider, error) {
if dockerUrlPrefix.MatchString(u) {
return newDocker(u)
}
if !httpUrlPrefix.MatchString(u) {
u = fmt.Sprintf("https://%s", u)
}
Expand All @@ -41,9 +47,5 @@ func New(u string) (Provider, error) {
return newGitHub(purl)
}

if strings.Contains(purl.Host, "hub.docker.com") || strings.Contains(purl.Host, "docker.io") {
return newDocker(purl)
}

return nil, fmt.Errorf("Can't find provider for url %s", u)
}

0 comments on commit cae6a22

Please sign in to comment.