Merge pull request #1005 from docker/feat-compose-pull

Implement `docker compose pull`
This commit is contained in:
Guillaume Tardif 2020-12-03 16:24:14 +01:00 committed by GitHub
commit 809c2bc45a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 249 additions and 18 deletions

View File

@ -52,6 +52,10 @@ func (cs *aciComposeService) Push(ctx context.Context, project *types.Project) e
return errdefs.ErrNotImplemented return errdefs.ErrNotImplemented
} }
func (cs *aciComposeService) Pull(ctx context.Context, project *types.Project) error {
return errdefs.ErrNotImplemented
}
func (cs *aciComposeService) Up(ctx context.Context, project *types.Project, detach bool) error { func (cs *aciComposeService) Up(ctx context.Context, project *types.Project, detach bool) error {
logrus.Debugf("Up on project with name %q", project.Name) logrus.Debugf("Up on project with name %q", project.Name)

View File

@ -37,32 +37,30 @@ func (c *composeService) Push(ctx context.Context, project *types.Project) error
return errdefs.ErrNotImplemented return errdefs.ErrNotImplemented
} }
// Up executes the equivalent to a `compose up` func (c *composeService) Pull(ctx context.Context, project *types.Project) error {
return errdefs.ErrNotImplemented
}
func (c *composeService) Up(context.Context, *types.Project, bool) error { func (c *composeService) Up(context.Context, *types.Project, bool) error {
return errdefs.ErrNotImplemented return errdefs.ErrNotImplemented
} }
// Down executes the equivalent to a `compose down`
func (c *composeService) Down(context.Context, string) error { func (c *composeService) Down(context.Context, string) error {
return errdefs.ErrNotImplemented return errdefs.ErrNotImplemented
} }
// Logs executes the equivalent to a `compose logs`
func (c *composeService) Logs(context.Context, string, io.Writer) error { func (c *composeService) Logs(context.Context, string, io.Writer) error {
return errdefs.ErrNotImplemented return errdefs.ErrNotImplemented
} }
// Ps executes the equivalent to a `compose ps`
func (c *composeService) Ps(context.Context, string) ([]compose.ServiceStatus, error) { func (c *composeService) Ps(context.Context, string) ([]compose.ServiceStatus, error) {
return nil, errdefs.ErrNotImplemented return nil, errdefs.ErrNotImplemented
} }
// List executes the equivalent to a `docker stack ls`
func (c *composeService) List(context.Context, string) ([]compose.Stack, error) { func (c *composeService) List(context.Context, string) ([]compose.Stack, error) {
return nil, errdefs.ErrNotImplemented return nil, errdefs.ErrNotImplemented
} }
// Convert translate compose model into backend's native format
func (c *composeService) Convert(context.Context, *types.Project, string) ([]byte, error) { func (c *composeService) Convert(context.Context, *types.Project, string) ([]byte, error) {
return nil, errdefs.ErrNotImplemented return nil, errdefs.ErrNotImplemented
} }

View File

@ -29,6 +29,8 @@ type Service interface {
Build(ctx context.Context, project *types.Project) error Build(ctx context.Context, project *types.Project) error
// Push executes the equivalent ot a `compose push` // Push executes the equivalent ot a `compose push`
Push(ctx context.Context, project *types.Project) error Push(ctx context.Context, project *types.Project) error
// Pull executes the equivalent of a `compose pull`
Pull(ctx context.Context, project *types.Project) error
// Up executes the equivalent to a `compose up` // Up executes the equivalent to a `compose up`
Up(ctx context.Context, project *types.Project, detach bool) error Up(ctx context.Context, project *types.Project, detach bool) error
// Down executes the equivalent to a `compose down` // Down executes the equivalent to a `compose down`

View File

@ -93,7 +93,9 @@ func Command(contextType string) *cobra.Command {
if contextType == store.LocalContextType { if contextType == store.LocalContextType {
command.AddCommand( command.AddCommand(
buildCommand(), buildCommand(),
pushCommand()) pushCommand(),
pullCommand(),
)
} }
return command return command

71
cli/cmd/compose/pull.go Normal file
View File

@ -0,0 +1,71 @@
/*
Copyright 2020 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package compose
import (
"context"
"github.com/compose-spec/compose-go/cli"
"github.com/spf13/cobra"
"github.com/docker/compose-cli/api/client"
"github.com/docker/compose-cli/progress"
)
type pullOptions struct {
composeOptions
}
func pullCommand() *cobra.Command {
opts := pullOptions{}
pullCmd := &cobra.Command{
Use: "pull [SERVICE...]",
RunE: func(cmd *cobra.Command, args []string) error {
return runPull(cmd.Context(), opts, args)
},
}
pullCmd.Flags().StringVar(&opts.WorkingDir, "workdir", "", "Work dir")
pullCmd.Flags().StringArrayVarP(&opts.ConfigPaths, "file", "f", []string{}, "Compose configuration files")
return pullCmd
}
func runPull(ctx context.Context, opts pullOptions, services []string) error {
c, err := client.New(ctx)
if err != nil {
return err
}
_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
options, err := opts.toProjectOptions()
if err != nil {
return "", err
}
project, err := cli.ProjectFromOptions(options)
if err != nil {
return "", err
}
err = filter(project, services)
if err != nil {
return "", err
}
return "", c.ComposeService().Pull(ctx, project)
})
return err
}

View File

@ -49,6 +49,10 @@ func (e ecsLocalSimulation) Push(ctx context.Context, project *types.Project) er
return errdefs.ErrNotImplemented return errdefs.ErrNotImplemented
} }
func (e ecsLocalSimulation) Pull(ctx context.Context, project *types.Project) error {
return errdefs.ErrNotImplemented
}
func (e ecsLocalSimulation) Up(ctx context.Context, project *types.Project, detach bool) error { func (e ecsLocalSimulation) Up(ctx context.Context, project *types.Project, detach bool) error {
cmd := exec.Command("docker-compose", "version", "--short") cmd := exec.Command("docker-compose", "version", "--short")
b := bytes.Buffer{} b := bytes.Buffer{}

View File

@ -35,6 +35,10 @@ func (b *ecsAPIService) Push(ctx context.Context, project *types.Project) error
return errdefs.ErrNotImplemented return errdefs.ErrNotImplemented
} }
func (b *ecsAPIService) Pull(ctx context.Context, project *types.Project) error {
return errdefs.ErrNotImplemented
}
func (b *ecsAPIService) Up(ctx context.Context, project *types.Project, detach bool) error { func (b *ecsAPIService) Up(ctx context.Context, project *types.Project, detach bool) error {
err := b.aws.CheckRequirements(ctx, b.Region) err := b.aws.CheckRequirements(ctx, b.Region)
if err != nil { if err != nil {

View File

@ -147,6 +147,10 @@ func (cs *composeService) Push(ctx context.Context, project *types.Project) erro
return errdefs.ErrNotImplemented return errdefs.ErrNotImplemented
} }
func (cs *composeService) Pull(ctx context.Context, project *types.Project) error {
return errdefs.ErrNotImplemented
}
func (cs *composeService) Up(ctx context.Context, project *types.Project, detach bool) error { func (cs *composeService) Up(ctx context.Context, project *types.Project, detach bool) error {
fmt.Printf("Up command on project %q", project.Name) fmt.Printf("Up command on project %q", project.Name)
return nil return nil

View File

@ -32,7 +32,7 @@ import (
"github.com/compose-spec/compose-go/cli" "github.com/compose-spec/compose-go/cli"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
"github.com/docker/buildx/build" "github.com/docker/buildx/build"
"github.com/docker/cli/cli/config" cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
moby "github.com/docker/docker/api/types" moby "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
@ -52,6 +52,7 @@ import (
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/compose"
"github.com/docker/compose-cli/config"
"github.com/docker/compose-cli/formatter" "github.com/docker/compose-cli/formatter"
"github.com/docker/compose-cli/progress" "github.com/docker/compose-cli/progress"
) )
@ -72,7 +73,7 @@ func (s *composeService) Build(ctx context.Context, project *types.Project) erro
} }
func (s *composeService) Push(ctx context.Context, project *types.Project) error { func (s *composeService) Push(ctx context.Context, project *types.Project) error {
configFile, err := config.Load(config.Dir()) configFile, err := cliconfig.Load(config.Dir(ctx))
if err != nil { if err != nil {
return err return err
} }
@ -136,7 +137,7 @@ func (s *composeService) Push(ctx context.Context, project *types.Project) error
if jm.Error != nil { if jm.Error != nil {
return errors.New(jm.Error.Message) return errors.New(jm.Error.Message)
} }
toProgressEvent(service.Name, jm, w) toProgressEvent("Pushing "+service.Name, jm, w)
} }
return nil return nil
}) })
@ -144,6 +145,127 @@ func (s *composeService) Push(ctx context.Context, project *types.Project) error
return eg.Wait() return eg.Wait()
} }
func (s *composeService) Pull(ctx context.Context, project *types.Project) error {
configFile, err := cliconfig.Load(config.Dir(ctx))
if err != nil {
return err
}
info, err := s.apiClient.Info(ctx)
if err != nil {
return err
}
if info.IndexServerAddress == "" {
info.IndexServerAddress = registry.IndexServer
}
w := progress.ContextWriter(ctx)
eg, ctx := errgroup.WithContext(ctx)
for _, srv := range project.Services {
service := srv
eg.Go(func() error {
w.Event(progress.Event{
ID: service.Name,
Status: progress.Working,
Text: "Pulling",
})
ref, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
return err
}
repoInfo, err := registry.ParseRepositoryInfo(ref)
if err != nil {
return err
}
key := repoInfo.Index.Name
if repoInfo.Index.Official {
key = info.IndexServerAddress
}
authConfig, err := configFile.GetAuthConfig(key)
if err != nil {
return err
}
buf, err := json.Marshal(authConfig)
if err != nil {
return err
}
stream, err := s.apiClient.ImagePull(ctx, service.Image, moby.ImagePullOptions{
RegistryAuth: base64.URLEncoding.EncodeToString(buf),
})
if err != nil {
w.Event(progress.Event{
ID: service.Name,
Status: progress.Error,
Text: "Error",
})
return err
}
dec := json.NewDecoder(stream)
for {
var jm jsonmessage.JSONMessage
if err := dec.Decode(&jm); err != nil {
if err == io.EOF {
break
}
return err
}
if jm.Error != nil {
return errors.New(jm.Error.Message)
}
toPullProgressEvent(service.Name, jm, w)
}
w.Event(progress.Event{
ID: service.Name,
Status: progress.Done,
Text: "Pulled",
})
return nil
})
}
return eg.Wait()
}
func toPullProgressEvent(parent string, jm jsonmessage.JSONMessage, w progress.Writer) {
if jm.ID == "" || jm.Progress == nil {
return
}
var (
text string
status = progress.Working
)
text = jm.Progress.String()
if jm.Status == "Pull complete" ||
jm.Status == "Already exists" ||
strings.Contains(jm.Status, "Image is up to date") ||
strings.Contains(jm.Status, "Downloaded newer image") {
status = progress.Done
}
if jm.Error != nil {
status = progress.Error
text = jm.Error.Message
}
w.Event(progress.Event{
ID: jm.ID,
ParentID: parent,
Text: jm.Status,
Status: status,
StatusText: text,
})
}
func toProgressEvent(prefix string, jm jsonmessage.JSONMessage, w progress.Writer) { func toProgressEvent(prefix string, jm jsonmessage.JSONMessage, w progress.Writer) {
if jm.ID == "" { if jm.ID == "" {
// skipped // skipped

View File

@ -33,6 +33,7 @@ const (
// Event represents a progress event. // Event represents a progress event.
type Event struct { type Event struct {
ID string ID string
ParentID string
Text string Text string
Status EventStatus Status EventStatus
StatusText string StatusText string

View File

@ -79,6 +79,7 @@ func (w *ttyWriter) Event(e Event) {
last.Status = e.Status last.Status = e.Status
last.Text = e.Text last.Text = e.Text
last.StatusText = e.StatusText last.StatusText = e.StatusText
last.ParentID = e.ParentID
w.events[e.ID] = last w.events[e.ID] = last
} else { } else {
e.startTime = time.Now() e.startTime = time.Now()
@ -116,24 +117,41 @@ func (w *ttyWriter) print() {
var statusPadding int var statusPadding int
for _, v := range w.eventIDs { for _, v := range w.eventIDs {
l := len(fmt.Sprintf("%s %s", w.events[v].ID, w.events[v].Text)) event := w.events[v]
l := len(fmt.Sprintf("%s %s", event.ID, event.Text))
if statusPadding < l { if statusPadding < l {
statusPadding = l statusPadding = l
} }
if event.ParentID != "" {
statusPadding -= 2
}
} }
numLines := 0 numLines := 0
for _, v := range w.eventIDs { for _, v := range w.eventIDs {
line := lineText(w.events[v], terminalWidth, statusPadding, runtime.GOOS != "windows") event := w.events[v]
if event.ParentID != "" {
continue
}
line := lineText(event, "", terminalWidth, statusPadding, runtime.GOOS != "windows")
// nolint: errcheck // nolint: errcheck
fmt.Fprint(w.out, line) fmt.Fprint(w.out, line)
numLines++ numLines++
for _, v := range w.eventIDs {
ev := w.events[v]
if ev.ParentID == event.ID {
line := lineText(ev, " ", terminalWidth, statusPadding, runtime.GOOS != "windows")
// nolint: errcheck
fmt.Fprint(w.out, line)
numLines++
}
}
} }
w.numLines = numLines w.numLines = numLines
} }
func lineText(event Event, terminalWidth, statusPadding int, color bool) string { func lineText(event Event, pad string, terminalWidth, statusPadding int, color bool) string {
endTime := time.Now() endTime := time.Now()
if event.Status != Working { if event.Status != Working {
endTime = event.endTime endTime = event.endTime
@ -154,7 +172,8 @@ func lineText(event Event, terminalWidth, statusPadding int, color bool) string
if maxStatusLen > 0 && len(status) > maxStatusLen { if maxStatusLen > 0 && len(status) > maxStatusLen {
status = status[:maxStatusLen] + "..." status = status[:maxStatusLen] + "..."
} }
text := fmt.Sprintf(" %s %s %s%s %s", text := fmt.Sprintf("%s %s %s %s%s %s",
pad,
event.spinner.String(), event.spinner.String(),
event.ID, event.ID,
event.Text, event.Text,

View File

@ -41,18 +41,18 @@ func TestLineText(t *testing.T) {
lineWidth := len(fmt.Sprintf("%s %s", ev.ID, ev.Text)) lineWidth := len(fmt.Sprintf("%s %s", ev.ID, ev.Text))
out := lineText(ev, 50, lineWidth, true) out := lineText(ev, "", 50, lineWidth, true)
assert.Equal(t, out, "\x1b[37m . id Text Status 0.0s\n\x1b[0m") assert.Equal(t, out, "\x1b[37m . id Text Status 0.0s\n\x1b[0m")
out = lineText(ev, 50, lineWidth, false) out = lineText(ev, "", 50, lineWidth, false)
assert.Equal(t, out, " . id Text Status 0.0s\n") assert.Equal(t, out, " . id Text Status 0.0s\n")
ev.Status = Done ev.Status = Done
out = lineText(ev, 50, lineWidth, true) out = lineText(ev, "", 50, lineWidth, true)
assert.Equal(t, out, "\x1b[34m . id Text Status 0.0s\n\x1b[0m") assert.Equal(t, out, "\x1b[34m . id Text Status 0.0s\n\x1b[0m")
ev.Status = Error ev.Status = Error
out = lineText(ev, 50, lineWidth, true) out = lineText(ev, "", 50, lineWidth, true)
assert.Equal(t, out, "\x1b[31m . id Text Status 0.0s\n\x1b[0m") assert.Equal(t, out, "\x1b[31m . id Text Status 0.0s\n\x1b[0m")
} }