Merge pull request #1085 from jimmyxian/improve-build

Improve build
This commit is contained in:
Victor Vieux 2015-08-04 11:44:17 -07:00
commit 2606db4486
19 changed files with 307 additions and 59 deletions

2
Godeps/Godeps.json generated
View File

@ -105,7 +105,7 @@
},
{
"ImportPath": "github.com/samalba/dockerclient",
"Rev": "15ebe064ca62ad0d64834e7ef0d4ed8ce9d02cde"
"Rev": "68832c185bb6304fed26a986891fd27b6ed17987"
},
{
"ImportPath": "github.com/samuel/go-zookeeper/zk",

View File

@ -14,8 +14,25 @@ type AuthConfig struct {
}
// encode the auth configuration struct into base64 for the X-Registry-Auth header
func (c *AuthConfig) encode() string {
func (c *AuthConfig) encode() (string, error) {
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(c)
return base64.URLEncoding.EncodeToString(buf.Bytes())
if err := json.NewEncoder(&buf).Encode(c); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(buf.Bytes()), nil
}
// ConfigFile holds parameters for authenticating during a BuildImage request
type ConfigFile struct {
Configs map[string]AuthConfig `json:"configs,omitempty"`
rootPath string
}
// encode the configuration struct into base64 for the X-Registry-Config header
func (c *ConfigFile) encode() (string, error) {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(c); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(buf.Bytes()), nil
}

View File

@ -7,7 +7,7 @@ import (
func TestAuthEncode(t *testing.T) {
a := AuthConfig{Username: "foo", Password: "password", Email: "bar@baz.com"}
expected := "eyJ1c2VybmFtZSI6ImZvbyIsInBhc3N3b3JkIjoicGFzc3dvcmQiLCJlbWFpbCI6ImJhckBiYXouY29tIn0K"
got := a.encode()
got, _ := a.encode()
if expected != got {
t.Errorf("testAuthEncode failed. Expected [%s] got [%s]", expected, got)

View File

@ -235,21 +235,30 @@ func (client *DockerClient) readJSONStream(stream io.ReadCloser, decode func(*js
resultChan := make(chan decodingResult)
go func() {
decoder := json.NewDecoder(stream)
stopped := make(chan struct{})
decodeChan := make(chan decodingResult)
go func() {
<-stopChan
stream.Close()
stopped <- struct{}{}
decoder := json.NewDecoder(stream)
for {
decodeResult := decode(decoder)
decodeChan <- decodeResult
if decodeResult.err != nil {
close(decodeChan)
return
}
}
}()
defer close(resultChan)
for {
decodeResult := decode(decoder)
select {
case <-stopped:
case <-stopChan:
stream.Close()
for range decodeChan {
}
return
default:
case decodeResult := <-decodeChan:
resultChan <- decodeResult
if decodeResult.err != nil {
stream.Close()
@ -257,6 +266,7 @@ func (client *DockerClient) readJSONStream(stream io.ReadCloser, decode func(*js
}
}
}
}()
return resultChan
@ -366,13 +376,17 @@ func (client *DockerClient) StartMonitorEvents(cb Callback, ec chan error, args
go func() {
eventErrChan, err := client.MonitorEvents(nil, client.eventStopChan)
if err != nil {
ec <- err
if ec != nil {
ec <- err
}
return
}
for e := range eventErrChan {
if e.Error != nil {
ec <- err
if ec != nil {
ec <- err
}
return
}
cb(&e.Event, ec, args...)
@ -447,7 +461,11 @@ func (client *DockerClient) PullImage(name string, auth *AuthConfig) error {
uri := fmt.Sprintf("/%s/images/create?%s", APIVersion, v.Encode())
req, err := http.NewRequest("POST", client.URL.String()+uri, nil)
if auth != nil {
req.Header.Add("X-Registry-Auth", auth.encode())
encoded_auth, err := auth.encode()
if err != nil {
return err
}
req.Header.Add("X-Registry-Auth", encoded_auth)
}
resp, err := client.HTTPClient.Do(req)
if err != nil {
@ -521,8 +539,12 @@ func (client *DockerClient) RemoveContainer(id string, force, volumes bool) erro
return err
}
func (client *DockerClient) ListImages() ([]*Image, error) {
uri := fmt.Sprintf("/%s/images/json", APIVersion)
func (client *DockerClient) ListImages(all bool) ([]*Image, error) {
argAll := 0
if all {
argAll = 1
}
uri := fmt.Sprintf("/%s/images/json?all=%d", APIVersion, argAll)
data, err := client.doRequest("GET", uri, nil, nil)
if err != nil {
return nil, err
@ -615,3 +637,58 @@ func (client *DockerClient) ImportImage(source string, repository string, tag st
}
return client.doStreamRequest("POST", "/images/create?"+v.Encode(), in, nil)
}
func (client *DockerClient) BuildImage(image *BuildImage) (io.ReadCloser, error) {
v := url.Values{}
if image.DockerfileName != "" {
v.Set("dockerfile", image.DockerfileName)
}
if image.RepoName != "" {
v.Set("t", image.RepoName)
}
if image.RemoteURL != "" {
v.Set("remote", image.RemoteURL)
}
if image.NoCache {
v.Set("nocache", "1")
}
if image.Pull {
v.Set("pull", "1")
}
if image.Remove {
v.Set("rm", "1")
} else {
v.Set("rm", "0")
}
if image.ForceRemove {
v.Set("forcerm", "1")
}
if image.SuppressOutput {
v.Set("q", "1")
}
v.Set("memory", strconv.FormatInt(image.Memory, 10))
v.Set("memswap", strconv.FormatInt(image.MemorySwap, 10))
v.Set("cpushares", strconv.FormatInt(image.CpuShares, 10))
v.Set("cpuperiod", strconv.FormatInt(image.CpuPeriod, 10))
v.Set("cpuquota", strconv.FormatInt(image.CpuQuota, 10))
v.Set("cpusetcpus", image.CpuSetCpus)
v.Set("cpusetmems", image.CpuSetMems)
v.Set("cgroupparent", image.CgroupParent)
headers := make(map[string]string)
if image.Config != nil {
encoded_config, err := image.Config.encode()
if err != nil {
return nil, err
}
headers["X-Registry-Config"] = encoded_config
}
if image.Context != nil {
headers["Content-Type"] = "application/tar"
}
uri := fmt.Sprintf("/%s/build?%s", APIVersion, v.Encode())
return client.doStreamRequest("POST", uri, image.Context, headers)
}

View File

@ -98,7 +98,7 @@ func handleContainerLogs(w http.ResponseWriter, r *http.Request) {
for ; i < 50; i++ {
line := fmt.Sprintf("line %d", i)
if getBoolValue(r.Form.Get("timestamps")) {
l := &jsonlog.JSONLog{Log: line, Created: time.Now()}
l := &jsonlog.JSONLog{Log: line, Created: time.Now().UTC()}
line = fmt.Sprintf("%s %s", l.Created.Format(timeutils.RFC3339NanoFixed), line)
}
if i%2 == 0 && stderr {

View File

@ -35,10 +35,11 @@ type Client interface {
PullImage(name string, auth *AuthConfig) error
LoadImage(reader io.Reader) error
RemoveContainer(id string, force, volumes bool) error
ListImages() ([]*Image, error)
ListImages(all bool) ([]*Image, error)
RemoveImage(name string) ([]*ImageDelete, error)
PauseContainer(name string) error
UnpauseContainer(name string) error
RenameContainer(oldName string, newName string) error
ImportImage(source string, repository string, tag string, tar io.Reader) (io.ReadCloser, error)
BuildImage(image *BuildImage) (io.ReadCloser, error)
}

View File

@ -116,8 +116,8 @@ func (client *MockClient) RemoveContainer(id string, force, volumes bool) error
return args.Error(0)
}
func (client *MockClient) ListImages() ([]*dockerclient.Image, error) {
args := client.Mock.Called()
func (client *MockClient) ListImages(all bool) ([]*dockerclient.Image, error) {
args := client.Mock.Called(all)
return args.Get(0).([]*dockerclient.Image), args.Error(1)
}
@ -150,3 +150,8 @@ func (client *MockClient) ImportImage(source string, repository string, tag stri
args := client.Mock.Called(source, repository, tag, tar)
return args.Get(0).(io.ReadCloser), args.Error(1)
}
func (client *MockClient) BuildImage(image *dockerclient.BuildImage) (io.ReadCloser, error) {
args := client.Mock.Called(image)
return args.Get(0).(io.ReadCloser), args.Error(1)
}

View File

@ -2,6 +2,7 @@ package dockerclient
import (
"fmt"
"io"
"time"
"github.com/docker/docker/pkg/units"
@ -415,3 +416,24 @@ type LogConfig struct {
Type string `json:"type"`
Config map[string]string `json:"config"`
}
type BuildImage struct {
Config *ConfigFile
DockerfileName string
Context io.Reader
RemoteURL string
RepoName string
SuppressOutput bool
NoCache bool
Remove bool
ForceRemove bool
Pull bool
Memory int64
MemorySwap int64
CpuShares int64
CpuPeriod int64
CpuQuota int64
CpuSetCpus string
CpuSetMems string
CgroupParent string
}

View File

@ -620,31 +620,6 @@ func proxyRandom(c *context, w http.ResponseWriter, r *http.Request) {
}
// Proxy a request to a random node and force refresh
func proxyRandomAndForceRefresh(c *context, w http.ResponseWriter, r *http.Request) {
engine, err := c.cluster.RANDOMENGINE()
if err != nil {
httpError(w, err.Error(), http.StatusInternalServerError)
return
}
if engine == nil {
httpError(w, "no node available in the cluster", http.StatusInternalServerError)
return
}
cb := func(resp *http.Response) {
if resp.StatusCode == http.StatusOK {
engine.RefreshImages()
}
}
if err := proxyAsync(c.tlsConfig, engine.Addr, w, r, cb); err != nil {
httpError(w, err.Error(), http.StatusInternalServerError)
}
}
// POST /commit
func postCommit(c *context, w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
@ -678,6 +653,50 @@ func postCommit(c *context, w http.ResponseWriter, r *http.Request) {
}
}
// POST /build
func postBuild(c *context, w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
httpError(w, err.Error(), http.StatusInternalServerError)
return
}
buildImage := &dockerclient.BuildImage{
DockerfileName: r.Form.Get("dockerfile"),
RepoName: r.Form.Get("t"),
RemoteURL: r.Form.Get("remote"),
NoCache: boolValue(r, "nocache"),
Pull: boolValue(r, "pull"),
Remove: boolValue(r, "rm"),
ForceRemove: boolValue(r, "forcerm"),
SuppressOutput: boolValue(r, "q"),
Memory: int64ValueOrZero(r, "memory"),
MemorySwap: int64ValueOrZero(r, "memswap"),
CpuShares: int64ValueOrZero(r, "cpushares"),
CpuPeriod: int64ValueOrZero(r, "cpuperiod"),
CpuQuota: int64ValueOrZero(r, "cpuquota"),
CpuSetCpus: r.Form.Get("cpusetcpus"),
CpuSetMems: r.Form.Get("cpusetmems"),
CgroupParent: r.Form.Get("cgroupparent"),
Context: r.Body,
}
authEncoded := r.Header.Get("X-Registry-Auth")
if authEncoded != "" {
buf, err := base64.URLEncoding.DecodeString(r.Header.Get("X-Registry-Auth"))
if err == nil {
json.Unmarshal(buf, &buildImage.Config)
}
}
w.Header().Set("Content-Type", "application/json")
wf := NewWriteFlusher(w)
err := c.cluster.BuildImage(buildImage, wf)
if err != nil {
httpError(w, err.Error(), http.StatusInternalServerError)
}
}
// POST /containers/{name:.*}/rename
func postRenameContainer(c *context, w http.ResponseWriter, r *http.Request) {
_, container, err := getContainerFromVars(c, mux.Vars(r))

View File

@ -51,7 +51,7 @@ var routes = map[string]map[string]handler{
"POST": {
"/auth": proxyRandom,
"/commit": postCommit,
"/build": proxyRandomAndForceRefresh,
"/build": postBuild,
"/images/create": postImagesCreate,
"/images/load": postImagesLoad,
"/images/{name:.*}/push": proxyImageTagOptional,

View File

@ -163,3 +163,11 @@ func intValueOrZero(r *http.Request, k string) int {
}
return val
}
func int64ValueOrZero(r *http.Request, k string) int64 {
val, err := strconv.ParseInt(r.FormValue(k), 10, 64)
if err != nil {
return 0
}
return val
}

View File

@ -53,3 +53,24 @@ func TestIntValueOrZero(t *testing.T) {
}
}
}
func TestInti64ValueOrZero(t *testing.T) {
cases := map[string]int64{
"": 0,
"asdf": 0,
"0": 0,
"1": 1,
}
for c, e := range cases {
v := url.Values{}
v.Set("test", c)
r, _ := http.NewRequest("POST", "", nil)
r.Form = v
a := int64ValueOrZero(r, "test")
if a != e {
t.Fatalf("Value: %s, expected: %v, actual: %v", c, e, a)
}
}
}

View File

@ -68,4 +68,7 @@ type Cluster interface {
// RenameContainer rename a container
RenameContainer(container *Container, newName string) error
// BuildImage build an image
BuildImage(*dockerclient.BuildImage, io.Writer) error
}

View File

@ -185,7 +185,7 @@ func (e *Engine) RemoveImage(image *Image, name string) ([]*dockerclient.ImageDe
// RefreshImages refreshes the list of images on the engine.
func (e *Engine) RefreshImages() error {
images, err := e.client.ListImages()
images, err := e.client.ListImages(false)
if err != nil {
return err
}
@ -614,3 +614,9 @@ func (e *Engine) RenameContainer(container *Container, newName string) error {
// refresh container
return e.refreshContainer(container.Id, true)
}
// BuildImage build an image
func (e *Engine) BuildImage(buildImage *dockerclient.BuildImage) (io.ReadCloser, error) {
return e.client.BuildImage(buildImage)
}

View File

@ -64,7 +64,7 @@ func TestEngineCpusMemory(t *testing.T) {
client.On("Info").Return(mockInfo, nil)
client.On("Version").Return(mockVersion, nil)
client.On("ListContainers", true, false, "").Return([]dockerclient.Container{}, nil)
client.On("ListImages").Return([]*dockerclient.Image{}, nil)
client.On("ListImages", false).Return([]*dockerclient.Image{}, nil)
client.On("StartMonitorEvents", mock.Anything, mock.Anything, mock.Anything).Return()
assert.NoError(t, engine.ConnectWithClient(client))
@ -85,7 +85,7 @@ func TestEngineSpecs(t *testing.T) {
client.On("Info").Return(mockInfo, nil)
client.On("Version").Return(mockVersion, nil)
client.On("ListContainers", true, false, "").Return([]dockerclient.Container{}, nil)
client.On("ListImages").Return([]*dockerclient.Image{}, nil)
client.On("ListImages", false).Return([]*dockerclient.Image{}, nil)
client.On("StartMonitorEvents", mock.Anything, mock.Anything, mock.Anything).Return()
assert.NoError(t, engine.ConnectWithClient(client))
@ -114,7 +114,7 @@ func TestEngineState(t *testing.T) {
// The client will return one container at first, then a second one will appear.
client.On("ListContainers", true, false, "").Return([]dockerclient.Container{{Id: "one"}}, nil).Once()
client.On("ListImages").Return([]*dockerclient.Image{}, nil).Once()
client.On("ListImages", false).Return([]*dockerclient.Image{}, nil).Once()
client.On("InspectContainer", "one").Return(&dockerclient.ContainerInfo{Config: &dockerclient.ContainerConfig{CpuShares: 100}}, nil).Once()
client.On("ListContainers", true, false, fmt.Sprintf("{%q:[%q]}", "id", "two")).Return([]dockerclient.Container{{Id: "two"}}, nil).Once()
client.On("InspectContainer", "two").Return(&dockerclient.ContainerInfo{Config: &dockerclient.ContainerConfig{CpuShares: 100}}, nil).Once()
@ -159,7 +159,7 @@ func TestCreateContainer(t *testing.T) {
client.On("Version").Return(mockVersion, nil)
client.On("StartMonitorEvents", mock.Anything, mock.Anything, mock.Anything).Return()
client.On("ListContainers", true, false, "").Return([]dockerclient.Container{}, nil).Once()
client.On("ListImages").Return([]*dockerclient.Image{}, nil).Once()
client.On("ListImages", false).Return([]*dockerclient.Image{}, nil).Once()
assert.NoError(t, engine.ConnectWithClient(client))
assert.True(t, engine.isConnected())
@ -172,7 +172,7 @@ func TestCreateContainer(t *testing.T) {
id := "id1"
client.On("CreateContainer", &mockConfig, name).Return(id, nil).Once()
client.On("ListContainers", true, false, fmt.Sprintf(`{"id":[%q]}`, id)).Return([]dockerclient.Container{{Id: id}}, nil).Once()
client.On("ListImages").Return([]*dockerclient.Image{}, nil).Once()
client.On("ListImages", false).Return([]*dockerclient.Image{}, nil).Once()
client.On("InspectContainer", id).Return(&dockerclient.ContainerInfo{Config: &config.ContainerConfig}, nil).Once()
container, err := engine.Create(config, name, false)
assert.Nil(t, err)
@ -195,7 +195,7 @@ func TestCreateContainer(t *testing.T) {
client.On("CreateContainer", &mockConfig, name).Return("", dockerclient.ErrNotFound).Once()
client.On("CreateContainer", &mockConfig, name).Return(id, nil).Once()
client.On("ListContainers", true, false, fmt.Sprintf(`{"id":[%q]}`, id)).Return([]dockerclient.Container{{Id: id}}, nil).Once()
client.On("ListImages").Return([]*dockerclient.Image{}, nil).Once()
client.On("ListImages", false).Return([]*dockerclient.Image{}, nil).Once()
client.On("InspectContainer", id).Return(&dockerclient.ContainerInfo{Config: &config.ContainerConfig}, nil).Once()
container, err = engine.Create(config, name, true)
assert.Nil(t, err)
@ -241,7 +241,7 @@ func TestUsedCpus(t *testing.T) {
client.On("Info").Return(mockInfo, nil)
client.On("Version").Return(mockVersion, nil)
client.On("StartMonitorEvents", mock.Anything, mock.Anything, mock.Anything).Return()
client.On("ListImages").Return([]*dockerclient.Image{}, nil).Once()
client.On("ListImages", false).Return([]*dockerclient.Image{}, nil).Once()
client.On("ListContainers", true, false, "").Return([]dockerclient.Container{{Id: "test"}}, nil).Once()
client.On("InspectContainer", "test").Return(&dockerclient.ContainerInfo{Config: &dockerclient.ContainerConfig{CpuShares: cpuShares}}, nil).Once()
engine.ConnectWithClient(client)
@ -268,7 +268,7 @@ func TestContainerRemovedDuringRefresh(t *testing.T) {
client.On("Info").Return(mockInfo, nil)
client.On("Version").Return(mockVersion, nil)
client.On("ListImages").Return([]*dockerclient.Image{}, nil)
client.On("ListImages", false).Return([]*dockerclient.Image{}, nil)
client.On("StartMonitorEvents", mock.Anything, mock.Anything, mock.Anything).Return()
client.On("ListContainers", true, false, "").Return([]dockerclient.Container{container1, container2}, nil)
client.On("InspectContainer", "c1").Return(info1, errors.New("Not found"))

View File

@ -510,3 +510,31 @@ func (c *Cluster) RANDOMENGINE() (*cluster.Engine, error) {
}
return nil, nil
}
// BuildImage build an image
func (c *Cluster) BuildImage(buildImage *dockerclient.BuildImage, out io.Writer) error {
c.scheduler.Lock()
// get an engine
config := &cluster.ContainerConfig{dockerclient.ContainerConfig{
CpuShares: buildImage.CpuShares,
Memory: buildImage.Memory,
}}
n, err := c.scheduler.SelectNodeForContainer(c.listNodes(), config)
c.scheduler.Unlock()
if err != nil {
return err
}
reader, err := c.slaves[n.ID].engine.BuildImage(buildImage)
if err != nil {
return err
}
if _, err := io.Copy(out, reader); err != nil {
return err
}
c.slaves[n.ID].engine.RefreshImages()
return nil
}

View File

@ -598,3 +598,31 @@ func (c *Cluster) RenameContainer(container *cluster.Container, newName string)
st.Name = newName
return c.store.Replace(container.Id, st)
}
// BuildImage build an image
func (c *Cluster) BuildImage(buildImage *dockerclient.BuildImage, out io.Writer) error {
c.scheduler.Lock()
// get an engine
config := &cluster.ContainerConfig{dockerclient.ContainerConfig{
CpuShares: buildImage.CpuShares,
Memory: buildImage.Memory,
}}
n, err := c.scheduler.SelectNodeForContainer(c.listNodes(), config)
c.scheduler.Unlock()
if err != nil {
return err
}
reader, err := c.engines[n.ID].BuildImage(buildImage)
if err != nil {
return err
}
if _, err := io.Copy(out, reader); err != nil {
return err
}
c.engines[n.ID].RefreshImages()
return nil
}

View File

@ -127,7 +127,7 @@ func TestImportImage(t *testing.T) {
client.On("Version").Return(mockVersion, nil)
client.On("StartMonitorEvents", mock.Anything, mock.Anything, mock.Anything).Return()
client.On("ListContainers", true, false, "").Return([]dockerclient.Container{}, nil).Once()
client.On("ListImages").Return([]*dockerclient.Image{}, nil)
client.On("ListImages", false).Return([]*dockerclient.Image{}, nil)
// connect client
engine.ConnectWithClient(client)
@ -175,7 +175,7 @@ func TestLoadImage(t *testing.T) {
client.On("Version").Return(mockVersion, nil)
client.On("StartMonitorEvents", mock.Anything, mock.Anything, mock.Anything).Return()
client.On("ListContainers", true, false, "").Return([]dockerclient.Container{}, nil).Once()
client.On("ListImages").Return([]*dockerclient.Image{}, nil)
client.On("ListImages", false).Return([]*dockerclient.Image{}, nil)
// connect client
engine.ConnectWithClient(client)

View File

@ -7,4 +7,17 @@ function teardown() {
stop_docker
}
@test "docker build" {
start_docker 2
swarm_manage
run docker_swarm images -q
[ "$status" -eq 0 ]
[ "${#lines[@]}" -eq 0 ]
docker_swarm build -t test $TESTDATA/build
run docker_swarm images -q
[ "$status" -eq 0 ]
[ "${#lines[@]}" -eq 1 ]
}