package docker import ( "bytes" "encoding/json" "fmt" "github.com/dotcloud/docker/auth" "github.com/shin-/cookiejar" "io" "io/ioutil" "net/http" "net/url" "os" "path" "strings" ) //FIXME: Set the endpoint in a conf file or via commandline const INDEX_ENDPOINT = auth.INDEX_SERVER + "/v1" // Build an Image object from raw json data func NewImgJson(src []byte) (*Image, error) { ret := &Image{} Debugf("Json string: {%s}\n", src) // FIXME: Is there a cleaner way to "purify" the input json? if err := json.Unmarshal(src, ret); err != nil { return nil, err } return ret, nil } func doWithCookies(c *http.Client, req *http.Request) (*http.Response, error) { for _, cookie := range c.Jar.Cookies(req.URL) { req.AddCookie(cookie) } return c.Do(req) } // Retrieve the history of a given image from the Registry. // Return a list of the parent's json (requested image included) func (graph *Graph) getRemoteHistory(imgId, registry string, token []string) ([]string, error) { client := graph.getHttpClient() req, err := http.NewRequest("GET", registry+"/images/"+imgId+"/ancestry", nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Token "+strings.Join(token, ", ")) res, err := client.Do(req) if err != nil || res.StatusCode != 200 { if res != nil { return nil, fmt.Errorf("Internal server error: %d trying to fetch remote history for %s", res.StatusCode, imgId) } return nil, err } defer res.Body.Close() jsonString, err := ioutil.ReadAll(res.Body) if err != nil { return nil, fmt.Errorf("Error while reading the http response: %s\n", err) } Debugf("Ancestry: %s", jsonString) history := new([]string) if err := json.Unmarshal(jsonString, history); err != nil { return nil, err } return *history, nil } func (graph *Graph) getHttpClient() *http.Client { if graph.httpClient == nil { graph.httpClient = &http.Client{} graph.httpClient.Jar = cookiejar.NewCookieJar() } return graph.httpClient } // Check if an image exists in the Registry func (graph *Graph) LookupRemoteImage(imgId, registry string, authConfig *auth.AuthConfig) bool { rt := &http.Transport{Proxy: http.ProxyFromEnvironment} req, err := http.NewRequest("GET", registry+"/images/"+imgId+"/json", nil) if err != nil { return false } req.SetBasicAuth(authConfig.Username, authConfig.Password) res, err := rt.RoundTrip(req) return err == nil && res.StatusCode == 307 } func (graph *Graph) getImagesInRepository(repository string, authConfig *auth.AuthConfig) ([]map[string]string, error) { u := INDEX_ENDPOINT + "/repositories/" + repository + "/images" req, err := http.NewRequest("GET", u, nil) if err != nil { return nil, err } if authConfig != nil && len(authConfig.Username) > 0 { req.SetBasicAuth(authConfig.Username, authConfig.Password) } res, err := graph.getHttpClient().Do(req) if err != nil { return nil, err } defer res.Body.Close() // Repository doesn't exist yet if res.StatusCode == 404 { return nil, nil } jsonData, err := ioutil.ReadAll(res.Body) if err != nil { return nil, err } imageList := []map[string]string{} err = json.Unmarshal(jsonData, &imageList) if err != nil { Debugf("Body: %s (%s)\n", res.Body, u) return nil, err } return imageList, nil } // Retrieve an image from the Registry. // Returns the Image object as well as the layer as an Archive (io.Reader) func (graph *Graph) getRemoteImage(stdout io.Writer, imgId, registry string, token []string) (*Image, Archive, error) { client := graph.getHttpClient() fmt.Fprintf(stdout, "Pulling %s metadata\r\n", imgId) // Get the Json req, err := http.NewRequest("GET", registry+"/images/"+imgId+"/json", nil) if err != nil { return nil, nil, fmt.Errorf("Failed to download json: %s", err) } req.Header.Set("Authorization", "Token "+strings.Join(token, ", ")) res, err := client.Do(req) if err != nil { return nil, nil, fmt.Errorf("Failed to download json: %s", err) } if res.StatusCode != 200 { return nil, nil, fmt.Errorf("HTTP code %d", res.StatusCode) } defer res.Body.Close() jsonString, err := ioutil.ReadAll(res.Body) if err != nil { return nil, nil, fmt.Errorf("Failed to download json: %s", err) } img, err := NewImgJson(jsonString) if err != nil { return nil, nil, fmt.Errorf("Failed to parse json: %s", err) } img.Id = imgId // Get the layer fmt.Fprintf(stdout, "Pulling %s fs layer\r\n", imgId) req, err = http.NewRequest("GET", registry+"/images/"+imgId+"/layer", nil) if err != nil { return nil, nil, fmt.Errorf("Error while getting from the server: %s\n", err) } req.Header.Set("Authorization", "Token "+strings.Join(token, ", ")) res, err = client.Do(req) if err != nil { return nil, nil, err } return img, ProgressReader(res.Body, int(res.ContentLength), stdout, "Downloading %v/%v (%v)"), nil } func (graph *Graph) getRemoteTags(stdout io.Writer, registries []string, repository string, token []string) (map[string]string, error) { client := graph.getHttpClient() if strings.Count(repository, "/") == 0 { // This will be removed once the Registry supports auto-resolution on // the "library" namespace repository = "library/" + repository } for _, host := range registries { endpoint := fmt.Sprintf("https://%s/v1/repositories/%s/tags", host, repository) req, err := http.NewRequest("GET", endpoint, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Token "+strings.Join(token, ", ")) res, err := client.Do(req) defer res.Body.Close() Debugf("Got status code %d from %s", res.StatusCode, endpoint) if err != nil || (res.StatusCode != 200 && res.StatusCode != 404) { continue } else if res.StatusCode == 404 { return nil, fmt.Errorf("Repository not found") } result := make(map[string]string) rawJson, err := ioutil.ReadAll(res.Body) if err != nil { return nil, err } if err = json.Unmarshal(rawJson, &result); err != nil { return nil, err } return result, nil } return nil, fmt.Errorf("Could not reach any registry endpoint") } func (graph *Graph) getImageForTag(stdout io.Writer, tag, remote, registry string, token []string) (string, error) { client := graph.getHttpClient() if !strings.Contains(remote, "/") { remote = "library/" + remote } registryEndpoint := "https://" + registry + "/v1" repositoryTarget := registryEndpoint + "/repositories/" + remote + "/tags/" + tag req, err := http.NewRequest("GET", repositoryTarget, nil) if err != nil { return "", err } req.Header.Set("Authorization", "Token "+strings.Join(token, ", ")) res, err := client.Do(req) if err != nil { return "", fmt.Errorf("Error while retrieving repository info: %v", err) } defer res.Body.Close() if res.StatusCode == 403 { return "", fmt.Errorf("You aren't authorized to access this resource") } else if res.StatusCode != 200 { return "", fmt.Errorf("HTTP code: %d", res.StatusCode) } var imgId string rawJson, err := ioutil.ReadAll(res.Body) if err != nil { return "", err } if err = json.Unmarshal(rawJson, &imgId); err != nil { return "", err } return imgId, nil } func (graph *Graph) PullImage(stdout io.Writer, imgId, registry string, token []string) error { history, err := graph.getRemoteHistory(imgId, registry, token) if err != nil { return err } // FIXME: Try to stream the images? // FIXME: Launch the getRemoteImage() in goroutines for _, id := range history { if !graph.Exists(id) { img, layer, err := graph.getRemoteImage(stdout, id, registry, token) if err != nil { // FIXME: Keep goging in case of error? return err } if err = graph.Register(layer, false, img); err != nil { return err } } } return nil } func (graph *Graph) PullRepository(stdout io.Writer, remote, askedTag string, repositories *TagStore, authConfig *auth.AuthConfig) error { client := graph.getHttpClient() fmt.Fprintf(stdout, "Pulling repository %s from %s\r\n", remote, INDEX_ENDPOINT) repositoryTarget := INDEX_ENDPOINT + "/repositories/" + remote + "/images" req, err := http.NewRequest("GET", repositoryTarget, nil) if err != nil { return err } if authConfig != nil && len(authConfig.Username) > 0 { req.SetBasicAuth(authConfig.Username, authConfig.Password) } req.Header.Set("X-Docker-Token", "true") res, err := client.Do(req) if err != nil { return err } defer res.Body.Close() if res.StatusCode == 401 { return fmt.Errorf("Please login first (HTTP code %d)", res.StatusCode) } // TODO: Right now we're ignoring checksums in the response body. // In the future, we need to use them to check image validity. if res.StatusCode != 200 { return fmt.Errorf("HTTP code: %d", res.StatusCode) } var token, endpoints []string if res.Header.Get("X-Docker-Token") != "" { token = res.Header["X-Docker-Token"] } if res.Header.Get("X-Docker-Endpoints") != "" { endpoints = res.Header["X-Docker-Endpoints"] } else { return fmt.Errorf("Index response didn't contain any endpoints") } checksumsJson, err := ioutil.ReadAll(res.Body) if err != nil { return err } // Reload the json file to make sure not to overwrite faster sums err = func() error { localChecksums := make(map[string]string) remoteChecksums := []ImgListJson{} checksumDictPth := path.Join(graph.Root, "checksums") if err := json.Unmarshal(checksumsJson, &remoteChecksums); err != nil { return err } graph.lockSumFile.Lock() defer graph.lockSumFile.Unlock() if checksumDict, err := ioutil.ReadFile(checksumDictPth); err == nil { if err := json.Unmarshal(checksumDict, &localChecksums); err != nil { return err } } for _, elem := range remoteChecksums { localChecksums[elem.Id] = elem.Checksum } checksumsJson, err = json.Marshal(localChecksums) if err != nil { return err } if err := ioutil.WriteFile(checksumDictPth, checksumsJson, 0600); err != nil { return err } return nil }() if err != nil { return err } var tagsList map[string]string if askedTag == "" { tagsList, err = graph.getRemoteTags(stdout, endpoints, remote, token) if err != nil { return err } } else { tagsList = map[string]string{askedTag: ""} } for askedTag, imgId := range tagsList { fmt.Fprintf(stdout, "Resolving tag \"%s:%s\" from %s\n", remote, askedTag, endpoints) success := false for _, registry := range endpoints { if imgId == "" { imgId, err = graph.getImageForTag(stdout, askedTag, remote, registry, token) if err != nil { fmt.Fprintf(stdout, "Error while retrieving image for tag: %v (%v) ; "+ "checking next endpoint", askedTag, err) continue } } if err := graph.PullImage(stdout, imgId, "https://"+registry+"/v1", token); err != nil { return err } if err = repositories.Set(remote, askedTag, imgId, true); err != nil { return err } success = true } if !success { return fmt.Errorf("Could not find repository on any of the indexed registries.") } } if err = repositories.Save(); err != nil { return err } return nil } // Push a local image to the registry func (graph *Graph) PushImage(stdout io.Writer, img *Image, registry string, token []string) error { registry = "https://" + registry + "/v1" client := graph.getHttpClient() jsonRaw, err := ioutil.ReadFile(path.Join(graph.Root, img.Id, "json")) if err != nil { return fmt.Errorf("Error while retreiving the path for {%s}: %s", img.Id, err) } fmt.Fprintf(stdout, "Pushing %s metadata\r\n", img.Id) // FIXME: try json with UTF8 jsonData := strings.NewReader(string(jsonRaw)) req, err := http.NewRequest("PUT", registry+"/images/"+img.Id+"/json", jsonData) if err != nil { return err } req.Header.Add("Content-type", "application/json") req.Header.Set("Authorization", "Token "+strings.Join(token, ",")) checksum, err := img.Checksum() if err != nil { return fmt.Errorf("Error while retrieving checksum for %s: %v", img.Id, err) } req.Header.Set("X-Docker-Checksum", checksum) Debugf("Setting checksum for %s: %s", img.ShortId(), checksum) res, err := doWithCookies(client, req) if err != nil { return fmt.Errorf("Failed to upload metadata: %s", err) } defer res.Body.Close() if len(res.Cookies()) > 0 { client.Jar.SetCookies(req.URL, res.Cookies()) } if res.StatusCode != 200 { errBody, err := ioutil.ReadAll(res.Body) if err != nil { return fmt.Errorf("HTTP code %d while uploading metadata and error when"+ " trying to parse response body: %v", res.StatusCode, err) } var jsonBody map[string]string if err := json.Unmarshal(errBody, &jsonBody); err != nil { errBody = []byte(err.Error()) } else if jsonBody["error"] == "Image already exists" { fmt.Fprintf(stdout, "Image %v already uploaded ; skipping\n", img.Id) return nil } return fmt.Errorf("HTTP code %d while uploading metadata: %s", res.StatusCode, errBody) } fmt.Fprintf(stdout, "Pushing %s fs layer\r\n", img.Id) root, err := img.root() if err != nil { return err } var layerData *TempArchive // If the archive exists, use it file, err := os.Open(layerArchivePath(root)) if err != nil { if os.IsNotExist(err) { // If the archive does not exist, create one from the layer layerData, err = graph.TempLayerArchive(img.Id, Xz, stdout) if err != nil { return fmt.Errorf("Failed to generate layer archive: %s", err) } } else { return err } } else { defer file.Close() st, err := file.Stat() if err != nil { return err } layerData = &TempArchive{file, st.Size()} } req3, err := http.NewRequest("PUT", registry+"/images/"+img.Id+"/layer", ProgressReader(layerData, int(layerData.Size), stdout, "")) if err != nil { return err } req3.ContentLength = -1 req3.TransferEncoding = []string{"chunked"} req3.Header.Set("Authorization", "Token "+strings.Join(token, ",")) res3, err := doWithCookies(client, req3) if err != nil { return fmt.Errorf("Failed to upload layer: %s", err) } defer res3.Body.Close() if res3.StatusCode != 200 { errBody, err := ioutil.ReadAll(res3.Body) if err != nil { return fmt.Errorf("HTTP code %d while uploading metadata and error when"+ " trying to parse response body: %v", res.StatusCode, err) } return fmt.Errorf("Received HTTP code %d while uploading layer: %s", res3.StatusCode, errBody) } return nil } // push a tag on the registry. // Remote has the format '/ func (graph *Graph) pushTag(remote, revision, tag, registry string, token []string) error { // "jsonify" the string revision = "\"" + revision + "\"" registry = "https://" + registry + "/v1" Debugf("Pushing tags for rev [%s] on {%s}\n", revision, registry+"/users/"+remote+"/"+tag) client := graph.getHttpClient() req, err := http.NewRequest("PUT", registry+"/repositories/"+remote+"/tags/"+tag, strings.NewReader(revision)) if err != nil { return err } req.Header.Add("Content-type", "application/json") req.Header.Set("Authorization", "Token "+strings.Join(token, ",")) req.ContentLength = int64(len(revision)) res, err := doWithCookies(client, req) if err != nil { return err } res.Body.Close() if res.StatusCode != 200 && res.StatusCode != 201 { return fmt.Errorf("Internal server error: %d trying to push tag %s on %s", res.StatusCode, tag, remote) } return nil } // FIXME: this should really be PushTag func (graph *Graph) pushPrimitive(stdout io.Writer, remote, tag, imgId, registry string, token []string) error { // Check if the local impage exists img, err := graph.Get(imgId) if err != nil { fmt.Fprintf(stdout, "Skipping tag %s:%s: %s does not exist\r\n", remote, tag, imgId) return nil } fmt.Fprintf(stdout, "Pushing image %s:%s\r\n", remote, tag) // Push the image if err = graph.PushImage(stdout, img, registry, token); err != nil { return err } fmt.Fprintf(stdout, "Registering tag %s:%s\r\n", remote, tag) // And then the tag if err = graph.pushTag(remote, imgId, tag, registry, token); err != nil { return err } return nil } // Retrieve the checksum of an image // Priority: // - Check on the stored checksums // - Check if the archive exists, if it does not, ask the registry // - If the archive does exists, process the checksum from it // - If the archive does not exists and not found on registry, process checksum from layer func (graph *Graph) getChecksum(imageId string) (string, error) { // FIXME: Use in-memory map instead of reading the file each time if sums, err := graph.getStoredChecksums(); err != nil { return "", err } else if checksum, exists := sums[imageId]; exists { return checksum, nil } img, err := graph.Get(imageId) if err != nil { return "", err } if _, err := os.Stat(layerArchivePath(graph.imageRoot(imageId))); err != nil { if os.IsNotExist(err) { // TODO: Ask the registry for the checksum // As the archive is not there, it is supposed to come from a pull. } else { return "", err } } checksum, err := img.Checksum() if err != nil { return "", err } return checksum, nil } type ImgListJson struct { Id string `json:"id"` Checksum string `json:"checksum,omitempty"` tag string } // Push a repository to the registry. // Remote has the format '/ func (graph *Graph) PushRepository(stdout io.Writer, remote string, localRepo Repository, authConfig *auth.AuthConfig) error { client := graph.getHttpClient() // FIXME: Do not reset the cookie each time? (need to reset it in case updating latest of a repo and repushing) client.Jar = cookiejar.NewCookieJar() var imgList []*ImgListJson fmt.Fprintf(stdout, "Processing checksums\n") imageSet := make(map[string]struct{}) for tag, id := range localRepo { img, err := graph.Get(id) if err != nil { return err } img.WalkHistory(func(img *Image) error { if _, exists := imageSet[img.Id]; exists { return nil } imageSet[img.Id] = struct{}{} checksum, err := graph.getChecksum(img.Id) if err != nil { return err } imgList = append([]*ImgListJson{{ Id: img.Id, Checksum: checksum, tag: tag, }}, imgList...) return nil }) } imgListJson, err := json.Marshal(imgList) if err != nil { return err } Debugf("json sent: %s\n", imgListJson) fmt.Fprintf(stdout, "Sending image list\n") req, err := http.NewRequest("PUT", INDEX_ENDPOINT+"/repositories/"+remote+"/", bytes.NewReader(imgListJson)) if err != nil { return err } req.SetBasicAuth(authConfig.Username, authConfig.Password) req.ContentLength = int64(len(imgListJson)) req.Header.Set("X-Docker-Token", "true") res, err := client.Do(req) if err != nil { return err } defer res.Body.Close() for res.StatusCode >= 300 && res.StatusCode < 400 { Debugf("Redirected to %s\n", res.Header.Get("Location")) req, err = http.NewRequest("PUT", res.Header.Get("Location"), bytes.NewReader(imgListJson)) if err != nil { return err } req.SetBasicAuth(authConfig.Username, authConfig.Password) req.ContentLength = int64(len(imgListJson)) req.Header.Set("X-Docker-Token", "true") res, err = client.Do(req) if err != nil { return err } defer res.Body.Close() } if res.StatusCode != 200 && res.StatusCode != 201 { errBody, err := ioutil.ReadAll(res.Body) if err != nil { return err } return fmt.Errorf("Error: Status %d trying to push repository %s: %s", res.StatusCode, remote, errBody) } var token, endpoints []string if res.Header.Get("X-Docker-Token") != "" { token = res.Header["X-Docker-Token"] Debugf("Auth token: %v", token) } else { return fmt.Errorf("Index response didn't contain an access token") } if res.Header.Get("X-Docker-Endpoints") != "" { endpoints = res.Header["X-Docker-Endpoints"] } else { return fmt.Errorf("Index response didn't contain any endpoints") } // FIXME: Send only needed images for _, registry := range endpoints { fmt.Fprintf(stdout, "Pushing repository %s to %s (%d tags)\r\n", remote, registry, len(localRepo)) // For each image within the repo, push them for _, elem := range imgList { if err := graph.pushPrimitive(stdout, remote, elem.tag, elem.Id, registry, token); err != nil { // FIXME: Continue on error? return err } } } req2, err := http.NewRequest("PUT", INDEX_ENDPOINT+"/repositories/"+remote+"/images", bytes.NewReader(imgListJson)) if err != nil { return err } req2.SetBasicAuth(authConfig.Username, authConfig.Password) req2.Header["X-Docker-Endpoints"] = endpoints req2.ContentLength = int64(len(imgListJson)) res2, err := client.Do(req2) if err != nil { return err } defer res2.Body.Close() if res2.StatusCode != 204 { if errBody, err := ioutil.ReadAll(res2.Body); err != nil { return err } else { return fmt.Errorf("Error: Status %d trying to push checksums %s: %s", res2.StatusCode, remote, errBody) } } return nil } type SearchResults struct { Query string `json:"query"` NumResults int `json:"num_results"` Results []map[string]string `json:"results"` } func (graph *Graph) SearchRepositories(stdout io.Writer, term string) (*SearchResults, error) { client := graph.getHttpClient() u := INDEX_ENDPOINT + "/search?q=" + url.QueryEscape(term) req, err := http.NewRequest("GET", u, nil) if err != nil { return nil, err } res, err := client.Do(req) if err != nil { return nil, err } defer res.Body.Close() if res.StatusCode != 200 { return nil, fmt.Errorf("Unexepected status code %d", res.StatusCode) } rawData, err := ioutil.ReadAll(res.Body) if err != nil { return nil, err } result := new(SearchResults) err = json.Unmarshal(rawData, result) return result, err }