2017-11-20 09:30:52 -05:00
|
|
|
package kubernetes
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2018-05-14 09:44:55 -04:00
|
|
|
"io"
|
2017-11-20 09:30:52 -05:00
|
|
|
|
2018-05-14 09:44:55 -04:00
|
|
|
"github.com/docker/cli/cli/command"
|
2017-12-04 06:30:39 -05:00
|
|
|
"github.com/docker/cli/cli/command/stack/options"
|
2018-06-26 08:07:26 -04:00
|
|
|
composetypes "github.com/docker/cli/cli/compose/types"
|
2018-05-14 09:44:55 -04:00
|
|
|
"github.com/morikuni/aec"
|
2017-11-20 09:30:52 -05:00
|
|
|
)
|
|
|
|
|
2017-12-04 06:30:39 -05:00
|
|
|
// RunDeploy is the kubernetes implementation of docker stack deploy
|
2018-06-26 08:07:26 -04:00
|
|
|
func RunDeploy(dockerCli *KubeCli, opts options.Deploy, cfg *composetypes.Config) error {
|
2017-11-20 09:30:52 -05:00
|
|
|
cmdOut := dockerCli.Out()
|
2018-01-29 16:18:43 -05:00
|
|
|
|
2018-01-31 09:37:14 -05:00
|
|
|
// Initialize clients
|
2018-04-09 09:07:11 -04:00
|
|
|
composeClient, err := dockerCli.composeClient()
|
2018-01-29 16:18:43 -05:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-04-09 09:13:16 -04:00
|
|
|
stacks, err := composeClient.Stacks(false)
|
2018-01-29 16:18:43 -05:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-02-26 09:59:44 -05:00
|
|
|
stack, err := stacks.FromCompose(dockerCli.Err(), opts.Namespace, cfg)
|
2017-11-20 09:30:52 -05:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-01-31 09:37:14 -05:00
|
|
|
|
2018-01-02 17:56:07 -05:00
|
|
|
configMaps := composeClient.ConfigMaps()
|
|
|
|
secrets := composeClient.Secrets()
|
|
|
|
services := composeClient.Services()
|
2017-11-20 09:30:52 -05:00
|
|
|
|
2018-01-31 09:37:14 -05:00
|
|
|
if err := stacks.IsColliding(services, stack); err != nil {
|
2017-11-20 09:30:52 -05:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-01-31 09:37:14 -05:00
|
|
|
if err := stack.createFileBasedConfigMaps(configMaps); err != nil {
|
2017-11-20 09:30:52 -05:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-01-31 09:37:14 -05:00
|
|
|
if err := stack.createFileBasedSecrets(secrets); err != nil {
|
2017-11-20 09:30:52 -05:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-01-31 09:37:14 -05:00
|
|
|
if err = stacks.CreateOrUpdate(stack); err != nil {
|
|
|
|
return err
|
2017-11-20 09:30:52 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Fprintln(cmdOut, "Waiting for the stack to be stable and running...")
|
2018-05-14 09:44:55 -04:00
|
|
|
v1beta1Cli, err := dockerCli.stacksv1beta1()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-11-20 09:30:52 -05:00
|
|
|
|
2018-05-14 09:44:55 -04:00
|
|
|
pods := composeClient.Pods()
|
|
|
|
watcher := &deployWatcher{
|
|
|
|
stacks: v1beta1Cli,
|
|
|
|
pods: pods,
|
|
|
|
}
|
|
|
|
statusUpdates := make(chan serviceStatus)
|
|
|
|
displayDone := make(chan struct{})
|
|
|
|
go func() {
|
|
|
|
defer close(displayDone)
|
|
|
|
display := newStatusDisplay(dockerCli.Out())
|
|
|
|
for status := range statusUpdates {
|
|
|
|
display.OnStatus(status)
|
|
|
|
}
|
|
|
|
}()
|
2017-11-20 09:30:52 -05:00
|
|
|
|
2018-06-27 10:41:00 -04:00
|
|
|
err = watcher.Watch(stack.Name, stack.getServices(), statusUpdates)
|
2018-05-14 09:44:55 -04:00
|
|
|
close(statusUpdates)
|
|
|
|
<-displayDone
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-06-27 10:41:00 -04:00
|
|
|
fmt.Fprintf(cmdOut, "\nStack %s is stable and running\n\n", stack.Name)
|
2017-11-20 09:30:52 -05:00
|
|
|
return nil
|
2018-05-14 09:44:55 -04:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
type statusDisplay interface {
|
|
|
|
OnStatus(serviceStatus)
|
|
|
|
}
|
|
|
|
type metaServiceState string
|
|
|
|
|
|
|
|
const (
|
|
|
|
metaServiceStateReady = metaServiceState("Ready")
|
|
|
|
metaServiceStatePending = metaServiceState("Pending")
|
|
|
|
metaServiceStateFailed = metaServiceState("Failed")
|
|
|
|
)
|
|
|
|
|
|
|
|
func metaStateFromStatus(status serviceStatus) metaServiceState {
|
|
|
|
switch {
|
|
|
|
case status.podsReady > 0:
|
|
|
|
return metaServiceStateReady
|
|
|
|
case status.podsPending > 0:
|
|
|
|
return metaServiceStatePending
|
|
|
|
default:
|
|
|
|
return metaServiceStateFailed
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type forwardOnlyStatusDisplay struct {
|
|
|
|
o *command.OutStream
|
|
|
|
states map[string]metaServiceState
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *forwardOnlyStatusDisplay) OnStatus(status serviceStatus) {
|
|
|
|
state := metaStateFromStatus(status)
|
|
|
|
if d.states[status.name] != state {
|
|
|
|
d.states[status.name] = state
|
|
|
|
fmt.Fprintf(d.o, "%s: %s\n", status.name, state)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type interactiveStatusDisplay struct {
|
|
|
|
o *command.OutStream
|
|
|
|
statuses []serviceStatus
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *interactiveStatusDisplay) OnStatus(status serviceStatus) {
|
|
|
|
b := aec.EmptyBuilder
|
|
|
|
for ix := 0; ix < len(d.statuses); ix++ {
|
|
|
|
b = b.Up(1).EraseLine(aec.EraseModes.All)
|
|
|
|
}
|
|
|
|
b = b.Column(0)
|
|
|
|
fmt.Fprint(d.o, b.ANSI)
|
|
|
|
updated := false
|
|
|
|
for ix, s := range d.statuses {
|
|
|
|
if s.name == status.name {
|
|
|
|
d.statuses[ix] = status
|
|
|
|
s = status
|
|
|
|
updated = true
|
|
|
|
}
|
|
|
|
displayInteractiveServiceStatus(s, d.o)
|
|
|
|
}
|
|
|
|
if !updated {
|
|
|
|
d.statuses = append(d.statuses, status)
|
|
|
|
displayInteractiveServiceStatus(status, d.o)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func displayInteractiveServiceStatus(status serviceStatus, o io.Writer) {
|
|
|
|
state := metaStateFromStatus(status)
|
|
|
|
totalFailed := status.podsFailed + status.podsSucceeded + status.podsUnknown
|
|
|
|
fmt.Fprintf(o, "%[1]s: %[2]s\t\t[pod status: %[3]d/%[6]d ready, %[4]d/%[6]d pending, %[5]d/%[6]d failed]\n", status.name, state,
|
|
|
|
status.podsReady, status.podsPending, totalFailed, status.podsTotal)
|
|
|
|
}
|
|
|
|
|
|
|
|
func newStatusDisplay(o *command.OutStream) statusDisplay {
|
|
|
|
if !o.IsTerminal() {
|
|
|
|
return &forwardOnlyStatusDisplay{o: o, states: map[string]metaServiceState{}}
|
|
|
|
}
|
|
|
|
return &interactiveStatusDisplay{o: o}
|
2017-11-20 09:30:52 -05:00
|
|
|
}
|