221 lines
6 KiB
Go
221 lines
6 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
|
|
}
|
|
// FIXME(vdemeester) fix this
|
|
apiClient.UpdateClientVersion(d.CurrentAPIVersion)
|
|
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{})
|
|
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
|
|
}
|