1
0
mirror of https://github.com/rancher/os.git synced 2025-09-09 10:40:30 +00:00

Bump libcompose and its dependencies

This commit is contained in:
Josh Curl
2016-05-23 17:22:40 -07:00
parent c18cd26e78
commit 50de80d09a
1109 changed files with 35052 additions and 125685 deletions

View File

@@ -0,0 +1,35 @@
package project
import (
"github.com/docker/engine-api/client"
composeclient "github.com/docker/libcompose/docker/client"
)
// ClientFactory is a factory to create docker clients.
type ClientFactory interface {
// Create constructs a Docker client for the given service. The passed in
// config may be nil in which case a generic client for the project should
// be returned.
Create(service Service) client.APIClient
}
type defaultClientFactory struct {
client client.APIClient
}
// NewDefaultClientFactory creates and returns the default client factory that uses
// github.com/docker/engine-api client.
func NewDefaultClientFactory(opts composeclient.Options) (ClientFactory, error) {
client, err := composeclient.Create(opts)
if err != nil {
return nil, err
}
return &defaultClientFactory{
client: client,
}, nil
}
func (s *defaultClientFactory) Create(service Service) client.APIClient {
return s.client
}

View File

@@ -0,0 +1,13 @@
package project
import (
"golang.org/x/net/context"
)
// Container defines what a libcompose container provides.
type Container interface {
ID() (string, error)
Name() string
Port(ctx context.Context, port string) (string, error)
IsRunning(ctx context.Context) (bool, error)
}

View File

@@ -10,6 +10,7 @@ import (
"strings"
"github.com/Sirupsen/logrus"
"github.com/docker/libcompose/config"
"github.com/docker/libcompose/logger"
)
@@ -18,51 +19,47 @@ var projectRegexp = regexp.MustCompile("[^a-zA-Z0-9_.-]")
// Context holds context meta information about a libcompose project, like
// the project name, the compose file, etc.
type Context struct {
Timeout uint
Log bool
Volume bool
ForceRecreate bool
NoRecreate bool
Signal int
ComposeFile string
ComposeBytes []byte
ComposeFiles []string
ComposeBytes [][]byte
ProjectName string
isOpen bool
ServiceFactory ServiceFactory
EnvironmentLookup EnvironmentLookup
ConfigLookup ConfigLookup
EnvironmentLookup config.EnvironmentLookup
ResourceLookup config.ResourceLookup
LoggerFactory logger.Factory
IgnoreMissingConfig bool
Project *Project
}
func (c *Context) readComposeFile() error {
func (c *Context) readComposeFiles() error {
if c.ComposeBytes != nil {
return nil
}
logrus.Debugf("Opening compose file: %s", c.ComposeFile)
logrus.Debugf("Opening compose files: %s", strings.Join(c.ComposeFiles, ","))
if c.ComposeFile == "-" {
// Handle STDIN (`-f -`)
if len(c.ComposeFiles) == 1 && c.ComposeFiles[0] == "-" {
composeBytes, err := ioutil.ReadAll(os.Stdin)
if err != nil {
logrus.Errorf("Failed to read compose file from stdin: %v", err)
return err
}
c.ComposeBytes = composeBytes
} else if c.ComposeFile != "" {
if composeBytes, err := ioutil.ReadFile(c.ComposeFile); os.IsNotExist(err) {
if c.IgnoreMissingConfig {
return nil
}
logrus.Errorf("Failed to find %s", c.ComposeFile)
c.ComposeBytes = [][]byte{composeBytes}
return nil
}
for _, composeFile := range c.ComposeFiles {
composeBytes, err := ioutil.ReadFile(composeFile)
if err != nil && !os.IsNotExist(err) {
logrus.Errorf("Failed to open the compose file: %s", composeFile)
return err
} else if err != nil {
logrus.Errorf("Failed to open %s", c.ComposeFile)
return err
} else {
c.ComposeBytes = composeBytes
}
if err != nil && !c.IgnoreMissingConfig {
logrus.Errorf("Failed to find the compose file: %s", composeFile)
return err
}
c.ComposeBytes = append(c.ComposeBytes, composeBytes)
}
return nil
@@ -74,16 +71,12 @@ func (c *Context) determineProject() error {
return err
}
c.ProjectName = projectRegexp.ReplaceAllString(strings.ToLower(name), "-")
c.ProjectName = normalizeName(name)
if c.ProjectName == "" {
return fmt.Errorf("Falied to determine project name")
}
if strings.ContainsAny(c.ProjectName[0:1], "_.-") {
c.ProjectName = "x" + c.ProjectName
}
return nil
}
@@ -96,9 +89,14 @@ func (c *Context) lookupProjectName() (string, error) {
return envProject, nil
}
f, err := filepath.Abs(c.ComposeFile)
file := "."
if len(c.ComposeFiles) > 0 {
file = c.ComposeFiles[0]
}
f, err := filepath.Abs(file)
if err != nil {
logrus.Errorf("Failed to get absolute directory for: %s", c.ComposeFile)
logrus.Errorf("Failed to get absolute directory for: %s", file)
return "", err
}
@@ -114,6 +112,11 @@ func (c *Context) lookupProjectName() (string, error) {
}
}
func normalizeName(name string) string {
r := regexp.MustCompile("[^a-z0-9]+")
return r.ReplaceAllString(strings.ToLower(name), "")
}
func toUnixPath(p string) string {
return strings.Replace(p, "\\", "/", -1)
}
@@ -123,7 +126,7 @@ func (c *Context) open() error {
return nil
}
if err := c.readComposeFile(); err != nil {
if err := c.readComposeFiles(); err != nil {
return err
}

View File

@@ -1,70 +1,96 @@
package project
import (
"golang.org/x/net/context"
"github.com/docker/libcompose/project/options"
)
// EmptyService is a struct that implements Service but does nothing.
type EmptyService struct {
}
// Create implements Service.Create but does nothing.
func (e *EmptyService) Create() error {
func (e *EmptyService) Create(ctx context.Context, options options.Create) error {
return nil
}
// Build implements Service.Build but does nothing.
func (e *EmptyService) Build() error {
func (e *EmptyService) Build(ctx context.Context, buildOptions options.Build) error {
return nil
}
// Up implements Service.Up but does nothing.
func (e *EmptyService) Up() error {
func (e *EmptyService) Up(ctx context.Context, options options.Up) error {
return nil
}
// Start implements Service.Start but does nothing.
func (e *EmptyService) Start() error {
func (e *EmptyService) Start(ctx context.Context) error {
return nil
}
// Down implements Service.Down but does nothing.
func (e *EmptyService) Down() error {
// Stop implements Service.Stop() but does nothing.
func (e *EmptyService) Stop(ctx context.Context, timeout int) error {
return nil
}
// Delete implements Service.Delete but does nothing.
func (e *EmptyService) Delete() error {
func (e *EmptyService) Delete(ctx context.Context, options options.Delete) error {
return nil
}
// Restart implements Service.Restart but does nothing.
func (e *EmptyService) Restart() error {
func (e *EmptyService) Restart(ctx context.Context, timeout int) error {
return nil
}
// Log implements Service.Log but does nothing.
func (e *EmptyService) Log() error {
func (e *EmptyService) Log(ctx context.Context, follow bool) error {
return nil
}
// Pull implements Service.Pull but does nothing.
func (e *EmptyService) Pull() error {
func (e *EmptyService) Pull(ctx context.Context) error {
return nil
}
// Kill implements Service.Kill but does nothing.
func (e *EmptyService) Kill() error {
func (e *EmptyService) Kill(ctx context.Context, signal string) error {
return nil
}
// Containers implements Service.Containers but does nothing.
func (e *EmptyService) Containers() ([]Container, error) {
func (e *EmptyService) Containers(ctx context.Context) ([]Container, error) {
return []Container{}, nil
}
// Scale implements Service.Scale but does nothing.
func (e *EmptyService) Scale(count int) error {
func (e *EmptyService) Scale(ctx context.Context, count int, timeout int) error {
return nil
}
// Info implements Service.Info but does nothing.
func (e *EmptyService) Info(qFlag bool) (InfoSet, error) {
func (e *EmptyService) Info(ctx context.Context, qFlag bool) (InfoSet, error) {
return InfoSet{}, nil
}
// Pause implements Service.Pause but does nothing.
func (e *EmptyService) Pause(ctx context.Context) error {
return nil
}
// Unpause implements Service.Pause but does nothing.
func (e *EmptyService) Unpause(ctx context.Context) error {
return nil
}
// Run implements Service.Run but does nothing.
func (e *EmptyService) Run(ctx context.Context, commandParts []string) (int, error) {
return 0, nil
}
// RemoveImage implements Service.RemoveImage but does nothing.
func (e *EmptyService) RemoveImage(ctx context.Context, imageType options.ImageType) error {
return nil
}

View File

@@ -0,0 +1,197 @@
// Package events holds event structures, methods and functions.
package events
import (
"fmt"
)
// Notifier defines the methods an event notifier should have.
type Notifier interface {
Notify(eventType EventType, serviceName string, data map[string]string)
}
// Emitter defines the methods an event emitter should have.
type Emitter interface {
AddListener(c chan<- Event)
}
// Event holds project-wide event informations.
type Event struct {
EventType EventType
ServiceName string
Data map[string]string
}
// EventType defines a type of libcompose event.
type EventType int
// Definitions of libcompose events
const (
NoEvent = EventType(iota)
ContainerCreated = EventType(iota)
ContainerStarted = EventType(iota)
ServiceAdd = EventType(iota)
ServiceUpStart = EventType(iota)
ServiceUpIgnored = EventType(iota)
ServiceUp = EventType(iota)
ServiceCreateStart = EventType(iota)
ServiceCreate = EventType(iota)
ServiceDeleteStart = EventType(iota)
ServiceDelete = EventType(iota)
ServiceDownStart = EventType(iota)
ServiceDown = EventType(iota)
ServiceRestartStart = EventType(iota)
ServiceRestart = EventType(iota)
ServicePullStart = EventType(iota)
ServicePull = EventType(iota)
ServiceKillStart = EventType(iota)
ServiceKill = EventType(iota)
ServiceStartStart = EventType(iota)
ServiceStart = EventType(iota)
ServiceBuildStart = EventType(iota)
ServiceBuild = EventType(iota)
ServicePauseStart = EventType(iota)
ServicePause = EventType(iota)
ServiceUnpauseStart = EventType(iota)
ServiceUnpause = EventType(iota)
ServiceStopStart = EventType(iota)
ServiceStop = EventType(iota)
ServiceRunStart = EventType(iota)
ServiceRun = EventType(iota)
VolumeAdd = EventType(iota)
NetworkAdd = EventType(iota)
ProjectDownStart = EventType(iota)
ProjectDownDone = EventType(iota)
ProjectCreateStart = EventType(iota)
ProjectCreateDone = EventType(iota)
ProjectUpStart = EventType(iota)
ProjectUpDone = EventType(iota)
ProjectDeleteStart = EventType(iota)
ProjectDeleteDone = EventType(iota)
ProjectRestartStart = EventType(iota)
ProjectRestartDone = EventType(iota)
ProjectReload = EventType(iota)
ProjectReloadTrigger = EventType(iota)
ProjectKillStart = EventType(iota)
ProjectKillDone = EventType(iota)
ProjectStartStart = EventType(iota)
ProjectStartDone = EventType(iota)
ProjectBuildStart = EventType(iota)
ProjectBuildDone = EventType(iota)
ProjectPauseStart = EventType(iota)
ProjectPauseDone = EventType(iota)
ProjectUnpauseStart = EventType(iota)
ProjectUnpauseDone = EventType(iota)
ProjectStopStart = EventType(iota)
ProjectStopDone = EventType(iota)
)
func (e EventType) String() string {
var m string
switch e {
case ContainerCreated:
m = "Created container"
case ContainerStarted:
m = "Started container"
case ServiceAdd:
m = "Adding"
case ServiceUpStart:
m = "Starting"
case ServiceUpIgnored:
m = "Ignoring"
case ServiceUp:
m = "Started"
case ServiceCreateStart:
m = "Creating"
case ServiceCreate:
m = "Created"
case ServiceDeleteStart:
m = "Deleting"
case ServiceDelete:
m = "Deleted"
case ServiceStopStart:
m = "Stopping"
case ServiceStop:
m = "Stopped"
case ServiceDownStart:
m = "Stopping"
case ServiceDown:
m = "Stopped"
case ServiceRestartStart:
m = "Restarting"
case ServiceRestart:
m = "Restarted"
case ServicePullStart:
m = "Pulling"
case ServicePull:
m = "Pulled"
case ServiceKillStart:
m = "Killing"
case ServiceKill:
m = "Killed"
case ServiceStartStart:
m = "Starting"
case ServiceStart:
m = "Started"
case ServiceBuildStart:
m = "Building"
case ServiceBuild:
m = "Built"
case ServiceRunStart:
m = "Executing"
case ServiceRun:
m = "Executed"
case ProjectDownStart:
m = "Stopping project"
case ProjectDownDone:
m = "Project stopped"
case ProjectStopStart:
m = "Stopping project"
case ProjectStopDone:
m = "Project stopped"
case ProjectCreateStart:
m = "Creating project"
case ProjectCreateDone:
m = "Project created"
case ProjectUpStart:
m = "Starting project"
case ProjectUpDone:
m = "Project started"
case ProjectDeleteStart:
m = "Deleting project"
case ProjectDeleteDone:
m = "Project deleted"
case ProjectRestartStart:
m = "Restarting project"
case ProjectRestartDone:
m = "Project restarted"
case ProjectReload:
m = "Reloading project"
case ProjectReloadTrigger:
m = "Triggering project reload"
case ProjectKillStart:
m = "Killing project"
case ProjectKillDone:
m = "Project killed"
case ProjectStartStart:
m = "Starting project"
case ProjectStartDone:
m = "Project started"
case ProjectBuildStart:
m = "Building project"
case ProjectBuildDone:
m = "Project built"
}
if m == "" {
m = fmt.Sprintf("EventType: %d", int(e))
}
return m
}

View File

@@ -1,106 +0,0 @@
package project
import (
"crypto/sha1"
"encoding/hex"
"fmt"
"io"
"reflect"
"sort"
)
// GetServiceHash computes and returns a hash that will identify a service.
// This hash will be then used to detect if the service definition/configuration
// have changed and needs to be recreated.
func GetServiceHash(name string, config *ServiceConfig) string {
hash := sha1.New()
io.WriteString(hash, name)
//Get values of Service through reflection
val := reflect.ValueOf(config).Elem()
//Create slice to sort the keys in Service Config, which allow constant hash ordering
serviceKeys := []string{}
//Create a data structure of map of values keyed by a string
unsortedKeyValue := make(map[string]interface{})
//Get all keys and values in Service Configuration
for i := 0; i < val.NumField(); i++ {
valueField := val.Field(i)
keyField := val.Type().Field(i)
serviceKeys = append(serviceKeys, keyField.Name)
unsortedKeyValue[keyField.Name] = valueField.Interface()
}
//Sort serviceKeys alphabetically
sort.Strings(serviceKeys)
//Go through keys and write hash
for _, serviceKey := range serviceKeys {
serviceValue := unsortedKeyValue[serviceKey]
io.WriteString(hash, fmt.Sprintf("\n %v: ", serviceKey))
switch s := serviceValue.(type) {
case SliceorMap:
sliceKeys := []string{}
for lkey := range s.MapParts() {
sliceKeys = append(sliceKeys, lkey)
}
sort.Strings(sliceKeys)
for _, sliceKey := range sliceKeys {
io.WriteString(hash, fmt.Sprintf("%s=%v, ", sliceKey, s.MapParts()[sliceKey]))
}
case MaporEqualSlice:
sliceKeys := s.Slice()
// do not sort keys as the order matters
for _, sliceKey := range sliceKeys {
io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey))
}
case MaporColonSlice:
sliceKeys := s.Slice()
// do not sort keys as the order matters
for _, sliceKey := range sliceKeys {
io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey))
}
case MaporSpaceSlice:
sliceKeys := s.Slice()
// do not sort keys as the order matters
for _, sliceKey := range sliceKeys {
io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey))
}
case Command:
sliceKeys := s.Slice()
// do not sort keys as the order matters
for _, sliceKey := range sliceKeys {
io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey))
}
case Stringorslice:
sliceKeys := s.Slice()
sort.Strings(sliceKeys)
for _, sliceKey := range sliceKeys {
io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey))
}
case []string:
sliceKeys := s
sort.Strings(sliceKeys)
for _, sliceKey := range sliceKeys {
io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey))
}
default:
io.WriteString(hash, fmt.Sprintf("%v", serviceValue))
}
}
return hex.EncodeToString(hash.Sum(nil))
}

View File

@@ -6,6 +6,17 @@ import (
"text/tabwriter"
)
// InfoPart holds key/value strings.
type InfoPart struct {
Key, Value string
}
// InfoSet holds a list of Info.
type InfoSet []Info
// Info holds a list of InfoPart.
type Info []InfoPart
func (infos InfoSet) String(titleFlag bool) string {
//no error checking, none of this should fail
buffer := bytes.NewBuffer(make([]byte, 0, 1024))

View File

@@ -0,0 +1,39 @@
package project
import (
"golang.org/x/net/context"
"github.com/docker/libcompose/config"
"github.com/docker/libcompose/project/events"
"github.com/docker/libcompose/project/options"
)
// APIProject is an interface defining the methods a libcompose project should implement.
type APIProject interface {
events.Notifier
events.Emitter
Build(ctx context.Context, options options.Build, sevice ...string) error
Create(ctx context.Context, options options.Create, services ...string) error
Delete(ctx context.Context, options options.Delete, services ...string) error
Down(ctx context.Context, options options.Down, services ...string) error
Kill(ctx context.Context, signal string, services ...string) error
Log(ctx context.Context, follow bool, services ...string) error
Pause(ctx context.Context, services ...string) error
Ps(ctx context.Context, onlyID bool, services ...string) (InfoSet, error)
// FIXME(vdemeester) we could use nat.Port instead ?
Port(ctx context.Context, index int, protocol, serviceName, privatePort string) (string, error)
Pull(ctx context.Context, services ...string) error
Restart(ctx context.Context, timeout int, services ...string) error
Run(ctx context.Context, serviceName string, commandParts []string) (int, error)
Scale(ctx context.Context, timeout int, servicesScale map[string]int) error
Start(ctx context.Context, services ...string) error
Stop(ctx context.Context, timeout int, services ...string) error
Unpause(ctx context.Context, services ...string) error
Up(ctx context.Context, options options.Up, services ...string) error
Parse() error
CreateService(name string) (Service, error)
AddConfig(name string, config *config.ServiceConfig) error
Load(bytes []byte) error
}

View File

@@ -1,179 +0,0 @@
package project
import (
"bytes"
"fmt"
"strconv"
"strings"
"github.com/Sirupsen/logrus"
)
func isNum(c uint8) bool {
return c >= '0' && c <= '9'
}
func validVariableNameChar(c uint8) bool {
return c == '_' ||
c >= 'A' && c <= 'Z' ||
c >= 'a' && c <= 'z' ||
isNum(c)
}
func parseVariable(line string, pos int, mapping func(string) string) (string, int, bool) {
var buffer bytes.Buffer
for ; pos < len(line); pos++ {
c := line[pos]
switch {
case validVariableNameChar(c):
buffer.WriteByte(c)
default:
return mapping(buffer.String()), pos - 1, true
}
}
return mapping(buffer.String()), pos, true
}
func parseVariableWithBraces(line string, pos int, mapping func(string) string) (string, int, bool) {
var buffer bytes.Buffer
for ; pos < len(line); pos++ {
c := line[pos]
switch {
case c == '}':
bufferString := buffer.String()
if bufferString == "" {
return "", 0, false
}
return mapping(buffer.String()), pos, true
case validVariableNameChar(c):
buffer.WriteByte(c)
default:
return "", 0, false
}
}
return "", 0, false
}
func parseInterpolationExpression(line string, pos int, mapping func(string) string) (string, int, bool) {
c := line[pos]
switch {
case c == '$':
return "$", pos, true
case c == '{':
return parseVariableWithBraces(line, pos+1, mapping)
case !isNum(c) && validVariableNameChar(c):
// Variables can't start with a number
return parseVariable(line, pos, mapping)
default:
return "", 0, false
}
}
func parseLine(line string, mapping func(string) string) (string, bool) {
var buffer bytes.Buffer
for pos := 0; pos < len(line); pos++ {
c := line[pos]
switch {
case c == '$':
var replaced string
var success bool
replaced, pos, success = parseInterpolationExpression(line, pos+1, mapping)
if !success {
return "", false
}
buffer.WriteString(replaced)
default:
buffer.WriteByte(c)
}
}
return buffer.String(), true
}
func parseConfig(option, service string, data *interface{}, mapping func(string) string) error {
switch typedData := (*data).(type) {
case string:
var success bool
interpolatedLine, success := parseLine(typedData, mapping)
if !success {
return fmt.Errorf("Invalid interpolation format for \"%s\" option in service \"%s\": \"%s\"", option, service, typedData)
}
// If possible, convert the value to an integer
// If the type should be a string and not an int, go-yaml will convert it back into a string
lineAsInteger, err := strconv.Atoi(interpolatedLine)
if err == nil {
*data = lineAsInteger
} else {
*data = interpolatedLine
}
case []interface{}:
for k, v := range typedData {
err := parseConfig(option, service, &v, mapping)
if err != nil {
return err
}
typedData[k] = v
}
case map[interface{}]interface{}:
for k, v := range typedData {
err := parseConfig(option, service, &v, mapping)
if err != nil {
return err
}
typedData[k] = v
}
}
return nil
}
func interpolate(environmentLookup EnvironmentLookup, config *rawServiceMap) error {
for k, v := range *config {
for k2, v2 := range v {
err := parseConfig(k2, k, &v2, func(s string) string {
values := environmentLookup.Lookup(s, k, nil)
if len(values) == 0 {
logrus.Warnf("The %s variable is not set. Substituting a blank string.", s)
return ""
}
// Use first result if many are given
value := values[0]
// Environment variables come in key=value format
// Return everything past first '='
return strings.SplitN(value, "=", 2)[1]
})
if err != nil {
return err
}
(*config)[k][k2] = v2
}
}
return nil
}

View File

@@ -1,226 +0,0 @@
package project
import (
"fmt"
"os"
"testing"
yaml "github.com/cloudfoundry-incubator/candiedyaml"
"github.com/stretchr/testify/assert"
)
func testInterpolatedLine(t *testing.T, expectedLine, interpolatedLine string, envVariables map[string]string) {
interpolatedLine, _ = parseLine(interpolatedLine, func(s string) string {
return envVariables[s]
})
assert.Equal(t, expectedLine, interpolatedLine)
}
func testInvalidInterpolatedLine(t *testing.T, line string) {
_, success := parseLine(line, func(string) string {
return ""
})
assert.Equal(t, false, success)
}
func TestParseLine(t *testing.T) {
variables := map[string]string{
"A": "ABC",
"X": "XYZ",
"E": "",
"lower": "WORKED",
"MiXeD": "WORKED",
"split_VaLue": "WORKED",
"9aNumber": "WORKED",
"a9Number": "WORKED",
}
testInterpolatedLine(t, "WORKED", "$lower", variables)
testInterpolatedLine(t, "WORKED", "${MiXeD}", variables)
testInterpolatedLine(t, "WORKED", "${split_VaLue}", variables)
// Starting with a number isn't valid
testInterpolatedLine(t, "", "$9aNumber", variables)
testInterpolatedLine(t, "WORKED", "$a9Number", variables)
testInterpolatedLine(t, "ABC", "$A", variables)
testInterpolatedLine(t, "ABC", "${A}", variables)
testInterpolatedLine(t, "ABC DE", "$A DE", variables)
testInterpolatedLine(t, "ABCDE", "${A}DE", variables)
testInterpolatedLine(t, "$A", "$$A", variables)
testInterpolatedLine(t, "${A}", "$${A}", variables)
testInterpolatedLine(t, "$ABC", "$$${A}", variables)
testInterpolatedLine(t, "$ABC", "$$$A", variables)
testInterpolatedLine(t, "ABC XYZ", "$A $X", variables)
testInterpolatedLine(t, "ABCXYZ", "$A$X", variables)
testInterpolatedLine(t, "ABCXYZ", "${A}${X}", variables)
testInterpolatedLine(t, "", "$B", variables)
testInterpolatedLine(t, "", "${B}", variables)
testInterpolatedLine(t, "", "$ADE", variables)
testInterpolatedLine(t, "", "$E", variables)
testInterpolatedLine(t, "", "${E}", variables)
testInvalidInterpolatedLine(t, "${")
testInvalidInterpolatedLine(t, "$}")
testInvalidInterpolatedLine(t, "${}")
testInvalidInterpolatedLine(t, "${ }")
testInvalidInterpolatedLine(t, "${A }")
testInvalidInterpolatedLine(t, "${ A}")
testInvalidInterpolatedLine(t, "${A!}")
testInvalidInterpolatedLine(t, "$!")
}
type MockEnvironmentLookup struct {
Variables map[string]string
}
func (m MockEnvironmentLookup) Lookup(key, serviceName string, config *ServiceConfig) []string {
return []string{fmt.Sprintf("%s=%s", key, m.Variables[key])}
}
func testInterpolatedConfig(t *testing.T, expectedConfig, interpolatedConfig string, envVariables map[string]string) {
for k, v := range envVariables {
os.Setenv(k, v)
}
expectedConfigBytes := []byte(expectedConfig)
interpolatedConfigBytes := []byte(interpolatedConfig)
expectedData := make(rawServiceMap)
interpolatedData := make(rawServiceMap)
yaml.Unmarshal(expectedConfigBytes, &expectedData)
yaml.Unmarshal(interpolatedConfigBytes, &interpolatedData)
_ = interpolate(MockEnvironmentLookup{envVariables}, &interpolatedData)
for k := range envVariables {
os.Unsetenv(k)
}
assert.Equal(t, expectedData, interpolatedData)
}
func testInvalidInterpolatedConfig(t *testing.T, interpolatedConfig string) {
interpolatedConfigBytes := []byte(interpolatedConfig)
interpolatedData := make(rawServiceMap)
yaml.Unmarshal(interpolatedConfigBytes, &interpolatedData)
err := interpolate(new(MockEnvironmentLookup), &interpolatedData)
assert.NotNil(t, err)
}
func TestInterpolate(t *testing.T) {
testInterpolatedConfig(t,
`web:
# unbracketed name
image: busybox
# array element
ports:
- "80:8000"
# dictionary item value
labels:
mylabel: "myvalue"
# unset value
hostname: "host-"
# escaped interpolation
command: "${ESCAPED}"`,
`web:
# unbracketed name
image: $IMAGE
# array element
ports:
- "${HOST_PORT}:8000"
# dictionary item value
labels:
mylabel: "${LABEL_VALUE}"
# unset value
hostname: "host-${UNSET_VALUE}"
# escaped interpolation
command: "$${ESCAPED}"`, map[string]string{
"IMAGE": "busybox",
"HOST_PORT": "80",
"LABEL_VALUE": "myvalue",
})
// Same as above, but testing with equal signs in variables
testInterpolatedConfig(t,
`web:
# unbracketed name
image: =busybox
# array element
ports:
- "=:8000"
# dictionary item value
labels:
mylabel: "myvalue=="
# unset value
hostname: "host-"
# escaped interpolation
command: "${ESCAPED}"`,
`web:
# unbracketed name
image: $IMAGE
# array element
ports:
- "${HOST_PORT}:8000"
# dictionary item value
labels:
mylabel: "${LABEL_VALUE}"
# unset value
hostname: "host-${UNSET_VALUE}"
# escaped interpolation
command: "$${ESCAPED}"`, map[string]string{
"IMAGE": "=busybox",
"HOST_PORT": "=",
"LABEL_VALUE": "myvalue==",
})
testInvalidInterpolatedConfig(t,
`web:
image: "${"`)
testInvalidInterpolatedConfig(t,
`web:
image: busybox
# array element
ports:
- "${}:8000"`)
testInvalidInterpolatedConfig(t,
`web:
image: busybox
# array element
ports:
- "80:8000"
# dictionary item value
labels:
mylabel: "${ LABEL_VALUE}"`)
}

View File

@@ -4,39 +4,40 @@ import (
"bytes"
"github.com/Sirupsen/logrus"
"github.com/docker/libcompose/project/events"
)
var (
infoEvents = map[EventType]bool{
EventProjectDeleteDone: true,
EventProjectDeleteStart: true,
EventProjectDownDone: true,
EventProjectDownStart: true,
EventProjectRestartDone: true,
EventProjectRestartStart: true,
EventProjectUpDone: true,
EventProjectUpStart: true,
EventServiceDeleteStart: true,
EventServiceDelete: true,
EventServiceDownStart: true,
EventServiceDown: true,
EventServiceRestartStart: true,
EventServiceRestart: true,
EventServiceUpStart: true,
EventServiceUp: true,
infoEvents = map[events.EventType]bool{
events.ProjectDeleteDone: true,
events.ProjectDeleteStart: true,
events.ProjectDownDone: true,
events.ProjectDownStart: true,
events.ProjectRestartDone: true,
events.ProjectRestartStart: true,
events.ProjectUpDone: true,
events.ProjectUpStart: true,
events.ServiceDeleteStart: true,
events.ServiceDelete: true,
events.ServiceDownStart: true,
events.ServiceDown: true,
events.ServiceRestartStart: true,
events.ServiceRestart: true,
events.ServiceUpStart: true,
events.ServiceUp: true,
}
)
type defaultListener struct {
project *Project
listenChan chan Event
listenChan chan events.Event
upCount int
}
// NewDefaultListener create a default listener for the specified project.
func NewDefaultListener(p *Project) chan<- Event {
func NewDefaultListener(p *Project) chan<- events.Event {
l := defaultListener{
listenChan: make(chan Event),
listenChan: make(chan events.Event),
project: p,
}
go l.start()
@@ -57,7 +58,7 @@ func (d *defaultListener) start() {
}
}
if event.EventType == EventServiceUp {
if event.EventType == events.ServiceUp {
d.upCount++
}
@@ -70,7 +71,7 @@ func (d *defaultListener) start() {
if event.ServiceName == "" {
logf("Project [%s]: %s %s", d.project.Name, event.EventType, buffer.Bytes())
} else {
logf("[%d/%d] [%s]: %s %s", d.upCount, len(d.project.Configs), event.ServiceName, event.EventType, buffer.Bytes())
logf("[%d/%d] [%s]: %s %s", d.upCount, d.project.ServiceConfigs.Len(), event.ServiceName, event.EventType, buffer.Bytes())
}
}
}

View File

@@ -1,299 +0,0 @@
package project
import (
"bufio"
"bytes"
"fmt"
"path"
"strings"
"github.com/Sirupsen/logrus"
yaml "github.com/cloudfoundry-incubator/candiedyaml"
"github.com/docker/libcompose/utils"
)
var (
// ValidRemotes list the of valid prefixes that can be sent to Docker as a build remote location
// This is public for consumers of libcompose to use
ValidRemotes = []string{
"git://",
"git@github.com:",
"github.com",
"http:",
"https:",
}
noMerge = []string{
"links",
"volumes_from",
}
)
type rawService map[string]interface{}
type rawServiceMap map[string]rawService
func mergeProject(p *Project, bytes []byte) (map[string]*ServiceConfig, error) {
configs := make(map[string]*ServiceConfig)
datas := make(rawServiceMap)
if err := yaml.Unmarshal(bytes, &datas); err != nil {
return nil, err
}
if err := interpolate(p.context.EnvironmentLookup, &datas); err != nil {
return nil, err
}
for name, data := range datas {
data, err := parse(p.context.ConfigLookup, p.context.EnvironmentLookup, p.File, data, datas)
if err != nil {
logrus.Errorf("Failed to parse service %s: %v", name, err)
return nil, err
}
datas[name] = data
}
if err := utils.Convert(datas, &configs); err != nil {
return nil, err
}
adjustValues(configs)
return configs, nil
}
func adjustValues(configs map[string]*ServiceConfig) {
// yaml parser turns "no" into "false" but that is not valid for a restart policy
for _, v := range configs {
if v.Restart == "false" {
v.Restart = "no"
}
}
}
func readEnvFile(configLookup ConfigLookup, inFile string, serviceData rawService) (rawService, error) {
var config ServiceConfig
if err := utils.Convert(serviceData, &config); err != nil {
return nil, err
}
if len(config.EnvFile.Slice()) == 0 {
return serviceData, nil
}
if configLookup == nil {
return nil, fmt.Errorf("Can not use env_file in file %s no mechanism provided to load files", inFile)
}
vars := config.Environment.Slice()
for i := len(config.EnvFile.Slice()) - 1; i >= 0; i-- {
envFile := config.EnvFile.Slice()[i]
content, _, err := configLookup.Lookup(envFile, inFile)
if err != nil {
return nil, err
}
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(bytes.NewBuffer(content))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
key := strings.SplitAfter(line, "=")[0]
found := false
for _, v := range vars {
if strings.HasPrefix(v, key) {
found = true
break
}
}
if !found {
vars = append(vars, line)
}
}
if scanner.Err() != nil {
return nil, scanner.Err()
}
}
serviceData["environment"] = vars
delete(serviceData, "env_file")
return serviceData, nil
}
func resolveBuild(inFile string, serviceData rawService) (rawService, error) {
build := asString(serviceData["build"])
if build == "" {
return serviceData, nil
}
for _, remote := range ValidRemotes {
if strings.HasPrefix(build, remote) {
return serviceData, nil
}
}
current := path.Dir(inFile)
if build == "." {
build = current
} else {
current = path.Join(current, build)
}
serviceData["build"] = current
return serviceData, nil
}
func parse(configLookup ConfigLookup, environmentLookup EnvironmentLookup, inFile string, serviceData rawService, datas rawServiceMap) (rawService, error) {
serviceData, err := readEnvFile(configLookup, inFile, serviceData)
if err != nil {
return nil, err
}
serviceData, err = resolveBuild(inFile, serviceData)
if err != nil {
return nil, err
}
value, ok := serviceData["extends"]
if !ok {
return serviceData, nil
}
mapValue, ok := value.(map[interface{}]interface{})
if !ok {
return serviceData, nil
}
if configLookup == nil {
return nil, fmt.Errorf("Can not use extends in file %s no mechanism provided to files", inFile)
}
file := asString(mapValue["file"])
service := asString(mapValue["service"])
if service == "" {
return serviceData, nil
}
var baseService rawService
if file == "" {
if serviceData, ok := datas[service]; ok {
baseService, err = parse(configLookup, environmentLookup, inFile, serviceData, datas)
} else {
return nil, fmt.Errorf("Failed to find service %s to extend", service)
}
} else {
bytes, resolved, err := configLookup.Lookup(file, inFile)
if err != nil {
logrus.Errorf("Failed to lookup file %s: %v", file, err)
return nil, err
}
var baseRawServices rawServiceMap
if err := yaml.Unmarshal(bytes, &baseRawServices); err != nil {
return nil, err
}
err = interpolate(environmentLookup, &baseRawServices)
if err != nil {
return nil, err
}
baseService, ok = baseRawServices[service]
if !ok {
return nil, fmt.Errorf("Failed to find service %s in file %s", service, file)
}
baseService, err = parse(configLookup, environmentLookup, resolved, baseService, baseRawServices)
}
if err != nil {
return nil, err
}
baseService = clone(baseService)
logrus.Debugf("Merging %#v, %#v", baseService, serviceData)
for _, k := range noMerge {
if _, ok := baseService[k]; ok {
source := file
if source == "" {
source = inFile
}
return nil, fmt.Errorf("Cannot extend service '%s' in %s: services with '%s' cannot be extended", service, source, k)
}
}
for k, v := range serviceData {
// Image and build are mutually exclusive in merge
if k == "image" {
delete(baseService, "build")
} else if k == "build" {
delete(baseService, "image")
}
existing, ok := baseService[k]
if ok {
baseService[k] = merge(existing, v)
} else {
baseService[k] = v
}
}
logrus.Debugf("Merged result %#v", baseService)
return baseService, nil
}
func merge(existing, value interface{}) interface{} {
// append strings
if left, lok := existing.([]interface{}); lok {
if right, rok := value.([]interface{}); rok {
return append(left, right...)
}
}
//merge maps
if left, lok := existing.(map[interface{}]interface{}); lok {
if right, rok := value.(map[interface{}]interface{}); rok {
newLeft := make(map[interface{}]interface{})
for k, v := range left {
newLeft[k] = v
}
for k, v := range right {
newLeft[k] = v
}
return newLeft
}
}
return value
}
func clone(in rawService) rawService {
result := rawService{}
for k, v := range in {
result[k] = v
}
return result
}
func asString(obj interface{}) string {
if v, ok := obj.(string); ok {
return v
}
return ""
}

View File

@@ -1,192 +0,0 @@
package project
import "testing"
type NullLookup struct {
}
func (n *NullLookup) Lookup(file, relativeTo string) ([]byte, string, error) {
return nil, "", nil
}
func TestExtendsInheritImage(t *testing.T) {
p := NewProject(&Context{
ConfigLookup: &NullLookup{},
})
config, err := mergeProject(p, []byte(`
parent:
image: foo
child:
extends:
service: parent
`))
if err != nil {
t.Fatal(err)
}
parent := config["parent"]
child := config["child"]
if parent.Image != "foo" {
t.Fatal("Invalid image", parent.Image)
}
if child.Build != "" {
t.Fatal("Invalid build", child.Build)
}
if child.Image != "foo" {
t.Fatal("Invalid image", child.Image)
}
}
func TestExtendsInheritBuild(t *testing.T) {
p := NewProject(&Context{
ConfigLookup: &NullLookup{},
})
config, err := mergeProject(p, []byte(`
parent:
build: .
child:
extends:
service: parent
`))
if err != nil {
t.Fatal(err)
}
parent := config["parent"]
child := config["child"]
if parent.Build != "." {
t.Fatal("Invalid build", parent.Build)
}
if child.Build != "." {
t.Fatal("Invalid build", child.Build)
}
if child.Image != "" {
t.Fatal("Invalid image", child.Image)
}
}
func TestExtendBuildOverImage(t *testing.T) {
p := NewProject(&Context{
ConfigLookup: &NullLookup{},
})
config, err := mergeProject(p, []byte(`
parent:
image: foo
child:
build: .
extends:
service: parent
`))
if err != nil {
t.Fatal(err)
}
parent := config["parent"]
child := config["child"]
if parent.Image != "foo" {
t.Fatal("Invalid image", parent.Image)
}
if child.Build != "." {
t.Fatal("Invalid build", child.Build)
}
if child.Image != "" {
t.Fatal("Invalid image", child.Image)
}
}
func TestExtendImageOverBuild(t *testing.T) {
p := NewProject(&Context{
ConfigLookup: &NullLookup{},
})
config, err := mergeProject(p, []byte(`
parent:
build: .
child:
image: foo
extends:
service: parent
`))
if err != nil {
t.Fatal(err)
}
parent := config["parent"]
child := config["child"]
if parent.Image != "" {
t.Fatal("Invalid image", parent.Image)
}
if parent.Build != "." {
t.Fatal("Invalid build", parent.Build)
}
if child.Build != "" {
t.Fatal("Invalid build", child.Build)
}
if child.Image != "foo" {
t.Fatal("Invalid image", child.Image)
}
}
func TestRestartNo(t *testing.T) {
p := NewProject(&Context{
ConfigLookup: &NullLookup{},
})
config, err := mergeProject(p, []byte(`
test:
restart: no
image: foo
`))
if err != nil {
t.Fatal(err)
}
test := config["test"]
if test.Restart != "no" {
t.Fatal("Invalid restart policy", test.Restart)
}
}
func TestRestartAlways(t *testing.T) {
p := NewProject(&Context{
ConfigLookup: &NullLookup{},
})
config, err := mergeProject(p, []byte(`
test:
restart: always
image: foo
`))
if err != nil {
t.Fatal(err)
}
test := config["test"]
if test.Restart != "always" {
t.Fatal("Invalid restart policy", test.Restart)
}
}

View File

@@ -0,0 +1,47 @@
package options
// Build holds options of compose build.
type Build struct {
NoCache bool
ForceRemove bool
Pull bool
}
// Delete holds options of compose rm.
type Delete struct {
RemoveVolume bool
BeforeDeleteCallback func([]string) bool
}
// Down holds options of compose down.
type Down struct {
RemoveVolume bool
RemoveImages ImageType
RemoveOrphans bool
}
// Create holds options of compose create.
type Create struct {
NoRecreate bool
ForceRecreate bool
NoBuild bool
// ForceBuild bool
}
// Up holds options of compose up.
type Up struct {
Create
}
// ImageType defines the type of image (local, all)
type ImageType string
// Valid indicates whether the image type is valid.
func (i ImageType) Valid() bool {
switch string(i) {
case "", "local", "all":
return true
default:
return false
}
}

View File

@@ -5,41 +5,49 @@ import (
"fmt"
"strings"
"golang.org/x/net/context"
log "github.com/Sirupsen/logrus"
"github.com/docker/engine-api/types"
"github.com/docker/engine-api/types/filters"
"github.com/docker/libcompose/config"
"github.com/docker/libcompose/labels"
"github.com/docker/libcompose/logger"
"github.com/docker/libcompose/project/events"
"github.com/docker/libcompose/project/options"
"github.com/docker/libcompose/utils"
)
// ServiceState holds the state of a service.
type ServiceState string
// State definitions
var (
StateExecuted = ServiceState("executed")
StateUnknown = ServiceState("unknown")
)
// Error definitions
var (
ErrRestart = errors.New("Restart execution")
ErrUnsupported = errors.New("UnsupportedOperation")
)
// Event holds project-wide event informations.
type Event struct {
EventType EventType
ServiceName string
Data map[string]string
}
type wrapperAction func(*serviceWrapper, map[string]*serviceWrapper)
type serviceAction func(service Service) error
// NewProject create a new project with the specified context.
func NewProject(context *Context) *Project {
// Project holds libcompose project information.
type Project struct {
Name string
ServiceConfigs *config.ServiceConfigs
VolumeConfigs map[string]*config.VolumeConfig
NetworkConfigs map[string]*config.NetworkConfig
Files []string
ReloadCallback func() error
ParseOptions *config.ParseOptions
context *Context
clientFactory ClientFactory
reload []string
upCount int
listeners []chan<- events.Event
hasListeners bool
}
// NewProject creates a new project with the specified context.
func NewProject(clientFactory ClientFactory, context *Context, parseOptions *config.ParseOptions) *Project {
p := &Project{
context: context,
Configs: make(map[string]*ServiceConfig),
context: context,
clientFactory: clientFactory,
ParseOptions: parseOptions,
ServiceConfigs: config.NewServiceConfigs(),
VolumeConfigs: make(map[string]*config.VolumeConfig),
NetworkConfigs: make(map[string]*config.NetworkConfig),
}
if context.LoggerFactory == nil {
@@ -48,7 +56,7 @@ func NewProject(context *Context) *Project {
context.Project = p
p.listeners = []chan<- Event{NewDefaultListener(p)}
p.listeners = []chan<- events.Event{NewDefaultListener(p)}
return p
}
@@ -63,23 +71,31 @@ func (p *Project) Parse() error {
p.Name = p.context.ProjectName
if p.context.ComposeFile == "-" {
p.File = "."
} else {
p.File = p.context.ComposeFile
p.Files = p.context.ComposeFiles
if len(p.Files) == 1 && p.Files[0] == "-" {
p.Files = []string{"."}
}
if p.context.ComposeBytes != nil {
return p.Load(p.context.ComposeBytes)
for i, composeBytes := range p.context.ComposeBytes {
file := ""
if i < len(p.context.ComposeFiles) {
file = p.Files[i]
}
if err := p.load(file, composeBytes); err != nil {
return err
}
}
}
return nil
}
// CreateService creates a service with the specified name based. It there
// CreateService creates a service with the specified name based. If there
// is no config in the project for this service, it will return an error.
func (p *Project) CreateService(name string) (Service, error) {
existing, ok := p.Configs[name]
existing, ok := p.ServiceConfigs.Get(name)
if !ok {
return nil, fmt.Errorf("Failed to find service: %s", name)
}
@@ -88,9 +104,9 @@ func (p *Project) CreateService(name string) (Service, error) {
config := *existing
if p.context.EnvironmentLookup != nil {
parsedEnv := make([]string, 0, len(config.Environment.Slice()))
parsedEnv := make([]string, 0, len(config.Environment))
for _, env := range config.Environment.Slice() {
for _, env := range config.Environment {
parts := strings.SplitN(env, "=", 2)
if len(parts) > 1 && parts[1] != "" {
parsedEnv = append(parsedEnv, env)
@@ -104,39 +120,71 @@ func (p *Project) CreateService(name string) (Service, error) {
}
}
config.Environment = NewMaporEqualSlice(parsedEnv)
config.Environment = parsedEnv
}
return p.context.ServiceFactory.Create(p, name, &config)
}
// AddConfig adds the specified service config for the specified name.
func (p *Project) AddConfig(name string, config *ServiceConfig) error {
p.Notify(EventServiceAdd, name, nil)
func (p *Project) AddConfig(name string, config *config.ServiceConfig) error {
p.Notify(events.ServiceAdd, name, nil)
p.Configs[name] = config
p.ServiceConfigs.Add(name, config)
p.reload = append(p.reload, name)
return nil
}
// AddVolumeConfig adds the specified volume config for the specified name.
func (p *Project) AddVolumeConfig(name string, config *config.VolumeConfig) error {
p.Notify(events.VolumeAdd, name, nil)
p.VolumeConfigs[name] = config
return nil
}
// AddNetworkConfig adds the specified network config for the specified name.
func (p *Project) AddNetworkConfig(name string, config *config.NetworkConfig) error {
p.Notify(events.NetworkAdd, name, nil)
p.NetworkConfigs[name] = config
return nil
}
// Load loads the specified byte array (the composefile content) and adds the
// service configuration to the project.
// FIXME is it needed ?
func (p *Project) Load(bytes []byte) error {
configs := make(map[string]*ServiceConfig)
configs, err := mergeProject(p, bytes)
return p.load("", bytes)
}
func (p *Project) load(file string, bytes []byte) error {
serviceConfigs, volumeConfigs, networkConfigs, err := config.Merge(p.ServiceConfigs, p.context.EnvironmentLookup, p.context.ResourceLookup, file, bytes, p.ParseOptions)
if err != nil {
log.Errorf("Could not parse config for project %s : %v", p.Name, err)
return err
}
for name, config := range configs {
for name, config := range serviceConfigs {
err := p.AddConfig(name, config)
if err != nil {
return err
}
}
for name, config := range volumeConfigs {
err := p.AddVolumeConfig(name, config)
if err != nil {
return err
}
}
for name, config := range networkConfigs {
err := p.AddNetworkConfig(name, config)
if err != nil {
return err
}
}
return nil
}
@@ -153,98 +201,313 @@ func (p *Project) loadWrappers(wrappers map[string]*serviceWrapper, servicesToCo
}
// Build builds the specified services (like docker build).
func (p *Project) Build(services ...string) error {
return p.perform(EventProjectBuildStart, EventProjectBuildDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, EventServiceBuildStart, EventServiceBuild, func(service Service) error {
return service.Build()
func (p *Project) Build(ctx context.Context, buildOptions options.Build, services ...string) error {
return p.perform(events.ProjectBuildStart, events.ProjectBuildDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, events.ServiceBuildStart, events.ServiceBuild, func(service Service) error {
return service.Build(ctx, buildOptions)
})
}), nil)
}
// Create creates the specified services (like docker create).
func (p *Project) Create(services ...string) error {
return p.perform(EventProjectCreateStart, EventProjectCreateDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, EventServiceCreateStart, EventServiceCreate, func(service Service) error {
return service.Create()
func (p *Project) Create(ctx context.Context, options options.Create, services ...string) error {
if options.NoRecreate && options.ForceRecreate {
return fmt.Errorf("no-recreate and force-recreate cannot be combined")
}
return p.perform(events.ProjectCreateStart, events.ProjectCreateDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, events.ServiceCreateStart, events.ServiceCreate, func(service Service) error {
return service.Create(ctx, options)
})
}), nil)
}
// Down stops the specified services (like docker stop).
func (p *Project) Down(services ...string) error {
return p.perform(EventProjectDownStart, EventProjectDownDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, EventServiceDownStart, EventServiceDown, func(service Service) error {
return service.Down()
// Stop stops the specified services (like docker stop).
func (p *Project) Stop(ctx context.Context, timeout int, services ...string) error {
return p.perform(events.ProjectStopStart, events.ProjectStopDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, events.ServiceStopStart, events.ServiceStop, func(service Service) error {
return service.Stop(ctx, timeout)
})
}), nil)
}
// Restart restarts the specified services (like docker restart).
func (p *Project) Restart(services ...string) error {
return p.perform(EventProjectRestartStart, EventProjectRestartDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, EventServiceRestartStart, EventServiceRestart, func(service Service) error {
return service.Restart()
})
}), nil)
}
// Down stops the specified services and clean related containers (like docker stop + docker rm).
func (p *Project) Down(ctx context.Context, opts options.Down, services ...string) error {
if !opts.RemoveImages.Valid() {
return fmt.Errorf("--rmi flag must be local, all or empty")
}
if err := p.Stop(ctx, 10, services...); err != nil {
return err
}
if opts.RemoveOrphans {
if err := p.removeOrphanContainers(); err != nil {
return err
}
}
if err := p.Delete(ctx, options.Delete{
RemoveVolume: opts.RemoveVolume,
}, services...); err != nil {
return err
}
// Start starts the specified services (like docker start).
func (p *Project) Start(services ...string) error {
return p.perform(EventProjectStartStart, EventProjectStartDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, EventServiceStartStart, EventServiceStart, func(service Service) error {
return service.Start()
})
}), nil)
}
// Up create and start the specified services (kinda like docker run).
func (p *Project) Up(services ...string) error {
return p.perform(EventProjectUpStart, EventProjectUpDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, EventServiceUpStart, EventServiceUp, func(service Service) error {
return service.Up()
return p.forEach([]string{}, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, events.NoEvent, events.NoEvent, func(service Service) error {
return service.RemoveImage(ctx, opts.RemoveImages)
})
}), func(service Service) error {
return service.Create()
return service.Create(ctx, options.Create{})
})
}
// Log aggregate and prints out the logs for the specified services.
func (p *Project) Log(services ...string) error {
return p.forEach(services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, NoEvent, NoEvent, func(service Service) error {
return service.Log()
func (p *Project) removeOrphanContainers() error {
client := p.clientFactory.Create(nil)
filter := filters.NewArgs()
filter.Add("label", labels.PROJECT.EqString(p.Name))
containers, err := client.ContainerList(context.Background(), types.ContainerListOptions{
Filter: filter,
})
if err != nil {
return err
}
currentServices := map[string]struct{}{}
for _, serviceName := range p.ServiceConfigs.Keys() {
currentServices[serviceName] = struct{}{}
}
for _, container := range containers {
serviceLabel := container.Labels[labels.SERVICE.Str()]
if _, ok := currentServices[serviceLabel]; !ok {
if err := client.ContainerKill(context.Background(), container.ID, "SIGKILL"); err != nil {
return err
}
if err := client.ContainerRemove(context.Background(), container.ID, types.ContainerRemoveOptions{
Force: true,
}); err != nil {
return err
}
}
}
return nil
}
// Restart restarts the specified services (like docker restart).
func (p *Project) Restart(ctx context.Context, timeout int, services ...string) error {
return p.perform(events.ProjectRestartStart, events.ProjectRestartDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, events.ServiceRestartStart, events.ServiceRestart, func(service Service) error {
return service.Restart(ctx, timeout)
})
}), nil)
}
// Port returns the public port for a port binding of the specified service.
func (p *Project) Port(ctx context.Context, index int, protocol, serviceName, privatePort string) (string, error) {
service, err := p.CreateService(serviceName)
if err != nil {
return "", err
}
containers, err := service.Containers(ctx)
if err != nil {
return "", err
}
if index < 1 || index > len(containers) {
return "", fmt.Errorf("Invalid index %d", index)
}
return containers[index-1].Port(ctx, fmt.Sprintf("%s/%s", privatePort, protocol))
}
// Ps list containers for the specified services.
func (p *Project) Ps(ctx context.Context, onlyID bool, services ...string) (InfoSet, error) {
allInfo := InfoSet{}
for _, name := range p.ServiceConfigs.Keys() {
service, err := p.CreateService(name)
if err != nil {
return nil, err
}
info, err := service.Info(ctx, onlyID)
if err != nil {
return nil, err
}
allInfo = append(allInfo, info...)
}
return allInfo, nil
}
// Start starts the specified services (like docker start).
func (p *Project) Start(ctx context.Context, services ...string) error {
return p.perform(events.ProjectStartStart, events.ProjectStartDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, events.ServiceStartStart, events.ServiceStart, func(service Service) error {
return service.Start(ctx)
})
}), nil)
}
// Run executes a one off command (like `docker run image command`).
func (p *Project) Run(ctx context.Context, serviceName string, commandParts []string) (int, error) {
if !p.ServiceConfigs.Has(serviceName) {
return 1, fmt.Errorf("%s is not defined in the template", serviceName)
}
var exitCode int
err := p.forEach([]string{}, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, events.ServiceRunStart, events.ServiceRun, func(service Service) error {
if service.Name() == serviceName {
code, err := service.Run(ctx, commandParts)
exitCode = code
return err
}
return nil
})
}), func(service Service) error {
return service.Create(ctx, options.Create{})
})
return exitCode, err
}
// Up creates and starts the specified services (kinda like docker run).
func (p *Project) Up(ctx context.Context, options options.Up, services ...string) error {
return p.perform(events.ProjectUpStart, events.ProjectUpDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, events.ServiceUpStart, events.ServiceUp, func(service Service) error {
return service.Up(ctx, options)
})
}), func(service Service) error {
return service.Create(ctx, options.Create)
})
}
// Log aggregates and prints out the logs for the specified services.
func (p *Project) Log(ctx context.Context, follow bool, services ...string) error {
return p.forEach(services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, events.NoEvent, events.NoEvent, func(service Service) error {
return service.Log(ctx, follow)
})
}), nil)
}
// Scale scales the specified services.
func (p *Project) Scale(ctx context.Context, timeout int, servicesScale map[string]int) error {
// This code is a bit verbose but I wanted to parse everything up front
order := make([]string, 0, 0)
services := make(map[string]Service)
for name := range servicesScale {
if !p.ServiceConfigs.Has(name) {
return fmt.Errorf("%s is not defined in the template", name)
}
service, err := p.CreateService(name)
if err != nil {
return fmt.Errorf("Failed to lookup service: %s: %v", service, err)
}
order = append(order, name)
services[name] = service
}
for _, name := range order {
scale := servicesScale[name]
log.Infof("Setting scale %s=%d...", name, scale)
err := services[name].Scale(ctx, scale, timeout)
if err != nil {
return fmt.Errorf("Failed to set the scale %s=%d: %v", name, scale, err)
}
}
return nil
}
// Pull pulls the specified services (like docker pull).
func (p *Project) Pull(services ...string) error {
func (p *Project) Pull(ctx context.Context, services ...string) error {
return p.forEach(services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, EventServicePullStart, EventServicePull, func(service Service) error {
return service.Pull()
wrapper.Do(nil, events.ServicePullStart, events.ServicePull, func(service Service) error {
return service.Pull(ctx)
})
}), nil)
}
// listStoppedContainers lists the stopped containers for the specified services.
func (p *Project) listStoppedContainers(ctx context.Context, services ...string) ([]string, error) {
stoppedContainers := []string{}
err := p.forEach(services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, events.NoEvent, events.NoEvent, func(service Service) error {
containers, innerErr := service.Containers(ctx)
if innerErr != nil {
return innerErr
}
for _, container := range containers {
running, innerErr := container.IsRunning(ctx)
if innerErr != nil {
log.Error(innerErr)
}
if !running {
containerID, innerErr := container.ID()
if innerErr != nil {
log.Error(innerErr)
}
stoppedContainers = append(stoppedContainers, containerID)
}
}
return nil
})
}), nil)
if err != nil {
return nil, err
}
return stoppedContainers, nil
}
// Delete removes the specified services (like docker rm).
func (p *Project) Delete(services ...string) error {
return p.perform(EventProjectDeleteStart, EventProjectDeleteDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, EventServiceDeleteStart, EventServiceDelete, func(service Service) error {
return service.Delete()
func (p *Project) Delete(ctx context.Context, options options.Delete, services ...string) error {
stoppedContainers, err := p.listStoppedContainers(ctx, services...)
if err != nil {
return err
}
if len(stoppedContainers) == 0 {
p.Notify(events.ProjectDeleteDone, "", nil)
fmt.Println("No stopped containers")
return nil
}
if options.BeforeDeleteCallback != nil && !options.BeforeDeleteCallback(stoppedContainers) {
return nil
}
return p.perform(events.ProjectDeleteStart, events.ProjectDeleteDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, events.ServiceDeleteStart, events.ServiceDelete, func(service Service) error {
return service.Delete(ctx, options)
})
}), nil)
}
// Kill kills the specified services (like docker kill).
func (p *Project) Kill(services ...string) error {
return p.perform(EventProjectKillStart, EventProjectKillDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, EventServiceKillStart, EventServiceKill, func(service Service) error {
return service.Kill()
func (p *Project) Kill(ctx context.Context, signal string, services ...string) error {
return p.perform(events.ProjectKillStart, events.ProjectKillDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, events.ServiceKillStart, events.ServiceKill, func(service Service) error {
return service.Kill(ctx, signal)
})
}), nil)
}
func (p *Project) perform(start, done EventType, services []string, action wrapperAction, cycleAction serviceAction) error {
// Pause pauses the specified services containers (like docker pause).
func (p *Project) Pause(ctx context.Context, services ...string) error {
return p.perform(events.ProjectPauseStart, events.ProjectPauseDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, events.ServicePauseStart, events.ServicePause, func(service Service) error {
return service.Pause(ctx)
})
}), nil)
}
// Unpause pauses the specified services containers (like docker pause).
func (p *Project) Unpause(ctx context.Context, services ...string) error {
return p.perform(events.ProjectUnpauseStart, events.ProjectUnpauseDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, events.ServiceUnpauseStart, events.ServiceUnpause, func(service Service) error {
return service.Unpause(ctx)
})
}), nil)
}
func (p *Project) perform(start, done events.EventType, services []string, action wrapperAction, cycleAction serviceAction) error {
p.Notify(start, "", nil)
err := p.forEach(services, action, cycleAction)
@@ -324,7 +587,7 @@ func (p *Project) traverse(start bool, selected map[string]bool, wrappers map[st
wrapperList := []string{}
if start {
for name := range p.Configs {
for _, name := range p.ServiceConfigs.Keys() {
wrapperList = append(wrapperList, name)
}
} else {
@@ -349,7 +612,9 @@ func (p *Project) traverse(start bool, selected map[string]bool, wrappers map[st
launched := map[string]bool{}
for _, wrapper := range wrappers {
p.startService(wrappers, []string{}, selected, launched, wrapper, action, cycleAction)
if err := p.startService(wrappers, []string{}, selected, launched, wrapper, action, cycleAction); err != nil {
return err
}
}
var firstError error
@@ -380,25 +645,27 @@ func (p *Project) traverse(start bool, selected map[string]bool, wrappers map[st
}
// AddListener adds the specified listener to the project.
func (p *Project) AddListener(c chan<- Event) {
// This implements implicitly events.Emitter.
func (p *Project) AddListener(c chan<- events.Event) {
if !p.hasListeners {
for _, l := range p.listeners {
close(l)
}
p.hasListeners = true
p.listeners = []chan<- Event{c}
p.listeners = []chan<- events.Event{c}
} else {
p.listeners = append(p.listeners, c)
}
}
// Notify notifies all project listener with the specified eventType, service name and datas.
func (p *Project) Notify(eventType EventType, serviceName string, data map[string]string) {
if eventType == NoEvent {
// This implements implicitly events.Notifier interface.
func (p *Project) Notify(eventType events.EventType, serviceName string, data map[string]string) {
if eventType == events.NoEvent {
return
}
event := Event{
event := events.Event{
EventType: eventType,
ServiceName: serviceName,
Data: data,

View File

@@ -1,148 +0,0 @@
package project
import (
"fmt"
"reflect"
"strings"
"testing"
)
type TestServiceFactory struct {
Counts map[string]int
}
type TestService struct {
factory *TestServiceFactory
name string
config *ServiceConfig
EmptyService
Count int
}
func (t *TestService) Config() *ServiceConfig {
return t.config
}
func (t *TestService) Name() string {
return t.name
}
func (t *TestService) Create() error {
key := t.name + ".create"
t.factory.Counts[key] = t.factory.Counts[key] + 1
return nil
}
func (t *TestService) DependentServices() []ServiceRelationship {
return nil
}
func (t *TestServiceFactory) Create(project *Project, name string, serviceConfig *ServiceConfig) (Service, error) {
return &TestService{
factory: t,
config: serviceConfig,
name: name,
}, nil
}
func TestTwoCall(t *testing.T) {
factory := &TestServiceFactory{
Counts: map[string]int{},
}
p := NewProject(&Context{
ServiceFactory: factory,
})
p.Configs = map[string]*ServiceConfig{
"foo": {},
}
if err := p.Create("foo"); err != nil {
t.Fatal(err)
}
if err := p.Create("foo"); err != nil {
t.Fatal(err)
}
if factory.Counts["foo.create"] != 2 {
t.Fatal("Failed to create twice")
}
}
func TestEventEquality(t *testing.T) {
if fmt.Sprintf("%s", EventServiceStart) != "Started" ||
fmt.Sprintf("%v", EventServiceStart) != "Started" {
t.Fatalf("EventServiceStart String() doesn't work: %s %v", EventServiceStart, EventServiceStart)
}
if fmt.Sprintf("%s", EventServiceStart) != fmt.Sprintf("%s", EventServiceUp) {
t.Fatal("Event messages do not match")
}
if EventServiceStart == EventServiceUp {
t.Fatal("Events match")
}
}
func TestParseWithBadContent(t *testing.T) {
p := NewProject(&Context{
ComposeBytes: []byte("garbage"),
})
err := p.Parse()
if err == nil {
t.Fatal("Should have failed parse")
}
if !strings.HasPrefix(err.Error(), "Unknown resolution for 'garbage'") {
t.Fatalf("Should have failed parse: %#v", err)
}
}
func TestParseWithGoodContent(t *testing.T) {
p := NewProject(&Context{
ComposeBytes: []byte("not-garbage:\n image: foo"),
})
err := p.Parse()
if err != nil {
t.Fatal(err)
}
}
type TestEnvironmentLookup struct {
}
func (t *TestEnvironmentLookup) Lookup(key, serviceName string, config *ServiceConfig) []string {
return []string{fmt.Sprintf("%s=X", key)}
}
func TestEnvironmentResolve(t *testing.T) {
factory := &TestServiceFactory{
Counts: map[string]int{},
}
p := NewProject(&Context{
ServiceFactory: factory,
EnvironmentLookup: &TestEnvironmentLookup{},
})
p.Configs = map[string]*ServiceConfig{
"foo": {
Environment: NewMaporEqualSlice([]string{
"A",
"A=",
"A=B",
}),
},
}
service, err := p.CreateService("foo")
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(service.Config().Environment.Slice(), []string{"A=X", "A=X", "A=B"}) {
t.Fatal("Invalid environment", service.Config().Environment.Slice())
}
}

View File

@@ -4,6 +4,7 @@ import (
"sync"
log "github.com/Sirupsen/logrus"
"github.com/docker/libcompose/project/events"
)
type serviceWrapper struct {
@@ -55,7 +56,7 @@ func (s *serviceWrapper) Ignore() {
defer s.done.Done()
s.state = StateExecuted
s.project.Notify(EventServiceUpIgnored, s.service.Name(), nil)
s.project.Notify(events.ServiceUpIgnored, s.service.Name(), nil)
}
func (s *serviceWrapper) waitForDeps(wrappers map[string]*serviceWrapper) bool {
@@ -70,7 +71,7 @@ func (s *serviceWrapper) waitForDeps(wrappers map[string]*serviceWrapper) bool {
if wrapper, ok := wrappers[dep.Target]; ok {
if wrapper.Wait() == ErrRestart {
s.project.Notify(EventProjectReload, wrapper.service.Name(), nil)
s.project.Notify(events.ProjectReload, wrapper.service.Name(), nil)
s.err = ErrRestart
return false
}
@@ -82,7 +83,7 @@ func (s *serviceWrapper) waitForDeps(wrappers map[string]*serviceWrapper) bool {
return true
}
func (s *serviceWrapper) Do(wrappers map[string]*serviceWrapper, start, done EventType, action func(service Service) error) {
func (s *serviceWrapper) Do(wrappers map[string]*serviceWrapper, start, done events.EventType, action func(service Service) error) {
defer s.done.Done()
if s.state == StateExecuted {
@@ -100,7 +101,7 @@ func (s *serviceWrapper) Do(wrappers map[string]*serviceWrapper, start, done Eve
s.err = action(s.service)
if s.err == ErrRestart {
s.project.Notify(done, s.service.Name(), nil)
s.project.Notify(EventProjectReloadTrigger, s.service.Name(), nil)
s.project.Notify(events.ProjectReloadTrigger, s.service.Name(), nil)
} else if s.err != nil {
log.Errorf("Failed %s %s : %v", start, s.name, s.err)
} else {

92
vendor/github.com/docker/libcompose/project/service.go generated vendored Normal file
View File

@@ -0,0 +1,92 @@
package project
import (
"errors"
"golang.org/x/net/context"
"github.com/docker/libcompose/config"
"github.com/docker/libcompose/project/options"
)
// Service defines what a libcompose service provides.
type Service interface {
Build(ctx context.Context, buildOptions options.Build) error
Create(ctx context.Context, options options.Create) error
Delete(ctx context.Context, options options.Delete) error
Info(ctx context.Context, qFlag bool) (InfoSet, error)
Log(ctx context.Context, follow bool) error
Kill(ctx context.Context, signal string) error
Pause(ctx context.Context) error
Pull(ctx context.Context) error
Restart(ctx context.Context, timeout int) error
Run(ctx context.Context, commandParts []string) (int, error)
Scale(ctx context.Context, count int, timeout int) error
Start(ctx context.Context) error
Stop(ctx context.Context, timeout int) error
Unpause(ctx context.Context) error
Up(ctx context.Context, options options.Up) error
RemoveImage(ctx context.Context, imageType options.ImageType) error
Containers(ctx context.Context) ([]Container, error)
DependentServices() []ServiceRelationship
Config() *config.ServiceConfig
Name() string
}
// ServiceState holds the state of a service.
type ServiceState string
// State definitions
var (
StateExecuted = ServiceState("executed")
StateUnknown = ServiceState("unknown")
)
// Error definitions
var (
ErrRestart = errors.New("Restart execution")
ErrUnsupported = errors.New("UnsupportedOperation")
)
// ServiceFactory is an interface factory to create Service object for the specified
// project, with the specified name and service configuration.
type ServiceFactory interface {
Create(project *Project, name string, serviceConfig *config.ServiceConfig) (Service, error)
}
// ServiceRelationshipType defines the type of service relationship.
type ServiceRelationshipType string
// RelTypeLink means the services are linked (docker links).
const RelTypeLink = ServiceRelationshipType("")
// RelTypeNetNamespace means the services share the same network namespace.
const RelTypeNetNamespace = ServiceRelationshipType("netns")
// RelTypeIpcNamespace means the service share the same ipc namespace.
const RelTypeIpcNamespace = ServiceRelationshipType("ipc")
// RelTypeVolumesFrom means the services share some volumes.
const RelTypeVolumesFrom = ServiceRelationshipType("volumesFrom")
// RelTypeDependsOn means the dependency was explicitly set using 'depends_on'.
const RelTypeDependsOn = ServiceRelationshipType("dependsOn")
// ServiceRelationship holds the relationship information between two services.
type ServiceRelationship struct {
Target, Alias string
Type ServiceRelationshipType
Optional bool
}
// NewServiceRelationship creates a new Relationship based on the specified alias
// and relationship type.
func NewServiceRelationship(nameAlias string, relType ServiceRelationshipType) ServiceRelationship {
name, alias := NameAlias(nameAlias)
return ServiceRelationship{
Target: name,
Alias: alias,
Type: relType,
}
}

View File

@@ -1,295 +0,0 @@
package project
import "fmt"
// EventType defines a type of libcompose event.
type EventType int
// Definitions of libcompose events
const (
NoEvent = EventType(iota)
EventContainerCreated = EventType(iota)
EventContainerStarted = EventType(iota)
EventServiceAdd = EventType(iota)
EventServiceUpStart = EventType(iota)
EventServiceUpIgnored = EventType(iota)
EventServiceUp = EventType(iota)
EventServiceCreateStart = EventType(iota)
EventServiceCreate = EventType(iota)
EventServiceDeleteStart = EventType(iota)
EventServiceDelete = EventType(iota)
EventServiceDownStart = EventType(iota)
EventServiceDown = EventType(iota)
EventServiceRestartStart = EventType(iota)
EventServiceRestart = EventType(iota)
EventServicePullStart = EventType(iota)
EventServicePull = EventType(iota)
EventServiceKillStart = EventType(iota)
EventServiceKill = EventType(iota)
EventServiceStartStart = EventType(iota)
EventServiceStart = EventType(iota)
EventServiceBuildStart = EventType(iota)
EventServiceBuild = EventType(iota)
EventProjectDownStart = EventType(iota)
EventProjectDownDone = EventType(iota)
EventProjectCreateStart = EventType(iota)
EventProjectCreateDone = EventType(iota)
EventProjectUpStart = EventType(iota)
EventProjectUpDone = EventType(iota)
EventProjectDeleteStart = EventType(iota)
EventProjectDeleteDone = EventType(iota)
EventProjectRestartStart = EventType(iota)
EventProjectRestartDone = EventType(iota)
EventProjectReload = EventType(iota)
EventProjectReloadTrigger = EventType(iota)
EventProjectKillStart = EventType(iota)
EventProjectKillDone = EventType(iota)
EventProjectStartStart = EventType(iota)
EventProjectStartDone = EventType(iota)
EventProjectBuildStart = EventType(iota)
EventProjectBuildDone = EventType(iota)
)
func (e EventType) String() string {
var m string
switch e {
case EventContainerCreated:
m = "Created container"
case EventContainerStarted:
m = "Started container"
case EventServiceAdd:
m = "Adding"
case EventServiceUpStart:
m = "Starting"
case EventServiceUpIgnored:
m = "Ignoring"
case EventServiceUp:
m = "Started"
case EventServiceCreateStart:
m = "Creating"
case EventServiceCreate:
m = "Created"
case EventServiceDeleteStart:
m = "Deleting"
case EventServiceDelete:
m = "Deleted"
case EventServiceDownStart:
m = "Stopping"
case EventServiceDown:
m = "Stopped"
case EventServiceRestartStart:
m = "Restarting"
case EventServiceRestart:
m = "Restarted"
case EventServicePullStart:
m = "Pulling"
case EventServicePull:
m = "Pulled"
case EventServiceKillStart:
m = "Killing"
case EventServiceKill:
m = "Killed"
case EventServiceStartStart:
m = "Starting"
case EventServiceStart:
m = "Started"
case EventServiceBuildStart:
m = "Building"
case EventServiceBuild:
m = "Built"
case EventProjectDownStart:
m = "Stopping project"
case EventProjectDownDone:
m = "Project stopped"
case EventProjectCreateStart:
m = "Creating project"
case EventProjectCreateDone:
m = "Project created"
case EventProjectUpStart:
m = "Starting project"
case EventProjectUpDone:
m = "Project started"
case EventProjectDeleteStart:
m = "Deleting project"
case EventProjectDeleteDone:
m = "Project deleted"
case EventProjectRestartStart:
m = "Restarting project"
case EventProjectRestartDone:
m = "Project restarted"
case EventProjectReload:
m = "Reloading project"
case EventProjectReloadTrigger:
m = "Triggering project reload"
case EventProjectKillStart:
m = "Killing project"
case EventProjectKillDone:
m = "Project killed"
case EventProjectStartStart:
m = "Starting project"
case EventProjectStartDone:
m = "Project started"
case EventProjectBuildStart:
m = "Building project"
case EventProjectBuildDone:
m = "Project built"
}
if m == "" {
m = fmt.Sprintf("EventType: %d", int(e))
}
return m
}
// InfoPart holds key/value strings.
type InfoPart struct {
Key, Value string
}
// InfoSet holds a list of Info.
type InfoSet []Info
// Info holds a list of InfoPart.
type Info []InfoPart
// ServiceConfig holds libcompose service configuration
type ServiceConfig struct {
Build string `yaml:"build,omitempty"`
CapAdd []string `yaml:"cap_add,omitempty"`
CapDrop []string `yaml:"cap_drop,omitempty"`
CPUSet string `yaml:"cpuset,omitempty"`
CPUShares int64 `yaml:"cpu_shares,omitempty"`
Command Command `yaml:"command,flow,omitempty"`
ContainerName string `yaml:"container_name,omitempty"`
Devices []string `yaml:"devices,omitempty"`
DNS Stringorslice `yaml:"dns,omitempty"`
DNSSearch Stringorslice `yaml:"dns_search,omitempty"`
Dockerfile string `yaml:"dockerfile,omitempty"`
DomainName string `yaml:"domainname,omitempty"`
Entrypoint Command `yaml:"entrypoint,flow,omitempty"`
EnvFile Stringorslice `yaml:"env_file,omitempty"`
Environment MaporEqualSlice `yaml:"environment,omitempty"`
Hostname string `yaml:"hostname,omitempty"`
Image string `yaml:"image,omitempty"`
Labels SliceorMap `yaml:"labels,omitempty"`
Links MaporColonSlice `yaml:"links,omitempty"`
LogDriver string `yaml:"log_driver,omitempty"`
MemLimit int64 `yaml:"mem_limit,omitempty"`
MemSwapLimit int64 `yaml:"memswap_limit,omitempty"`
Name string `yaml:"name,omitempty"`
Net string `yaml:"net,omitempty"`
Pid string `yaml:"pid,omitempty"`
Uts string `yaml:"uts,omitempty"`
Ipc string `yaml:"ipc,omitempty"`
Ports []string `yaml:"ports,omitempty"`
Privileged bool `yaml:"privileged,omitempty"`
Restart string `yaml:"restart,omitempty"`
ReadOnly bool `yaml:"read_only,omitempty"`
StdinOpen bool `yaml:"stdin_open,omitempty"`
SecurityOpt []string `yaml:"security_opt,omitempty"`
Tty bool `yaml:"tty,omitempty"`
User string `yaml:"user,omitempty"`
VolumeDriver string `yaml:"volume_driver,omitempty"`
Volumes []string `yaml:"volumes,omitempty"`
VolumesFrom []string `yaml:"volumes_from,omitempty"`
WorkingDir string `yaml:"working_dir,omitempty"`
Expose []string `yaml:"expose,omitempty"`
ExternalLinks []string `yaml:"external_links,omitempty"`
LogOpt map[string]string `yaml:"log_opt,omitempty"`
ExtraHosts []string `yaml:"extra_hosts,omitempty"`
}
// EnvironmentLookup defines methods to provides environment variable loading.
type EnvironmentLookup interface {
Lookup(key, serviceName string, config *ServiceConfig) []string
}
// ConfigLookup defines methods to provides file loading.
type ConfigLookup interface {
Lookup(file, relativeTo string) ([]byte, string, error)
}
// Project holds libcompose project information.
type Project struct {
Name string
Configs map[string]*ServiceConfig
File string
ReloadCallback func() error
context *Context
reload []string
upCount int
listeners []chan<- Event
hasListeners bool
}
// Service defines what a libcompose service provides.
type Service interface {
Info(qFlag bool) (InfoSet, error)
Name() string
Build() error
Create() error
Up() error
Start() error
Down() error
Delete() error
Restart() error
Log() error
Pull() error
Kill() error
Config() *ServiceConfig
DependentServices() []ServiceRelationship
Containers() ([]Container, error)
Scale(count int) error
}
// Container defines what a libcompose container provides.
type Container interface {
ID() (string, error)
Name() string
Port(port string) (string, error)
}
// ServiceFactory is an interface factory to create Service object for the specified
// project, with the specified name and service configuration.
type ServiceFactory interface {
Create(project *Project, name string, serviceConfig *ServiceConfig) (Service, error)
}
// ServiceRelationshipType defines the type of service relationship.
type ServiceRelationshipType string
// RelTypeLink means the services are linked (docker links).
const RelTypeLink = ServiceRelationshipType("")
// RelTypeNetNamespace means the services share the same network namespace.
const RelTypeNetNamespace = ServiceRelationshipType("netns")
// RelTypeIpcNamespace means the service share the same ipc namespace.
const RelTypeIpcNamespace = ServiceRelationshipType("ipc")
// RelTypeVolumesFrom means the services share some volumes.
const RelTypeVolumesFrom = ServiceRelationshipType("volumesFrom")
// ServiceRelationship holds the relationship information between two services.
type ServiceRelationship struct {
Target, Alias string
Type ServiceRelationshipType
Optional bool
}
// NewServiceRelationship creates a new Relationship based on the specified alias
// and relationship type.
func NewServiceRelationship(nameAlias string, relType ServiceRelationshipType) ServiceRelationship {
name, alias := NameAlias(nameAlias)
return ServiceRelationship{
Target: name,
Alias: alias,
Type: relType,
}
}

View File

@@ -1,328 +0,0 @@
package project
import (
"fmt"
"strings"
"github.com/flynn/go-shlex"
)
// Stringorslice represents a string or an array of strings.
// TODO use docker/docker/pkg/stringutils.StrSlice once 1.9.x is released.
type Stringorslice struct {
parts []string
}
// MarshalYAML implements the Marshaller interface.
func (s Stringorslice) MarshalYAML() (tag string, value interface{}, err error) {
return "", s.parts, nil
}
func toStrings(s []interface{}) ([]string, error) {
if len(s) == 0 {
return nil, nil
}
r := make([]string, len(s))
for k, v := range s {
if sv, ok := v.(string); ok {
r[k] = sv
} else {
return nil, fmt.Errorf("Cannot unmarshal '%v' of type %T into a string value", v, v)
}
}
return r, nil
}
// UnmarshalYAML implements the Unmarshaller interface.
func (s *Stringorslice) UnmarshalYAML(tag string, value interface{}) error {
switch value := value.(type) {
case []interface{}:
parts, err := toStrings(value)
if err != nil {
return err
}
s.parts = parts
case string:
s.parts = []string{value}
default:
return fmt.Errorf("Failed to unmarshal Stringorslice: %#v", value)
}
return nil
}
// Len returns the number of parts of the Stringorslice.
func (s *Stringorslice) Len() int {
if s == nil {
return 0
}
return len(s.parts)
}
// Slice gets the parts of the StrSlice as a Slice of string.
func (s *Stringorslice) Slice() []string {
if s == nil {
return nil
}
return s.parts
}
// NewStringorslice creates an Stringorslice based on the specified parts (as strings).
func NewStringorslice(parts ...string) Stringorslice {
return Stringorslice{parts}
}
// Command represents a docker command, can be a string or an array of strings.
// FIXME why not use Stringorslice (type Command struct { Stringorslice }
type Command struct {
parts []string
}
// MarshalYAML implements the Marshaller interface.
func (s Command) MarshalYAML() (tag string, value interface{}, err error) {
return "", s.parts, nil
}
// UnmarshalYAML implements the Unmarshaller interface.
func (s *Command) UnmarshalYAML(tag string, value interface{}) error {
switch value := value.(type) {
case []interface{}:
parts, err := toStrings(value)
if err != nil {
return err
}
s.parts = parts
case string:
parts, err := shlex.Split(value)
if err != nil {
return err
}
s.parts = parts
default:
return fmt.Errorf("Failed to unmarshal Command: %#v", value)
}
return nil
}
// ToString returns the parts of the command as a string (joined by spaces).
func (s *Command) ToString() string {
return strings.Join(s.parts, " ")
}
// Slice gets the parts of the Command as a Slice of string.
func (s *Command) Slice() []string {
return s.parts
}
// NewCommand create a Command based on the specified parts (as strings).
func NewCommand(parts ...string) Command {
return Command{parts}
}
// SliceorMap represents a slice or a map of strings.
type SliceorMap struct {
parts map[string]string
}
// MarshalYAML implements the Marshaller interface.
func (s SliceorMap) MarshalYAML() (tag string, value interface{}, err error) {
return "", s.parts, nil
}
// UnmarshalYAML implements the Unmarshaller interface.
func (s *SliceorMap) UnmarshalYAML(tag string, value interface{}) error {
switch value := value.(type) {
case map[interface{}]interface{}:
parts := map[string]string{}
for k, v := range value {
if sk, ok := k.(string); ok {
if sv, ok := v.(string); ok {
parts[sk] = sv
} else {
return fmt.Errorf("Cannot unmarshal '%v' of type %T into a string value", v, v)
}
} else {
return fmt.Errorf("Cannot unmarshal '%v' of type %T into a string value", k, k)
}
}
s.parts = parts
case []interface{}:
parts := map[string]string{}
for _, s := range value {
if str, ok := s.(string); ok {
str := strings.TrimSpace(str)
keyValueSlice := strings.SplitN(str, "=", 2)
key := keyValueSlice[0]
val := ""
if len(keyValueSlice) == 2 {
val = keyValueSlice[1]
}
parts[key] = val
} else {
return fmt.Errorf("Cannot unmarshal '%v' of type %T into a string value", s, s)
}
}
s.parts = parts
default:
return fmt.Errorf("Failed to unmarshal SliceorMap: %#v", value)
}
return nil
}
// MapParts get the parts of the SliceorMap as a Map of string.
func (s *SliceorMap) MapParts() map[string]string {
if s == nil {
return nil
}
return s.parts
}
// NewSliceorMap creates a new SliceorMap based on the specified parts (as map of string).
func NewSliceorMap(parts map[string]string) SliceorMap {
return SliceorMap{parts}
}
// MaporEqualSlice represents a slice of strings that gets unmarshal from a
// YAML map into 'key=value' string.
type MaporEqualSlice struct {
parts []string
}
// MarshalYAML implements the Marshaller interface.
func (s MaporEqualSlice) MarshalYAML() (tag string, value interface{}, err error) {
return "", s.parts, nil
}
func toSepMapParts(value map[interface{}]interface{}, sep string) ([]string, error) {
if len(value) == 0 {
return nil, nil
}
parts := make([]string, 0, len(value))
for k, v := range value {
if sk, ok := k.(string); ok {
if sv, ok := v.(string); ok {
parts = append(parts, sk+sep+sv)
} else {
return nil, fmt.Errorf("Cannot unmarshal '%v' of type %T into a string value", v, v)
}
} else {
return nil, fmt.Errorf("Cannot unmarshal '%v' of type %T into a string value", k, k)
}
}
return parts, nil
}
// UnmarshalYAML implements the Unmarshaller interface.
func (s *MaporEqualSlice) UnmarshalYAML(tag string, value interface{}) error {
switch value := value.(type) {
case []interface{}:
parts, err := toStrings(value)
if err != nil {
return err
}
s.parts = parts
case map[interface{}]interface{}:
parts, err := toSepMapParts(value, "=")
if err != nil {
return err
}
s.parts = parts
default:
return fmt.Errorf("Failed to unmarshal MaporEqualSlice: %#v", value)
}
return nil
}
// Slice gets the parts of the MaporEqualSlice as a Slice of string.
func (s *MaporEqualSlice) Slice() []string {
return s.parts
}
// NewMaporEqualSlice creates a new MaporEqualSlice based on the specified parts.
func NewMaporEqualSlice(parts []string) MaporEqualSlice {
return MaporEqualSlice{parts}
}
// MaporColonSlice represents a slice of strings that gets unmarshal from a
// YAML map into 'key:value' string.
type MaporColonSlice struct {
parts []string
}
// MarshalYAML implements the Marshaller interface.
func (s MaporColonSlice) MarshalYAML() (tag string, value interface{}, err error) {
return "", s.parts, nil
}
// UnmarshalYAML implements the Unmarshaller interface.
func (s *MaporColonSlice) UnmarshalYAML(tag string, value interface{}) error {
switch value := value.(type) {
case []interface{}:
parts, err := toStrings(value)
if err != nil {
return err
}
s.parts = parts
case map[interface{}]interface{}:
parts, err := toSepMapParts(value, ":")
if err != nil {
return err
}
s.parts = parts
default:
return fmt.Errorf("Failed to unmarshal MaporColonSlice: %#v", value)
}
return nil
}
// Slice gets the parts of the MaporColonSlice as a Slice of string.
func (s *MaporColonSlice) Slice() []string {
return s.parts
}
// NewMaporColonSlice creates a new MaporColonSlice based on the specified parts.
func NewMaporColonSlice(parts []string) MaporColonSlice {
return MaporColonSlice{parts}
}
// MaporSpaceSlice represents a slice of strings that gets unmarshal from a
// YAML map into 'key value' string.
type MaporSpaceSlice struct {
parts []string
}
// MarshalYAML implements the Marshaller interface.
func (s MaporSpaceSlice) MarshalYAML() (tag string, value interface{}, err error) {
return "", s.parts, nil
}
// UnmarshalYAML implements the Unmarshaller interface.
func (s *MaporSpaceSlice) UnmarshalYAML(tag string, value interface{}) error {
switch value := value.(type) {
case []interface{}:
parts, err := toStrings(value)
if err != nil {
return err
}
s.parts = parts
case map[interface{}]interface{}:
parts, err := toSepMapParts(value, " ")
if err != nil {
return err
}
s.parts = parts
default:
return fmt.Errorf("Failed to unmarshal MaporSpaceSlice: %#v", value)
}
return nil
}
// Slice gets the parts of the MaporSpaceSlice as a Slice of string.
func (s *MaporSpaceSlice) Slice() []string {
return s.parts
}
// NewMaporSpaceSlice creates a new MaporSpaceSlice based on the specified parts.
func NewMaporSpaceSlice(parts []string) MaporSpaceSlice {
return MaporSpaceSlice{parts}
}

View File

@@ -1,235 +0,0 @@
package project
import (
"fmt"
"strings"
"testing"
yaml "github.com/cloudfoundry-incubator/candiedyaml"
"github.com/stretchr/testify/assert"
)
type StructStringorslice struct {
Foo Stringorslice
}
type TestConfig struct {
SystemContainers map[string]*ServiceConfig
}
func newTestConfig() TestConfig {
return TestConfig{
SystemContainers: map[string]*ServiceConfig{
"udev": {
Image: "udev",
Restart: "always",
Net: "host",
Privileged: true,
DNS: Stringorslice{[]string{"8.8.8.8", "8.8.4.4"}},
Environment: MaporEqualSlice{[]string{
"DAEMON=true",
}},
Labels: SliceorMap{map[string]string{
"io.rancher.os.detach": "true",
"io.rancher.os.scope": "system",
}},
VolumesFrom: []string{
"system-volumes",
},
},
"system-volumes": {
Image: "state",
Net: "none",
ReadOnly: true,
Privileged: true,
Labels: SliceorMap{map[string]string{
"io.rancher.os.createonly": "true",
"io.rancher.os.scope": "system",
}},
Volumes: []string{
"/dev:/host/dev",
"/var/lib/rancher/conf:/var/lib/rancher/conf",
"/etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt.rancher",
"/lib/modules:/lib/modules",
"/lib/firmware:/lib/firmware",
"/var/run:/var/run",
"/var/log:/var/log",
},
LogDriver: "json-file",
},
},
}
}
func TestMarshalConfig(t *testing.T) {
config := newTestConfig()
bytes, err := yaml.Marshal(config)
assert.Nil(t, err)
config2 := TestConfig{}
err = yaml.Unmarshal(bytes, &config2)
assert.Nil(t, err)
assert.Equal(t, config, config2)
}
func TestMarshalServiceConfig(t *testing.T) {
configPtr := newTestConfig().SystemContainers["udev"]
bytes, err := yaml.Marshal(configPtr)
assert.Nil(t, err)
configPtr2 := &ServiceConfig{}
err = yaml.Unmarshal(bytes, configPtr2)
assert.Nil(t, err)
assert.Equal(t, configPtr, configPtr2)
}
func TestStringorsliceYaml(t *testing.T) {
str := `{foo: [bar, baz]}`
s := StructStringorslice{}
yaml.Unmarshal([]byte(str), &s)
assert.Equal(t, []string{"bar", "baz"}, s.Foo.parts)
d, err := yaml.Marshal(&s)
assert.Nil(t, err)
s2 := StructStringorslice{}
yaml.Unmarshal(d, &s2)
assert.Equal(t, []string{"bar", "baz"}, s2.Foo.parts)
}
type StructSliceorMap struct {
Foos SliceorMap `yaml:"foos,omitempty"`
Bars []string `yaml:"bars"`
}
type StructCommand struct {
Entrypoint Command `yaml:"entrypoint,flow,omitempty"`
Command Command `yaml:"command,flow,omitempty"`
}
func TestSliceOrMapYaml(t *testing.T) {
str := `{foos: [bar=baz, far=faz]}`
s := StructSliceorMap{}
yaml.Unmarshal([]byte(str), &s)
assert.Equal(t, map[string]string{"bar": "baz", "far": "faz"}, s.Foos.parts)
d, err := yaml.Marshal(&s)
assert.Nil(t, err)
s2 := StructSliceorMap{}
yaml.Unmarshal(d, &s2)
assert.Equal(t, map[string]string{"bar": "baz", "far": "faz"}, s2.Foos.parts)
}
var sampleStructSliceorMap = `
foos:
io.rancher.os.bar: baz
io.rancher.os.far: true
bars: []
`
func TestUnmarshalSliceOrMap(t *testing.T) {
s := StructSliceorMap{}
err := yaml.Unmarshal([]byte(sampleStructSliceorMap), &s)
assert.Equal(t, fmt.Errorf("Cannot unmarshal 'true' of type bool into a string value"), err)
}
func TestStr2SliceOrMapPtrMap(t *testing.T) {
s := map[string]*StructSliceorMap{"udav": {
Foos: SliceorMap{map[string]string{"io.rancher.os.bar": "baz", "io.rancher.os.far": "true"}},
Bars: []string{},
}}
d, err := yaml.Marshal(&s)
assert.Nil(t, err)
s2 := map[string]*StructSliceorMap{}
yaml.Unmarshal(d, &s2)
assert.Equal(t, s, s2)
}
type StructMaporslice struct {
Foo MaporEqualSlice
}
func contains(list []string, item string) bool {
for _, test := range list {
if test == item {
return true
}
}
return false
}
func TestMaporsliceYaml(t *testing.T) {
str := `{foo: {bar: baz, far: faz}}`
s := StructMaporslice{}
yaml.Unmarshal([]byte(str), &s)
assert.Equal(t, 2, len(s.Foo.parts))
assert.True(t, contains(s.Foo.parts, "bar=baz"))
assert.True(t, contains(s.Foo.parts, "far=faz"))
d, err := yaml.Marshal(&s)
assert.Nil(t, err)
s2 := StructMaporslice{}
yaml.Unmarshal(d, &s2)
assert.Equal(t, 2, len(s2.Foo.parts))
assert.True(t, contains(s2.Foo.parts, "bar=baz"))
assert.True(t, contains(s2.Foo.parts, "far=faz"))
}
var sampleStructCommand = `command: bash`
func TestUnmarshalCommand(t *testing.T) {
s := &StructCommand{}
err := yaml.Unmarshal([]byte(sampleStructCommand), s)
assert.Nil(t, err)
assert.Equal(t, []string{"bash"}, s.Command.Slice())
assert.Nil(t, s.Entrypoint.Slice())
bytes, err := yaml.Marshal(s)
assert.Nil(t, err)
s2 := &StructCommand{}
err = yaml.Unmarshal(bytes, s2)
assert.Nil(t, err)
assert.Equal(t, []string{"bash"}, s2.Command.Slice())
assert.Nil(t, s2.Entrypoint.Slice())
}
var sampleEmptyCommand = `{}`
func TestUnmarshalEmptyCommand(t *testing.T) {
s := &StructCommand{}
err := yaml.Unmarshal([]byte(sampleEmptyCommand), s)
assert.Nil(t, err)
assert.Nil(t, s.Command.Slice())
bytes, err := yaml.Marshal(s)
assert.Nil(t, err)
assert.Equal(t, "entrypoint: []\ncommand: []", strings.TrimSpace(string(bytes)))
s2 := &StructCommand{}
err = yaml.Unmarshal(bytes, s2)
assert.Nil(t, err)
assert.Nil(t, s2.Command.Slice())
}

View File

@@ -3,7 +3,7 @@ package project
import (
"strings"
"github.com/docker/docker/runconfig"
"github.com/docker/engine-api/types/container"
)
// DefaultDependentServices return the dependent services (as an array of ServiceRelationship)
@@ -15,7 +15,7 @@ func DefaultDependentServices(p *Project, s Service) []ServiceRelationship {
}
result := []ServiceRelationship{}
for _, link := range config.Links.Slice() {
for _, link := range config.Links {
result = append(result, NewServiceRelationship(link, RelTypeLink))
}
@@ -23,7 +23,11 @@ func DefaultDependentServices(p *Project, s Service) []ServiceRelationship {
result = append(result, NewServiceRelationship(volumesFrom, RelTypeVolumesFrom))
}
result = appendNs(p, result, s.Config().Net, RelTypeNetNamespace)
for _, dependsOn := range config.DependsOn {
result = append(result, NewServiceRelationship(dependsOn, RelTypeDependsOn))
}
result = appendNs(p, result, s.Config().NetworkMode, RelTypeNetNamespace)
result = appendNs(p, result, s.Config().Ipc, RelTypeIpcNamespace)
return result
@@ -51,7 +55,7 @@ func NameAlias(name string) (string, string) {
// GetContainerFromIpcLikeConfig returns name of the service that shares the IPC
// namespace with the specified service.
func GetContainerFromIpcLikeConfig(p *Project, conf string) string {
ipc := runconfig.IpcMode(conf)
ipc := container.IpcMode(conf)
if !ipc.IsContainer() {
return ""
}
@@ -61,7 +65,7 @@ func GetContainerFromIpcLikeConfig(p *Project, conf string) string {
return ""
}
if _, ok := p.Configs[name]; ok {
if p.ServiceConfigs.Has(name) {
return name
}
return ""

View File

@@ -1,54 +0,0 @@
package project
import (
"testing"
yaml "github.com/cloudfoundry-incubator/candiedyaml"
"github.com/stretchr/testify/assert"
)
type structStringorslice struct {
Foo Stringorslice `yaml:"foo,flow,omitempty"`
}
func TestMarshal(t *testing.T) {
s := &structStringorslice{Foo: NewStringorslice("a", "b", "c")}
b, err := yaml.Marshal(s)
assert.Equal(t, "foo: [a, b, c]\n", string(b))
assert.Nil(t, err)
}
func TestMarshalEmpty(t *testing.T) {
s := &structStringorslice{}
b, err := yaml.Marshal(s)
assert.Equal(t, "foo: []\n", string(b))
assert.Nil(t, err)
}
func TestUnmarshalSlice(t *testing.T) {
expected := &structStringorslice{Foo: NewStringorslice("a", "b", "c")}
b := []byte("foo: [a, b, c]\n")
s := &structStringorslice{}
err := yaml.Unmarshal(b, s)
assert.Equal(t, expected, s)
assert.Nil(t, err)
}
func TestUnmarshalString(t *testing.T) {
expected := &structStringorslice{Foo: NewStringorslice("abc")}
b := []byte("foo: abc\n")
s := &structStringorslice{}
err := yaml.Unmarshal(b, s)
assert.Equal(t, expected, s)
assert.Nil(t, err)
}
func TestUnmarshalEmpty(t *testing.T) {
expected := &structStringorslice{Foo: NewStringorslice()}
b := []byte("{}\n")
s := &structStringorslice{}
err := yaml.Unmarshal(b, s)
assert.Equal(t, expected, s)
assert.Nil(t, err)
}