diff --git a/app/app.go b/app/app.go index e15ec08..c316671 100644 --- a/app/app.go +++ b/app/app.go @@ -105,34 +105,6 @@ func DoesMatch(s, ex string) bool { return matched } -func isHostname(s string) bool { - if strings.Contains(s, ".") { - return true - } - - if strings.Contains(s, ":") { - return true - } - - if s == "localhost" { - return true - } - - return false -} - -// GetRegistryNameFromRepo tries to get Docker registry name from repository name -// .. if it is not possible it returns default registry name (usually Docker Hub) -func GetRegistryNameFromRepo(repository, defaultRegistry string) string { - r := strings.Split(repository, "/")[0] - - if isHostname(r) { - return r - } - - return defaultRegistry -} - // GeneratePathFromHostname generates "/"-delimited path from a hostname[:port] func GeneratePathFromHostname(hostname string) string { allParts := strings.Split(hostname, ":") diff --git a/app/app_test.go b/app/app_test.go index 4d4d9c1..2ee4a56 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -79,28 +79,6 @@ func TestDoesMatch(t *testing.T) { } } -func TestGetRegistryNameFromRepo(t *testing.T) { - expected := map[string]string{ - "mesosphere/marathon": dockerHub, - "bogohost/my/inner/troll": dockerHub, - "registry.hipsta.io/hype/hotshit": "registry.hipsta.io", - "localhost/my/image": "localhost", - "bogohost:5000/mymymy/img": "bogohost:5000", - } - - for repo, expectedRegistryName := range expected { - registryName := GetRegistryNameFromRepo(repo, dockerHub) - - if registryName != expectedRegistryName { - t.Fatalf( - "Got unexpected Docker registry name '%s' from repo '%s' (expected: '%s')", - registryName, - repo, - expectedRegistryName, - ) - } - } -} func TestGeneratePathFromHostname(t *testing.T) { examples := map[string]string{ "localhost": "/localhost", diff --git a/docker/client/api/version/version.go b/docker/client/api/version/version.go new file mode 100644 index 0000000..a0ca774 --- /dev/null +++ b/docker/client/api/version/version.go @@ -0,0 +1,46 @@ +package version + +import ( + "encoding/json" + "io" + "net/http" + "time" + + "github.com/tv42/httpunix" +) + +// Detect detects Docker API version through the passed Docker socket +func Detect(dockerSocket string) (string, error) { + hc := http.Client{Transport: getTransport(dockerSocket)} + + resp, err := hc.Get("http+unix://docker/version") + if err != nil { + return "", err + } + + return parseJSON(resp.Body) +} + +func getTransport(dockerSocket string) *httpunix.Transport { + t := &httpunix.Transport{ + DialTimeout: 200 * time.Millisecond, + RequestTimeout: 2 * time.Second, + ResponseHeaderTimeout: 2 * time.Second, + } + t.RegisterLocation("docker", dockerSocket) + + return t +} + +func parseJSON(data io.ReadCloser) (string, error) { + v := struct { + APIVersion string `json:"ApiVersion"` + }{} + + err := json.NewDecoder(data).Decode(&v) + if err != nil { + return "", err + } + + return v.APIVersion, nil +} diff --git a/docker/client/api/version/version_test.go b/docker/client/api/version/version_test.go new file mode 100644 index 0000000..c92cc6b --- /dev/null +++ b/docker/client/api/version/version_test.go @@ -0,0 +1,28 @@ +package version + +import ( + "testing" +) + +const dockerSocket = "/var/run/docker.sock" +const invalidSocket = "/var/run/somethinginvalid.sock" + +func TestDetect(t *testing.T) { + _, err := Detect(dockerSocket) + if err != nil { + t.Fatalf( + "Unable to detect Docker API version: %s", + err.Error(), + ) + } +} + +func TestWithInvalidSocket(t *testing.T) { + _, err := Detect(invalidSocket) + if err == nil { + t.Fatalf( + "Unable to detect Docker API version: %s", + err.Error(), + ) + } +} diff --git a/docker/client/client.go b/docker/client/client.go new file mode 100644 index 0000000..5d36bd9 --- /dev/null +++ b/docker/client/client.go @@ -0,0 +1,108 @@ +package client + +import ( + "io/ioutil" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/moby/moby/client" + + "golang.org/x/net/context" + + "github.com/ivanilves/lstags/docker" + "github.com/ivanilves/lstags/docker/client/api/version" + "github.com/ivanilves/lstags/docker/config" +) + +// DockerSocket is a socket we use to connect to the Docker daemon +var DockerSocket = "/var/run/docker.sock" + +// DockerClient is a raw Docker client convenience wrapper +type DockerClient struct { + cli *client.Client + cnf *config.Config +} + +// New creates new instance of DockerClient (our Docker client wrapper) +func New(cnf *config.Config) (*DockerClient, error) { + apiVersion, err := version.Detect(DockerSocket) + if err != nil { + return nil, err + } + + cli, err := client.NewClient("unix://"+DockerSocket, apiVersion, nil, nil) + if err != nil { + return nil, err + } + + return &DockerClient{cli: cli, cnf: cnf}, nil +} + +// ListImagesForRepo lists images present locally for the repo specified +func (dc *DockerClient) ListImagesForRepo(repo string) ([]types.ImageSummary, error) { + listOptions, err := buildImageListOptions(repo) + if err != nil { + return nil, err + } + + return dc.cli.ImageList(context.Background(), listOptions) +} + +func buildImageListOptions(repo string) (types.ImageListOptions, error) { + repoFilter := "reference=" + repo + filterArgs := filters.NewArgs() + + filterArgs, err := filters.ParseFlag(repoFilter, filterArgs) + if err != nil { + return types.ImageListOptions{}, err + } + + return types.ImageListOptions{Filters: filterArgs}, nil +} + +// Pull pulls Docker image specified +func (dc *DockerClient) Pull(ref string) error { + registryAuth, _ := dc.cnf.GetRegistryAuth( + docker.GetRegistry(ref), + ) + + pullOptions := types.ImagePullOptions{RegistryAuth: registryAuth} + if registryAuth == "" { + pullOptions = types.ImagePullOptions{} + } + + resp, err := dc.cli.ImagePull(context.Background(), ref, pullOptions) + if err != nil { + return err + } + + _, err = ioutil.ReadAll(resp) + + return err +} + +// Push pushes Docker image specified +func (dc *DockerClient) Push(ref string) error { + registryAuth, _ := dc.cnf.GetRegistryAuth( + docker.GetRegistry(ref), + ) + + pushOptions := types.ImagePushOptions{RegistryAuth: registryAuth} + if registryAuth == "" { + pushOptions = types.ImagePushOptions{} + } + + resp, err := dc.cli.ImagePush(context.Background(), ref, pushOptions) + if err != nil { + return err + } + + _, err = ioutil.ReadAll(resp) + + return err +} + +// Tag puts a "dst" tag on "src" Docker image +func (dc *DockerClient) Tag(src, dst string) error { + return dc.cli.ImageTag(context.Background(), src, dst) +} diff --git a/docker/docker.go b/docker/docker.go new file mode 100644 index 0000000..7df63f6 --- /dev/null +++ b/docker/docker.go @@ -0,0 +1,36 @@ +package docker + +import ( + "strings" +) + +// DefaultRegistry is a registry we use if none could be resolved from image ref +var DefaultRegistry = "registry.hub.docker.com" + +// GetRegistry tries to get Docker registry name from a repository or reference +// .. if it is not possible it returns default registry name (usually Docker Hub) +func GetRegistry(repoOrRef string) string { + r := strings.Split(repoOrRef, "/")[0] + + if isHostname(r) { + return r + } + + return DefaultRegistry +} + +func isHostname(s string) bool { + if strings.Contains(s, ".") { + return true + } + + if strings.Contains(s, ":") { + return true + } + + if s == "localhost" { + return true + } + + return false +} diff --git a/docker/docker_test.go b/docker/docker_test.go new file mode 100644 index 0000000..7ea1bae --- /dev/null +++ b/docker/docker_test.go @@ -0,0 +1,32 @@ +package docker + +import ( + "testing" +) + +func TestGetRegistry(t *testing.T) { + examples := map[string]string{ + "mesosphere/marathon": DefaultRegistry, + "bogohost/my/inner/troll": DefaultRegistry, + "bogohost/my/inner/troll:1.0.1": DefaultRegistry, + "registry.hipsta.io/hype/hotshit": "registry.hipsta.io", + "localhost/my/image": "localhost", + "localhost/my/image:latest": "localhost", + "bogohost:5000/mymymy/img": "bogohost:5000", + "bogohost:5000/mymymy/img:0.0.1": "bogohost:5000", + "bogohost:5000/mymymy/img:edge": "bogohost:5000", + } + + for repoOrRef, expectedRegistry := range examples { + registry := GetRegistry(repoOrRef) + + if registry != expectedRegistry { + t.Fatalf( + "Got unexpected Docker registry name '%s' from repo/ref '%s' (expected: '%s')", + registry, + repoOrRef, + expectedRegistry, + ) + } + } +} diff --git a/main.go b/main.go index ecb4fab..80319e9 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,8 @@ import ( "github.com/ivanilves/lstags/app" "github.com/ivanilves/lstags/auth" + "github.com/ivanilves/lstags/docker" + dockerclient "github.com/ivanilves/lstags/docker/client" dockerconfig "github.com/ivanilves/lstags/docker/config" "github.com/ivanilves/lstags/tag" "github.com/ivanilves/lstags/tag/local" @@ -65,9 +67,9 @@ func main() { pullAuths := make(map[string]string) - var pushAuth string - if o.PushRegistry != "" { - pushAuth, _ = dockerConfig.GetRegistryAuth(o.PushRegistry) + dc, err := dockerclient.New(dockerConfig) + if err != nil { + suicide(err) } type tagResult struct { @@ -86,7 +88,7 @@ func main() { suicide(err) } - registryName := app.GetRegistryNameFromRepo(repository, o.DefaultRegistry) + registryName := docker.GetRegistry(repository) repoRegistryName := registry.FormatRepoName(repository, registryName) repoLocalName := local.FormatRepoName(repository, registryName) @@ -106,7 +108,12 @@ func main() { if err != nil { suicide(err) } - localTags, err := local.FetchTags(repoLocalName) + + imageSummaries, err := dc.ListImagesForRepo(repoLocalName) + if err != nil { + suicide(err) + } + localTags, err := local.FetchTags(repoLocalName, imageSummaries) if err != nil { suicide(err) } @@ -168,7 +175,7 @@ func main() { ref := repo + ":" + tg.GetName() fmt.Printf("PULLING %s\n", ref) - err := local.Pull(ref, pullAuths[repo]) + err := dc.Pull(ref) if err != nil { suicide(err) } @@ -208,12 +215,12 @@ func main() { fmt.Printf("PUSHING %s => %s\n", srcRef, dstRef) - err := local.Tag(srcRef, dstRef) + err := dc.Tag(srcRef, dstRef) if err != nil { suicide(err) } - err = local.Push(dstRef, pushAuth) + err = dc.Push(dstRef) if err != nil { suicide(err) } diff --git a/tag/local/local.go b/tag/local/local.go index 974ec9d..e08dbc9 100644 --- a/tag/local/local.go +++ b/tag/local/local.go @@ -1,86 +1,40 @@ package local import ( - "encoding/json" - "io" - "io/ioutil" - "net/http" "strings" - "time" "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/moby/moby/client" - - "github.com/tv42/httpunix" - "golang.org/x/net/context" "github.com/ivanilves/lstags/tag" ) -const dockerSocket = "/var/run/docker.sock" - -type apiVersionResponse struct { - APIVersion string `json:"ApiVersion"` -} - -func getAPITransport() *httpunix.Transport { - t := &httpunix.Transport{ - DialTimeout: 200 * time.Millisecond, - RequestTimeout: 2 * time.Second, - ResponseHeaderTimeout: 2 * time.Second, - } - t.RegisterLocation("docker", dockerSocket) - - return t -} - -func parseAPIVersionJSON(data io.ReadCloser) (string, error) { - v := apiVersionResponse{} - - err := json.NewDecoder(data).Decode(&v) - if err != nil { - return "", err - } - - return v.APIVersion, nil -} - -func detectAPIVersion() (string, error) { - hc := http.Client{Transport: getAPITransport()} - - resp, err := hc.Get("http+unix://docker/version") - if err != nil { - return "", err - } +// FetchTags looks up Docker repo tags and IDs present on local Docker daemon +func FetchTags(repo string, imageSummaries []types.ImageSummary) (map[string]*tag.Tag, error) { + tags := make(map[string]*tag.Tag) - return parseAPIVersionJSON(resp.Body) -} + for _, imageSummary := range imageSummaries { + repoDigest := extractRepoDigest(imageSummary.RepoDigests) + tagNames := extractTagNames(imageSummary.RepoTags, repo) -func newClient() (*client.Client, error) { - apiVersion, err := detectAPIVersion() - if err != nil { - return nil, err - } + if repoDigest == "" { + repoDigest = "this.image.is.bad.it.has.no.digest.fuuu!" + } - cli, err := client.NewClient("unix://"+dockerSocket, apiVersion, nil, nil) - if err != nil { - return nil, err - } + for _, tagName := range tagNames { + tg, err := tag.New(tagName, repoDigest) + if err != nil { + return nil, err + } - return cli, err -} + tg.SetImageID(imageSummary.ID) -func newImageListOptions(repo string) (types.ImageListOptions, error) { - repoFilter := "reference=" + repo - filterArgs := filters.NewArgs() + tg.SetCreated(imageSummary.Created) - filterArgs, err := filters.ParseFlag(repoFilter, filterArgs) - if err != nil { - return types.ImageListOptions{}, err + tags[tg.GetName()] = tg + } } - return types.ImageListOptions{Filters: filterArgs}, nil + return tags, nil } func extractRepoDigest(repoDigests []string) string { @@ -107,49 +61,6 @@ func extractTagNames(repoTags []string, repo string) []string { return tagNames } -// FetchTags looks up Docker repo tags and IDs present on local Docker daemon -func FetchTags(repo string) (map[string]*tag.Tag, error) { - cli, err := newClient() - if err != nil { - return nil, err - } - - listOptions, err := newImageListOptions(repo) - if err != nil { - return nil, err - } - imageSummaries, err := cli.ImageList(context.Background(), listOptions) - if err != nil { - return nil, err - } - - tags := make(map[string]*tag.Tag) - - for _, imageSummary := range imageSummaries { - repoDigest := extractRepoDigest(imageSummary.RepoDigests) - tagNames := extractTagNames(imageSummary.RepoTags, repo) - - if repoDigest == "" { - repoDigest = "this.image.is.bad.it.has.no.digest.fuuu!" - } - - for _, tagName := range tagNames { - tg, err := tag.New(tagName, repoDigest) - if err != nil { - return nil, err - } - - tg.SetImageID(imageSummary.ID) - - tg.SetCreated(imageSummary.Created) - - tags[tg.GetName()] = tg - } - } - - return tags, nil -} - // FormatRepoName formats repository name for use with local Docker daemon func FormatRepoName(repository, registry string) string { if registry == "registry.hub.docker.com" { @@ -166,57 +77,3 @@ func FormatRepoName(repository, registry string) string { return registry + "/" + repository } - -// Pull pulls Docker image specified locally -func Pull(ref, auth string) error { - cli, err := newClient() - if err != nil { - return err - } - - pullOptions := types.ImagePullOptions{RegistryAuth: auth} - if auth == "" { - pullOptions = types.ImagePullOptions{} - } - - resp, err := cli.ImagePull(context.Background(), ref, pullOptions) - if err != nil { - return err - } - - _, err = ioutil.ReadAll(resp) - - return err -} - -// Push pushes Docker image to a specified registry -func Push(ref, auth string) error { - cli, err := newClient() - if err != nil { - return err - } - - pushOptions := types.ImagePushOptions{RegistryAuth: auth} - if auth == "" { - pushOptions = types.ImagePushOptions{} - } - - resp, err := cli.ImagePush(context.Background(), ref, pushOptions) - if err != nil { - return err - } - - _, err = ioutil.ReadAll(resp) - - return err -} - -// Tag puts a "dst" tag on "src" Docker image -func Tag(src, dst string) error { - cli, err := newClient() - if err != nil { - return err - } - - return cli.ImageTag(context.Background(), src, dst) -} diff --git a/tag/local/local_test.go b/tag/local/local_test.go index f87078f..7fa9557 100644 --- a/tag/local/local_test.go +++ b/tag/local/local_test.go @@ -4,7 +4,7 @@ import ( "testing" ) -func TestFormatRepoNameForPublicRegistry(t *testing.T) { +func TestFormatRepoNameForDockerHub(t *testing.T) { const registry = "registry.hub.docker.com" expectations := map[string]string{