forked from LaconicNetwork/kompose
vendoring dependencies
This commit is contained in:
+191
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user