mirror of https://github.com/docker/docs.git
libmachine/mcnutils: check the version of cached iso and download the latest one when it is out-of-date
Signed-off-by: Soshi Katsuta <soshi.katsuta@gmail.com>
This commit is contained in:
parent
98a1c8c159
commit
bbe7a00db5
|
@ -2,6 +2,7 @@ package mcnutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -15,10 +16,22 @@ import (
|
||||||
"github.com/docker/machine/libmachine/log"
|
"github.com/docker/machine/libmachine/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultURL = "https://api.github.com/repos/boot2docker/boot2docker/releases/latest"
|
||||||
|
defaultISOFilename = "boot2docker.iso"
|
||||||
|
defaultVolumeIDOffset = int64(0x8028)
|
||||||
|
defaultVolumeIDLength = 32
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
GithubAPIToken string
|
GithubAPIToken string
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errGitHubAPIResponse = errors.New(`Error getting a version tag from the Github API response.
|
||||||
|
You may be getting rate limited by Github.`)
|
||||||
|
)
|
||||||
|
|
||||||
func defaultTimeout(network, addr string) (net.Conn, error) {
|
func defaultTimeout(network, addr string) (net.Conn, error) {
|
||||||
return net.Dial(network, addr)
|
return net.Dial(network, addr)
|
||||||
}
|
}
|
||||||
|
@ -37,28 +50,7 @@ func getClient() *http.Client {
|
||||||
return &client
|
return &client
|
||||||
}
|
}
|
||||||
|
|
||||||
type B2dUtils struct {
|
func getRequest(apiURL string) (*http.Request, error) {
|
||||||
storePath string
|
|
||||||
isoFilename string
|
|
||||||
commonIsoPath string
|
|
||||||
imgCachePath string
|
|
||||||
githubAPIBaseURL string
|
|
||||||
githubBaseURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewB2dUtils(storePath string) *B2dUtils {
|
|
||||||
imgCachePath := filepath.Join(storePath, "cache")
|
|
||||||
isoFilename := "boot2docker.iso"
|
|
||||||
|
|
||||||
return &B2dUtils{
|
|
||||||
storePath: storePath,
|
|
||||||
isoFilename: isoFilename,
|
|
||||||
imgCachePath: imgCachePath,
|
|
||||||
commonIsoPath: filepath.Join(imgCachePath, isoFilename),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *B2dUtils) getReleasesRequest(apiURL string) (*http.Request, error) {
|
|
||||||
req, err := http.NewRequest("GET", apiURL, nil)
|
req, err := http.NewRequest("GET", apiURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -71,68 +63,92 @@ func (b *B2dUtils) getReleasesRequest(apiURL string) (*http.Request, error) {
|
||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLatestBoot2DockerReleaseURL gets the latest boot2docker release tag name (e.g. "v0.6.0").
|
// releaseGetter is a client that gets release information of a product and downloads it.
|
||||||
// FIXME: find or create some other way to get the "latest release" of boot2docker since the GitHub API has a pretty low rate limit on API requests
|
type releaseGetter interface {
|
||||||
func (b *B2dUtils) GetLatestBoot2DockerReleaseURL(apiURL string) (string, error) {
|
// filename returns filename of the product.
|
||||||
|
filename() string
|
||||||
|
// getReleaseTag gets a release tag from the given URL.
|
||||||
|
getReleaseTag(apiURL string) (string, error)
|
||||||
|
// getReleaseURL gets the latest release download URL from the given URL.
|
||||||
|
getReleaseURL(apiURL string) (string, error)
|
||||||
|
// download downloads a file from the given dlURL and saves it under dir.
|
||||||
|
download(dir, file, dlURL string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// b2dReleaseGetter implements the releaseGetter interface for getting the release of Boot2Docker.
|
||||||
|
type b2dReleaseGetter struct {
|
||||||
|
isoFilename string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *b2dReleaseGetter) filename() string {
|
||||||
|
if b == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return b.isoFilename
|
||||||
|
}
|
||||||
|
|
||||||
|
// getReleaseTag gets the release tag of Boot2Docker from apiURL.
|
||||||
|
func (*b2dReleaseGetter) getReleaseTag(apiURL string) (string, error) {
|
||||||
if apiURL == "" {
|
if apiURL == "" {
|
||||||
apiURL = "https://api.github.com/repos/boot2docker/boot2docker/releases"
|
apiURL = defaultURL
|
||||||
}
|
}
|
||||||
isoURL := ""
|
|
||||||
|
client := getClient()
|
||||||
|
req, err := getRequest(apiURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
rsp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer rsp.Body.Close()
|
||||||
|
|
||||||
|
var t struct {
|
||||||
|
TagName string `json:"tag_name"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(rsp.Body).Decode(&t); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if t.TagName == "" {
|
||||||
|
return "", errGitHubAPIResponse
|
||||||
|
}
|
||||||
|
return t.TagName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getReleaseURL gets the latest release URL of Boot2Docker.
|
||||||
|
// FIXME: find or create some other way to get the "latest release" of boot2docker since the GitHub API has a pretty low rate limit on API requests
|
||||||
|
func (b *b2dReleaseGetter) getReleaseURL(apiURL string) (string, error) {
|
||||||
|
if apiURL == "" {
|
||||||
|
apiURL = defaultURL
|
||||||
|
}
|
||||||
|
|
||||||
// match github (enterprise) release urls:
|
// match github (enterprise) release urls:
|
||||||
// https://api.github.com/repos/../../releases or
|
// https://api.github.com/repos/../../releases/latest or
|
||||||
// https://some.github.enterprise/api/v3/repos/../../releases
|
// https://some.github.enterprise/api/v3/repos/../../releases/latest
|
||||||
re := regexp.MustCompile("(https?)://([^/]+)(/api/v3)?/repos/([^/]+)/([^/]+)/releases")
|
re := regexp.MustCompile("(https?)://([^/]+)(/api/v3)?/repos/([^/]+)/([^/]+)/releases/latest")
|
||||||
if matches := re.FindStringSubmatch(apiURL); len(matches) == 6 {
|
matches := re.FindStringSubmatch(apiURL)
|
||||||
scheme := matches[1]
|
if len(matches) != 6 {
|
||||||
host := matches[2]
|
// does not match a github releases api URL
|
||||||
org := matches[4]
|
return apiURL, nil
|
||||||
repo := matches[5]
|
|
||||||
if host == "api.github.com" {
|
|
||||||
host = "github.com"
|
|
||||||
}
|
|
||||||
client := getClient()
|
|
||||||
req, err := b.getReleasesRequest(apiURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
rsp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer rsp.Body.Close()
|
|
||||||
|
|
||||||
var t []struct {
|
|
||||||
TagName string `json:"tag_name"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(rsp.Body).Decode(&t); err != nil {
|
|
||||||
return "", fmt.Errorf("Error demarshaling the Github API response: %s\nYou may be getting rate limited by Github.", err)
|
|
||||||
}
|
|
||||||
if len(t) == 0 {
|
|
||||||
return "", fmt.Errorf("no releases found")
|
|
||||||
}
|
|
||||||
|
|
||||||
tag := t[0].TagName
|
|
||||||
log.Infof("Latest release for %s/%s/%s is %s", host, org, repo, tag)
|
|
||||||
isoURL = fmt.Sprintf("%s://%s/%s/%s/releases/download/%s/boot2docker.iso", scheme, host, org, repo, tag)
|
|
||||||
} else {
|
|
||||||
//does not match a github releases api url
|
|
||||||
isoURL = apiURL
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return isoURL, nil
|
scheme, host, org, repo := matches[1], matches[2], matches[4], matches[5]
|
||||||
}
|
if host == "api.github.com" {
|
||||||
|
host = "github.com"
|
||||||
func removeFileIfExists(name string) error {
|
|
||||||
if _, err := os.Stat(name); err == nil {
|
|
||||||
if err := os.Remove(name); err != nil {
|
|
||||||
return fmt.Errorf("Error removing temporary download file: %s", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
tag, err := b.getReleaseTag(apiURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Latest release for %s/%s/%s is %s", host, org, repo, tag)
|
||||||
|
url := fmt.Sprintf("%s://%s/%s/%s/releases/download/%s/%s", scheme, host, org, repo, tag, b.isoFilename)
|
||||||
|
return url, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadISO downloads boot2docker ISO image for the given tag and save it at dest.
|
func (*b2dReleaseGetter) download(dir, file, isoURL string) error {
|
||||||
func (b *B2dUtils) DownloadISO(dir, file, isoURL string) error {
|
|
||||||
u, err := url.Parse(isoURL)
|
u, err := url.Parse(isoURL)
|
||||||
|
|
||||||
var src io.ReadCloser
|
var src io.ReadCloser
|
||||||
|
@ -195,6 +211,103 @@ func (b *B2dUtils) DownloadISO(dir, file, isoURL string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// iso is an ISO volume.
|
||||||
|
type iso interface {
|
||||||
|
// path returns the path of the ISO.
|
||||||
|
path() string
|
||||||
|
// exists reports whether the ISO exists.
|
||||||
|
exists() bool
|
||||||
|
// version returns version information of the ISO.
|
||||||
|
version() (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// b2dISO represents a Boot2Docker ISO. It implements the ISO interface.
|
||||||
|
type b2dISO struct {
|
||||||
|
// path of Boot2Docker ISO
|
||||||
|
commonIsoPath string
|
||||||
|
|
||||||
|
// offset and length of ISO volume ID
|
||||||
|
// cf. http://serverfault.com/questions/361474/is-there-a-way-to-change-a-iso-files-volume-id-from-the-command-line
|
||||||
|
volumeIDOffset int64
|
||||||
|
volumeIDLength int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *b2dISO) path() string {
|
||||||
|
if b == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return b.commonIsoPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *b2dISO) exists() bool {
|
||||||
|
if b == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := os.Stat(b.commonIsoPath)
|
||||||
|
return !os.IsNotExist(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// version scans the volume ID in b and returns its version tag.
|
||||||
|
func (b *b2dISO) version() (string, error) {
|
||||||
|
if b == nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
iso, err := os.Open(b.commonIsoPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer iso.Close()
|
||||||
|
|
||||||
|
isoMetadata := make([]byte, b.volumeIDLength)
|
||||||
|
_, err = iso.ReadAt(isoMetadata, b.volumeIDOffset)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
verRegex := regexp.MustCompile(`v\d+\.\d+\.\d+`)
|
||||||
|
ver := string(verRegex.Find(isoMetadata))
|
||||||
|
log.Debug("local Boot2Docker ISO version: ", ver)
|
||||||
|
return ver, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeFileIfExists(name string) error {
|
||||||
|
if _, err := os.Stat(name); err == nil {
|
||||||
|
if err := os.Remove(name); err != nil {
|
||||||
|
return fmt.Errorf("Error removing temporary download file: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type B2dUtils struct {
|
||||||
|
releaseGetter
|
||||||
|
iso
|
||||||
|
storePath string
|
||||||
|
imgCachePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewB2dUtils(storePath string) *B2dUtils {
|
||||||
|
imgCachePath := filepath.Join(storePath, "cache")
|
||||||
|
|
||||||
|
return &B2dUtils{
|
||||||
|
releaseGetter: &b2dReleaseGetter{isoFilename: defaultISOFilename},
|
||||||
|
iso: &b2dISO{
|
||||||
|
commonIsoPath: filepath.Join(imgCachePath, defaultISOFilename),
|
||||||
|
volumeIDOffset: defaultVolumeIDOffset,
|
||||||
|
volumeIDLength: defaultVolumeIDLength,
|
||||||
|
},
|
||||||
|
storePath: storePath,
|
||||||
|
imgCachePath: imgCachePath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadISO downloads boot2docker ISO image for the given tag and save it at dest.
|
||||||
|
func (b *B2dUtils) DownloadISO(dir, file, isoURL string) error {
|
||||||
|
return b.download(dir, file, isoURL)
|
||||||
|
}
|
||||||
|
|
||||||
type ReaderWithProgress struct {
|
type ReaderWithProgress struct {
|
||||||
io.ReadCloser
|
io.ReadCloser
|
||||||
out io.Writer
|
out io.Writer
|
||||||
|
@ -229,7 +342,7 @@ func (r *ReaderWithProgress) Close() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *B2dUtils) DownloadLatestBoot2Docker(apiURL string) error {
|
func (b *B2dUtils) DownloadLatestBoot2Docker(apiURL string) error {
|
||||||
latestReleaseURL, err := b.GetLatestBoot2DockerReleaseURL(apiURL)
|
latestReleaseURL, err := b.getReleaseURL(apiURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -238,8 +351,8 @@ func (b *B2dUtils) DownloadLatestBoot2Docker(apiURL string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *B2dUtils) DownloadISOFromURL(latestReleaseURL string) error {
|
func (b *B2dUtils) DownloadISOFromURL(latestReleaseURL string) error {
|
||||||
log.Infof("Downloading %s to %s...", latestReleaseURL, b.commonIsoPath)
|
log.Infof("Downloading %s to %s...", latestReleaseURL, b.path())
|
||||||
if err := b.DownloadISO(b.imgCachePath, b.isoFilename, latestReleaseURL); err != nil {
|
if err := b.DownloadISO(b.imgCachePath, b.filename(), latestReleaseURL); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,7 +362,7 @@ func (b *B2dUtils) DownloadISOFromURL(latestReleaseURL string) error {
|
||||||
func (b *B2dUtils) CopyIsoToMachineDir(isoURL, machineName string) error {
|
func (b *B2dUtils) CopyIsoToMachineDir(isoURL, machineName string) error {
|
||||||
// TODO: This is a bit off-color.
|
// TODO: This is a bit off-color.
|
||||||
machineDir := filepath.Join(b.storePath, "machines", machineName)
|
machineDir := filepath.Join(b.storePath, "machines", machineName)
|
||||||
machineIsoPath := filepath.Join(machineDir, b.isoFilename)
|
machineIsoPath := filepath.Join(machineDir, b.filename())
|
||||||
|
|
||||||
// just in case the cache dir has been manually deleted,
|
// just in case the cache dir has been manually deleted,
|
||||||
// check for it and recreate it if it's gone
|
// check for it and recreate it if it's gone
|
||||||
|
@ -260,39 +373,69 @@ func (b *B2dUtils) CopyIsoToMachineDir(isoURL, machineName string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// By default just copy the existing "cached" iso to
|
// By default just copy the existing "cached" iso to the machine's directory...
|
||||||
// the machine's directory...
|
|
||||||
if isoURL == "" {
|
if isoURL == "" {
|
||||||
if err := b.copyDefaultIsoToMachine(machineIsoPath); err != nil {
|
return b.copyDefaultISOToMachine(machineIsoPath)
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
//if ISO is specified, check if it matches a github releases url or fallback
|
|
||||||
//to a direct download
|
|
||||||
if downloadURL, err := b.GetLatestBoot2DockerReleaseURL(isoURL); err == nil {
|
|
||||||
log.Infof("Downloading %s from %s...", b.isoFilename, downloadURL)
|
|
||||||
if err := b.DownloadISO(machineDir, b.isoFilename, downloadURL); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// if ISO is specified, check if it matches a github releases url or fallback to a direct download
|
||||||
}
|
downloadURL, err := b.getReleaseURL(isoURL)
|
||||||
|
if err != nil {
|
||||||
func (b *B2dUtils) copyDefaultIsoToMachine(machineIsoPath string) error {
|
|
||||||
if _, err := os.Stat(b.commonIsoPath); os.IsNotExist(err) {
|
|
||||||
log.Info("No default boot2docker iso found locally, downloading the latest release...")
|
|
||||||
if err := b.DownloadLatestBoot2Docker(""); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := CopyFile(b.commonIsoPath, machineIsoPath); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
log.Infof("Downloading %s from %s...", b.filename(), downloadURL)
|
||||||
|
return b.DownloadISO(machineDir, b.filename(), downloadURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *B2dUtils) copyDefaultISOToMachine(machineIsoPath string) error {
|
||||||
|
// just in case the cache dir has been manually deleted,
|
||||||
|
// check for it and recreate it if it's gone
|
||||||
|
if _, err := os.Stat(b.imgCachePath); os.IsNotExist(err) {
|
||||||
|
log.Infof("Image cache directory does not exist, creating it at %s...", b.imgCachePath)
|
||||||
|
if err := os.Mkdir(b.imgCachePath, 0700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exists := b.exists()
|
||||||
|
latest := b.isLatest()
|
||||||
|
|
||||||
|
if exists && latest {
|
||||||
|
log.Infof("Latest Boot2Docker ISO found locally, copying it to %s...", machineIsoPath)
|
||||||
|
return CopyFile(b.path(), machineIsoPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
log.Info("No default Boot2Docker ISO found locally, downloading the latest release...")
|
||||||
|
} else if !latest {
|
||||||
|
log.Info("Default Boot2Docker ISO is out-of-date, downloading the latest release...")
|
||||||
|
}
|
||||||
|
if err := b.DownloadLatestBoot2Docker(""); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Copying %s to %s...", b.path(), machineIsoPath)
|
||||||
|
return CopyFile(b.path(), machineIsoPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isLatest checks the latest release tag and
|
||||||
|
// reports whether the local ISO cache is the latest version.
|
||||||
|
//
|
||||||
|
// It returns false if failing to get the local ISO version
|
||||||
|
// and true if failing to fetch the latest release tag.
|
||||||
|
func (b *B2dUtils) isLatest() bool {
|
||||||
|
localVer, err := b.version()
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Unable to get the local Boot2Docker ISO version: ", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
latestVer, err := b.getReleaseTag("")
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Unable to get the latest Boot2Docker ISO release version: ", err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return localVer == latestVer
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,84 @@
|
||||||
package mcnutils
|
package mcnutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"bytes"
|
"github.com/docker/machine/libmachine/log"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetLatestBoot2DockerReleaseUrl(t *testing.T) {
|
func TestGetReleaseURL(t *testing.T) {
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
ts := newTestServer(`{"tag_name": "v0.1"}`)
|
||||||
respText := `[{"tag_name": "0.1"}]`
|
|
||||||
w.Write([]byte(respText))
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
b := NewB2dUtils("/tmp/isos")
|
testCases := []struct {
|
||||||
isoURL, err := b.GetLatestBoot2DockerReleaseURL(ts.URL + "/repos/org/repo/releases")
|
apiURL string
|
||||||
|
isoURL string
|
||||||
|
}{
|
||||||
|
{ts.URL + "/repos/org/repo/releases/latest", ts.URL + "/org/repo/releases/download/v0.1/boot2docker.iso"},
|
||||||
|
{"http://dummy.com/boot2docker.iso", "http://dummy.com/boot2docker.iso"},
|
||||||
|
}
|
||||||
|
|
||||||
assert.NoError(t, err)
|
for _, tt := range testCases {
|
||||||
assert.Equal(t, fmt.Sprintf("%s/org/repo/releases/download/0.1/boot2docker.iso", ts.URL), isoURL)
|
b := NewB2dUtils("/tmp/isos")
|
||||||
|
isoURL, err := b.getReleaseURL(tt.apiURL)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, isoURL, tt.isoURL)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDownloadIso(t *testing.T) {
|
func TestGetReleaseURLError(t *testing.T) {
|
||||||
|
// GitHub API error response in case of rate limit
|
||||||
|
ts := newTestServer(`{"message": "API rate limit exceeded for 127.0.0.1.",
|
||||||
|
"documentation_url": "https://developer.github.com/v3/#rate-limiting"}`)
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
apiURL string
|
||||||
|
}{
|
||||||
|
{ts.URL + "/repos/org/repo/releases/latest"},
|
||||||
|
{"http://127.0.0.1/repos/org/repo/releases/latest"}, // dummy API URL. cannot connect it.
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range testCases {
|
||||||
|
b := NewB2dUtils("/tmp/isos")
|
||||||
|
_, err := b.getReleaseURL(tt.apiURL)
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVersion(t *testing.T) {
|
||||||
|
want := "v0.1.0"
|
||||||
|
isopath, off, err := newDummyISO("", defaultISOFilename, want)
|
||||||
|
defer removeFileIfExists(isopath)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
b := &b2dISO{
|
||||||
|
commonIsoPath: isopath,
|
||||||
|
volumeIDOffset: off,
|
||||||
|
volumeIDLength: defaultVolumeIDLength,
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := b.version()
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, want, string(got))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloadISO(t *testing.T) {
|
||||||
testData := "test-download"
|
testData := "test-download"
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
ts := newTestServer(testData)
|
||||||
w.Write([]byte(testData))
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
filename := "test"
|
filename := "test"
|
||||||
|
@ -51,25 +98,23 @@ func TestDownloadIso(t *testing.T) {
|
||||||
assert.Equal(t, testData, string(data))
|
assert.Equal(t, testData, string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetReleasesRequestNoToken(t *testing.T) {
|
func TestGetRequest(t *testing.T) {
|
||||||
GithubAPIToken = ""
|
testCases := []struct {
|
||||||
|
token string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
{"CATBUG", "token CATBUG"},
|
||||||
|
}
|
||||||
|
|
||||||
b2d := NewB2dUtils("/tmp/store")
|
for _, tt := range testCases {
|
||||||
req, err := b2d.getReleasesRequest("http://some.github.api")
|
GithubAPIToken = tt.token
|
||||||
|
|
||||||
assert.NoError(t, err)
|
req, err := getRequest("http://some.github.api")
|
||||||
assert.Empty(t, req.Header.Get("Authorization"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetReleasesRequest(t *testing.T) {
|
assert.NoError(t, err)
|
||||||
expectedToken := "CATBUG"
|
assert.Equal(t, tt.want, req.Header.Get("Authorization"))
|
||||||
GithubAPIToken = expectedToken
|
}
|
||||||
|
|
||||||
b2d := NewB2dUtils("/tmp/store")
|
|
||||||
req, err := b2d.getReleasesRequest("http://some.github.api")
|
|
||||||
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, fmt.Sprintf("token %s", expectedToken), req.Header.Get("Authorization"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type MockReadCloser struct {
|
type MockReadCloser struct {
|
||||||
|
@ -110,3 +155,164 @@ func TestReaderWithProgress(t *testing.T) {
|
||||||
readerWithProgress.Close()
|
readerWithProgress.Close()
|
||||||
assert.Equal(t, "0%....10%....20%....30%....40%....50%....60%....70%....80%....90%....100%\n", output.String())
|
assert.Equal(t, "0%....10%....20%....30%....40%....50%....60%....70%....80%....90%....100%\n", output.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mockReleaseGetter struct {
|
||||||
|
ver string
|
||||||
|
apiErr error
|
||||||
|
verCh chan<- string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockReleaseGetter) filename() string {
|
||||||
|
return defaultISOFilename
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockReleaseGetter) getReleaseTag(apiURL string) (string, error) {
|
||||||
|
return m.ver, m.apiErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockReleaseGetter) getReleaseURL(apiURL string) (string, error) {
|
||||||
|
return "http://127.0.0.1/dummy", m.apiErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockReleaseGetter) download(dir, file, isoURL string) error {
|
||||||
|
path := filepath.Join(dir, file)
|
||||||
|
var err error
|
||||||
|
if _, e := os.Stat(path); os.IsNotExist(e) {
|
||||||
|
err = ioutil.WriteFile(path, dummyISOData(" ", m.ver), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// send a signal of downloading the latest version
|
||||||
|
m.verCh <- m.ver
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockISO struct {
|
||||||
|
isopath string
|
||||||
|
exist bool
|
||||||
|
ver string
|
||||||
|
verCh <-chan string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockISO) path() string {
|
||||||
|
return m.isopath
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockISO) exists() bool {
|
||||||
|
return m.exist
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockISO) version() (string, error) {
|
||||||
|
select {
|
||||||
|
// receive version of a downloaded iso
|
||||||
|
case ver := <-m.verCh:
|
||||||
|
return ver, nil
|
||||||
|
default:
|
||||||
|
return m.ver, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCopyDefaultISOToMachine(t *testing.T) {
|
||||||
|
apiErr := errors.New("api error")
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
machineName string
|
||||||
|
create bool
|
||||||
|
localVer string
|
||||||
|
latestVer string
|
||||||
|
apiErr error
|
||||||
|
wantVer string
|
||||||
|
}{
|
||||||
|
{"none", false, "", "v1.0.0", nil, "v1.0.0"}, // none => downloading
|
||||||
|
{"latest", true, "v1.0.0", "v1.0.0", nil, "v1.0.0"}, // latest iso => as is
|
||||||
|
{"old-badurl", true, "v0.1.0", "", apiErr, "v0.1.0"}, // old iso with bad api => as is
|
||||||
|
{"old", true, "v0.1.0", "v1.0.0", nil, "v1.0.0"}, // old iso => updating
|
||||||
|
}
|
||||||
|
|
||||||
|
var isopath string
|
||||||
|
var err error
|
||||||
|
verCh := make(chan string, 1)
|
||||||
|
for _, tt := range testCases {
|
||||||
|
if tt.create {
|
||||||
|
isopath, _, err = newDummyISO("cache", defaultISOFilename, tt.localVer)
|
||||||
|
} else {
|
||||||
|
if dir, e := ioutil.TempDir("", "machine-test"); e == nil {
|
||||||
|
isopath = filepath.Join(dir, "cache", defaultISOFilename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isopath: "$TMPDIR/machine-test-xxxxxx/cache/boot2docker.iso"
|
||||||
|
// tmpDir: "$TMPDIR/machine-test-xxxxxx"
|
||||||
|
imgCachePath := filepath.Dir(isopath)
|
||||||
|
storePath := filepath.Dir(imgCachePath)
|
||||||
|
|
||||||
|
b := &B2dUtils{
|
||||||
|
releaseGetter: &mockReleaseGetter{
|
||||||
|
ver: tt.latestVer,
|
||||||
|
apiErr: tt.apiErr,
|
||||||
|
verCh: verCh,
|
||||||
|
},
|
||||||
|
iso: &mockISO{
|
||||||
|
isopath: isopath,
|
||||||
|
exist: tt.create,
|
||||||
|
ver: tt.localVer,
|
||||||
|
verCh: verCh,
|
||||||
|
},
|
||||||
|
storePath: storePath,
|
||||||
|
imgCachePath: imgCachePath,
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := filepath.Join(storePath, tt.machineName)
|
||||||
|
err = os.MkdirAll(dir, 0700)
|
||||||
|
|
||||||
|
assert.NoError(t, err, "machine: %s", tt.machineName)
|
||||||
|
|
||||||
|
dest := filepath.Join(dir, b.filename())
|
||||||
|
err = b.copyDefaultISOToMachine(dest)
|
||||||
|
_, pathErr := os.Stat(dest)
|
||||||
|
|
||||||
|
assert.NoError(t, err, "machine: %s", tt.machineName)
|
||||||
|
assert.True(t, !os.IsNotExist(pathErr), "machine: %s", tt.machineName)
|
||||||
|
|
||||||
|
ver, err := b.version()
|
||||||
|
|
||||||
|
assert.NoError(t, err, "machine: %s", tt.machineName)
|
||||||
|
assert.Equal(t, tt.wantVer, ver, "machine: %s", tt.machineName)
|
||||||
|
|
||||||
|
err = removeFileIfExists(isopath)
|
||||||
|
assert.NoError(t, err, "machine: %s", tt.machineName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newTestServer creates a new httptest.Server that returns respText as a response body.
|
||||||
|
func newTestServer(respText string) *httptest.Server {
|
||||||
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte(respText))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// newDummyISO creates a dummy ISO file that contains the given version info,
|
||||||
|
// and returns its path and offset value to fetch the version info.
|
||||||
|
func newDummyISO(dir, name, version string) (string, int64, error) {
|
||||||
|
tmpDir, err := ioutil.TempDir("", "machine-test-")
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpDir = filepath.Join(tmpDir, dir)
|
||||||
|
if e := os.MkdirAll(tmpDir, 755); e != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
isopath := filepath.Join(tmpDir, name)
|
||||||
|
log.Info("TEST: dummy ISO created at ", isopath)
|
||||||
|
|
||||||
|
// dummy ISO data mimicking the real byte data of a Boot2Docker ISO image
|
||||||
|
padding := " "
|
||||||
|
data := dummyISOData(padding, version)
|
||||||
|
return isopath, int64(len(padding)), ioutil.WriteFile(isopath, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dummyISOData returns mock data that contains given padding and version.
|
||||||
|
func dummyISOData(padding, version string) []byte {
|
||||||
|
return []byte(fmt.Sprintf("%sBoot2Docker-%s ", padding, version))
|
||||||
|
}
|
||||||
|
|
|
@ -66,16 +66,6 @@ func (provisioner *Boot2DockerProvisioner) upgradeIso() error {
|
||||||
}
|
}
|
||||||
json.Unmarshal(jsonDriver, &d)
|
json.Unmarshal(jsonDriver, &d)
|
||||||
|
|
||||||
log.Info("Downloading latest boot2docker iso...")
|
|
||||||
|
|
||||||
// Usually we call this implicitly, but call it here explicitly to get
|
|
||||||
// the latest default boot2docker ISO.
|
|
||||||
if d.Boot2DockerURL == "" {
|
|
||||||
if err := b2dutils.DownloadLatestBoot2Docker(d.Boot2DockerURL); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Stopping machine to do the upgrade...")
|
log.Info("Stopping machine to do the upgrade...")
|
||||||
|
|
||||||
if err := provisioner.Driver.Stop(); err != nil {
|
if err := provisioner.Driver.Stop(); err != nil {
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
|
load ${BASE_TEST_DIR}/helpers.bash
|
||||||
|
|
||||||
|
only_if_env DRIVER virtualbox
|
||||||
|
|
||||||
|
export CACHE_DIR="$MACHINE_STORAGE_PATH/cache"
|
||||||
|
export ISO_PATH="$CACHE_DIR/boot2docker.iso"
|
||||||
|
export OLD_ISO_URL="https://github.com/boot2docker/boot2docker/releases/download/v1.4.1/boot2docker.iso"
|
||||||
|
|
||||||
|
@test "$DRIVER: download the old version iso" {
|
||||||
|
run mkdir -p $CACHE_DIR
|
||||||
|
run curl $OLD_ISO_URL -L -o $ISO_PATH
|
||||||
|
echo ${output}
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "$DRIVER: create with upgrading" {
|
||||||
|
run machine create -d $DRIVER $NAME
|
||||||
|
echo ${output}
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "$DRIVER: create is correct version" {
|
||||||
|
SERVER_VERSION=$(docker $(machine config $NAME) version | grep 'Server version' | awk '{ print $3; }')
|
||||||
|
[[ "$SERVER_VERSION" != "1.4.1" ]]
|
||||||
|
}
|
Loading…
Reference in New Issue