diff --git a/cmd/install.go b/cmd/install.go index f4727a3..1c766b8 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -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 { diff --git a/go.mod b/go.mod index 2fe5e7c..01027a0 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/pkg/providers/docker.go b/pkg/providers/docker.go index 1bcd8d5..cf40d32 100644 --- a/pkg/providers/docker.go +++ b/pkg/providers/docker.go @@ -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 } @@ -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 } diff --git a/pkg/providers/docker_test.go b/pkg/providers/docker_test.go index 4f9a0a0..3a92134 100644 --- a/pkg/providers/docker_test.go +++ b/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) } }) } diff --git a/pkg/providers/providers.go b/pkg/providers/providers.go index 9099d3c..dbca0b1 100644 --- a/pkg/providers/providers.go +++ b/pkg/providers/providers.go @@ -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) } @@ -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) }