traefik/vendor/github.com/libkermit/compose/compose.go
2018-01-22 12:16:03 +01:00

223 lines
6.1 KiB
Go

// Package compose aims to provide simple "helper" methods to ease the use of
// compose (through libcompose) in (integration) tests.
package compose
import (
"fmt"
"regexp"
"strings"
"golang.org/x/net/context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/docker/libcompose/config"
"github.com/docker/libcompose/docker"
"github.com/docker/libcompose/docker/ctx"
"github.com/docker/libcompose/project"
"github.com/docker/libcompose/project/events"
"github.com/docker/libcompose/project/options"
d "github.com/libkermit/docker"
)
// Project holds compose related project attributes
type Project struct {
composeFiles []string
composeProject project.APIProject
name string
listenChan chan events.Event
started chan struct{}
stopped chan struct{}
deleted chan struct{}
client client.APIClient
hasOpenedChan bool
}
// CreateProject creates a compose project with the given name based on the
// specified compose files
func CreateProject(name string, composeFiles ...string) (*Project, error) {
// FIXME(vdemeester) temporarly normalize the project name, should not be needed.
r := regexp.MustCompile("[^a-z0-9]+")
name = r.ReplaceAllString(strings.ToLower(name), "")
apiClient, err := client.NewEnvClient()
if err != nil {
return nil, err
}
ping := types.Ping{APIVersion: d.CurrentAPIVersion}
apiClient.NegotiateAPIVersionPing(ping)
composeProject, err := docker.NewProject(&ctx.Context{
Context: project.Context{
ComposeFiles: composeFiles,
ProjectName: name,
},
}, &config.ParseOptions{
Interpolate: true,
Validate: true,
})
if err != nil {
return nil, err
}
p := &Project{
composeFiles: composeFiles,
composeProject: composeProject,
name: name,
listenChan: make(chan events.Event),
started: make(chan struct{}),
stopped: make(chan struct{}),
deleted: make(chan struct{}),
client: apiClient,
hasOpenedChan: true,
}
// Listen to compose events
go p.startListening()
p.composeProject.AddListener(p.listenChan)
return p, nil
}
// Start creates and starts the compose project.
func (p *Project) Start(services ...string) error {
// If project chan are closed, recreate new compose project
if !p.hasOpenedChan {
newProject, _ := CreateProject(p.name, p.composeFiles...)
*p = *newProject
}
ctx := context.Background()
err := p.composeProject.Create(ctx, options.Create{}, services...)
if err != nil {
return err
}
return p.StartOnly(services...)
}
// StartOnly only starts created services which are stopped.
func (p *Project) StartOnly(services ...string) error {
ctx := context.Background()
err := p.composeProject.Start(ctx, services...)
if err != nil {
return err
}
// Wait for compose to start
<-p.started
return nil
}
// StopOnly only stop services without delete them.
func (p *Project) StopOnly(services ...string) error {
ctx := context.Background()
err := p.composeProject.Stop(ctx, 10, services...)
if err != nil {
return err
}
<-p.stopped
return nil
}
// Stop shuts down and clean the project
func (p *Project) Stop(services ...string) error {
// FIXME(vdemeester) handle timeout
err := p.StopOnly(services...)
if err != nil {
return err
}
err = p.composeProject.Delete(context.Background(), options.Delete{}, services...)
if err != nil {
return err
}
<-p.deleted
existingContainers, err := p.existContainers(project.AnyState)
if err != nil {
return err
}
// Close channels only if there are no running services
if !existingContainers {
p.hasOpenedChan = false
close(p.started)
close(p.stopped)
close(p.deleted)
close(p.listenChan)
}
return nil
}
// Check if containers exist in the desirated state for the given services
func (p *Project) existContainers(stateFiltered project.State, services ...string) (bool, error) {
existingContainers := false
var err error
containersFound, err := p.composeProject.Containers(context.Background(), project.Filter{stateFiltered})
if err == nil && containersFound != nil && len(containersFound) > 0 {
existingContainers = true
}
return existingContainers, err
}
// Scale scale a service up
func (p *Project) Scale(service string, count int) error {
return p.composeProject.Scale(context.Background(), 10, map[string]int{
service: count,
})
}
func (p *Project) startListening() {
for event := range p.listenChan {
// FIXME Add a timeout on event ?
if event.EventType == events.ProjectStartDone {
p.started <- struct{}{}
}
if event.EventType == events.ProjectStopDone {
p.stopped <- struct{}{}
}
if event.EventType == events.ProjectDeleteDone {
p.deleted <- struct{}{}
}
}
}
// Containers lists containers for a given services.
func (p *Project) Containers(service string) ([]types.ContainerJSON, error) {
ctx := context.Background()
containers := []types.ContainerJSON{}
// Let's use engine-api for now as there is nothing really useful in
// libcompose for now.
filter := filters.NewArgs()
filter.Add("label", "com.docker.compose.project="+p.name)
filter.Add("label", "com.docker.compose.service="+service)
containerList, err := p.client.ContainerList(ctx, types.ContainerListOptions{
Filters: filter,
})
if err != nil {
return containers, err
}
for _, c := range containerList {
container, err := p.client.ContainerInspect(ctx, c.ID)
if err != nil {
return containers, err
}
containers = append(containers, container)
}
return containers, nil
}
// Container returns the one and only container for a given services. It returns an error
// if the service has more than one container (in case of scale)
func (p *Project) Container(service string) (types.ContainerJSON, error) {
containers, err := p.Containers(service)
if err != nil {
return types.ContainerJSON{}, err
}
if len(containers) > 1 {
return types.ContainerJSON{}, fmt.Errorf("More than one container are running for '%s' service", service)
}
if len(containers) == 0 {
return types.ContainerJSON{}, fmt.Errorf("No container found for '%s' service", service)
}
return containers[0], nil
}