vendoring dependencies

This commit is contained in:
ngtuna
2016-06-30 22:47:32 +07:00
parent 9bdc4cf2dd
commit 5dd2cf81bf
1317 changed files with 463670 additions and 0 deletions
+191
View File
@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2015 Docker, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+76
View File
@@ -0,0 +1,76 @@
package logger
import (
"fmt"
"os"
"strconv"
"github.com/docker/libcompose/logger"
"golang.org/x/crypto/ssh/terminal"
)
// ColorLoggerFactory implements logger.Factory interface using ColorLogger.
type ColorLoggerFactory struct {
maxLength int
tty bool
}
// ColorLogger implements logger.Logger interface with color support.
type ColorLogger struct {
name string
colorPrefix string
factory *ColorLoggerFactory
}
// NewColorLoggerFactory creates a new ColorLoggerFactory.
func NewColorLoggerFactory() *ColorLoggerFactory {
return &ColorLoggerFactory{
tty: terminal.IsTerminal(int(os.Stdout.Fd())),
}
}
// Create implements logger.Factory.Create.
func (c *ColorLoggerFactory) Create(name string) logger.Logger {
if c.maxLength < len(name) {
c.maxLength = len(name)
}
return &ColorLogger{
name: name,
factory: c,
colorPrefix: <-colorPrefix,
}
}
// Out implements logger.Logger.Out.
func (c *ColorLogger) Out(bytes []byte) {
if len(bytes) == 0 {
return
}
logFmt, name := c.getLogFmt()
message := fmt.Sprintf(logFmt, name, string(bytes))
fmt.Print(message)
}
// Err implements logger.Logger.Err.
func (c *ColorLogger) Err(bytes []byte) {
if len(bytes) == 0 {
return
}
logFmt, name := c.getLogFmt()
message := fmt.Sprintf(logFmt, name, string(bytes))
fmt.Fprint(os.Stderr, message)
}
func (c *ColorLogger) getLogFmt() (string, string) {
pad := c.factory.maxLength
logFmt := "%s | %s"
if c.factory.tty {
logFmt = c.colorPrefix + " %s"
}
name := fmt.Sprintf("%-"+strconv.Itoa(pad)+"s", c.name)
return logFmt, name
}
+34
View File
@@ -0,0 +1,34 @@
package logger
import "fmt"
var (
colorPrefix = make(chan string)
)
func generateColors() {
i := 0
colorOrder := []string{
"36", // cyan
"33", // yellow
"32", // green
"35", // magenta
"31", // red
"34", // blue
"36;1", // intense cyan
"33;1", // intense yellow
"32;1", // intense green
"35;1", // intense magenta
"31;1", // intense red
"34;1", // intense blue
}
for {
colorPrefix <- fmt.Sprintf("\033[%sm%%s |\033[0m", colorOrder[i])
i = (i + 1) % len(colorOrder)
}
}
func init() {
go generateColors()
}
+168
View File
@@ -0,0 +1,168 @@
package docker
import (
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/fileutils"
"github.com/docker/docker/utils"
"github.com/docker/libcompose/project"
dockerclient "github.com/fsouza/go-dockerclient"
)
// DefaultDockerfileName is the default name of a Dockerfile
const DefaultDockerfileName = "Dockerfile"
// Builder defines methods to provide a docker builder. This makes libcompose
// not tied up to the docker daemon builder.
type Builder interface {
Build(p *project.Project, service project.Service) (string, error)
}
// DaemonBuilder is the daemon "docker build" Builder implementation.
type DaemonBuilder struct {
context *Context
}
// NewDaemonBuilder creates a DaemonBuilder based on the specified context.
func NewDaemonBuilder(context *Context) *DaemonBuilder {
return &DaemonBuilder{
context: context,
}
}
// Build implements Builder. It consumes the docker build API endpoint and sends
// a tar of the specified service build context.
func (d *DaemonBuilder) Build(p *project.Project, service project.Service) (string, error) {
if service.Config().Build == "" {
return service.Config().Image, nil
}
tag := fmt.Sprintf("%s_%s", p.Name, service.Name())
context, err := CreateTar(p, service.Name())
if err != nil {
return "", err
}
defer context.Close()
client := d.context.ClientFactory.Create(service)
logrus.Infof("Building %s...", tag)
err = client.BuildImage(dockerclient.BuildImageOptions{
InputStream: context,
OutputStream: os.Stdout,
RawJSONStream: false,
Name: tag,
RmTmpContainer: true,
Dockerfile: service.Config().Dockerfile,
NoCache: d.context.NoCache,
})
if err != nil {
return "", err
}
return tag, nil
}
// CreateTar create a build context tar for the specified project and service name.
func CreateTar(p *project.Project, name string) (io.ReadCloser, error) {
// This code was ripped off from docker/api/client/build.go
serviceConfig := p.Configs[name]
root := serviceConfig.Build
dockerfileName := filepath.Join(root, serviceConfig.Dockerfile)
absRoot, err := filepath.Abs(root)
if err != nil {
return nil, err
}
filename := dockerfileName
if dockerfileName == "" {
// No -f/--file was specified so use the default
dockerfileName = DefaultDockerfileName
filename = filepath.Join(absRoot, dockerfileName)
// Just to be nice ;-) look for 'dockerfile' too but only
// use it if we found it, otherwise ignore this check
if _, err = os.Lstat(filename); os.IsNotExist(err) {
tmpFN := path.Join(absRoot, strings.ToLower(dockerfileName))
if _, err = os.Lstat(tmpFN); err == nil {
dockerfileName = strings.ToLower(dockerfileName)
filename = tmpFN
}
}
}
origDockerfile := dockerfileName // used for error msg
if filename, err = filepath.Abs(filename); err != nil {
return nil, err
}
// Now reset the dockerfileName to be relative to the build context
dockerfileName, err = filepath.Rel(absRoot, filename)
if err != nil {
return nil, err
}
// And canonicalize dockerfile name to a platform-independent one
dockerfileName, err = archive.CanonicalTarNameForPath(dockerfileName)
if err != nil {
return nil, fmt.Errorf("Cannot canonicalize dockerfile path %s: %v", dockerfileName, err)
}
if _, err = os.Lstat(filename); os.IsNotExist(err) {
return nil, fmt.Errorf("Cannot locate Dockerfile: %s", origDockerfile)
}
var includes = []string{"."}
var excludes []string
dockerIgnorePath := path.Join(root, ".dockerignore")
dockerIgnore, err := os.Open(dockerIgnorePath)
if err != nil {
if !os.IsNotExist(err) {
return nil, err
}
logrus.Warnf("Error while reading .dockerignore (%s) : %s", dockerIgnorePath, err.Error())
excludes = make([]string, 0)
} else {
excludes, err = utils.ReadDockerIgnore(dockerIgnore)
if err != nil {
return nil, err
}
}
// If .dockerignore mentions .dockerignore or the Dockerfile
// then make sure we send both files over to the daemon
// because Dockerfile is, obviously, needed no matter what, and
// .dockerignore is needed to know if either one needs to be
// removed. The deamon will remove them for us, if needed, after it
// parses the Dockerfile.
keepThem1, _ := fileutils.Matches(".dockerignore", excludes)
keepThem2, _ := fileutils.Matches(dockerfileName, excludes)
if keepThem1 || keepThem2 {
includes = append(includes, ".dockerignore", dockerfileName)
}
if err := utils.ValidateContextDirectory(root, excludes); err != nil {
return nil, fmt.Errorf("Error checking context is accessible: '%s'. Please check permissions and try again.", err)
}
options := &archive.TarOptions{
Compression: archive.Uncompressed,
ExcludePatterns: excludes,
IncludeFiles: includes,
}
return archive.TarWithOptions(root, options)
}
+102
View File
@@ -0,0 +1,102 @@
package docker
import (
"fmt"
"os"
"path/filepath"
"runtime"
"github.com/docker/docker/cliconfig"
"github.com/docker/docker/opts"
"github.com/docker/docker/pkg/homedir"
"github.com/docker/docker/pkg/tlsconfig"
dockerclient "github.com/fsouza/go-dockerclient"
)
const (
// DefaultAPIVersion is the default docker API version set by libcompose
DefaultAPIVersion = "1.20"
defaultTrustKeyFile = "key.json"
defaultCaFile = "ca.pem"
defaultKeyFile = "key.pem"
defaultCertFile = "cert.pem"
)
var (
dockerCertPath = os.Getenv("DOCKER_CERT_PATH")
)
func init() {
if dockerCertPath == "" {
dockerCertPath = cliconfig.ConfigDir()
}
}
// ClientOpts holds docker client options (host, tls, ..)
type ClientOpts struct {
TLS bool
TLSVerify bool
TLSOptions tlsconfig.Options
TrustKey string
Host string
APIVersion string
}
// CreateClient creates a docker client based on the specified options.
func CreateClient(c ClientOpts) (*dockerclient.Client, error) {
if c.TLSOptions.CAFile == "" {
c.TLSOptions.CAFile = filepath.Join(dockerCertPath, defaultCaFile)
}
if c.TLSOptions.CertFile == "" {
c.TLSOptions.CertFile = filepath.Join(dockerCertPath, defaultCertFile)
}
if c.TLSOptions.KeyFile == "" {
c.TLSOptions.KeyFile = filepath.Join(dockerCertPath, defaultKeyFile)
}
if c.Host == "" {
defaultHost := os.Getenv("DOCKER_HOST")
if defaultHost == "" {
if runtime.GOOS != "windows" {
// If we do not have a host, default to unix socket
defaultHost = fmt.Sprintf("unix://%s", opts.DefaultUnixSocket)
} else {
// If we do not have a host, default to TCP socket on Windows
defaultHost = fmt.Sprintf("tcp://%s:%d", opts.DefaultHTTPHost, opts.DefaultHTTPPort)
}
}
defaultHost, err := opts.ValidateHost(defaultHost)
if err != nil {
return nil, err
}
c.Host = defaultHost
}
if c.TrustKey == "" {
c.TrustKey = filepath.Join(homedir.Get(), ".docker", defaultTrustKeyFile)
}
if c.TLSVerify {
c.TLS = true
}
if c.TLS {
c.TLSOptions.InsecureSkipVerify = !c.TLSVerify
}
apiVersion := c.APIVersion
if apiVersion == "" {
apiVersion = DefaultAPIVersion
}
if c.TLS {
client, err := dockerclient.NewVersionedTLSClient(c.Host, c.TLSOptions.CertFile, c.TLSOptions.KeyFile, c.TLSOptions.CAFile, apiVersion)
if err != nil {
return nil, err
}
if c.TLSOptions.InsecureSkipVerify {
client.TLSConfig.InsecureSkipVerify = true
}
return client, nil
}
return dockerclient.NewVersionedClient(c.Host, apiVersion)
}
+35
View File
@@ -0,0 +1,35 @@
package docker
import (
"github.com/docker/libcompose/project"
dockerclient "github.com/fsouza/go-dockerclient"
)
// 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 project.Service) *dockerclient.Client
}
type defaultClientFactory struct {
client *dockerclient.Client
}
// NewDefaultClientFactory creates and returns the default client factory that uses
// github.com/samalba/dockerclient.
func NewDefaultClientFactory(opts ClientOpts) (ClientFactory, error) {
client, err := CreateClient(opts)
if err != nil {
return nil, err
}
return &defaultClientFactory{
client: client,
}, nil
}
func (s *defaultClientFactory) Create(service project.Service) *dockerclient.Client {
return s.client
}
+561
View File
@@ -0,0 +1,561 @@
package docker
import (
"fmt"
"math"
"os"
"strings"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/cliconfig"
"github.com/docker/docker/pkg/parsers"
"github.com/docker/docker/registry"
"github.com/docker/docker/utils"
"github.com/docker/libcompose/logger"
"github.com/docker/libcompose/project"
util "github.com/docker/libcompose/utils"
dockerclient "github.com/fsouza/go-dockerclient"
)
// DefaultTag is the name of the default tag of an image.
const DefaultTag = "latest"
// Container holds information about a docker container and the service it is tied on.
// It implements Service interface by encapsulating a EmptyService.
type Container struct {
project.EmptyService
name string
service *Service
client *dockerclient.Client
}
// NewContainer creates a container struct with the specified docker client, name and service.
func NewContainer(client *dockerclient.Client, name string, service *Service) *Container {
return &Container{
client: client,
name: name,
service: service,
}
}
func (c *Container) findExisting() (*dockerclient.APIContainers, error) {
return GetContainerByName(c.client, c.name)
}
func (c *Container) findInfo() (*dockerclient.Container, error) {
container, err := c.findExisting()
if err != nil {
return nil, err
}
return c.client.InspectContainer(container.ID)
}
// Info returns info about the container, like name, command, state or ports.
func (c *Container) Info(qFlag bool) (project.Info, error) {
container, err := c.findExisting()
if err != nil {
return nil, err
}
result := project.Info{}
if qFlag {
result = append(result, project.InfoPart{Key: "Id", Value: container.ID})
} else {
result = append(result, project.InfoPart{Key: "Name", Value: name(container.Names)})
result = append(result, project.InfoPart{Key: "Command", Value: container.Command})
result = append(result, project.InfoPart{Key: "State", Value: container.Status})
result = append(result, project.InfoPart{Key: "Ports", Value: portString(container.Ports)})
}
return result, nil
}
func portString(ports []dockerclient.APIPort) string {
result := []string{}
for _, port := range ports {
if port.PublicPort > 0 {
result = append(result, fmt.Sprintf("%s:%d->%d/%s", port.IP, port.PublicPort, port.PrivatePort, port.Type))
} else {
result = append(result, fmt.Sprintf("%d/%s", port.PrivatePort, port.Type))
}
}
return strings.Join(result, ", ")
}
func name(names []string) string {
max := math.MaxInt32
var current string
for _, v := range names {
if len(v) < max {
max = len(v)
current = v
}
}
return current[1:]
}
// Recreate will not refresh the container by means of relaxation and enjoyment,
// just delete it and create a new one with the current configuration
func (c *Container) Recreate(imageName string) (*dockerclient.APIContainers, error) {
info, err := c.findInfo()
if err != nil {
return nil, err
} else if info == nil {
return nil, fmt.Errorf("Can not find container to recreate for service: %s", c.service.Name())
}
hash := info.Config.Labels[HASH.Str()]
if hash == "" {
return nil, fmt.Errorf("Failed to find hash on old container: %s", info.Name)
}
name := info.Name[1:]
newName := fmt.Sprintf("%s_%s", name, info.ID[:12])
logrus.Debugf("Renaming %s => %s", name, newName)
if err := c.client.RenameContainer(dockerclient.RenameContainerOptions{ID: info.ID, Name: newName}); err != nil {
logrus.Errorf("Failed to rename old container %s", c.name)
return nil, err
}
newContainer, err := c.createContainer(imageName, info.ID)
if err != nil {
return nil, err
}
logrus.Debugf("Created replacement container %s", newContainer.ID)
if err := c.client.RemoveContainer(
dockerclient.RemoveContainerOptions{ID: info.ID, Force: true, RemoveVolumes: false}); err != nil {
logrus.Errorf("Failed to remove old container %s", c.name)
return nil, err
}
logrus.Debugf("Removed old container %s %s", c.name, info.ID)
return newContainer, nil
}
// Create creates the container based on the specified image name and send an event
// to notify the container has been created. If the container already exists, does
// nothing.
func (c *Container) Create(imageName string) (*dockerclient.APIContainers, error) {
container, err := c.findExisting()
if err != nil {
return nil, err
}
if container == nil {
container, err = c.createContainer(imageName, "")
if err != nil {
return nil, err
}
c.service.context.Project.Notify(project.EventContainerCreated, c.service.Name(), map[string]string{
"name": c.Name(),
})
}
return container, err
}
// Down stops the container.
func (c *Container) Down() error {
return c.withContainer(func(container *dockerclient.APIContainers) error {
return c.client.StopContainer(container.ID, c.service.context.Timeout)
})
}
// Pause pauses the container. If the containers are already paused, don't fail.
func (c *Container) Pause() error {
return c.withContainer(func(container *dockerclient.APIContainers) error {
if !strings.Contains(container.Status, "Paused") {
return c.client.PauseContainer(container.ID)
}
return nil
})
}
// Unpause unpauses the container. If the containers are not paused, don't fail.
func (c *Container) Unpause() error {
return c.withContainer(func(container *dockerclient.APIContainers) error {
if strings.Contains(container.Status, "Paused") {
return c.client.UnpauseContainer(container.ID)
}
return nil
})
}
// Kill kill the container.
func (c *Container) Kill() error {
return c.withContainer(func(container *dockerclient.APIContainers) error {
return c.client.KillContainer(dockerclient.KillContainerOptions{ID: container.ID, Signal: dockerclient.Signal(c.service.context.Signal)})
})
}
// Delete removes the container if existing. If the container is running, it tries
// to stop it first.
func (c *Container) Delete() error {
container, err := c.findExisting()
if err != nil || container == nil {
return err
}
info, err := c.client.InspectContainer(container.ID)
if err != nil {
return err
}
if info.State.Running {
err := c.client.StopContainer(container.ID, c.service.context.Timeout)
if err != nil {
return err
}
}
return c.client.RemoveContainer(dockerclient.RemoveContainerOptions{ID: container.ID, Force: true, RemoveVolumes: c.service.context.Volume})
}
// Up creates and start the container based on the image name and send an event
// to notify the container has been created. If the container exists but is stopped
// it tries to start it.
func (c *Container) Up(imageName string) error {
var err error
defer func() {
if err == nil && c.service.context.Log {
go c.Log()
}
}()
container, err := c.Create(imageName)
if err != nil {
return err
}
info, err := c.client.InspectContainer(container.ID)
if err != nil {
return err
}
if !info.State.Running {
logrus.WithFields(logrus.Fields{"container.ID": container.ID, "c.name": c.name}).Debug("Starting container")
if err = c.client.StartContainer(container.ID, nil); err != nil {
logrus.WithFields(logrus.Fields{"container.ID": container.ID, "c.name": c.name}).Debug("Failed to start container")
return err
}
c.service.context.Project.Notify(project.EventContainerStarted, c.service.Name(), map[string]string{
"name": c.Name(),
})
}
return nil
}
// OutOfSync checks if the container is out of sync with the service definition.
// It looks if the the service hash container label is the same as the computed one.
func (c *Container) OutOfSync(imageName string) (bool, error) {
info, err := c.findInfo()
if err != nil || info == nil {
return false, err
}
if info.Config.Image != imageName {
logrus.Debugf("Images for %s do not match %s!=%s", c.name, info.Config.Image, imageName)
return true, nil
}
if info.Config.Labels[HASH.Str()] != c.getHash() {
logrus.Debugf("Hashes for %s do not match %s!=%s", c.name, info.Config.Labels[HASH.Str()], c.getHash())
return true, nil
}
image, err := c.client.InspectImage(info.Config.Image)
if err != nil && (err.Error() == "Not found" || image == nil) {
logrus.Debugf("Image %s do not exist, do not know if it's out of sync", info.Config.Image)
return false, nil
} else if err != nil {
return false, err
}
logrus.Debugf("Checking existing image name vs id: %s == %s", image.ID, info.Image)
return image.ID != info.Image, err
}
func (c *Container) getHash() string {
return project.GetServiceHash(c.service.Name(), c.service.Config())
}
func volumeBinds(volumes map[string]struct{}, container *dockerclient.Container) []string {
result := make([]string, 0, len(container.Mounts))
for _, mount := range container.Mounts {
if _, ok := volumes[mount.Destination]; ok {
result = append(result, fmt.Sprint(mount.Source, ":", mount.Destination))
}
}
return result
}
func (c *Container) createContainer(imageName, oldContainer string) (*dockerclient.APIContainers, error) {
createOpts, err := ConvertToAPI(c.service.serviceConfig, c.name)
if err != nil {
return nil, err
}
createOpts.Config.Image = imageName
if createOpts.Config.Labels == nil {
createOpts.Config.Labels = map[string]string{}
}
createOpts.Config.Labels[NAME.Str()] = c.name
createOpts.Config.Labels[SERVICE.Str()] = c.service.name
createOpts.Config.Labels[PROJECT.Str()] = c.service.context.Project.Name
createOpts.Config.Labels[HASH.Str()] = c.getHash()
err = c.populateAdditionalHostConfig(createOpts.HostConfig)
if err != nil {
return nil, err
}
if oldContainer != "" {
info, err := c.client.InspectContainer(oldContainer)
if err != nil {
return nil, err
}
createOpts.HostConfig.Binds = util.Merge(createOpts.HostConfig.Binds, volumeBinds(createOpts.Config.Volumes, info))
}
logrus.Debugf("Creating container %s %#v", c.name, createOpts)
container, err := c.client.CreateContainer(*createOpts)
if err != nil && err == dockerclient.ErrNoSuchImage {
logrus.Debugf("Not Found, pulling image %s", createOpts.Config.Image)
if err = c.pull(createOpts.Config.Image); err != nil {
return nil, err
}
if container, err = c.client.CreateContainer(*createOpts); err != nil {
return nil, err
}
}
if err != nil {
logrus.Debugf("Failed to create container %s: %v", c.name, err)
return nil, err
}
return GetContainerByID(c.client, container.ID)
}
func (c *Container) populateAdditionalHostConfig(hostConfig *dockerclient.HostConfig) error {
links := map[string]string{}
for _, link := range c.service.DependentServices() {
if _, ok := c.service.context.Project.Configs[link.Target]; !ok {
continue
}
service, err := c.service.context.Project.CreateService(link.Target)
if err != nil {
return err
}
containers, err := service.Containers()
if err != nil {
return err
}
if link.Type == project.RelTypeLink {
c.addLinks(links, service, link, containers)
} else if link.Type == project.RelTypeIpcNamespace {
hostConfig, err = c.addIpc(hostConfig, service, containers)
} else if link.Type == project.RelTypeNetNamespace {
hostConfig, err = c.addNetNs(hostConfig, service, containers)
}
if err != nil {
return err
}
}
hostConfig.Links = []string{}
for k, v := range links {
hostConfig.Links = append(hostConfig.Links, strings.Join([]string{v, k}, ":"))
}
for _, v := range c.service.Config().ExternalLinks {
hostConfig.Links = append(hostConfig.Links, v)
}
return nil
}
func (c *Container) addLinks(links map[string]string, service project.Service, rel project.ServiceRelationship, containers []project.Container) {
for _, container := range containers {
if _, ok := links[rel.Alias]; !ok {
links[rel.Alias] = container.Name()
}
links[container.Name()] = container.Name()
}
}
func (c *Container) addIpc(config *dockerclient.HostConfig, service project.Service, containers []project.Container) (*dockerclient.HostConfig, error) {
if len(containers) == 0 {
return nil, fmt.Errorf("Failed to find container for IPC %v", c.service.Config().Ipc)
}
id, err := containers[0].ID()
if err != nil {
return nil, err
}
config.IpcMode = "container:" + id
return config, nil
}
func (c *Container) addNetNs(config *dockerclient.HostConfig, service project.Service, containers []project.Container) (*dockerclient.HostConfig, error) {
if len(containers) == 0 {
return nil, fmt.Errorf("Failed to find container for networks ns %v", c.service.Config().Net)
}
id, err := containers[0].ID()
if err != nil {
return nil, err
}
config.NetworkMode = "container:" + id
return config, nil
}
// ID returns the container Id.
func (c *Container) ID() (string, error) {
container, err := c.findExisting()
if container == nil {
return "", err
}
return container.ID, err
}
// Name returns the container name.
func (c *Container) Name() string {
return c.name
}
// Pull pulls the image the container is based on.
func (c *Container) Pull() error {
return c.pull(c.service.serviceConfig.Image)
}
// Restart restarts the container if existing, does nothing otherwise.
func (c *Container) Restart() error {
container, err := c.findExisting()
if err != nil || container == nil {
return err
}
return c.client.RestartContainer(container.ID, c.service.context.Timeout)
}
// Log forwards container logs to the project configured logger.
func (c *Container) Log() error {
container, err := c.findExisting()
if container == nil || err != nil {
return err
}
info, err := c.client.InspectContainer(container.ID)
if info == nil || err != nil {
return err
}
l := c.service.context.LoggerFactory.Create(c.name)
err = c.client.Logs(dockerclient.LogsOptions{
Container: c.name,
Follow: true,
Stdout: true,
Stderr: true,
Tail: "0",
OutputStream: &logger.Wrapper{Logger: l},
ErrorStream: &logger.Wrapper{Logger: l, Err: true},
RawTerminal: info.Config.Tty,
})
logrus.WithFields(logrus.Fields{"Logger": l, "err": err}).Debug("c.client.Logs() returned error")
return err
}
func (c *Container) pull(image string) error {
return pullImage(c.client, c.service, image)
}
func pullImage(client *dockerclient.Client, service *Service, image string) error {
taglessRemote, tag := parsers.ParseRepositoryTag(image)
if tag == "" {
image = utils.ImageReference(taglessRemote, DefaultTag)
}
repoInfo, err := registry.ParseRepositoryInfo(taglessRemote)
if err != nil {
return err
}
authConfig := cliconfig.AuthConfig{}
if service.context.ConfigFile != nil && repoInfo != nil && repoInfo.Index != nil {
authConfig = registry.ResolveAuthConfig(service.context.ConfigFile, repoInfo.Index)
}
err = client.PullImage(
dockerclient.PullImageOptions{
Repository: image,
OutputStream: os.Stderr, // TODO maybe get the stream from some configured place
},
dockerclient.AuthConfiguration{
Username: authConfig.Username,
Password: authConfig.Password,
Email: authConfig.Email,
},
)
if err != nil {
logrus.Errorf("Failed to pull image %s: %v", image, err)
}
return err
}
func (c *Container) withContainer(action func(*dockerclient.APIContainers) error) error {
container, err := c.findExisting()
if err != nil {
return err
}
if container != nil {
return action(container)
}
return nil
}
// Port returns the host port the specified port is mapped on.
func (c *Container) Port(port string) (string, error) {
info, err := c.findInfo()
if err != nil {
return "", err
}
if bindings, ok := info.NetworkSettings.Ports[dockerclient.Port(port)]; ok {
result := []string{}
for _, binding := range bindings {
result = append(result, binding.HostIP+":"+binding.HostPort)
}
return strings.Join(result, "\n"), nil
}
return "", nil
}
+36
View File
@@ -0,0 +1,36 @@
package docker
import (
"github.com/docker/docker/cliconfig"
"github.com/docker/libcompose/project"
)
// Context holds context meta information about a libcompose project and docker
// client information (like configuration file, builder to use, …)
type Context struct {
project.Context
Builder Builder
ClientFactory ClientFactory
ConfigDir string
ConfigFile *cliconfig.ConfigFile
}
func (c *Context) open() error {
return c.LookupConfig()
}
// LookupConfig tries to load the docker configuration files, if any.
func (c *Context) LookupConfig() error {
if c.ConfigFile != nil {
return nil
}
config, err := cliconfig.Load(c.ConfigDir)
if err != nil {
return err
}
c.ConfigFile = config
return nil
}
+177
View File
@@ -0,0 +1,177 @@
package docker
import (
"strings"
"github.com/docker/docker/pkg/nat"
"github.com/docker/docker/runconfig"
"github.com/docker/libcompose/project"
"github.com/docker/libcompose/utils"
dockerclient "github.com/fsouza/go-dockerclient"
)
// Filter filters the specified string slice with the specified function.
func Filter(vs []string, f func(string) bool) []string {
r := make([]string, 0, len(vs))
for _, v := range vs {
if f(v) {
r = append(r, v)
}
}
return r
}
func isBind(s string) bool {
return strings.ContainsRune(s, ':')
}
func isVolume(s string) bool {
return !isBind(s)
}
// ConvertToAPI converts a service configuration to a docker API container configuration.
func ConvertToAPI(c *project.ServiceConfig, name string) (*dockerclient.CreateContainerOptions, error) {
config, hostConfig, err := Convert(c)
if err != nil {
return nil, err
}
result := dockerclient.CreateContainerOptions{
Name: name,
Config: config,
HostConfig: hostConfig,
}
return &result, nil
}
func volumes(c *project.ServiceConfig) map[string]struct{} {
vs := Filter(c.Volumes, isVolume)
volumes := make(map[string]struct{}, len(vs))
for _, v := range vs {
volumes[v] = struct{}{}
}
return volumes
}
func restartPolicy(c *project.ServiceConfig) (*dockerclient.RestartPolicy, error) {
restart, err := runconfig.ParseRestartPolicy(c.Restart)
if err != nil {
return nil, err
}
return &dockerclient.RestartPolicy{Name: restart.Name, MaximumRetryCount: restart.MaximumRetryCount}, nil
}
func ports(c *project.ServiceConfig) (map[dockerclient.Port]struct{}, map[dockerclient.Port][]dockerclient.PortBinding, error) {
ports, binding, err := nat.ParsePortSpecs(c.Ports)
if err != nil {
return nil, nil, err
}
exPorts, _, err := nat.ParsePortSpecs(c.Expose)
if err != nil {
return nil, nil, err
}
for k, v := range exPorts {
ports[k] = v
}
exposedPorts := map[dockerclient.Port]struct{}{}
for k, v := range ports {
exposedPorts[dockerclient.Port(k)] = v
}
portBindings := map[dockerclient.Port][]dockerclient.PortBinding{}
for k, bv := range binding {
dcbs := make([]dockerclient.PortBinding, len(bv))
for k, v := range bv {
dcbs[k] = dockerclient.PortBinding{HostIP: v.HostIP, HostPort: v.HostPort}
}
portBindings[dockerclient.Port(k)] = dcbs
}
return exposedPorts, portBindings, nil
}
// Convert converts a service configuration to an docker API structures (Config and HostConfig)
func Convert(c *project.ServiceConfig) (*dockerclient.Config, *dockerclient.HostConfig, error) {
restartPolicy, err := restartPolicy(c)
if err != nil {
return nil, nil, err
}
exposedPorts, portBindings, err := ports(c)
if err != nil {
return nil, nil, err
}
deviceMappings, err := parseDevices(c.Devices)
if err != nil {
return nil, nil, err
}
config := &dockerclient.Config{
Entrypoint: utils.CopySlice(c.Entrypoint.Slice()),
Hostname: c.Hostname,
Domainname: c.DomainName,
User: c.User,
Env: utils.CopySlice(c.Environment.Slice()),
Cmd: utils.CopySlice(c.Command.Slice()),
Image: c.Image,
Labels: utils.CopyMap(c.Labels.MapParts()),
ExposedPorts: exposedPorts,
Tty: c.Tty,
OpenStdin: c.StdinOpen,
WorkingDir: c.WorkingDir,
VolumeDriver: c.VolumeDriver,
Volumes: volumes(c),
}
hostConfig := &dockerclient.HostConfig{
VolumesFrom: utils.CopySlice(c.VolumesFrom),
CapAdd: utils.CopySlice(c.CapAdd),
CapDrop: utils.CopySlice(c.CapDrop),
CPUShares: c.CPUShares,
CPUSetCPUs: c.CPUSet,
ExtraHosts: utils.CopySlice(c.ExtraHosts),
Privileged: c.Privileged,
Binds: Filter(c.Volumes, isBind),
Devices: deviceMappings,
DNS: utils.CopySlice(c.DNS.Slice()),
DNSSearch: utils.CopySlice(c.DNSSearch.Slice()),
LogConfig: dockerclient.LogConfig{
Type: c.LogDriver,
Config: utils.CopyMap(c.LogOpt),
},
Memory: c.MemLimit,
MemorySwap: c.MemSwapLimit,
NetworkMode: c.Net,
ReadonlyRootfs: c.ReadOnly,
PidMode: c.Pid,
UTSMode: c.Uts,
IpcMode: c.Ipc,
PortBindings: portBindings,
RestartPolicy: *restartPolicy,
SecurityOpt: utils.CopySlice(c.SecurityOpt),
}
return config, hostConfig, nil
}
func parseDevices(devices []string) ([]dockerclient.Device, error) {
// parse device mappings
deviceMappings := []dockerclient.Device{}
for _, device := range devices {
v, err := runconfig.ParseDevice(device)
if err != nil {
return nil, err
}
deviceMappings = append(deviceMappings, dockerclient.Device{
PathOnHost: v.PathOnHost,
PathInContainer: v.PathInContainer,
CgroupPermissions: v.CgroupPermissions,
})
}
return deviceMappings, nil
}
+52
View File
@@ -0,0 +1,52 @@
package docker
import (
dockerclient "github.com/fsouza/go-dockerclient"
)
// GetContainersByFilter looks up the hosts containers with the specified filters and
// returns a list of container matching it, or an error.
func GetContainersByFilter(client *dockerclient.Client, filters ...map[string][]string) ([]dockerclient.APIContainers, error) {
var filterResult map[string][]string
for _, filter := range filters {
if filterResult == nil {
filterResult = filter
} else {
filterResult = And(filterResult, filter)
}
}
return client.ListContainers(dockerclient.ListContainersOptions{All: true, Filters: filterResult})
}
// GetContainerByName looks up the hosts containers with the specified name and
// returns it, or an error.
func GetContainerByName(client *dockerclient.Client, name string) (*dockerclient.APIContainers, error) {
containers, err := client.ListContainers(dockerclient.ListContainersOptions{All: true, Filters: NAME.Eq(name)})
if err != nil {
return nil, err
}
if len(containers) == 0 {
return nil, nil
}
return &containers[0], nil
}
// GetContainerByID looks up the hosts containers with the specified Id and
// returns it, or an error.
func GetContainerByID(client *dockerclient.Client, id string) (*dockerclient.APIContainers, error) {
containers, err := client.ListContainers(
dockerclient.ListContainersOptions{All: true, Filters: map[string][]string{"id": {id}}})
if err != nil {
return nil, err
}
if len(containers) == 0 {
return nil, nil
}
return &containers[0], nil
}
+75
View File
@@ -0,0 +1,75 @@
package docker
import (
"encoding/json"
"github.com/docker/libcompose/utils"
)
// Label represents a docker label.
type Label string
// Libcompose default labels.
const (
NAME = Label("io.docker.compose.name")
PROJECT = Label("io.docker.compose.project")
SERVICE = Label("io.docker.compose.service")
HASH = Label("io.docker.compose.config-hash")
)
// EqString returns a label json string representation with the specified value.
func (f Label) EqString(value string) string {
return utils.LabelFilterString(string(f), value)
}
// Eq returns a label map representation with the specified value.
func (f Label) Eq(value string) map[string][]string {
return utils.LabelFilter(string(f), value)
}
// AndString returns a json list of labels by merging the two specified values (left and right) serialized as string.
func AndString(left, right string) string {
leftMap := map[string][]string{}
rightMap := map[string][]string{}
// Ignore errors
json.Unmarshal([]byte(left), &leftMap)
json.Unmarshal([]byte(right), &rightMap)
for k, v := range rightMap {
existing, ok := leftMap[k]
if ok {
leftMap[k] = append(existing, v...)
} else {
leftMap[k] = v
}
}
result, _ := json.Marshal(leftMap)
return string(result)
}
// And returns a map of labels by merging the two specified values (left and right).
func And(left, right map[string][]string) map[string][]string {
result := map[string][]string{}
for k, v := range left {
result[k] = v
}
for k, v := range right {
existing, ok := result[k]
if ok {
result[k] = append(existing, v...)
} else {
result[k] = v
}
}
return result
}
// Str returns the label name.
func (f Label) Str() string {
return string(f)
}
+81
View File
@@ -0,0 +1,81 @@
package docker
import (
"fmt"
"io"
"time"
dockerclient "github.com/fsouza/go-dockerclient"
)
const format = "%s_%s_%d"
// Namer defines method to provide container name.
type Namer interface {
io.Closer
Next() string
}
type inOrderNamer struct {
names chan string
done chan bool
}
type singleNamer struct {
name string
}
// NewSingleNamer returns a namer that only allows a single name.
func NewSingleNamer(name string) Namer {
return &singleNamer{name}
}
// NewNamer returns a namer that returns names based on the specified project and
// service name and an inner counter, e.g. project_service_1, project_service_2…
func NewNamer(client *dockerclient.Client, project, service string) Namer {
namer := &inOrderNamer{
names: make(chan string),
done: make(chan bool),
}
go func() {
for i := 1; true; i++ {
name := fmt.Sprintf(format, project, service, i)
c, err := GetContainerByName(client, name)
if err != nil {
// Sleep here to avoid crazy tight loop when things go south
time.Sleep(time.Second * 1)
continue
}
if c != nil {
continue
}
select {
case namer.names <- name:
case <-namer.done:
close(namer.names)
return
}
}
}()
return namer
}
func (i *inOrderNamer) Next() string {
return <-i.names
}
func (i *inOrderNamer) Close() error {
close(i.done)
return nil
}
func (s *singleNamer) Next() string {
return s.name
}
func (s *singleNamer) Close() error {
return nil
}
+51
View File
@@ -0,0 +1,51 @@
package docker
import (
"github.com/Sirupsen/logrus"
"github.com/docker/libcompose/lookup"
"github.com/docker/libcompose/project"
)
// NewProject creates a Project with the specified context.
func NewProject(context *Context) (*project.Project, error) {
if context.ConfigLookup == nil {
context.ConfigLookup = &lookup.FileConfigLookup{}
}
if context.EnvironmentLookup == nil {
context.EnvironmentLookup = &lookup.OsEnvLookup{}
}
if context.ServiceFactory == nil {
context.ServiceFactory = &ServiceFactory{
context: context,
}
}
if context.Builder == nil {
context.Builder = NewDaemonBuilder(context)
}
if context.ClientFactory == nil {
factory, err := NewDefaultClientFactory(ClientOpts{})
if err != nil {
return nil, err
}
context.ClientFactory = factory
}
p := project.NewProject(&context.Context)
err := p.Parse()
if err != nil {
return nil, err
}
if err = context.open(); err != nil {
logrus.Errorf("Failed to open project %s: %v", p.Name, err)
return nil, err
}
return p, err
}
+372
View File
@@ -0,0 +1,372 @@
package docker
import (
"fmt"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/pkg/nat"
"github.com/docker/libcompose/project"
"github.com/docker/libcompose/utils"
)
// Service is a project.Service implementations.
type Service struct {
name string
serviceConfig *project.ServiceConfig
context *Context
}
// NewService creates a service
func NewService(name string, serviceConfig *project.ServiceConfig, context *Context) *Service {
return &Service{
name: name,
serviceConfig: serviceConfig,
context: context,
}
}
// Name returns the service name.
func (s *Service) Name() string {
return s.name
}
// Config returns the configuration of the service (project.ServiceConfig).
func (s *Service) Config() *project.ServiceConfig {
return s.serviceConfig
}
// DependentServices returns the dependent services (as an array of ServiceRelationship) of the service.
func (s *Service) DependentServices() []project.ServiceRelationship {
return project.DefaultDependentServices(s.context.Project, s)
}
// Create implements Service.Create.
func (s *Service) Create() error {
imageName, err := s.build()
if err != nil {
return err
}
_, err = s.createOne(imageName)
return err
}
func (s *Service) collectContainers() ([]*Container, error) {
client := s.context.ClientFactory.Create(s)
containers, err := GetContainersByFilter(client, SERVICE.Eq(s.name), PROJECT.Eq(s.context.Project.Name))
if err != nil {
return nil, err
}
result := []*Container{}
for _, container := range containers {
name := container.Labels[NAME.Str()]
result = append(result, NewContainer(client, name, s))
}
return result, nil
}
func (s *Service) createOne(imageName string) (*Container, error) {
containers, err := s.constructContainers(imageName, 1)
if err != nil {
return nil, err
}
return containers[0], err
}
// Build implements Service.Build. If an imageName is specified or if the context has
// no build to work with it will do nothing. Otherwise it will try to build
// the image and returns an error if any.
func (s *Service) Build() error {
_, err := s.build()
return err
}
func (s *Service) build() (string, error) {
if s.context.Builder == nil {
return s.Config().Image, nil
}
return s.context.Builder.Build(s.context.Project, s)
}
func (s *Service) constructContainers(imageName string, count int) ([]*Container, error) {
result, err := s.collectContainers()
if err != nil {
return nil, err
}
client := s.context.ClientFactory.Create(s)
var namer Namer
if s.serviceConfig.ContainerName != "" {
if count > 1 {
logrus.Warnf(`The "%s" service is using the custom container name "%s". Docker requires each container to have a unique name. Remove the custom name to scale the service.`, s.name, s.serviceConfig.ContainerName)
}
namer = NewSingleNamer(s.serviceConfig.ContainerName)
} else {
namer = NewNamer(client, s.context.Project.Name, s.name)
}
defer namer.Close()
for i := len(result); i < count; i++ {
containerName := namer.Next()
c := NewContainer(client, containerName, s)
dockerContainer, err := c.Create(imageName)
if err != nil {
return nil, err
}
logrus.Debugf("Created container %s: %v", dockerContainer.ID, dockerContainer.Names)
result = append(result, NewContainer(client, containerName, s))
}
return result, nil
}
// Up implements Service.Up. It builds the image if needed, creates a container
// and start it.
func (s *Service) Up() error {
imageName, err := s.build()
if err != nil {
return err
}
return s.up(imageName, true)
}
// Info implements Service.Info. It returns an project.InfoSet with the containers
// related to this service (can be multiple if using the scale command).
func (s *Service) Info(qFlag bool) (project.InfoSet, error) {
result := project.InfoSet{}
containers, err := s.collectContainers()
if err != nil {
return nil, err
}
for _, c := range containers {
info, err := c.Info(qFlag)
if err != nil {
return nil, err
}
result = append(result, info)
}
return result, nil
}
// Start implements Service.Start. It tries to start a container without creating it.
func (s *Service) Start() error {
return s.up("", false)
}
func (s *Service) up(imageName string, create bool) error {
containers, err := s.collectContainers()
if err != nil {
return err
}
logrus.Debugf("Found %d existing containers for service %s", len(containers), s.name)
if len(containers) == 0 && create {
c, err := s.createOne(imageName)
if err != nil {
return err
}
containers = []*Container{c}
}
return s.eachContainer(func(c *Container) error {
if create {
if err := s.recreateIfNeeded(imageName, c); err != nil {
return err
}
}
return c.Up(imageName)
})
}
func (s *Service) recreateIfNeeded(imageName string, c *Container) error {
if s.context.NoRecreate {
return nil
}
outOfSync, err := c.OutOfSync(imageName)
if err != nil {
return err
}
logrus.WithFields(logrus.Fields{
"outOfSync": outOfSync,
"ForceRecreate": s.context.ForceRecreate,
"NoRecreate": s.context.NoRecreate}).Debug("Going to decide if recreate is needed")
if s.context.ForceRecreate || outOfSync {
logrus.Infof("Recreating %s", s.name)
if _, err := c.Recreate(imageName); err != nil {
return err
}
}
return nil
}
func (s *Service) eachContainer(action func(*Container) error) error {
containers, err := s.collectContainers()
if err != nil {
return err
}
tasks := utils.InParallel{}
for _, container := range containers {
task := func(container *Container) func() error {
return func() error {
return action(container)
}
}(container)
tasks.Add(task)
}
return tasks.Wait()
}
// Down implements Service.Down. It stops any containers related to the service.
func (s *Service) Down() error {
return s.eachContainer(func(c *Container) error {
return c.Down()
})
}
// Restart implements Service.Restart. It restarts any containers related to the service.
func (s *Service) Restart() error {
return s.eachContainer(func(c *Container) error {
return c.Restart()
})
}
// Kill implements Service.Kill. It kills any containers related to the service.
func (s *Service) Kill() error {
return s.eachContainer(func(c *Container) error {
return c.Kill()
})
}
// Delete implements Service.Delete. It removes any containers related to the service.
func (s *Service) Delete() error {
return s.eachContainer(func(c *Container) error {
return c.Delete()
})
}
// Log implements Service.Log. It returns the docker logs for each container related to the service.
func (s *Service) Log() error {
return s.eachContainer(func(c *Container) error {
return c.Log()
})
}
// Scale implements Service.Scale. It creates or removes containers to have the specified number
// of related container to the service to run.
func (s *Service) Scale(scale int) error {
if s.specificiesHostPort() {
logrus.Warnf("The \"%s\" service specifies a port on the host. If multiple containers for this service are created on a single host, the port will clash.", s.Name())
}
foundCount := 0
err := s.eachContainer(func(c *Container) error {
foundCount++
if foundCount > scale {
err := c.Down()
if err != nil {
return err
}
return c.Delete()
}
return nil
})
if err != nil {
return err
}
if foundCount != scale {
imageName, err := s.build()
if err != nil {
return err
}
if _, err = s.constructContainers(imageName, scale); err != nil {
return err
}
}
return s.up("", false)
}
// Pull implements Service.Pull. It pulls or build the image of the service.
func (s *Service) Pull() error {
if s.Config().Image == "" {
return nil
}
return pullImage(s.context.ClientFactory.Create(s), s, s.Config().Image)
}
// Pause implements Service.Pause. It puts into pause the container(s) related
// to the service.
func (s *Service) Pause() error {
return s.eachContainer(func(c *Container) error {
return c.Pause()
})
}
// Unpause implements Service.Pause. It brings back from pause the container(s)
// related to the service.
func (s *Service) Unpause() error {
return s.eachContainer(func(c *Container) error {
return c.Unpause()
})
}
// Containers implements Service.Containers. It returns the list of containers
// that are related to the service.
func (s *Service) Containers() ([]project.Container, error) {
result := []project.Container{}
containers, err := s.collectContainers()
if err != nil {
return nil, err
}
for _, c := range containers {
result = append(result, c)
}
return result, nil
}
func (s *Service) specificiesHostPort() bool {
_, bindings, err := nat.ParsePortSpecs(s.Config().Ports)
if err != nil {
fmt.Println(err)
}
for _, portBindings := range bindings {
for _, portBinding := range portBindings {
if portBinding.HostPort != "" {
return true
}
}
}
return false
}
+13
View File
@@ -0,0 +1,13 @@
package docker
import "github.com/docker/libcompose/project"
// ServiceFactory is an implementation of project.ServiceFactory.
type ServiceFactory struct {
context *Context
}
// Create creates a Service based on the specified project, name and service configuration.
func (s *ServiceFactory) Create(project *project.Project, name string, serviceConfig *project.ServiceConfig) (project.Service, error) {
return NewService(name, serviceConfig, s.context), nil
}
+18
View File
@@ -0,0 +1,18 @@
package logger
// NullLogger is a logger.Logger and logger.Factory implementation that does nothing.
type NullLogger struct {
}
// Out is a no-op function.
func (n *NullLogger) Out(_ []byte) {
}
// Err is a no-op function.
func (n *NullLogger) Err(_ []byte) {
}
// Create implements logger.Factory and returns a NullLogger.
func (n *NullLogger) Create(_ string) Logger {
return &NullLogger{}
}
+29
View File
@@ -0,0 +1,29 @@
package logger
// Factory defines methods a factory should implement, to create a Logger
// based on the specified name.
type Factory interface {
Create(name string) Logger
}
// Logger defines methods to implement for being a logger.
type Logger interface {
Out(bytes []byte)
Err(bytes []byte)
}
// Wrapper is a wrapper around Logger that implements the Writer interface,
// mainly use by docker/pkg/stdcopy functions.
type Wrapper struct {
Err bool
Logger Logger
}
func (l *Wrapper) Write(bytes []byte) (int, error) {
if l.Err {
l.Logger.Err(bytes)
} else {
l.Logger.Out(bytes)
}
return len(bytes), nil
}
+30
View File
@@ -0,0 +1,30 @@
package lookup
import (
"io/ioutil"
"path"
"strings"
"github.com/Sirupsen/logrus"
)
// FileConfigLookup is a "bare" structure that implements the project.ConfigLookup interface
type FileConfigLookup struct {
}
// Lookup returns the content and the actual filename of the file that is "built" using the
// specified file and relativeTo string. file and relativeTo are supposed to be file path.
// If file starts with a slash ('/'), it tries to load it, otherwise it will build a
// filename using the folder part of relativeTo joined with file.
func (f *FileConfigLookup) Lookup(file, relativeTo string) ([]byte, string, error) {
if strings.HasPrefix(file, "/") {
logrus.Debugf("Reading file %s", file)
bytes, err := ioutil.ReadFile(file)
return bytes, file, err
}
fileName := path.Join(path.Dir(relativeTo), file)
logrus.Debugf("Reading file %s relative to %s", fileName, relativeTo)
bytes, err := ioutil.ReadFile(fileName)
return bytes, fileName, err
}
+24
View File
@@ -0,0 +1,24 @@
package lookup
import (
"fmt"
"os"
"github.com/docker/libcompose/project"
)
// OsEnvLookup is a "bare" structure that implements the project.EnvironmentLookup interface
type OsEnvLookup struct {
}
// Lookup creates a string slice of string containing a "docker-friendly" environment string
// in the form of 'key=value'. It gets environment values using os.Getenv.
// If the os environment variable does not exists, the slice is empty. serviceName and config
// are not used at all in this implementation.
func (o *OsEnvLookup) Lookup(key, serviceName string, config *project.ServiceConfig) []string {
ret := os.Getenv(key)
if ret == "" {
return []string{}
}
return []string{fmt.Sprintf("%s=%s", key, ret)}
}
+137
View File
@@ -0,0 +1,137 @@
package project
import (
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/Sirupsen/logrus"
"github.com/docker/libcompose/logger"
)
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
NoCache bool
Signal int
ComposeFile string
ComposeBytes []byte
ProjectName string
isOpen bool
ServiceFactory ServiceFactory
EnvironmentLookup EnvironmentLookup
ConfigLookup ConfigLookup
LoggerFactory logger.Factory
IgnoreMissingConfig bool
Project *Project
}
func (c *Context) readComposeFile() error {
if c.ComposeBytes != nil {
return nil
}
logrus.Debugf("Opening compose file: %s", c.ComposeFile)
if c.ComposeFile == "-" {
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)
return err
} else if err != nil {
logrus.Errorf("Failed to open %s", c.ComposeFile)
return err
} else {
c.ComposeBytes = composeBytes
}
}
return nil
}
func (c *Context) determineProject() error {
name, err := c.lookupProjectName()
if err != nil {
return err
}
c.ProjectName = projectRegexp.ReplaceAllString(strings.ToLower(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
}
func (c *Context) lookupProjectName() (string, error) {
if c.ProjectName != "" {
return c.ProjectName, nil
}
if envProject := os.Getenv("COMPOSE_PROJECT_NAME"); envProject != "" {
return envProject, nil
}
f, err := filepath.Abs(c.ComposeFile)
if err != nil {
logrus.Errorf("Failed to get absolute directory for: %s", c.ComposeFile)
return "", err
}
f = toUnixPath(f)
parent := path.Base(path.Dir(f))
if parent != "" && parent != "." {
return parent, nil
} else if wd, err := os.Getwd(); err != nil {
return "", err
} else {
return path.Base(toUnixPath(wd)), nil
}
}
func toUnixPath(p string) string {
return strings.Replace(p, "\\", "/", -1)
}
func (c *Context) open() error {
if c.isOpen {
return nil
}
if err := c.readComposeFile(); err != nil {
return err
}
if err := c.determineProject(); err != nil {
return err
}
c.isOpen = true
return nil
}
+80
View File
@@ -0,0 +1,80 @@
package project
// 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 {
return nil
}
// Build implements Service.Build but does nothing.
func (e *EmptyService) Build() error {
return nil
}
// Up implements Service.Up but does nothing.
func (e *EmptyService) Up() error {
return nil
}
// Start implements Service.Start but does nothing.
func (e *EmptyService) Start() error {
return nil
}
// Down implements Service.Down but does nothing.
func (e *EmptyService) Down() error {
return nil
}
// Delete implements Service.Delete but does nothing.
func (e *EmptyService) Delete() error {
return nil
}
// Restart implements Service.Restart but does nothing.
func (e *EmptyService) Restart() error {
return nil
}
// Log implements Service.Log but does nothing.
func (e *EmptyService) Log() error {
return nil
}
// Pull implements Service.Pull but does nothing.
func (e *EmptyService) Pull() error {
return nil
}
// Kill implements Service.Kill but does nothing.
func (e *EmptyService) Kill() error {
return nil
}
// Containers implements Service.Containers but does nothing.
func (e *EmptyService) Containers() ([]Container, error) {
return []Container{}, nil
}
// Scale implements Service.Scale but does nothing.
func (e *EmptyService) Scale(count int) error {
return nil
}
// Info implements Service.Info but does nothing.
func (e *EmptyService) Info(qFlag bool) (InfoSet, error) {
return InfoSet{}, nil
}
// Pause implements Service.Pause but does nothing.
func (e *EmptyService) Pause() error {
return nil
}
// Unpause implements Service.Pause but does nothing.
func (e *EmptyService) Unpause() error {
return nil
}
+106
View File
@@ -0,0 +1,106 @@
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))
}
+42
View File
@@ -0,0 +1,42 @@
package project
import (
"bytes"
"io"
"text/tabwriter"
)
func (infos InfoSet) String(titleFlag bool) string {
//no error checking, none of this should fail
buffer := bytes.NewBuffer(make([]byte, 0, 1024))
tabwriter := tabwriter.NewWriter(buffer, 4, 4, 2, ' ', 0)
first := true
for _, info := range infos {
if first && titleFlag {
writeLine(tabwriter, true, info)
}
first = false
writeLine(tabwriter, false, info)
}
tabwriter.Flush()
return buffer.String()
}
func writeLine(writer io.Writer, key bool, info Info) {
first := true
for _, part := range info {
if !first {
writer.Write([]byte{'\t'})
}
first = false
if key {
writer.Write([]byte(part.Key))
} else {
writer.Write([]byte(part.Value))
}
}
writer.Write([]byte{'\n'})
}
+179
View File
@@ -0,0 +1,179 @@
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
}
+76
View File
@@ -0,0 +1,76 @@
package project
import (
"bytes"
"github.com/Sirupsen/logrus"
)
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,
}
)
type defaultListener struct {
project *Project
listenChan chan Event
upCount int
}
// NewDefaultListener create a default listener for the specified project.
func NewDefaultListener(p *Project) chan<- Event {
l := defaultListener{
listenChan: make(chan Event),
project: p,
}
go l.start()
return l.listenChan
}
func (d *defaultListener) start() {
for event := range d.listenChan {
buffer := bytes.NewBuffer(nil)
if event.Data != nil {
for k, v := range event.Data {
if buffer.Len() > 0 {
buffer.WriteString(", ")
}
buffer.WriteString(k)
buffer.WriteString("=")
buffer.WriteString(v)
}
}
if event.EventType == EventServiceUp {
d.upCount++
}
logf := logrus.Debugf
if infoEvents[event.EventType] {
logf = logrus.Infof
}
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())
}
}
}
+299
View File
@@ -0,0 +1,299 @@
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 ""
}
+428
View File
@@ -0,0 +1,428 @@
package project
import (
"errors"
"fmt"
"strings"
log "github.com/Sirupsen/logrus"
"github.com/docker/libcompose/logger"
"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 {
p := &Project{
context: context,
Configs: make(map[string]*ServiceConfig),
}
if context.LoggerFactory == nil {
context.LoggerFactory = &logger.NullLogger{}
}
context.Project = p
p.listeners = []chan<- Event{NewDefaultListener(p)}
return p
}
// Parse populates project information based on its context. It sets up the name,
// the composefile and the composebytes (the composefile content).
func (p *Project) Parse() error {
err := p.context.open()
if err != nil {
return err
}
p.Name = p.context.ProjectName
if p.context.ComposeFile == "-" {
p.File = "."
} else {
p.File = p.context.ComposeFile
}
if p.context.ComposeBytes != nil {
return p.Load(p.context.ComposeBytes)
}
return nil
}
// CreateService creates a service with the specified name based. It 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]
if !ok {
return nil, fmt.Errorf("Failed to find service: %s", name)
}
// Copy because we are about to modify the environment
config := *existing
if p.context.EnvironmentLookup != nil {
parsedEnv := make([]string, 0, len(config.Environment.Slice()))
for _, env := range config.Environment.Slice() {
parts := strings.SplitN(env, "=", 2)
if len(parts) > 1 && parts[1] != "" {
parsedEnv = append(parsedEnv, env)
continue
} else {
env = parts[0]
}
for _, value := range p.context.EnvironmentLookup.Lookup(env, name, &config) {
parsedEnv = append(parsedEnv, value)
}
}
config.Environment = NewMaporEqualSlice(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)
p.Configs[name] = config
p.reload = append(p.reload, name)
return nil
}
// Load loads the specified byte array (the composefile content) and adds the
// service configuration to the project.
func (p *Project) Load(bytes []byte) error {
configs := make(map[string]*ServiceConfig)
configs, err := mergeProject(p, bytes)
if err != nil {
log.Errorf("Could not parse config for project %s : %v", p.Name, err)
return err
}
for name, config := range configs {
err := p.AddConfig(name, config)
if err != nil {
return err
}
}
return nil
}
func (p *Project) loadWrappers(wrappers map[string]*serviceWrapper, servicesToConstruct []string) error {
for _, name := range servicesToConstruct {
wrapper, err := newServiceWrapper(name, p)
if err != nil {
return err
}
wrappers[name] = wrapper
}
return nil
}
// 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()
})
}), 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()
})
}), 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()
})
}), 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)
}
// 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()
})
}), func(service Service) error {
return service.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()
})
}), nil)
}
// Pull pulls the specified services (like docker pull).
func (p *Project) Pull(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()
})
}), 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()
})
}), 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()
})
}), nil)
}
// Pause pauses the specified services containers (like docker pause).
func (p *Project) Pause(services ...string) error {
return p.perform(EventProjectPauseStart, EventProjectPauseDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, EventServicePauseStart, EventServicePause, func(service Service) error {
return service.Pause()
})
}), nil)
}
// Unpause pauses the specified services containers (like docker pause).
func (p *Project) Unpause(services ...string) error {
return p.perform(EventProjectUnpauseStart, EventProjectUnpauseDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, EventServiceUnpauseStart, EventServiceUnpause, func(service Service) error {
return service.Unpause()
})
}), nil)
}
func (p *Project) perform(start, done EventType, services []string, action wrapperAction, cycleAction serviceAction) error {
p.Notify(start, "", nil)
err := p.forEach(services, action, cycleAction)
p.Notify(done, "", nil)
return err
}
func isSelected(wrapper *serviceWrapper, selected map[string]bool) bool {
return len(selected) == 0 || selected[wrapper.name]
}
func (p *Project) forEach(services []string, action wrapperAction, cycleAction serviceAction) error {
selected := make(map[string]bool)
wrappers := make(map[string]*serviceWrapper)
for _, s := range services {
selected[s] = true
}
return p.traverse(true, selected, wrappers, action, cycleAction)
}
func (p *Project) startService(wrappers map[string]*serviceWrapper, history []string, selected, launched map[string]bool, wrapper *serviceWrapper, action wrapperAction, cycleAction serviceAction) error {
if launched[wrapper.name] {
return nil
}
launched[wrapper.name] = true
history = append(history, wrapper.name)
for _, dep := range wrapper.service.DependentServices() {
target := wrappers[dep.Target]
if target == nil {
log.Errorf("Failed to find %s", dep.Target)
continue
}
if utils.Contains(history, dep.Target) {
cycle := strings.Join(append(history, dep.Target), "->")
if dep.Optional {
log.Debugf("Ignoring cycle for %s", cycle)
wrapper.IgnoreDep(dep.Target)
if cycleAction != nil {
var err error
log.Debugf("Running cycle action for %s", cycle)
err = cycleAction(target.service)
if err != nil {
return err
}
}
} else {
return fmt.Errorf("Cycle detected in path %s", cycle)
}
continue
}
err := p.startService(wrappers, history, selected, launched, target, action, cycleAction)
if err != nil {
return err
}
}
if isSelected(wrapper, selected) {
log.Debugf("Launching action for %s", wrapper.name)
go action(wrapper, wrappers)
} else {
wrapper.Ignore()
}
return nil
}
func (p *Project) traverse(start bool, selected map[string]bool, wrappers map[string]*serviceWrapper, action wrapperAction, cycleAction serviceAction) error {
restart := false
wrapperList := []string{}
if start {
for name := range p.Configs {
wrapperList = append(wrapperList, name)
}
} else {
for _, wrapper := range wrappers {
if err := wrapper.Reset(); err != nil {
return err
}
}
wrapperList = p.reload
}
p.loadWrappers(wrappers, wrapperList)
p.reload = []string{}
// check service name
for s := range selected {
if wrappers[s] == nil {
return errors.New("No such service: " + s)
}
}
launched := map[string]bool{}
for _, wrapper := range wrappers {
p.startService(wrappers, []string{}, selected, launched, wrapper, action, cycleAction)
}
var firstError error
for _, wrapper := range wrappers {
if !isSelected(wrapper, selected) {
continue
}
if err := wrapper.Wait(); err == ErrRestart {
restart = true
} else if err != nil {
log.Errorf("Failed to start: %s : %v", wrapper.name, err)
if firstError == nil {
firstError = err
}
}
}
if restart {
if p.ReloadCallback != nil {
if err := p.ReloadCallback(); err != nil {
log.Errorf("Failed calling callback: %v", err)
}
}
return p.traverse(false, selected, wrappers, action, cycleAction)
}
return firstError
}
// AddListener adds the specified listener to the project.
func (p *Project) AddListener(c chan<- Event) {
if !p.hasListeners {
for _, l := range p.listeners {
close(l)
}
p.hasListeners = true
p.listeners = []chan<- 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 {
return
}
event := Event{
EventType: eventType,
ServiceName: serviceName,
Data: data,
}
for _, l := range p.listeners {
l <- event
}
}
+114
View File
@@ -0,0 +1,114 @@
package project
import (
"sync"
log "github.com/Sirupsen/logrus"
)
type serviceWrapper struct {
name string
service Service
done sync.WaitGroup
state ServiceState
err error
project *Project
noWait bool
ignored map[string]bool
}
func newServiceWrapper(name string, p *Project) (*serviceWrapper, error) {
wrapper := &serviceWrapper{
name: name,
state: StateUnknown,
project: p,
ignored: map[string]bool{},
}
return wrapper, wrapper.Reset()
}
func (s *serviceWrapper) IgnoreDep(name string) {
s.ignored[name] = true
}
func (s *serviceWrapper) Reset() error {
if s.state != StateExecuted {
service, err := s.project.CreateService(s.name)
if err != nil {
log.Errorf("Failed to create service for %s : %v", s.name, err)
return err
}
s.service = service
}
if s.err == ErrRestart {
s.err = nil
}
s.done.Add(1)
return nil
}
func (s *serviceWrapper) Ignore() {
defer s.done.Done()
s.state = StateExecuted
s.project.Notify(EventServiceUpIgnored, s.service.Name(), nil)
}
func (s *serviceWrapper) waitForDeps(wrappers map[string]*serviceWrapper) bool {
if s.noWait {
return true
}
for _, dep := range s.service.DependentServices() {
if s.ignored[dep.Target] {
continue
}
if wrapper, ok := wrappers[dep.Target]; ok {
if wrapper.Wait() == ErrRestart {
s.project.Notify(EventProjectReload, wrapper.service.Name(), nil)
s.err = ErrRestart
return false
}
} else {
log.Errorf("Failed to find %s", dep.Target)
}
}
return true
}
func (s *serviceWrapper) Do(wrappers map[string]*serviceWrapper, start, done EventType, action func(service Service) error) {
defer s.done.Done()
if s.state == StateExecuted {
return
}
if wrappers != nil && !s.waitForDeps(wrappers) {
return
}
s.state = StateExecuted
s.project.Notify(start, s.service.Name(), nil)
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)
} else if s.err != nil {
log.Errorf("Failed %s %s : %v", start, s.name, s.err)
} else {
s.project.Notify(done, s.service.Name(), nil)
}
}
func (s *serviceWrapper) Wait() error {
s.done.Wait()
return s.err
}
+305
View File
@@ -0,0 +1,305 @@
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)
EventServicePauseStart = EventType(iota)
EventServicePause = EventType(iota)
EventServiceUnpauseStart = EventType(iota)
EventServiceUnpause = 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)
EventProjectPauseStart = EventType(iota)
EventProjectPauseDone = EventType(iota)
EventProjectUnpauseStart = EventType(iota)
EventProjectUnpauseDone = 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
Pause() error
Unpause() 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,
}
}
+328
View File
@@ -0,0 +1,328 @@
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}
}
+68
View File
@@ -0,0 +1,68 @@
package project
import (
"strings"
"github.com/docker/docker/runconfig"
)
// DefaultDependentServices return the dependent services (as an array of ServiceRelationship)
// for the specified project and service. It looks for : links, volumesFrom, net and ipc configuration.
func DefaultDependentServices(p *Project, s Service) []ServiceRelationship {
config := s.Config()
if config == nil {
return []ServiceRelationship{}
}
result := []ServiceRelationship{}
for _, link := range config.Links.Slice() {
result = append(result, NewServiceRelationship(link, RelTypeLink))
}
for _, volumesFrom := range config.VolumesFrom {
result = append(result, NewServiceRelationship(volumesFrom, RelTypeVolumesFrom))
}
result = appendNs(p, result, s.Config().Net, RelTypeNetNamespace)
result = appendNs(p, result, s.Config().Ipc, RelTypeIpcNamespace)
return result
}
func appendNs(p *Project, rels []ServiceRelationship, conf string, relType ServiceRelationshipType) []ServiceRelationship {
service := GetContainerFromIpcLikeConfig(p, conf)
if service != "" {
rels = append(rels, NewServiceRelationship(service, relType))
}
return rels
}
// NameAlias returns the name and alias based on the specified string.
// If the name contains a colon (like name:alias) it will split it, otherwise
// it will return the specified name as name and alias.
func NameAlias(name string) (string, string) {
parts := strings.SplitN(name, ":", 2)
if len(parts) == 2 {
return parts[0], parts[1]
}
return parts[0], parts[0]
}
// 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)
if !ipc.IsContainer() {
return ""
}
name := ipc.Container()
if name == "" {
return ""
}
if _, ok := p.Configs[name]; ok {
return name
}
return ""
}
+154
View File
@@ -0,0 +1,154 @@
package utils
import (
"encoding/json"
"fmt"
"sync"
"github.com/Sirupsen/logrus"
yaml "github.com/cloudfoundry-incubator/candiedyaml"
)
// InParallel holds a pool and a waitgroup to execute tasks in parallel and to be able
// to wait for completion of all tasks.
type InParallel struct {
wg sync.WaitGroup
pool sync.Pool
}
// Add adds runs the specified task in parallel and add it to the waitGroup.
func (i *InParallel) Add(task func() error) {
i.wg.Add(1)
go func() {
defer i.wg.Done()
err := task()
if err != nil {
i.pool.Put(err)
}
}()
}
// Wait waits for all tasks to complete and returns the latests error encountered if any.
func (i *InParallel) Wait() error {
i.wg.Wait()
obj := i.pool.Get()
if err, ok := obj.(error); ok {
return err
}
return nil
}
// ConvertByJSON converts a struct (src) to another one (target) using json marshalling/unmarshalling.
// If the structure are not compatible, this will throw an error as the unmarshalling will fail.
func ConvertByJSON(src, target interface{}) error {
newBytes, err := json.Marshal(src)
if err != nil {
return err
}
err = json.Unmarshal(newBytes, target)
if err != nil {
logrus.Errorf("Failed to unmarshall: %v\n%s", err, string(newBytes))
}
return err
}
// Convert converts a struct (src) to another one (target) using yaml marshalling/unmarshalling.
// If the structure are not compatible, this will throw an error as the unmarshalling will fail.
func Convert(src, target interface{}) error {
newBytes, err := yaml.Marshal(src)
if err != nil {
return err
}
err = yaml.Unmarshal(newBytes, target)
if err != nil {
logrus.Errorf("Failed to unmarshall: %v\n%s", err, string(newBytes))
}
return err
}
// CopySlice creates an exact copy of the provided string slice
func CopySlice(s []string) []string {
if s == nil {
return nil
}
r := make([]string, len(s))
copy(r, s)
return r
}
// CopyMap creates an exact copy of the provided string-to-string map
func CopyMap(m map[string]string) map[string]string {
if m == nil {
return nil
}
r := map[string]string{}
for k, v := range m {
r[k] = v
}
return r
}
// FilterStringSet accepts a string set `s` (in the form of `map[string]bool`) and a filtering function `f`
// and returns a string set containing only the strings `x` for which `f(x) == true`
func FilterStringSet(s map[string]bool, f func(x string) bool) map[string]bool {
result := map[string]bool{}
for k := range s {
if f(k) {
result[k] = true
}
}
return result
}
// FilterString returns a json representation of the specified map
// that is used as filter for docker.
func FilterString(data map[string][]string) string {
// I can't imagine this would ever fail
bytes, _ := json.Marshal(data)
return string(bytes)
}
// LabelFilterString returns a label json string representation of the specifed couple (key,value)
// that is used as filter for docker.
func LabelFilterString(key, value string) string {
return FilterString(map[string][]string{
"label": {fmt.Sprintf("%s=%s", key, value)},
})
}
// LabelFilter returns a label map representation of the specifed couple (key,value)
// that is used as filter for docker.
func LabelFilter(key, value string) map[string][]string {
return map[string][]string{
"label": {fmt.Sprintf("%s=%s", key, value)},
}
}
// Contains checks if the specified string (key) is present in the specified collection.
func Contains(collection []string, key string) bool {
for _, value := range collection {
if value == key {
return true
}
}
return false
}
// Merge performs a union of two string slices: the result is an unordered slice
// that includes every item from either argument exactly once
func Merge(coll1, coll2 []string) []string {
m := map[string]struct{}{}
for _, v := range append(coll1, coll2...) {
m[v] = struct{}{}
}
r := make([]string, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}