forked from coop-cloud/abra
WIP: foundations for app deploy
This commit is contained in:
parent
b351760f6e
commit
239c925d66
191
client/convert/LICENSE
Normal file
191
client/convert/LICENSE
Normal file
@ -0,0 +1,191 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://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 2013-2017 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
|
||||
|
||||
https://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.
|
8
client/convert/README.md
Normal file
8
client/convert/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# github.com/docker/cli/cli/compose/convert
|
||||
|
||||
DISCLAIMER: This is like the entire `github.com/docker/cli/cli/compose/convert` package.
|
||||
This should be an easy import but importing it creates DEPENDENCY HELL
|
||||
I tried for an hour to fix it but it would work. TRY TO FIX AT YOUR OWN RISK!!!
|
||||
|
||||
Due to this literally just being copy-pasted from the lib, the Apache license will be posted in this folder.
|
||||
Small edits to the source code have been to function names and parts we don't need deleted.
|
199
client/convert/compose.go
Normal file
199
client/convert/compose.go
Normal file
@ -0,0 +1,199 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
networktypes "github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
)
|
||||
|
||||
const (
|
||||
// LabelNamespace is the label used to track stack resources
|
||||
LabelNamespace = "com.docker.stack.namespace"
|
||||
)
|
||||
|
||||
// Namespace mangles names by prepending the name
|
||||
type Namespace struct {
|
||||
name string
|
||||
}
|
||||
|
||||
// Scope prepends the namespace to a name
|
||||
func (n Namespace) Scope(name string) string {
|
||||
return n.name + "_" + name
|
||||
}
|
||||
|
||||
// Descope returns the name without the namespace prefix
|
||||
func (n Namespace) Descope(name string) string {
|
||||
return strings.TrimPrefix(name, n.name+"_")
|
||||
}
|
||||
|
||||
// Name returns the name of the namespace
|
||||
func (n Namespace) Name() string {
|
||||
return n.name
|
||||
}
|
||||
|
||||
// NewNamespace returns a new Namespace for scoping of names
|
||||
func NewNamespace(name string) Namespace {
|
||||
return Namespace{name: name}
|
||||
}
|
||||
|
||||
// AddStackLabel returns labels with the namespace label added
|
||||
func AddStackLabel(namespace Namespace, labels map[string]string) map[string]string {
|
||||
if labels == nil {
|
||||
labels = make(map[string]string)
|
||||
}
|
||||
labels[LabelNamespace] = namespace.name
|
||||
return labels
|
||||
}
|
||||
|
||||
type networkMap map[string]composetypes.NetworkConfig
|
||||
|
||||
// Networks from the compose-file type to the engine API type
|
||||
func Networks(namespace Namespace, networks networkMap, servicesNetworks map[string]struct{}) (map[string]types.NetworkCreate, []string) {
|
||||
if networks == nil {
|
||||
networks = make(map[string]composetypes.NetworkConfig)
|
||||
}
|
||||
|
||||
externalNetworks := []string{}
|
||||
result := make(map[string]types.NetworkCreate)
|
||||
for internalName := range servicesNetworks {
|
||||
network := networks[internalName]
|
||||
if network.External.External {
|
||||
externalNetworks = append(externalNetworks, network.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
createOpts := types.NetworkCreate{
|
||||
Labels: AddStackLabel(namespace, network.Labels),
|
||||
Driver: network.Driver,
|
||||
Options: network.DriverOpts,
|
||||
Internal: network.Internal,
|
||||
Attachable: network.Attachable,
|
||||
}
|
||||
|
||||
if network.Ipam.Driver != "" || len(network.Ipam.Config) > 0 {
|
||||
createOpts.IPAM = &networktypes.IPAM{}
|
||||
}
|
||||
|
||||
if network.Ipam.Driver != "" {
|
||||
createOpts.IPAM.Driver = network.Ipam.Driver
|
||||
}
|
||||
for _, ipamConfig := range network.Ipam.Config {
|
||||
config := networktypes.IPAMConfig{
|
||||
Subnet: ipamConfig.Subnet,
|
||||
}
|
||||
createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
|
||||
}
|
||||
|
||||
networkName := namespace.Scope(internalName)
|
||||
if network.Name != "" {
|
||||
networkName = network.Name
|
||||
}
|
||||
result[networkName] = createOpts
|
||||
}
|
||||
|
||||
return result, externalNetworks
|
||||
}
|
||||
|
||||
// Secrets converts secrets from the Compose type to the engine API type
|
||||
func Secrets(namespace Namespace, secrets map[string]composetypes.SecretConfig) ([]swarm.SecretSpec, error) {
|
||||
result := []swarm.SecretSpec{}
|
||||
for name, secret := range secrets {
|
||||
if secret.External.External {
|
||||
continue
|
||||
}
|
||||
|
||||
var obj swarmFileObject
|
||||
var err error
|
||||
if secret.Driver != "" {
|
||||
obj = driverObjectConfig(namespace, name, composetypes.FileObjectConfig(secret))
|
||||
} else {
|
||||
obj, err = fileObjectConfig(namespace, name, composetypes.FileObjectConfig(secret))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
spec := swarm.SecretSpec{Annotations: obj.Annotations, Data: obj.Data}
|
||||
if secret.Driver != "" {
|
||||
spec.Driver = &swarm.Driver{
|
||||
Name: secret.Driver,
|
||||
Options: secret.DriverOpts,
|
||||
}
|
||||
}
|
||||
if secret.TemplateDriver != "" {
|
||||
spec.Templating = &swarm.Driver{
|
||||
Name: secret.TemplateDriver,
|
||||
}
|
||||
}
|
||||
result = append(result, spec)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Configs converts config objects from the Compose type to the engine API type
|
||||
func Configs(namespace Namespace, configs map[string]composetypes.ConfigObjConfig) ([]swarm.ConfigSpec, error) {
|
||||
result := []swarm.ConfigSpec{}
|
||||
for name, config := range configs {
|
||||
if config.External.External {
|
||||
continue
|
||||
}
|
||||
|
||||
obj, err := fileObjectConfig(namespace, name, composetypes.FileObjectConfig(config))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
spec := swarm.ConfigSpec{Annotations: obj.Annotations, Data: obj.Data}
|
||||
if config.TemplateDriver != "" {
|
||||
spec.Templating = &swarm.Driver{
|
||||
Name: config.TemplateDriver,
|
||||
}
|
||||
}
|
||||
result = append(result, spec)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type swarmFileObject struct {
|
||||
Annotations swarm.Annotations
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func driverObjectConfig(namespace Namespace, name string, obj composetypes.FileObjectConfig) swarmFileObject {
|
||||
if obj.Name != "" {
|
||||
name = obj.Name
|
||||
} else {
|
||||
name = namespace.Scope(name)
|
||||
}
|
||||
|
||||
return swarmFileObject{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: name,
|
||||
Labels: AddStackLabel(namespace, obj.Labels),
|
||||
},
|
||||
Data: []byte{},
|
||||
}
|
||||
}
|
||||
|
||||
func fileObjectConfig(namespace Namespace, name string, obj composetypes.FileObjectConfig) (swarmFileObject, error) {
|
||||
data, err := ioutil.ReadFile(obj.File)
|
||||
if err != nil {
|
||||
return swarmFileObject{}, err
|
||||
}
|
||||
|
||||
if obj.Name != "" {
|
||||
name = obj.Name
|
||||
} else {
|
||||
name = namespace.Scope(name)
|
||||
}
|
||||
|
||||
return swarmFileObject{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: name,
|
||||
Labels: AddStackLabel(namespace, obj.Labels),
|
||||
},
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
171
client/convert/compose_test.go
Normal file
171
client/convert/compose_test.go
Normal file
@ -0,0 +1,171 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/fs"
|
||||
)
|
||||
|
||||
func TestNamespaceScope(t *testing.T) {
|
||||
scoped := Namespace{name: "foo"}.Scope("bar")
|
||||
assert.Check(t, is.Equal("foo_bar", scoped))
|
||||
}
|
||||
|
||||
func TestAddStackLabel(t *testing.T) {
|
||||
labels := map[string]string{
|
||||
"something": "labeled",
|
||||
}
|
||||
actual := AddStackLabel(Namespace{name: "foo"}, labels)
|
||||
expected := map[string]string{
|
||||
"something": "labeled",
|
||||
LabelNamespace: "foo",
|
||||
}
|
||||
assert.Check(t, is.DeepEqual(expected, actual))
|
||||
}
|
||||
|
||||
func TestNetworks(t *testing.T) {
|
||||
namespace := Namespace{name: "foo"}
|
||||
serviceNetworks := map[string]struct{}{
|
||||
"normal": {},
|
||||
"outside": {},
|
||||
"default": {},
|
||||
"attachablenet": {},
|
||||
"named": {},
|
||||
}
|
||||
source := networkMap{
|
||||
"normal": composetypes.NetworkConfig{
|
||||
Driver: "overlay",
|
||||
DriverOpts: map[string]string{
|
||||
"opt": "value",
|
||||
},
|
||||
Ipam: composetypes.IPAMConfig{
|
||||
Driver: "driver",
|
||||
Config: []*composetypes.IPAMPool{
|
||||
{
|
||||
Subnet: "10.0.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"something": "labeled",
|
||||
},
|
||||
},
|
||||
"outside": composetypes.NetworkConfig{
|
||||
External: composetypes.External{External: true},
|
||||
Name: "special",
|
||||
},
|
||||
"attachablenet": composetypes.NetworkConfig{
|
||||
Driver: "overlay",
|
||||
Attachable: true,
|
||||
},
|
||||
"named": composetypes.NetworkConfig{
|
||||
Name: "othername",
|
||||
},
|
||||
}
|
||||
expected := map[string]types.NetworkCreate{
|
||||
"foo_default": {
|
||||
Labels: map[string]string{
|
||||
LabelNamespace: "foo",
|
||||
},
|
||||
},
|
||||
"foo_normal": {
|
||||
Driver: "overlay",
|
||||
IPAM: &network.IPAM{
|
||||
Driver: "driver",
|
||||
Config: []network.IPAMConfig{
|
||||
{
|
||||
Subnet: "10.0.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
Options: map[string]string{
|
||||
"opt": "value",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
LabelNamespace: "foo",
|
||||
"something": "labeled",
|
||||
},
|
||||
},
|
||||
"foo_attachablenet": {
|
||||
Driver: "overlay",
|
||||
Attachable: true,
|
||||
Labels: map[string]string{
|
||||
LabelNamespace: "foo",
|
||||
},
|
||||
},
|
||||
"othername": {
|
||||
Labels: map[string]string{LabelNamespace: "foo"},
|
||||
},
|
||||
}
|
||||
|
||||
networks, externals := Networks(namespace, source, serviceNetworks)
|
||||
assert.DeepEqual(t, expected, networks)
|
||||
assert.DeepEqual(t, []string{"special"}, externals)
|
||||
}
|
||||
|
||||
func TestSecrets(t *testing.T) {
|
||||
namespace := Namespace{name: "foo"}
|
||||
|
||||
secretText := "this is the first secret"
|
||||
secretFile := fs.NewFile(t, "convert-secrets", fs.WithContent(secretText))
|
||||
defer secretFile.Remove()
|
||||
|
||||
source := map[string]composetypes.SecretConfig{
|
||||
"one": {
|
||||
File: secretFile.Path(),
|
||||
Labels: map[string]string{"monster": "mash"},
|
||||
},
|
||||
"ext": {
|
||||
External: composetypes.External{
|
||||
External: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
specs, err := Secrets(namespace, source)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, is.Len(specs, 1))
|
||||
secret := specs[0]
|
||||
assert.Check(t, is.Equal("foo_one", secret.Name))
|
||||
assert.Check(t, is.DeepEqual(map[string]string{
|
||||
"monster": "mash",
|
||||
LabelNamespace: "foo",
|
||||
}, secret.Labels))
|
||||
assert.Check(t, is.DeepEqual([]byte(secretText), secret.Data))
|
||||
}
|
||||
|
||||
func TestConfigs(t *testing.T) {
|
||||
namespace := Namespace{name: "foo"}
|
||||
|
||||
configText := "this is the first config"
|
||||
configFile := fs.NewFile(t, "convert-configs", fs.WithContent(configText))
|
||||
defer configFile.Remove()
|
||||
|
||||
source := map[string]composetypes.ConfigObjConfig{
|
||||
"one": {
|
||||
File: configFile.Path(),
|
||||
Labels: map[string]string{"monster": "mash"},
|
||||
},
|
||||
"ext": {
|
||||
External: composetypes.External{
|
||||
External: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
specs, err := Configs(namespace, source)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, is.Len(specs, 1))
|
||||
config := specs[0]
|
||||
assert.Check(t, is.Equal("foo_one", config.Name))
|
||||
assert.Check(t, is.DeepEqual(map[string]string{
|
||||
"monster": "mash",
|
||||
LabelNamespace: "foo",
|
||||
}, config.Labels))
|
||||
assert.Check(t, is.DeepEqual([]byte(configText), config.Data))
|
||||
}
|
861
client/convert/service.go
Normal file
861
client/convert/service.go
Normal file
@ -0,0 +1,861 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
swarmtypes "github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultNetwork = "default"
|
||||
// LabelImage is the label used to store image name provided in the compose file
|
||||
LabelImage = "com.docker.stack.image"
|
||||
)
|
||||
|
||||
// ParseSecrets retrieves the secrets with the requested names and fills
|
||||
// secret IDs into the secret references.
|
||||
func ParseSecrets(client client.SecretAPIClient, requestedSecrets []*swarmtypes.SecretReference) ([]*swarmtypes.SecretReference, error) {
|
||||
if len(requestedSecrets) == 0 {
|
||||
return []*swarmtypes.SecretReference{}, nil
|
||||
}
|
||||
|
||||
secretRefs := make(map[string]*swarmtypes.SecretReference)
|
||||
ctx := context.Background()
|
||||
|
||||
for _, secret := range requestedSecrets {
|
||||
if _, exists := secretRefs[secret.File.Name]; exists {
|
||||
return nil, errors.Errorf("duplicate secret target for %s not allowed", secret.SecretName)
|
||||
}
|
||||
secretRef := new(swarmtypes.SecretReference)
|
||||
*secretRef = *secret
|
||||
secretRefs[secret.File.Name] = secretRef
|
||||
}
|
||||
|
||||
args := filters.NewArgs()
|
||||
for _, s := range secretRefs {
|
||||
args.Add("name", s.SecretName)
|
||||
}
|
||||
|
||||
secrets, err := client.SecretList(ctx, types.SecretListOptions{
|
||||
Filters: args,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
foundSecrets := make(map[string]string)
|
||||
for _, secret := range secrets {
|
||||
foundSecrets[secret.Spec.Annotations.Name] = secret.ID
|
||||
}
|
||||
|
||||
addedSecrets := []*swarmtypes.SecretReference{}
|
||||
|
||||
for _, ref := range secretRefs {
|
||||
id, ok := foundSecrets[ref.SecretName]
|
||||
if !ok {
|
||||
return nil, errors.Errorf("secret not found: %s", ref.SecretName)
|
||||
}
|
||||
|
||||
// set the id for the ref to properly assign in swarm
|
||||
// since swarm needs the ID instead of the name
|
||||
ref.SecretID = id
|
||||
addedSecrets = append(addedSecrets, ref)
|
||||
}
|
||||
|
||||
return addedSecrets, nil
|
||||
}
|
||||
|
||||
// ParseConfigs retrieves the configs from the requested names and converts
|
||||
// them to config references to use with the spec
|
||||
func ParseConfigs(client client.ConfigAPIClient, requestedConfigs []*swarmtypes.ConfigReference) ([]*swarmtypes.ConfigReference, error) {
|
||||
if len(requestedConfigs) == 0 {
|
||||
return []*swarmtypes.ConfigReference{}, nil
|
||||
}
|
||||
|
||||
// the configRefs map has two purposes: it prevents duplication of config
|
||||
// target filenames, and it it used to get all configs so we can resolve
|
||||
// their IDs. unfortunately, there are other targets for ConfigReferences,
|
||||
// besides just a File; specifically, the Runtime target, which is used for
|
||||
// CredentialSpecs. Therefore, we need to have a list of ConfigReferences
|
||||
// that are not File targets as well. at this time of writing, the only use
|
||||
// for Runtime targets is CredentialSpecs. However, to future-proof this
|
||||
// functionality, we should handle the case where multiple Runtime targets
|
||||
// are in use for the same Config, and we should deduplicate
|
||||
// such ConfigReferences, as no matter how many times the Config is used,
|
||||
// it is only needed to be referenced once.
|
||||
configRefs := make(map[string]*swarmtypes.ConfigReference)
|
||||
runtimeRefs := make(map[string]*swarmtypes.ConfigReference)
|
||||
ctx := context.Background()
|
||||
|
||||
for _, config := range requestedConfigs {
|
||||
// copy the config, so we don't mutate the args
|
||||
configRef := new(swarmtypes.ConfigReference)
|
||||
*configRef = *config
|
||||
|
||||
if config.Runtime != nil {
|
||||
// by assigning to a map based on ConfigName, if the same Config
|
||||
// is required as a Runtime target for multiple purposes, we only
|
||||
// include it once in the final set of configs.
|
||||
runtimeRefs[config.ConfigName] = config
|
||||
// continue, so we skip the logic below for handling file-type
|
||||
// configs
|
||||
continue
|
||||
}
|
||||
|
||||
if _, exists := configRefs[config.File.Name]; exists {
|
||||
return nil, errors.Errorf("duplicate config target for %s not allowed", config.ConfigName)
|
||||
}
|
||||
|
||||
configRefs[config.File.Name] = configRef
|
||||
}
|
||||
|
||||
args := filters.NewArgs()
|
||||
for _, s := range configRefs {
|
||||
args.Add("name", s.ConfigName)
|
||||
}
|
||||
for _, s := range runtimeRefs {
|
||||
args.Add("name", s.ConfigName)
|
||||
}
|
||||
|
||||
configs, err := client.ConfigList(ctx, types.ConfigListOptions{
|
||||
Filters: args,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
foundConfigs := make(map[string]string)
|
||||
for _, config := range configs {
|
||||
foundConfigs[config.Spec.Annotations.Name] = config.ID
|
||||
}
|
||||
|
||||
addedConfigs := []*swarmtypes.ConfigReference{}
|
||||
|
||||
for _, ref := range configRefs {
|
||||
id, ok := foundConfigs[ref.ConfigName]
|
||||
if !ok {
|
||||
return nil, errors.Errorf("config not found: %s", ref.ConfigName)
|
||||
}
|
||||
|
||||
// set the id for the ref to properly assign in swarm
|
||||
// since swarm needs the ID instead of the name
|
||||
ref.ConfigID = id
|
||||
addedConfigs = append(addedConfigs, ref)
|
||||
}
|
||||
|
||||
// unfortunately, because the key of configRefs and runtimeRefs is different
|
||||
// values that may collide, we can't just do some fancy trickery to
|
||||
// concat maps, we need to do two separate loops
|
||||
for _, ref := range runtimeRefs {
|
||||
id, ok := foundConfigs[ref.ConfigName]
|
||||
if !ok {
|
||||
return nil, errors.Errorf("config not found: %s", ref.ConfigName)
|
||||
}
|
||||
|
||||
ref.ConfigID = id
|
||||
addedConfigs = append(addedConfigs, ref)
|
||||
}
|
||||
|
||||
return addedConfigs, nil
|
||||
}
|
||||
|
||||
// Services from compose-file types to engine API types
|
||||
func Services(
|
||||
namespace Namespace,
|
||||
config *composetypes.Config,
|
||||
client client.CommonAPIClient,
|
||||
) (map[string]swarm.ServiceSpec, error) {
|
||||
result := make(map[string]swarm.ServiceSpec)
|
||||
|
||||
services := config.Services
|
||||
volumes := config.Volumes
|
||||
networks := config.Networks
|
||||
|
||||
for _, service := range services {
|
||||
secrets, err := convertServiceSecrets(client, namespace, service.Secrets, config.Secrets)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "service %s", service.Name)
|
||||
}
|
||||
configs, err := convertServiceConfigObjs(client, namespace, service, config.Configs)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "service %s", service.Name)
|
||||
}
|
||||
|
||||
serviceSpec, err := Service(client.ClientVersion(), namespace, service, networks, volumes, secrets, configs)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "service %s", service.Name)
|
||||
}
|
||||
result[service.Name] = serviceSpec
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Service converts a ServiceConfig into a swarm ServiceSpec
|
||||
func Service(
|
||||
apiVersion string,
|
||||
namespace Namespace,
|
||||
service composetypes.ServiceConfig,
|
||||
networkConfigs map[string]composetypes.NetworkConfig,
|
||||
volumes map[string]composetypes.VolumeConfig,
|
||||
secrets []*swarm.SecretReference,
|
||||
configs []*swarm.ConfigReference,
|
||||
) (swarm.ServiceSpec, error) {
|
||||
name := namespace.Scope(service.Name)
|
||||
endpoint := convertEndpointSpec(service.Deploy.EndpointMode, service.Ports)
|
||||
|
||||
mode, err := convertDeployMode(service.Deploy.Mode, service.Deploy.Replicas)
|
||||
if err != nil {
|
||||
return swarm.ServiceSpec{}, err
|
||||
}
|
||||
|
||||
mounts, err := Volumes(service.Volumes, volumes, namespace)
|
||||
if err != nil {
|
||||
return swarm.ServiceSpec{}, err
|
||||
}
|
||||
|
||||
resources, err := convertResources(service.Deploy.Resources)
|
||||
if err != nil {
|
||||
return swarm.ServiceSpec{}, err
|
||||
}
|
||||
|
||||
restartPolicy, err := convertRestartPolicy(
|
||||
service.Restart, service.Deploy.RestartPolicy)
|
||||
if err != nil {
|
||||
return swarm.ServiceSpec{}, err
|
||||
}
|
||||
|
||||
healthcheck, err := convertHealthcheck(service.HealthCheck)
|
||||
if err != nil {
|
||||
return swarm.ServiceSpec{}, err
|
||||
}
|
||||
|
||||
networks, err := convertServiceNetworks(service.Networks, networkConfigs, namespace, service.Name)
|
||||
if err != nil {
|
||||
return swarm.ServiceSpec{}, err
|
||||
}
|
||||
|
||||
dnsConfig := convertDNSConfig(service.DNS, service.DNSSearch)
|
||||
|
||||
var privileges swarm.Privileges
|
||||
privileges.CredentialSpec, err = convertCredentialSpec(
|
||||
namespace, service.CredentialSpec, configs,
|
||||
)
|
||||
if err != nil {
|
||||
return swarm.ServiceSpec{}, err
|
||||
}
|
||||
|
||||
var logDriver *swarm.Driver
|
||||
if service.Logging != nil {
|
||||
logDriver = &swarm.Driver{
|
||||
Name: service.Logging.Driver,
|
||||
Options: service.Logging.Options,
|
||||
}
|
||||
}
|
||||
|
||||
capAdd, capDrop := opts.EffectiveCapAddCapDrop(service.CapAdd, service.CapDrop)
|
||||
|
||||
serviceSpec := swarm.ServiceSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: name,
|
||||
Labels: AddStackLabel(namespace, service.Deploy.Labels),
|
||||
},
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{
|
||||
Image: service.Image,
|
||||
Command: service.Entrypoint,
|
||||
Args: service.Command,
|
||||
Hostname: service.Hostname,
|
||||
Hosts: convertExtraHosts(service.ExtraHosts),
|
||||
DNSConfig: dnsConfig,
|
||||
Healthcheck: healthcheck,
|
||||
Env: sortStrings(convertEnvironment(service.Environment)),
|
||||
Labels: AddStackLabel(namespace, service.Labels),
|
||||
Dir: service.WorkingDir,
|
||||
User: service.User,
|
||||
Mounts: mounts,
|
||||
StopGracePeriod: composetypes.ConvertDurationPtr(service.StopGracePeriod),
|
||||
StopSignal: service.StopSignal,
|
||||
TTY: service.Tty,
|
||||
OpenStdin: service.StdinOpen,
|
||||
Secrets: secrets,
|
||||
Configs: configs,
|
||||
ReadOnly: service.ReadOnly,
|
||||
Privileges: &privileges,
|
||||
Isolation: container.Isolation(service.Isolation),
|
||||
Init: service.Init,
|
||||
Sysctls: service.Sysctls,
|
||||
CapabilityAdd: capAdd,
|
||||
CapabilityDrop: capDrop,
|
||||
Ulimits: convertUlimits(service.Ulimits),
|
||||
},
|
||||
LogDriver: logDriver,
|
||||
Resources: resources,
|
||||
RestartPolicy: restartPolicy,
|
||||
Placement: &swarm.Placement{
|
||||
Constraints: service.Deploy.Placement.Constraints,
|
||||
Preferences: getPlacementPreference(service.Deploy.Placement.Preferences),
|
||||
MaxReplicas: service.Deploy.Placement.MaxReplicas,
|
||||
},
|
||||
},
|
||||
EndpointSpec: endpoint,
|
||||
Mode: mode,
|
||||
UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig),
|
||||
RollbackConfig: convertUpdateConfig(service.Deploy.RollbackConfig),
|
||||
}
|
||||
|
||||
// add an image label to serviceSpec
|
||||
serviceSpec.Labels[LabelImage] = service.Image
|
||||
|
||||
// ServiceSpec.Networks is deprecated and should not have been used by
|
||||
// this package. It is possible to update TaskTemplate.Networks, but it
|
||||
// is not possible to update ServiceSpec.Networks. Unfortunately, we
|
||||
// can't unconditionally start using TaskTemplate.Networks, because that
|
||||
// will break with older daemons that don't support migrating from
|
||||
// ServiceSpec.Networks to TaskTemplate.Networks. So which field to use
|
||||
// is conditional on daemon version.
|
||||
if versions.LessThan(apiVersion, "1.29") {
|
||||
serviceSpec.Networks = networks
|
||||
} else {
|
||||
serviceSpec.TaskTemplate.Networks = networks
|
||||
}
|
||||
return serviceSpec, nil
|
||||
}
|
||||
|
||||
func getPlacementPreference(preferences []composetypes.PlacementPreferences) []swarm.PlacementPreference {
|
||||
result := []swarm.PlacementPreference{}
|
||||
for _, preference := range preferences {
|
||||
spreadDescriptor := preference.Spread
|
||||
result = append(result, swarm.PlacementPreference{
|
||||
Spread: &swarm.SpreadOver{
|
||||
SpreadDescriptor: spreadDescriptor,
|
||||
},
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func sortStrings(strs []string) []string {
|
||||
sort.Strings(strs)
|
||||
return strs
|
||||
}
|
||||
|
||||
func convertServiceNetworks(
|
||||
networks map[string]*composetypes.ServiceNetworkConfig,
|
||||
networkConfigs networkMap,
|
||||
namespace Namespace,
|
||||
name string,
|
||||
) ([]swarm.NetworkAttachmentConfig, error) {
|
||||
if len(networks) == 0 {
|
||||
networks = map[string]*composetypes.ServiceNetworkConfig{
|
||||
defaultNetwork: {},
|
||||
}
|
||||
}
|
||||
|
||||
nets := []swarm.NetworkAttachmentConfig{}
|
||||
for networkName, network := range networks {
|
||||
networkConfig, ok := networkConfigs[networkName]
|
||||
if !ok && networkName != defaultNetwork {
|
||||
return nil, errors.Errorf("undefined network %q", networkName)
|
||||
}
|
||||
var aliases []string
|
||||
if network != nil {
|
||||
aliases = network.Aliases
|
||||
}
|
||||
target := namespace.Scope(networkName)
|
||||
if networkConfig.Name != "" {
|
||||
target = networkConfig.Name
|
||||
}
|
||||
netAttachConfig := swarm.NetworkAttachmentConfig{
|
||||
Target: target,
|
||||
Aliases: aliases,
|
||||
}
|
||||
// Only add default aliases to user defined networks. Other networks do
|
||||
// not support aliases.
|
||||
if container.NetworkMode(target).IsUserDefined() {
|
||||
netAttachConfig.Aliases = append(netAttachConfig.Aliases, name)
|
||||
}
|
||||
nets = append(nets, netAttachConfig)
|
||||
}
|
||||
|
||||
sort.Slice(nets, func(i, j int) bool {
|
||||
return nets[i].Target < nets[j].Target
|
||||
})
|
||||
return nets, nil
|
||||
}
|
||||
|
||||
// TODO: fix secrets API so that SecretAPIClient is not required here
|
||||
func convertServiceSecrets(
|
||||
client client.SecretAPIClient,
|
||||
namespace Namespace,
|
||||
secrets []composetypes.ServiceSecretConfig,
|
||||
secretSpecs map[string]composetypes.SecretConfig,
|
||||
) ([]*swarm.SecretReference, error) {
|
||||
refs := []*swarm.SecretReference{}
|
||||
|
||||
lookup := func(key string) (composetypes.FileObjectConfig, error) {
|
||||
secretSpec, exists := secretSpecs[key]
|
||||
if !exists {
|
||||
return composetypes.FileObjectConfig{}, errors.Errorf("undefined secret %q", key)
|
||||
}
|
||||
return composetypes.FileObjectConfig(secretSpec), nil
|
||||
}
|
||||
for _, secret := range secrets {
|
||||
obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(secret), lookup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file := swarm.SecretReferenceFileTarget(obj.File)
|
||||
refs = append(refs, &swarm.SecretReference{
|
||||
File: &file,
|
||||
SecretName: obj.Name,
|
||||
})
|
||||
}
|
||||
|
||||
secrs, err := ParseSecrets(client, refs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// sort to ensure idempotence (don't restart services just because the entries are in different order)
|
||||
sort.SliceStable(secrs, func(i, j int) bool { return secrs[i].SecretName < secrs[j].SecretName })
|
||||
return secrs, err
|
||||
}
|
||||
|
||||
// convertServiceConfigObjs takes an API client, a namespace, a ServiceConfig,
|
||||
// and a set of compose Config specs, and creates the swarm ConfigReferences
|
||||
// required by the serivce. Unlike convertServiceSecrets, this takes the whole
|
||||
// ServiceConfig, because some Configs may be needed as a result of other
|
||||
// fields (like CredentialSpecs).
|
||||
//
|
||||
// TODO: fix configs API so that ConfigsAPIClient is not required here
|
||||
func convertServiceConfigObjs(
|
||||
client client.ConfigAPIClient,
|
||||
namespace Namespace,
|
||||
service composetypes.ServiceConfig,
|
||||
configSpecs map[string]composetypes.ConfigObjConfig,
|
||||
) ([]*swarm.ConfigReference, error) {
|
||||
refs := []*swarm.ConfigReference{}
|
||||
|
||||
lookup := func(key string) (composetypes.FileObjectConfig, error) {
|
||||
configSpec, exists := configSpecs[key]
|
||||
if !exists {
|
||||
return composetypes.FileObjectConfig{}, errors.Errorf("undefined config %q", key)
|
||||
}
|
||||
return composetypes.FileObjectConfig(configSpec), nil
|
||||
}
|
||||
for _, config := range service.Configs {
|
||||
obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(config), lookup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file := swarm.ConfigReferenceFileTarget(obj.File)
|
||||
refs = append(refs, &swarm.ConfigReference{
|
||||
File: &file,
|
||||
ConfigName: obj.Name,
|
||||
})
|
||||
}
|
||||
|
||||
// finally, after converting all of the file objects, create any
|
||||
// Runtime-type configs that are needed. these are configs that are not
|
||||
// mounted into the container, but are used in some other way by the
|
||||
// container runtime. Currently, this only means CredentialSpecs, but in
|
||||
// the future it may be used for other fields
|
||||
|
||||
// grab the CredentialSpec out of the Service
|
||||
credSpec := service.CredentialSpec
|
||||
// if the credSpec uses a config, then we should grab the config name, and
|
||||
// create a config reference for it. A File or Registry-type CredentialSpec
|
||||
// does not need this operation.
|
||||
if credSpec.Config != "" {
|
||||
// look up the config in the configSpecs.
|
||||
obj, err := lookup(credSpec.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get the actual correct name.
|
||||
name := namespace.Scope(credSpec.Config)
|
||||
if obj.Name != "" {
|
||||
name = obj.Name
|
||||
}
|
||||
|
||||
// now append a Runtime-type config.
|
||||
refs = append(refs, &swarm.ConfigReference{
|
||||
ConfigName: name,
|
||||
Runtime: &swarm.ConfigReferenceRuntimeTarget{},
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
confs, err := ParseConfigs(client, refs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// sort to ensure idempotence (don't restart services just because the entries are in different order)
|
||||
sort.SliceStable(confs, func(i, j int) bool { return confs[i].ConfigName < confs[j].ConfigName })
|
||||
return confs, err
|
||||
}
|
||||
|
||||
type swarmReferenceTarget struct {
|
||||
Name string
|
||||
UID string
|
||||
GID string
|
||||
Mode os.FileMode
|
||||
}
|
||||
|
||||
type swarmReferenceObject struct {
|
||||
File swarmReferenceTarget
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
|
||||
func convertFileObject(
|
||||
namespace Namespace,
|
||||
config composetypes.FileReferenceConfig,
|
||||
lookup func(key string) (composetypes.FileObjectConfig, error),
|
||||
) (swarmReferenceObject, error) {
|
||||
obj, err := lookup(config.Source)
|
||||
if err != nil {
|
||||
return swarmReferenceObject{}, err
|
||||
}
|
||||
|
||||
source := namespace.Scope(config.Source)
|
||||
if obj.Name != "" {
|
||||
source = obj.Name
|
||||
}
|
||||
|
||||
target := config.Target
|
||||
if target == "" {
|
||||
target = config.Source
|
||||
}
|
||||
|
||||
uid := config.UID
|
||||
gid := config.GID
|
||||
if uid == "" {
|
||||
uid = "0"
|
||||
}
|
||||
if gid == "" {
|
||||
gid = "0"
|
||||
}
|
||||
mode := config.Mode
|
||||
if mode == nil {
|
||||
mode = uint32Ptr(0444)
|
||||
}
|
||||
|
||||
return swarmReferenceObject{
|
||||
File: swarmReferenceTarget{
|
||||
Name: target,
|
||||
UID: uid,
|
||||
GID: gid,
|
||||
Mode: os.FileMode(*mode),
|
||||
},
|
||||
Name: source,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func uint32Ptr(value uint32) *uint32 {
|
||||
return &value
|
||||
}
|
||||
|
||||
// convertExtraHosts converts <host>:<ip> mappings to SwarmKit notation:
|
||||
// "IP-address hostname(s)". The original order of mappings is preserved.
|
||||
func convertExtraHosts(extraHosts composetypes.HostsList) []string {
|
||||
hosts := []string{}
|
||||
for _, hostIP := range extraHosts {
|
||||
if v := strings.SplitN(hostIP, ":", 2); len(v) == 2 {
|
||||
// Convert to SwarmKit notation: IP-address hostname(s)
|
||||
hosts = append(hosts, fmt.Sprintf("%s %s", v[1], v[0]))
|
||||
}
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
|
||||
func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) {
|
||||
if healthcheck == nil {
|
||||
return nil, nil
|
||||
}
|
||||
var (
|
||||
timeout, interval, startPeriod time.Duration
|
||||
retries int
|
||||
)
|
||||
if healthcheck.Disable {
|
||||
if len(healthcheck.Test) != 0 {
|
||||
return nil, errors.Errorf("test and disable can't be set at the same time")
|
||||
}
|
||||
return &container.HealthConfig{
|
||||
Test: []string{"NONE"},
|
||||
}, nil
|
||||
|
||||
}
|
||||
if healthcheck.Timeout != nil {
|
||||
timeout = time.Duration(*healthcheck.Timeout)
|
||||
}
|
||||
if healthcheck.Interval != nil {
|
||||
interval = time.Duration(*healthcheck.Interval)
|
||||
}
|
||||
if healthcheck.StartPeriod != nil {
|
||||
startPeriod = time.Duration(*healthcheck.StartPeriod)
|
||||
}
|
||||
if healthcheck.Retries != nil {
|
||||
retries = int(*healthcheck.Retries)
|
||||
}
|
||||
return &container.HealthConfig{
|
||||
Test: healthcheck.Test,
|
||||
Timeout: timeout,
|
||||
Interval: interval,
|
||||
Retries: retries,
|
||||
StartPeriod: startPeriod,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) {
|
||||
// TODO: log if restart is being ignored
|
||||
if source == nil {
|
||||
policy, err := opts.ParseRestartPolicy(restart)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch {
|
||||
case policy.IsNone():
|
||||
return nil, nil
|
||||
case policy.IsAlways(), policy.IsUnlessStopped():
|
||||
return &swarm.RestartPolicy{
|
||||
Condition: swarm.RestartPolicyConditionAny,
|
||||
}, nil
|
||||
case policy.IsOnFailure():
|
||||
attempts := uint64(policy.MaximumRetryCount)
|
||||
return &swarm.RestartPolicy{
|
||||
Condition: swarm.RestartPolicyConditionOnFailure,
|
||||
MaxAttempts: &attempts,
|
||||
}, nil
|
||||
default:
|
||||
return nil, errors.Errorf("unknown restart policy: %s", restart)
|
||||
}
|
||||
}
|
||||
|
||||
return &swarm.RestartPolicy{
|
||||
Condition: swarm.RestartPolicyCondition(source.Condition),
|
||||
Delay: composetypes.ConvertDurationPtr(source.Delay),
|
||||
MaxAttempts: source.MaxAttempts,
|
||||
Window: composetypes.ConvertDurationPtr(source.Window),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig {
|
||||
if source == nil {
|
||||
return nil
|
||||
}
|
||||
parallel := uint64(1)
|
||||
if source.Parallelism != nil {
|
||||
parallel = *source.Parallelism
|
||||
}
|
||||
return &swarm.UpdateConfig{
|
||||
Parallelism: parallel,
|
||||
Delay: time.Duration(source.Delay),
|
||||
FailureAction: source.FailureAction,
|
||||
Monitor: time.Duration(source.Monitor),
|
||||
MaxFailureRatio: source.MaxFailureRatio,
|
||||
Order: source.Order,
|
||||
}
|
||||
}
|
||||
|
||||
func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) {
|
||||
resources := &swarm.ResourceRequirements{}
|
||||
var err error
|
||||
if source.Limits != nil {
|
||||
var cpus int64
|
||||
if source.Limits.NanoCPUs != "" {
|
||||
cpus, err = opts.ParseCPUs(source.Limits.NanoCPUs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
resources.Limits = &swarm.Limit{
|
||||
NanoCPUs: cpus,
|
||||
MemoryBytes: int64(source.Limits.MemoryBytes),
|
||||
Pids: source.Limits.Pids,
|
||||
}
|
||||
}
|
||||
if source.Reservations != nil {
|
||||
var cpus int64
|
||||
if source.Reservations.NanoCPUs != "" {
|
||||
cpus, err = opts.ParseCPUs(source.Reservations.NanoCPUs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var generic []swarm.GenericResource
|
||||
for _, res := range source.Reservations.GenericResources {
|
||||
var r swarm.GenericResource
|
||||
|
||||
if res.DiscreteResourceSpec != nil {
|
||||
r.DiscreteResourceSpec = &swarm.DiscreteGenericResource{
|
||||
Kind: res.DiscreteResourceSpec.Kind,
|
||||
Value: res.DiscreteResourceSpec.Value,
|
||||
}
|
||||
}
|
||||
|
||||
generic = append(generic, r)
|
||||
}
|
||||
|
||||
resources.Reservations = &swarm.Resources{
|
||||
NanoCPUs: cpus,
|
||||
MemoryBytes: int64(source.Reservations.MemoryBytes),
|
||||
GenericResources: generic,
|
||||
}
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortConfig) *swarm.EndpointSpec {
|
||||
portConfigs := []swarm.PortConfig{}
|
||||
for _, port := range source {
|
||||
portConfig := swarm.PortConfig{
|
||||
Protocol: swarm.PortConfigProtocol(port.Protocol),
|
||||
TargetPort: port.Target,
|
||||
PublishedPort: port.Published,
|
||||
PublishMode: swarm.PortConfigPublishMode(port.Mode),
|
||||
}
|
||||
portConfigs = append(portConfigs, portConfig)
|
||||
}
|
||||
|
||||
sort.Slice(portConfigs, func(i, j int) bool {
|
||||
return portConfigs[i].PublishedPort < portConfigs[j].PublishedPort
|
||||
})
|
||||
|
||||
return &swarm.EndpointSpec{
|
||||
Mode: swarm.ResolutionMode(strings.ToLower(endpointMode)),
|
||||
Ports: portConfigs,
|
||||
}
|
||||
}
|
||||
|
||||
func convertEnvironment(source map[string]*string) []string {
|
||||
var output []string
|
||||
|
||||
for name, value := range source {
|
||||
switch value {
|
||||
case nil:
|
||||
output = append(output, name)
|
||||
default:
|
||||
output = append(output, fmt.Sprintf("%s=%s", name, *value))
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) {
|
||||
serviceMode := swarm.ServiceMode{}
|
||||
|
||||
switch mode {
|
||||
case "global":
|
||||
if replicas != nil {
|
||||
return serviceMode, errors.Errorf("replicas can only be used with replicated mode")
|
||||
}
|
||||
serviceMode.Global = &swarm.GlobalService{}
|
||||
case "replicated", "":
|
||||
serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas}
|
||||
default:
|
||||
return serviceMode, errors.Errorf("Unknown mode: %s", mode)
|
||||
}
|
||||
return serviceMode, nil
|
||||
}
|
||||
|
||||
func convertDNSConfig(DNS []string, DNSSearch []string) *swarm.DNSConfig {
|
||||
if DNS != nil || DNSSearch != nil {
|
||||
return &swarm.DNSConfig{
|
||||
Nameservers: DNS,
|
||||
Search: DNSSearch,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertCredentialSpec(namespace Namespace, spec composetypes.CredentialSpecConfig, refs []*swarm.ConfigReference) (*swarm.CredentialSpec, error) {
|
||||
var o []string
|
||||
|
||||
// Config was added in API v1.40
|
||||
if spec.Config != "" {
|
||||
o = append(o, `"Config"`)
|
||||
}
|
||||
if spec.File != "" {
|
||||
o = append(o, `"File"`)
|
||||
}
|
||||
if spec.Registry != "" {
|
||||
o = append(o, `"Registry"`)
|
||||
}
|
||||
l := len(o)
|
||||
switch {
|
||||
case l == 0:
|
||||
return nil, nil
|
||||
case l == 2:
|
||||
return nil, errors.Errorf("invalid credential spec: cannot specify both %s and %s", o[0], o[1])
|
||||
case l > 2:
|
||||
return nil, errors.Errorf("invalid credential spec: cannot specify both %s, and %s", strings.Join(o[:l-1], ", "), o[l-1])
|
||||
}
|
||||
swarmCredSpec := swarm.CredentialSpec(spec)
|
||||
// if we're using a swarm Config for the credential spec, over-write it
|
||||
// here with the config ID
|
||||
if swarmCredSpec.Config != "" {
|
||||
for _, config := range refs {
|
||||
if swarmCredSpec.Config == config.ConfigName {
|
||||
swarmCredSpec.Config = config.ConfigID
|
||||
return &swarmCredSpec, nil
|
||||
}
|
||||
}
|
||||
// if none of the configs match, try namespacing
|
||||
for _, config := range refs {
|
||||
if namespace.Scope(swarmCredSpec.Config) == config.ConfigName {
|
||||
swarmCredSpec.Config = config.ConfigID
|
||||
return &swarmCredSpec, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.Errorf("invalid credential spec: spec specifies config %v, but no such config can be found", swarmCredSpec.Config)
|
||||
}
|
||||
return &swarmCredSpec, nil
|
||||
}
|
||||
|
||||
func convertUlimits(origUlimits map[string]*composetypes.UlimitsConfig) []*units.Ulimit {
|
||||
newUlimits := make(map[string]*units.Ulimit)
|
||||
for name, u := range origUlimits {
|
||||
if u.Single != 0 {
|
||||
newUlimits[name] = &units.Ulimit{
|
||||
Name: name,
|
||||
Soft: int64(u.Single),
|
||||
Hard: int64(u.Single),
|
||||
}
|
||||
} else {
|
||||
newUlimits[name] = &units.Ulimit{
|
||||
Name: name,
|
||||
Soft: int64(u.Soft),
|
||||
Hard: int64(u.Hard),
|
||||
}
|
||||
}
|
||||
}
|
||||
var ulimits []*units.Ulimit
|
||||
for _, ulimit := range newUlimits {
|
||||
ulimits = append(ulimits, ulimit)
|
||||
}
|
||||
sort.SliceStable(ulimits, func(i, j int) bool {
|
||||
return ulimits[i].Name < ulimits[j].Name
|
||||
})
|
||||
return ulimits
|
||||
}
|
678
client/convert/service_test.go
Normal file
678
client/convert/service_test.go
Normal file
@ -0,0 +1,678 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestConvertRestartPolicyFromNone(t *testing.T) {
|
||||
policy, err := convertRestartPolicy("no", nil)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual((*swarm.RestartPolicy)(nil), policy))
|
||||
}
|
||||
|
||||
func TestConvertRestartPolicyFromUnknown(t *testing.T) {
|
||||
_, err := convertRestartPolicy("unknown", nil)
|
||||
assert.Error(t, err, "unknown restart policy: unknown")
|
||||
}
|
||||
|
||||
func TestConvertRestartPolicyFromAlways(t *testing.T) {
|
||||
policy, err := convertRestartPolicy("always", nil)
|
||||
expected := &swarm.RestartPolicy{
|
||||
Condition: swarm.RestartPolicyConditionAny,
|
||||
}
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(expected, policy))
|
||||
}
|
||||
|
||||
func TestConvertRestartPolicyFromFailure(t *testing.T) {
|
||||
policy, err := convertRestartPolicy("on-failure:4", nil)
|
||||
attempts := uint64(4)
|
||||
expected := &swarm.RestartPolicy{
|
||||
Condition: swarm.RestartPolicyConditionOnFailure,
|
||||
MaxAttempts: &attempts,
|
||||
}
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(expected, policy))
|
||||
}
|
||||
|
||||
func strPtr(val string) *string {
|
||||
return &val
|
||||
}
|
||||
|
||||
func TestConvertEnvironment(t *testing.T) {
|
||||
source := map[string]*string{
|
||||
"foo": strPtr("bar"),
|
||||
"key": strPtr("value"),
|
||||
}
|
||||
env := convertEnvironment(source)
|
||||
sort.Strings(env)
|
||||
assert.Check(t, is.DeepEqual([]string{"foo=bar", "key=value"}, env))
|
||||
}
|
||||
|
||||
func TestConvertExtraHosts(t *testing.T) {
|
||||
source := composetypes.HostsList{
|
||||
"zulu:127.0.0.2",
|
||||
"alpha:127.0.0.1",
|
||||
"zulu:ff02::1",
|
||||
}
|
||||
assert.Check(t, is.DeepEqual([]string{"127.0.0.2 zulu", "127.0.0.1 alpha", "ff02::1 zulu"}, convertExtraHosts(source)))
|
||||
}
|
||||
|
||||
func TestConvertResourcesFull(t *testing.T) {
|
||||
source := composetypes.Resources{
|
||||
Limits: &composetypes.ResourceLimit{
|
||||
NanoCPUs: "0.003",
|
||||
MemoryBytes: composetypes.UnitBytes(300000000),
|
||||
},
|
||||
Reservations: &composetypes.Resource{
|
||||
NanoCPUs: "0.002",
|
||||
MemoryBytes: composetypes.UnitBytes(200000000),
|
||||
},
|
||||
}
|
||||
resources, err := convertResources(source)
|
||||
assert.NilError(t, err)
|
||||
|
||||
expected := &swarm.ResourceRequirements{
|
||||
Limits: &swarm.Limit{
|
||||
NanoCPUs: 3000000,
|
||||
MemoryBytes: 300000000,
|
||||
},
|
||||
Reservations: &swarm.Resources{
|
||||
NanoCPUs: 2000000,
|
||||
MemoryBytes: 200000000,
|
||||
},
|
||||
}
|
||||
assert.Check(t, is.DeepEqual(expected, resources))
|
||||
}
|
||||
|
||||
func TestConvertResourcesOnlyMemory(t *testing.T) {
|
||||
source := composetypes.Resources{
|
||||
Limits: &composetypes.ResourceLimit{
|
||||
MemoryBytes: composetypes.UnitBytes(300000000),
|
||||
},
|
||||
Reservations: &composetypes.Resource{
|
||||
MemoryBytes: composetypes.UnitBytes(200000000),
|
||||
},
|
||||
}
|
||||
resources, err := convertResources(source)
|
||||
assert.NilError(t, err)
|
||||
|
||||
expected := &swarm.ResourceRequirements{
|
||||
Limits: &swarm.Limit{
|
||||
MemoryBytes: 300000000,
|
||||
},
|
||||
Reservations: &swarm.Resources{
|
||||
MemoryBytes: 200000000,
|
||||
},
|
||||
}
|
||||
assert.Check(t, is.DeepEqual(expected, resources))
|
||||
}
|
||||
|
||||
func TestConvertHealthcheck(t *testing.T) {
|
||||
retries := uint64(10)
|
||||
timeout := composetypes.Duration(30 * time.Second)
|
||||
interval := composetypes.Duration(2 * time.Millisecond)
|
||||
source := &composetypes.HealthCheckConfig{
|
||||
Test: []string{"EXEC", "touch", "/foo"},
|
||||
Timeout: &timeout,
|
||||
Interval: &interval,
|
||||
Retries: &retries,
|
||||
}
|
||||
expected := &container.HealthConfig{
|
||||
Test: source.Test,
|
||||
Timeout: time.Duration(timeout),
|
||||
Interval: time.Duration(interval),
|
||||
Retries: 10,
|
||||
}
|
||||
|
||||
healthcheck, err := convertHealthcheck(source)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(expected, healthcheck))
|
||||
}
|
||||
|
||||
func TestConvertHealthcheckDisable(t *testing.T) {
|
||||
source := &composetypes.HealthCheckConfig{Disable: true}
|
||||
expected := &container.HealthConfig{
|
||||
Test: []string{"NONE"},
|
||||
}
|
||||
|
||||
healthcheck, err := convertHealthcheck(source)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(expected, healthcheck))
|
||||
}
|
||||
|
||||
func TestConvertHealthcheckDisableWithTest(t *testing.T) {
|
||||
source := &composetypes.HealthCheckConfig{
|
||||
Disable: true,
|
||||
Test: []string{"EXEC", "touch"},
|
||||
}
|
||||
_, err := convertHealthcheck(source)
|
||||
assert.Error(t, err, "test and disable can't be set at the same time")
|
||||
}
|
||||
|
||||
func TestConvertEndpointSpec(t *testing.T) {
|
||||
source := []composetypes.ServicePortConfig{
|
||||
{
|
||||
Protocol: "udp",
|
||||
Target: 53,
|
||||
Published: 1053,
|
||||
Mode: "host",
|
||||
},
|
||||
{
|
||||
Target: 8080,
|
||||
Published: 80,
|
||||
},
|
||||
}
|
||||
endpoint := convertEndpointSpec("vip", source)
|
||||
|
||||
expected := swarm.EndpointSpec{
|
||||
Mode: swarm.ResolutionMode(strings.ToLower("vip")),
|
||||
Ports: []swarm.PortConfig{
|
||||
{
|
||||
TargetPort: 8080,
|
||||
PublishedPort: 80,
|
||||
},
|
||||
{
|
||||
Protocol: "udp",
|
||||
TargetPort: 53,
|
||||
PublishedPort: 1053,
|
||||
PublishMode: "host",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Check(t, is.DeepEqual(expected, *endpoint))
|
||||
}
|
||||
|
||||
func TestConvertServiceNetworksOnlyDefault(t *testing.T) {
|
||||
networkConfigs := networkMap{}
|
||||
|
||||
configs, err := convertServiceNetworks(
|
||||
nil, networkConfigs, NewNamespace("foo"), "service")
|
||||
|
||||
expected := []swarm.NetworkAttachmentConfig{
|
||||
{
|
||||
Target: "foo_default",
|
||||
Aliases: []string{"service"},
|
||||
},
|
||||
}
|
||||
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(expected, configs))
|
||||
}
|
||||
|
||||
func TestConvertServiceNetworks(t *testing.T) {
|
||||
networkConfigs := networkMap{
|
||||
"front": composetypes.NetworkConfig{
|
||||
External: composetypes.External{External: true},
|
||||
Name: "fronttier",
|
||||
},
|
||||
"back": composetypes.NetworkConfig{},
|
||||
}
|
||||
networks := map[string]*composetypes.ServiceNetworkConfig{
|
||||
"front": {
|
||||
Aliases: []string{"something"},
|
||||
},
|
||||
"back": {
|
||||
Aliases: []string{"other"},
|
||||
},
|
||||
}
|
||||
|
||||
configs, err := convertServiceNetworks(
|
||||
networks, networkConfigs, NewNamespace("foo"), "service")
|
||||
|
||||
expected := []swarm.NetworkAttachmentConfig{
|
||||
{
|
||||
Target: "foo_back",
|
||||
Aliases: []string{"other", "service"},
|
||||
},
|
||||
{
|
||||
Target: "fronttier",
|
||||
Aliases: []string{"something", "service"},
|
||||
},
|
||||
}
|
||||
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(expected, configs))
|
||||
}
|
||||
|
||||
func TestConvertServiceNetworksCustomDefault(t *testing.T) {
|
||||
networkConfigs := networkMap{
|
||||
"default": composetypes.NetworkConfig{
|
||||
External: composetypes.External{External: true},
|
||||
Name: "custom",
|
||||
},
|
||||
}
|
||||
networks := map[string]*composetypes.ServiceNetworkConfig{}
|
||||
|
||||
configs, err := convertServiceNetworks(
|
||||
networks, networkConfigs, NewNamespace("foo"), "service")
|
||||
|
||||
expected := []swarm.NetworkAttachmentConfig{
|
||||
{
|
||||
Target: "custom",
|
||||
Aliases: []string{"service"},
|
||||
},
|
||||
}
|
||||
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(expected, configs))
|
||||
}
|
||||
|
||||
func TestConvertDNSConfigEmpty(t *testing.T) {
|
||||
dnsConfig := convertDNSConfig(nil, nil)
|
||||
assert.Check(t, is.DeepEqual((*swarm.DNSConfig)(nil), dnsConfig))
|
||||
}
|
||||
|
||||
var (
|
||||
nameservers = []string{"8.8.8.8", "9.9.9.9"}
|
||||
search = []string{"dc1.example.com", "dc2.example.com"}
|
||||
)
|
||||
|
||||
func TestConvertDNSConfigAll(t *testing.T) {
|
||||
dnsConfig := convertDNSConfig(nameservers, search)
|
||||
assert.Check(t, is.DeepEqual(&swarm.DNSConfig{
|
||||
Nameservers: nameservers,
|
||||
Search: search,
|
||||
}, dnsConfig))
|
||||
}
|
||||
|
||||
func TestConvertDNSConfigNameservers(t *testing.T) {
|
||||
dnsConfig := convertDNSConfig(nameservers, nil)
|
||||
assert.Check(t, is.DeepEqual(&swarm.DNSConfig{
|
||||
Nameservers: nameservers,
|
||||
Search: nil,
|
||||
}, dnsConfig))
|
||||
}
|
||||
|
||||
func TestConvertDNSConfigSearch(t *testing.T) {
|
||||
dnsConfig := convertDNSConfig(nil, search)
|
||||
assert.Check(t, is.DeepEqual(&swarm.DNSConfig{
|
||||
Nameservers: nil,
|
||||
Search: search,
|
||||
}, dnsConfig))
|
||||
}
|
||||
|
||||
func TestConvertCredentialSpec(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in composetypes.CredentialSpecConfig
|
||||
out *swarm.CredentialSpec
|
||||
configs []*swarm.ConfigReference
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
},
|
||||
{
|
||||
name: "config-and-file",
|
||||
in: composetypes.CredentialSpecConfig{Config: "0bt9dmxjvjiqermk6xrop3ekq", File: "somefile.json"},
|
||||
expectedErr: `invalid credential spec: cannot specify both "Config" and "File"`,
|
||||
},
|
||||
{
|
||||
name: "config-and-registry",
|
||||
in: composetypes.CredentialSpecConfig{Config: "0bt9dmxjvjiqermk6xrop3ekq", Registry: "testing"},
|
||||
expectedErr: `invalid credential spec: cannot specify both "Config" and "Registry"`,
|
||||
},
|
||||
{
|
||||
name: "file-and-registry",
|
||||
in: composetypes.CredentialSpecConfig{File: "somefile.json", Registry: "testing"},
|
||||
expectedErr: `invalid credential spec: cannot specify both "File" and "Registry"`,
|
||||
},
|
||||
{
|
||||
name: "config-and-file-and-registry",
|
||||
in: composetypes.CredentialSpecConfig{Config: "0bt9dmxjvjiqermk6xrop3ekq", File: "somefile.json", Registry: "testing"},
|
||||
expectedErr: `invalid credential spec: cannot specify both "Config", "File", and "Registry"`,
|
||||
},
|
||||
{
|
||||
name: "missing-config-reference",
|
||||
in: composetypes.CredentialSpecConfig{Config: "missing"},
|
||||
expectedErr: "invalid credential spec: spec specifies config missing, but no such config can be found",
|
||||
configs: []*swarm.ConfigReference{
|
||||
{
|
||||
ConfigName: "someName",
|
||||
ConfigID: "missing",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "namespaced-config",
|
||||
in: composetypes.CredentialSpecConfig{Config: "name"},
|
||||
configs: []*swarm.ConfigReference{
|
||||
{
|
||||
ConfigName: "namespaced-config_name",
|
||||
ConfigID: "someID",
|
||||
},
|
||||
},
|
||||
out: &swarm.CredentialSpec{Config: "someID"},
|
||||
},
|
||||
{
|
||||
name: "config",
|
||||
in: composetypes.CredentialSpecConfig{Config: "someName"},
|
||||
configs: []*swarm.ConfigReference{
|
||||
{
|
||||
ConfigName: "someOtherName",
|
||||
ConfigID: "someOtherID",
|
||||
}, {
|
||||
ConfigName: "someName",
|
||||
ConfigID: "someID",
|
||||
},
|
||||
},
|
||||
out: &swarm.CredentialSpec{Config: "someID"},
|
||||
},
|
||||
{
|
||||
name: "file",
|
||||
in: composetypes.CredentialSpecConfig{File: "somefile.json"},
|
||||
out: &swarm.CredentialSpec{File: "somefile.json"},
|
||||
},
|
||||
{
|
||||
name: "registry",
|
||||
in: composetypes.CredentialSpecConfig{Registry: "testing"},
|
||||
out: &swarm.CredentialSpec{Registry: "testing"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
namespace := NewNamespace(tc.name)
|
||||
swarmSpec, err := convertCredentialSpec(namespace, tc.in, tc.configs)
|
||||
|
||||
if tc.expectedErr != "" {
|
||||
assert.Error(t, err, tc.expectedErr)
|
||||
} else {
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
assert.DeepEqual(t, swarmSpec, tc.out)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertUpdateConfigOrder(t *testing.T) {
|
||||
// test default behavior
|
||||
updateConfig := convertUpdateConfig(&composetypes.UpdateConfig{})
|
||||
assert.Check(t, is.Equal("", updateConfig.Order))
|
||||
|
||||
// test start-first
|
||||
updateConfig = convertUpdateConfig(&composetypes.UpdateConfig{
|
||||
Order: "start-first",
|
||||
})
|
||||
assert.Check(t, is.Equal(updateConfig.Order, "start-first"))
|
||||
|
||||
// test stop-first
|
||||
updateConfig = convertUpdateConfig(&composetypes.UpdateConfig{
|
||||
Order: "stop-first",
|
||||
})
|
||||
assert.Check(t, is.Equal(updateConfig.Order, "stop-first"))
|
||||
}
|
||||
|
||||
func TestConvertFileObject(t *testing.T) {
|
||||
namespace := NewNamespace("testing")
|
||||
config := composetypes.FileReferenceConfig{
|
||||
Source: "source",
|
||||
Target: "target",
|
||||
UID: "user",
|
||||
GID: "group",
|
||||
Mode: uint32Ptr(0644),
|
||||
}
|
||||
swarmRef, err := convertFileObject(namespace, config, lookupConfig)
|
||||
assert.NilError(t, err)
|
||||
|
||||
expected := swarmReferenceObject{
|
||||
Name: "testing_source",
|
||||
File: swarmReferenceTarget{
|
||||
Name: config.Target,
|
||||
UID: config.UID,
|
||||
GID: config.GID,
|
||||
Mode: os.FileMode(0644),
|
||||
},
|
||||
}
|
||||
assert.Check(t, is.DeepEqual(expected, swarmRef))
|
||||
}
|
||||
|
||||
func lookupConfig(key string) (composetypes.FileObjectConfig, error) {
|
||||
if key != "source" {
|
||||
return composetypes.FileObjectConfig{}, errors.New("bad key")
|
||||
}
|
||||
return composetypes.FileObjectConfig{}, nil
|
||||
}
|
||||
|
||||
func TestConvertFileObjectDefaults(t *testing.T) {
|
||||
namespace := NewNamespace("testing")
|
||||
config := composetypes.FileReferenceConfig{Source: "source"}
|
||||
swarmRef, err := convertFileObject(namespace, config, lookupConfig)
|
||||
assert.NilError(t, err)
|
||||
|
||||
expected := swarmReferenceObject{
|
||||
Name: "testing_source",
|
||||
File: swarmReferenceTarget{
|
||||
Name: config.Source,
|
||||
UID: "0",
|
||||
GID: "0",
|
||||
Mode: os.FileMode(0444),
|
||||
},
|
||||
}
|
||||
assert.Check(t, is.DeepEqual(expected, swarmRef))
|
||||
}
|
||||
|
||||
func TestServiceConvertsIsolation(t *testing.T) {
|
||||
src := composetypes.ServiceConfig{
|
||||
Isolation: "hyperv",
|
||||
}
|
||||
result, err := Service("1.35", Namespace{name: "foo"}, src, nil, nil, nil, nil)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(container.IsolationHyperV, result.TaskTemplate.ContainerSpec.Isolation))
|
||||
}
|
||||
|
||||
func TestConvertServiceSecrets(t *testing.T) {
|
||||
namespace := Namespace{name: "foo"}
|
||||
secrets := []composetypes.ServiceSecretConfig{
|
||||
{Source: "foo_secret"},
|
||||
{Source: "bar_secret"},
|
||||
}
|
||||
secretSpecs := map[string]composetypes.SecretConfig{
|
||||
"foo_secret": {
|
||||
Name: "foo_secret",
|
||||
},
|
||||
"bar_secret": {
|
||||
Name: "bar_secret",
|
||||
},
|
||||
}
|
||||
client := &fakeClient{
|
||||
secretListFunc: func(opts types.SecretListOptions) ([]swarm.Secret, error) {
|
||||
assert.Check(t, is.Contains(opts.Filters.Get("name"), "foo_secret"))
|
||||
assert.Check(t, is.Contains(opts.Filters.Get("name"), "bar_secret"))
|
||||
return []swarm.Secret{
|
||||
{Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "foo_secret"}}},
|
||||
{Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "bar_secret"}}},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
refs, err := convertServiceSecrets(client, namespace, secrets, secretSpecs)
|
||||
assert.NilError(t, err)
|
||||
expected := []*swarm.SecretReference{
|
||||
{
|
||||
SecretName: "bar_secret",
|
||||
File: &swarm.SecretReferenceFileTarget{
|
||||
Name: "bar_secret",
|
||||
UID: "0",
|
||||
GID: "0",
|
||||
Mode: 0444,
|
||||
},
|
||||
},
|
||||
{
|
||||
SecretName: "foo_secret",
|
||||
File: &swarm.SecretReferenceFileTarget{
|
||||
Name: "foo_secret",
|
||||
UID: "0",
|
||||
GID: "0",
|
||||
Mode: 0444,
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.DeepEqual(t, expected, refs)
|
||||
}
|
||||
|
||||
func TestConvertServiceConfigs(t *testing.T) {
|
||||
namespace := Namespace{name: "foo"}
|
||||
service := composetypes.ServiceConfig{
|
||||
Configs: []composetypes.ServiceConfigObjConfig{
|
||||
{Source: "foo_config"},
|
||||
{Source: "bar_config"},
|
||||
},
|
||||
CredentialSpec: composetypes.CredentialSpecConfig{
|
||||
Config: "baz_config",
|
||||
},
|
||||
}
|
||||
configSpecs := map[string]composetypes.ConfigObjConfig{
|
||||
"foo_config": {
|
||||
Name: "foo_config",
|
||||
},
|
||||
"bar_config": {
|
||||
Name: "bar_config",
|
||||
},
|
||||
"baz_config": {
|
||||
Name: "baz_config",
|
||||
},
|
||||
}
|
||||
client := &fakeClient{
|
||||
configListFunc: func(opts types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
assert.Check(t, is.Contains(opts.Filters.Get("name"), "foo_config"))
|
||||
assert.Check(t, is.Contains(opts.Filters.Get("name"), "bar_config"))
|
||||
assert.Check(t, is.Contains(opts.Filters.Get("name"), "baz_config"))
|
||||
return []swarm.Config{
|
||||
{Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "foo_config"}}},
|
||||
{Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "bar_config"}}},
|
||||
{Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "baz_config"}}},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
refs, err := convertServiceConfigObjs(client, namespace, service, configSpecs)
|
||||
assert.NilError(t, err)
|
||||
expected := []*swarm.ConfigReference{
|
||||
{
|
||||
ConfigName: "bar_config",
|
||||
File: &swarm.ConfigReferenceFileTarget{
|
||||
Name: "bar_config",
|
||||
UID: "0",
|
||||
GID: "0",
|
||||
Mode: 0444,
|
||||
},
|
||||
},
|
||||
{
|
||||
ConfigName: "baz_config",
|
||||
Runtime: &swarm.ConfigReferenceRuntimeTarget{},
|
||||
},
|
||||
{
|
||||
ConfigName: "foo_config",
|
||||
File: &swarm.ConfigReferenceFileTarget{
|
||||
Name: "foo_config",
|
||||
UID: "0",
|
||||
GID: "0",
|
||||
Mode: 0444,
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.DeepEqual(t, expected, refs)
|
||||
}
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
secretListFunc func(types.SecretListOptions) ([]swarm.Secret, error)
|
||||
configListFunc func(types.ConfigListOptions) ([]swarm.Config, error)
|
||||
}
|
||||
|
||||
func (c *fakeClient) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) {
|
||||
if c.secretListFunc != nil {
|
||||
return c.secretListFunc(options)
|
||||
}
|
||||
return []swarm.Secret{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
if c.configListFunc != nil {
|
||||
return c.configListFunc(options)
|
||||
}
|
||||
return []swarm.Config{}, nil
|
||||
}
|
||||
|
||||
func TestConvertUpdateConfigParallelism(t *testing.T) {
|
||||
parallel := uint64(4)
|
||||
|
||||
// test default behavior
|
||||
updateConfig := convertUpdateConfig(&composetypes.UpdateConfig{})
|
||||
assert.Check(t, is.Equal(uint64(1), updateConfig.Parallelism))
|
||||
|
||||
// Non default value
|
||||
updateConfig = convertUpdateConfig(&composetypes.UpdateConfig{
|
||||
Parallelism: ¶llel,
|
||||
})
|
||||
assert.Check(t, is.Equal(parallel, updateConfig.Parallelism))
|
||||
}
|
||||
|
||||
func TestConvertServiceCapAddAndCapDrop(t *testing.T) {
|
||||
tests := []struct {
|
||||
title string
|
||||
in, out composetypes.ServiceConfig
|
||||
}{
|
||||
{
|
||||
title: "default behavior",
|
||||
},
|
||||
{
|
||||
title: "some values",
|
||||
in: composetypes.ServiceConfig{
|
||||
CapAdd: []string{"SYS_NICE", "CAP_NET_ADMIN"},
|
||||
CapDrop: []string{"CHOWN", "CAP_NET_ADMIN", "DAC_OVERRIDE", "CAP_FSETID", "CAP_FOWNER"},
|
||||
},
|
||||
out: composetypes.ServiceConfig{
|
||||
CapAdd: []string{"CAP_NET_ADMIN", "CAP_SYS_NICE"},
|
||||
CapDrop: []string{"CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID"},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "adding ALL capabilities",
|
||||
in: composetypes.ServiceConfig{
|
||||
CapAdd: []string{"ALL", "CAP_NET_ADMIN"},
|
||||
CapDrop: []string{"CHOWN", "CAP_NET_ADMIN", "DAC_OVERRIDE", "CAP_FSETID", "CAP_FOWNER"},
|
||||
},
|
||||
out: composetypes.ServiceConfig{
|
||||
CapAdd: []string{"ALL"},
|
||||
CapDrop: []string{"CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID", "CAP_NET_ADMIN"},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "dropping ALL capabilities",
|
||||
in: composetypes.ServiceConfig{
|
||||
CapAdd: []string{"CHOWN", "CAP_NET_ADMIN", "DAC_OVERRIDE", "CAP_FSETID", "CAP_FOWNER"},
|
||||
CapDrop: []string{"ALL", "CAP_NET_ADMIN", "CAP_FOO"},
|
||||
},
|
||||
out: composetypes.ServiceConfig{
|
||||
CapAdd: []string{"CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID", "CAP_NET_ADMIN"},
|
||||
CapDrop: []string{"ALL"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.title, func(t *testing.T) {
|
||||
result, err := Service("1.41", Namespace{name: "foo"}, tc.in, nil, nil, nil, nil)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.CapabilityAdd, tc.out.CapAdd))
|
||||
assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.CapabilityDrop, tc.out.CapDrop))
|
||||
})
|
||||
}
|
||||
}
|
162
client/convert/volume.go
Normal file
162
client/convert/volume.go
Normal file
@ -0,0 +1,162 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type volumes map[string]composetypes.VolumeConfig
|
||||
|
||||
// Volumes from compose-file types to engine api types
|
||||
func Volumes(serviceVolumes []composetypes.ServiceVolumeConfig, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) {
|
||||
var mounts []mount.Mount
|
||||
|
||||
for _, volumeConfig := range serviceVolumes {
|
||||
mount, err := convertVolumeToMount(volumeConfig, stackVolumes, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mounts = append(mounts, mount)
|
||||
}
|
||||
return mounts, nil
|
||||
}
|
||||
|
||||
func createMountFromVolume(volume composetypes.ServiceVolumeConfig) mount.Mount {
|
||||
return mount.Mount{
|
||||
Type: mount.Type(volume.Type),
|
||||
Target: volume.Target,
|
||||
ReadOnly: volume.ReadOnly,
|
||||
Source: volume.Source,
|
||||
Consistency: mount.Consistency(volume.Consistency),
|
||||
}
|
||||
}
|
||||
|
||||
func handleVolumeToMount(
|
||||
volume composetypes.ServiceVolumeConfig,
|
||||
stackVolumes volumes,
|
||||
namespace Namespace,
|
||||
) (mount.Mount, error) {
|
||||
result := createMountFromVolume(volume)
|
||||
|
||||
if volume.Tmpfs != nil {
|
||||
return mount.Mount{}, errors.New("tmpfs options are incompatible with type volume")
|
||||
}
|
||||
if volume.Bind != nil {
|
||||
return mount.Mount{}, errors.New("bind options are incompatible with type volume")
|
||||
}
|
||||
// Anonymous volumes
|
||||
if volume.Source == "" {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
stackVolume, exists := stackVolumes[volume.Source]
|
||||
if !exists {
|
||||
return mount.Mount{}, errors.Errorf("undefined volume %q", volume.Source)
|
||||
}
|
||||
|
||||
result.Source = namespace.Scope(volume.Source)
|
||||
result.VolumeOptions = &mount.VolumeOptions{}
|
||||
|
||||
if volume.Volume != nil {
|
||||
result.VolumeOptions.NoCopy = volume.Volume.NoCopy
|
||||
}
|
||||
|
||||
if stackVolume.Name != "" {
|
||||
result.Source = stackVolume.Name
|
||||
}
|
||||
|
||||
// External named volumes
|
||||
if stackVolume.External.External {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
result.VolumeOptions.Labels = AddStackLabel(namespace, stackVolume.Labels)
|
||||
if stackVolume.Driver != "" || stackVolume.DriverOpts != nil {
|
||||
result.VolumeOptions.DriverConfig = &mount.Driver{
|
||||
Name: stackVolume.Driver,
|
||||
Options: stackVolume.DriverOpts,
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func handleBindToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
|
||||
result := createMountFromVolume(volume)
|
||||
|
||||
if volume.Source == "" {
|
||||
return mount.Mount{}, errors.New("invalid bind source, source cannot be empty")
|
||||
}
|
||||
if volume.Volume != nil {
|
||||
return mount.Mount{}, errors.New("volume options are incompatible with type bind")
|
||||
}
|
||||
if volume.Tmpfs != nil {
|
||||
return mount.Mount{}, errors.New("tmpfs options are incompatible with type bind")
|
||||
}
|
||||
if volume.Bind != nil {
|
||||
result.BindOptions = &mount.BindOptions{
|
||||
Propagation: mount.Propagation(volume.Bind.Propagation),
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func handleTmpfsToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
|
||||
result := createMountFromVolume(volume)
|
||||
|
||||
if volume.Source != "" {
|
||||
return mount.Mount{}, errors.New("invalid tmpfs source, source must be empty")
|
||||
}
|
||||
if volume.Bind != nil {
|
||||
return mount.Mount{}, errors.New("bind options are incompatible with type tmpfs")
|
||||
}
|
||||
if volume.Volume != nil {
|
||||
return mount.Mount{}, errors.New("volume options are incompatible with type tmpfs")
|
||||
}
|
||||
if volume.Tmpfs != nil {
|
||||
result.TmpfsOptions = &mount.TmpfsOptions{
|
||||
SizeBytes: volume.Tmpfs.Size,
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func handleNpipeToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
|
||||
result := createMountFromVolume(volume)
|
||||
|
||||
if volume.Source == "" {
|
||||
return mount.Mount{}, errors.New("invalid npipe source, source cannot be empty")
|
||||
}
|
||||
if volume.Volume != nil {
|
||||
return mount.Mount{}, errors.New("volume options are incompatible with type npipe")
|
||||
}
|
||||
if volume.Tmpfs != nil {
|
||||
return mount.Mount{}, errors.New("tmpfs options are incompatible with type npipe")
|
||||
}
|
||||
if volume.Bind != nil {
|
||||
result.BindOptions = &mount.BindOptions{
|
||||
Propagation: mount.Propagation(volume.Bind.Propagation),
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func convertVolumeToMount(
|
||||
volume composetypes.ServiceVolumeConfig,
|
||||
stackVolumes volumes,
|
||||
namespace Namespace,
|
||||
) (mount.Mount, error) {
|
||||
|
||||
switch volume.Type {
|
||||
case "volume", "":
|
||||
return handleVolumeToMount(volume, stackVolumes, namespace)
|
||||
case "bind":
|
||||
return handleBindToMount(volume)
|
||||
case "tmpfs":
|
||||
return handleTmpfsToMount(volume)
|
||||
case "npipe":
|
||||
return handleNpipeToMount(volume)
|
||||
}
|
||||
return mount.Mount{}, errors.New("volume type must be volume, bind, tmpfs or npipe")
|
||||
}
|
361
client/convert/volume_test.go
Normal file
361
client/convert/volume_test.go
Normal file
@ -0,0 +1,361 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestConvertVolumeToMountAnonymousVolume(t *testing.T) {
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "volume",
|
||||
Target: "/foo/bar",
|
||||
}
|
||||
expected := mount.Mount{
|
||||
Type: mount.TypeVolume,
|
||||
Target: "/foo/bar",
|
||||
}
|
||||
mount, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(expected, mount))
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountAnonymousBind(t *testing.T) {
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "bind",
|
||||
Target: "/foo/bar",
|
||||
Bind: &composetypes.ServiceVolumeBind{
|
||||
Propagation: "slave",
|
||||
},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
|
||||
assert.Error(t, err, "invalid bind source, source cannot be empty")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountUnapprovedType(t *testing.T) {
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "foo",
|
||||
Target: "/foo/bar",
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
|
||||
assert.Error(t, err, "volume type must be volume, bind, tmpfs or npipe")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountConflictingOptionsBindInVolume(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "volume",
|
||||
Source: "foo",
|
||||
Target: "/target",
|
||||
Bind: &composetypes.ServiceVolumeBind{
|
||||
Propagation: "slave",
|
||||
},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.Error(t, err, "bind options are incompatible with type volume")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountConflictingOptionsTmpfsInVolume(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "volume",
|
||||
Source: "foo",
|
||||
Target: "/target",
|
||||
Tmpfs: &composetypes.ServiceVolumeTmpfs{
|
||||
Size: 1000,
|
||||
},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.Error(t, err, "tmpfs options are incompatible with type volume")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountConflictingOptionsVolumeInBind(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "bind",
|
||||
Source: "/foo",
|
||||
Target: "/target",
|
||||
Volume: &composetypes.ServiceVolumeVolume{
|
||||
NoCopy: true,
|
||||
},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.Error(t, err, "volume options are incompatible with type bind")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountConflictingOptionsTmpfsInBind(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "bind",
|
||||
Source: "/foo",
|
||||
Target: "/target",
|
||||
Tmpfs: &composetypes.ServiceVolumeTmpfs{
|
||||
Size: 1000,
|
||||
},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.Error(t, err, "tmpfs options are incompatible with type bind")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountConflictingOptionsBindInTmpfs(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "tmpfs",
|
||||
Target: "/target",
|
||||
Bind: &composetypes.ServiceVolumeBind{
|
||||
Propagation: "slave",
|
||||
},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.Error(t, err, "bind options are incompatible with type tmpfs")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountConflictingOptionsVolumeInTmpfs(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "tmpfs",
|
||||
Target: "/target",
|
||||
Volume: &composetypes.ServiceVolumeVolume{
|
||||
NoCopy: true,
|
||||
},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.Error(t, err, "volume options are incompatible with type tmpfs")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountNamedVolume(t *testing.T) {
|
||||
stackVolumes := volumes{
|
||||
"normal": composetypes.VolumeConfig{
|
||||
Driver: "glusterfs",
|
||||
DriverOpts: map[string]string{
|
||||
"opt": "value",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"something": "labeled",
|
||||
},
|
||||
},
|
||||
}
|
||||
namespace := NewNamespace("foo")
|
||||
expected := mount.Mount{
|
||||
Type: mount.TypeVolume,
|
||||
Source: "foo_normal",
|
||||
Target: "/foo",
|
||||
ReadOnly: true,
|
||||
VolumeOptions: &mount.VolumeOptions{
|
||||
Labels: map[string]string{
|
||||
LabelNamespace: "foo",
|
||||
"something": "labeled",
|
||||
},
|
||||
DriverConfig: &mount.Driver{
|
||||
Name: "glusterfs",
|
||||
Options: map[string]string{
|
||||
"opt": "value",
|
||||
},
|
||||
},
|
||||
NoCopy: true,
|
||||
},
|
||||
}
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "volume",
|
||||
Source: "normal",
|
||||
Target: "/foo",
|
||||
ReadOnly: true,
|
||||
Volume: &composetypes.ServiceVolumeVolume{
|
||||
NoCopy: true,
|
||||
},
|
||||
}
|
||||
mount, err := convertVolumeToMount(config, stackVolumes, namespace)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(expected, mount))
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountNamedVolumeWithNameCustomizd(t *testing.T) {
|
||||
stackVolumes := volumes{
|
||||
"normal": composetypes.VolumeConfig{
|
||||
Name: "user_specified_name",
|
||||
Driver: "vsphere",
|
||||
DriverOpts: map[string]string{
|
||||
"opt": "value",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"something": "labeled",
|
||||
},
|
||||
},
|
||||
}
|
||||
namespace := NewNamespace("foo")
|
||||
expected := mount.Mount{
|
||||
Type: mount.TypeVolume,
|
||||
Source: "user_specified_name",
|
||||
Target: "/foo",
|
||||
ReadOnly: true,
|
||||
VolumeOptions: &mount.VolumeOptions{
|
||||
Labels: map[string]string{
|
||||
LabelNamespace: "foo",
|
||||
"something": "labeled",
|
||||
},
|
||||
DriverConfig: &mount.Driver{
|
||||
Name: "vsphere",
|
||||
Options: map[string]string{
|
||||
"opt": "value",
|
||||
},
|
||||
},
|
||||
NoCopy: true,
|
||||
},
|
||||
}
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "volume",
|
||||
Source: "normal",
|
||||
Target: "/foo",
|
||||
ReadOnly: true,
|
||||
Volume: &composetypes.ServiceVolumeVolume{
|
||||
NoCopy: true,
|
||||
},
|
||||
}
|
||||
mount, err := convertVolumeToMount(config, stackVolumes, namespace)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(expected, mount))
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountNamedVolumeExternal(t *testing.T) {
|
||||
stackVolumes := volumes{
|
||||
"outside": composetypes.VolumeConfig{
|
||||
Name: "special",
|
||||
External: composetypes.External{External: true},
|
||||
},
|
||||
}
|
||||
namespace := NewNamespace("foo")
|
||||
expected := mount.Mount{
|
||||
Type: mount.TypeVolume,
|
||||
Source: "special",
|
||||
Target: "/foo",
|
||||
VolumeOptions: &mount.VolumeOptions{NoCopy: false},
|
||||
}
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "volume",
|
||||
Source: "outside",
|
||||
Target: "/foo",
|
||||
}
|
||||
mount, err := convertVolumeToMount(config, stackVolumes, namespace)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(expected, mount))
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountNamedVolumeExternalNoCopy(t *testing.T) {
|
||||
stackVolumes := volumes{
|
||||
"outside": composetypes.VolumeConfig{
|
||||
Name: "special",
|
||||
External: composetypes.External{External: true},
|
||||
},
|
||||
}
|
||||
namespace := NewNamespace("foo")
|
||||
expected := mount.Mount{
|
||||
Type: mount.TypeVolume,
|
||||
Source: "special",
|
||||
Target: "/foo",
|
||||
VolumeOptions: &mount.VolumeOptions{
|
||||
NoCopy: true,
|
||||
},
|
||||
}
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "volume",
|
||||
Source: "outside",
|
||||
Target: "/foo",
|
||||
Volume: &composetypes.ServiceVolumeVolume{
|
||||
NoCopy: true,
|
||||
},
|
||||
}
|
||||
mount, err := convertVolumeToMount(config, stackVolumes, namespace)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(expected, mount))
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountBind(t *testing.T) {
|
||||
stackVolumes := volumes{}
|
||||
namespace := NewNamespace("foo")
|
||||
expected := mount.Mount{
|
||||
Type: mount.TypeBind,
|
||||
Source: "/bar",
|
||||
Target: "/foo",
|
||||
ReadOnly: true,
|
||||
BindOptions: &mount.BindOptions{Propagation: mount.PropagationShared},
|
||||
}
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "bind",
|
||||
Source: "/bar",
|
||||
Target: "/foo",
|
||||
ReadOnly: true,
|
||||
Bind: &composetypes.ServiceVolumeBind{Propagation: "shared"},
|
||||
}
|
||||
mount, err := convertVolumeToMount(config, stackVolumes, namespace)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(expected, mount))
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountVolumeDoesNotExist(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "volume",
|
||||
Source: "unknown",
|
||||
Target: "/foo",
|
||||
ReadOnly: true,
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.Error(t, err, "undefined volume \"unknown\"")
|
||||
}
|
||||
|
||||
func TestConvertTmpfsToMountVolume(t *testing.T) {
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "tmpfs",
|
||||
Target: "/foo/bar",
|
||||
Tmpfs: &composetypes.ServiceVolumeTmpfs{
|
||||
Size: 1000,
|
||||
},
|
||||
}
|
||||
expected := mount.Mount{
|
||||
Type: mount.TypeTmpfs,
|
||||
Target: "/foo/bar",
|
||||
TmpfsOptions: &mount.TmpfsOptions{SizeBytes: 1000},
|
||||
}
|
||||
mount, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(expected, mount))
|
||||
}
|
||||
|
||||
func TestConvertTmpfsToMountVolumeWithSource(t *testing.T) {
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "tmpfs",
|
||||
Source: "/bar",
|
||||
Target: "/foo/bar",
|
||||
Tmpfs: &composetypes.ServiceVolumeTmpfs{
|
||||
Size: 1000,
|
||||
},
|
||||
}
|
||||
|
||||
_, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
|
||||
assert.Error(t, err, "invalid tmpfs source, source must be empty")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountAnonymousNpipe(t *testing.T) {
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "npipe",
|
||||
Source: `\\.\pipe\foo`,
|
||||
Target: `\\.\pipe\foo`,
|
||||
}
|
||||
expected := mount.Mount{
|
||||
Type: mount.TypeNamedPipe,
|
||||
Source: `\\.\pipe\foo`,
|
||||
Target: `\\.\pipe\foo`,
|
||||
}
|
||||
mount, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(expected, mount))
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
)
|
||||
|
||||
const StackNamespace = "com.docker.stack.namespace"
|
||||
|
||||
type StackStatus struct {
|
||||
Services []swarm.Service
|
||||
Err error
|
||||
}
|
||||
|
||||
func QueryStackStatus(contextName string) StackStatus {
|
||||
cl, err := NewClientWithContext(contextName)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "does not exist") {
|
||||
// No local context found, bail out gracefully
|
||||
return StackStatus{[]swarm.Service{}, nil}
|
||||
}
|
||||
return StackStatus{[]swarm.Service{}, err}
|
||||
}
|
||||
ctx := context.Background()
|
||||
filter := filters.NewArgs()
|
||||
filter.Add("label", StackNamespace)
|
||||
services, err := cl.ServiceList(ctx, types.ServiceListOptions{Filters: filter})
|
||||
if err != nil {
|
||||
return StackStatus{[]swarm.Service{}, err}
|
||||
}
|
||||
return StackStatus{services, nil}
|
||||
}
|
133
client/stack/loader.go
Normal file
133
client/stack/loader.go
Normal file
@ -0,0 +1,133 @@
|
||||
package stack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
"github.com/docker/cli/cli/compose/schema"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// LoadComposefile parse the composefile specified in the cli and returns its Config and version.
|
||||
func LoadComposefile(opts options.Deploy) (*composetypes.Config, error) {
|
||||
configDetails, err := getConfigDetails(opts.Composefiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dicts := getDictsFrom(configDetails.ConfigFiles)
|
||||
config, err := loader.Load(configDetails)
|
||||
if err != nil {
|
||||
if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok {
|
||||
return nil, fmt.Errorf("compose file contains unsupported options:\n\n%s",
|
||||
propertyWarnings(fpe.Properties))
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unsupportedProperties := loader.GetUnsupportedProperties(dicts...)
|
||||
if len(unsupportedProperties) > 0 {
|
||||
logrus.Warnf("Ignoring unsupported options: %s\n\n",
|
||||
strings.Join(unsupportedProperties, ", "))
|
||||
}
|
||||
|
||||
deprecatedProperties := loader.GetDeprecatedProperties(dicts...)
|
||||
if len(deprecatedProperties) > 0 {
|
||||
logrus.Warnf("Ignoring deprecated options:\n\n%s\n\n",
|
||||
propertyWarnings(deprecatedProperties))
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func getDictsFrom(configFiles []composetypes.ConfigFile) []map[string]interface{} {
|
||||
dicts := []map[string]interface{}{}
|
||||
|
||||
for _, configFile := range configFiles {
|
||||
dicts = append(dicts, configFile.Config)
|
||||
}
|
||||
|
||||
return dicts
|
||||
}
|
||||
|
||||
func propertyWarnings(properties map[string]string) string {
|
||||
var msgs []string
|
||||
for name, description := range properties {
|
||||
msgs = append(msgs, fmt.Sprintf("%s: %s", name, description))
|
||||
}
|
||||
sort.Strings(msgs)
|
||||
return strings.Join(msgs, "\n\n")
|
||||
}
|
||||
|
||||
func getConfigDetails(composefiles []string) (composetypes.ConfigDetails, error) {
|
||||
var details composetypes.ConfigDetails
|
||||
|
||||
absPath, err := filepath.Abs(composefiles[0])
|
||||
if err != nil {
|
||||
return details, err
|
||||
}
|
||||
details.WorkingDir = filepath.Dir(absPath)
|
||||
|
||||
details.ConfigFiles, err = loadConfigFiles(composefiles)
|
||||
if err != nil {
|
||||
return details, err
|
||||
}
|
||||
// Take the first file version (2 files can't have different version)
|
||||
details.Version = schema.Version(details.ConfigFiles[0].Config)
|
||||
details.Environment, err = buildEnvironment(os.Environ())
|
||||
return details, err
|
||||
}
|
||||
|
||||
func buildEnvironment(env []string) (map[string]string, error) {
|
||||
result := make(map[string]string, len(env))
|
||||
for _, s := range env {
|
||||
// if value is empty, s is like "K=", not "K".
|
||||
if !strings.Contains(s, "=") {
|
||||
return result, fmt.Errorf("unexpected environment %q", s)
|
||||
}
|
||||
kv := strings.SplitN(s, "=", 2)
|
||||
result[kv[0]] = kv[1]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func loadConfigFiles(filenames []string) ([]composetypes.ConfigFile, error) {
|
||||
var configFiles []composetypes.ConfigFile
|
||||
|
||||
for _, filename := range filenames {
|
||||
configFile, err := loadConfigFile(filename)
|
||||
if err != nil {
|
||||
return configFiles, err
|
||||
}
|
||||
configFiles = append(configFiles, *configFile)
|
||||
}
|
||||
|
||||
return configFiles, nil
|
||||
}
|
||||
|
||||
func loadConfigFile(filename string) (*composetypes.ConfigFile, error) {
|
||||
var bytes []byte
|
||||
var err error
|
||||
|
||||
bytes, err = ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := loader.ParseYAML(bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &composetypes.ConfigFile{
|
||||
Filename: filename,
|
||||
Config: config,
|
||||
}, nil
|
||||
}
|
104
client/stack/stack.go
Normal file
104
client/stack/stack.go
Normal file
@ -0,0 +1,104 @@
|
||||
package stack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
abraClient "coopcloud.tech/abra/client"
|
||||
"coopcloud.tech/abra/client/convert"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type StackStatus struct {
|
||||
Services []swarm.Service
|
||||
Err error
|
||||
}
|
||||
|
||||
func getStackFilter(namespace string) filters.Args {
|
||||
filter := filters.NewArgs()
|
||||
filter.Add("label", convert.LabelNamespace+"="+namespace)
|
||||
return filter
|
||||
}
|
||||
|
||||
func getStackServiceFilter(namespace string) filters.Args {
|
||||
return getStackFilter(namespace)
|
||||
}
|
||||
|
||||
func getStackFilterFromOpt(namespace string, opt opts.FilterOpt) filters.Args {
|
||||
filter := opt.Value()
|
||||
filter.Add("label", convert.LabelNamespace+"="+namespace)
|
||||
return filter
|
||||
}
|
||||
|
||||
func getAllStacksFilter() filters.Args {
|
||||
filter := filters.NewArgs()
|
||||
filter.Add("label", convert.LabelNamespace)
|
||||
return filter
|
||||
}
|
||||
|
||||
func getStackServices(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Service, error) {
|
||||
return apiclient.ServiceList(ctx, types.ServiceListOptions{Filters: getStackServiceFilter(namespace)})
|
||||
}
|
||||
|
||||
func GetAllDeployedServices(contextName string) StackStatus {
|
||||
cl, err := abraClient.NewClientWithContext(contextName)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "does not exist") {
|
||||
// No local context found, bail out gracefully
|
||||
return StackStatus{[]swarm.Service{}, nil}
|
||||
}
|
||||
return StackStatus{[]swarm.Service{}, err}
|
||||
}
|
||||
ctx := context.Background()
|
||||
services, err := cl.ServiceList(ctx, types.ServiceListOptions{Filters: getAllStacksFilter()})
|
||||
if err != nil {
|
||||
return StackStatus{[]swarm.Service{}, err}
|
||||
}
|
||||
return StackStatus{services, nil}
|
||||
}
|
||||
|
||||
func checkDaemonIsSwarmManager(contextName string) error {
|
||||
cl, err := abraClient.NewClientWithContext(contextName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := cl.Info(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.Swarm.ControlAvailable {
|
||||
return errors.New("this server is not a swarm manager. Did you run \"abra server init\"?")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateStackName checks if the provided string is a valid stack name (namespace).
|
||||
// It currently only does a rudimentary check if the string is empty, or consists
|
||||
// of only whitespace and quoting characters.
|
||||
func validateStackName(namespace string) error {
|
||||
v := strings.TrimFunc(namespace, quotesOrWhitespace)
|
||||
if v == "" {
|
||||
return fmt.Errorf("invalid stack name: %q", namespace)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func quotesOrWhitespace(r rune) bool {
|
||||
return unicode.IsSpace(r) || r == '"' || r == '\''
|
||||
}
|
||||
|
||||
func DeployStack(namespace string) {
|
||||
}
|
||||
|
||||
// TODO: Prune services from stack
|
||||
|
||||
func pruneServices() error {
|
||||
return nil
|
||||
}
|
@ -8,7 +8,8 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/client"
|
||||
"coopcloud.tech/abra/client/convert"
|
||||
"coopcloud.tech/abra/client/stack"
|
||||
)
|
||||
|
||||
// Type aliases to make code hints easier to understand
|
||||
@ -200,10 +201,10 @@ func SanitiseAppName(name string) string {
|
||||
// GetAppStatuses queries servers to check the deployment status of given apps
|
||||
func GetAppStatuses(appFiles AppFiles) (map[string]string, error) {
|
||||
servers := appFiles.GetServers()
|
||||
ch := make(chan client.StackStatus, len(servers))
|
||||
ch := make(chan stack.StackStatus, len(servers))
|
||||
for _, server := range servers {
|
||||
go func(s string) {
|
||||
ch <- client.QueryStackStatus(s)
|
||||
ch <- stack.GetAllDeployedServices(s)
|
||||
}(server)
|
||||
}
|
||||
|
||||
@ -211,7 +212,7 @@ func GetAppStatuses(appFiles AppFiles) (map[string]string, error) {
|
||||
for range servers {
|
||||
status := <-ch
|
||||
for _, service := range status.Services {
|
||||
name := service.Spec.Labels[client.StackNamespace]
|
||||
name := service.Spec.Labels[convert.LabelNamespace]
|
||||
if _, ok := statuses[name]; !ok {
|
||||
statuses[name] = "deployed"
|
||||
}
|
||||
|
3
go.mod
3
go.mod
@ -12,13 +12,16 @@ require (
|
||||
github.com/docker/go-units v0.4.0
|
||||
github.com/fvbommel/sortorder v1.0.2 // indirect
|
||||
github.com/go-git/go-git/v5 v5.4.2
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/hetznercloud/hcloud-go v1.28.0
|
||||
github.com/moby/sys/mount v0.2.0 // indirect
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/schultz-is/passgen v1.0.1
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/theupdateframework/notary v0.7.0 // indirect
|
||||
github.com/urfave/cli/v2 v2.3.0 //ct
|
||||
gotest.tools/v3 v3.0.3
|
||||
)
|
||||
|
5
go.sum
5
go.sum
@ -400,6 +400,8 @@ github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hf
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
@ -731,8 +733,11 @@ github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT
|
||||
github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
|
||||
github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
|
||||
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f h1:mvXjJIHRZyhNuGassLTcXTwjiWq7NmjdavZsUnmFybQ=
|
||||
github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
|
Loading…
Reference in New Issue
Block a user