Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a079b78de | |||
| 7524a785ce | |||
| 1013f669bb |
@ -157,8 +157,7 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
|||||||
ResolveImage: stack.ResolveImageAlways,
|
ResolveImage: stack.ResolveImageAlways,
|
||||||
Detach: false,
|
Detach: false,
|
||||||
}
|
}
|
||||||
|
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
||||||
compose, err := appPkg.GetAppComposeConfig(composeFiles, app.Env)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import (
|
|||||||
"coopcloud.tech/abra/pkg/i18n"
|
"coopcloud.tech/abra/pkg/i18n"
|
||||||
"coopcloud.tech/abra/pkg/log"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||||
composeGoTypes "github.com/compose-spec/compose-go/v2/types"
|
composetypes "github.com/docker/cli/cli/compose/types"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
dockerClient "github.com/docker/docker/client"
|
dockerClient "github.com/docker/docker/client"
|
||||||
@ -80,13 +80,13 @@ var AppLabelsCommand = &cobra.Command{
|
|||||||
|
|
||||||
rows = append(rows, []string{i18n.G("RECIPE LABELS"), "---"})
|
rows = append(rows, []string{i18n.G("RECIPE LABELS"), "---"})
|
||||||
|
|
||||||
config, err := app.Recipe.GetComposeConfig()
|
config, err := app.Recipe.GetComposeConfig(app.Env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var localLabelKeys []string
|
var localLabelKeys []string
|
||||||
var appServiceConfig composeGoTypes.ServiceConfig
|
var appServiceConfig composetypes.ServiceConfig
|
||||||
for _, service := range config.Services {
|
for _, service := range config.Services {
|
||||||
if service.Name == "app" {
|
if service.Name == "app" {
|
||||||
appServiceConfig = service
|
appServiceConfig = service
|
||||||
|
|||||||
@ -262,7 +262,8 @@ func getAppResources(cl *dockerclient.Client, app app.App) (*AppResources, error
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
compose, err := appPkg.GetAppComposeConfig(composeFiles, app.Env)
|
opts := stack.Deploy{Composefiles: composeFiles, Namespace: app.StackName()}
|
||||||
|
compose, err := appPkg.GetAppComposeConfig(app.Name, opts, app.Env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"sort"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
@ -88,19 +87,26 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
compose, err := appPkg.GetAppComposeConfig(composeFiles, app.Env)
|
deployOpts := stack.Deploy{
|
||||||
|
Composefiles: composeFiles,
|
||||||
|
Namespace: app.StackName(),
|
||||||
|
Prune: false,
|
||||||
|
ResolveImage: stack.ResolveImageAlways,
|
||||||
|
}
|
||||||
|
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
services := compose.Services
|
services := compose.Services
|
||||||
|
sort.Slice(services, func(i, j int) bool {
|
||||||
|
return services[i].Name < services[j].Name
|
||||||
|
})
|
||||||
|
|
||||||
var rows [][]string
|
var rows [][]string
|
||||||
allContainerStats := make(map[string]map[string]string)
|
allContainerStats := make(map[string]map[string]string)
|
||||||
for _, serviceName := range slices.Sorted(maps.Keys(compose.Services)) {
|
for _, service := range services {
|
||||||
service := services[serviceName]
|
|
||||||
|
|
||||||
filters := filters.NewArgs()
|
filters := filters.NewArgs()
|
||||||
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service.Name))
|
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service.Name))
|
||||||
|
|
||||||
|
|||||||
@ -173,7 +173,7 @@ beforehand. See "abra app backup" for more.`),
|
|||||||
Detach: false,
|
Detach: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
compose, err := appPkg.GetAppComposeConfig(composeFiles, app.Env)
|
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -89,7 +89,8 @@ Passing "--prune/-p" does not remove those volumes.`),
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
compose, err := appPkg.GetAppComposeConfig(composeFiles, app.Env)
|
opts := stack.Deploy{Composefiles: composeFiles, Namespace: stackName}
|
||||||
|
compose, err := appPkg.GetAppComposeConfig(app.Name, opts, app.Env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -185,7 +185,7 @@ beforehand. See "abra app backup" for more.`),
|
|||||||
Detach: false,
|
Detach: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
compose, err := appPkg.GetAppComposeConfig(composeFiles, app.Env)
|
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -238,7 +238,7 @@ beforehand. See "abra app backup" for more.`),
|
|||||||
if upgradeReleaseNotes == "" {
|
if upgradeReleaseNotes == "" {
|
||||||
upgradeWarnMessages = append(
|
upgradeWarnMessages = append(
|
||||||
upgradeWarnMessages,
|
upgradeWarnMessages,
|
||||||
fmt.Sprintf("no release notes available for %s", chosenUpgrade),
|
i18n.G("no release notes for upgrading from %s to %s", deployMeta.Version, chosenUpgrade),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -92,11 +92,10 @@ func SetBumpType(bumpType string) {
|
|||||||
func GetMainAppImage(recipe recipe.Recipe) (string, error) {
|
func GetMainAppImage(recipe recipe.Recipe) (string, error) {
|
||||||
var path string
|
var path string
|
||||||
|
|
||||||
config, err := recipe.GetComposeConfig()
|
config, err := recipe.GetComposeConfig(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, service := range config.Services {
|
for _, service := range config.Services {
|
||||||
if service.Name == "app" {
|
if service.Name == "app" {
|
||||||
img, err := reference.ParseNormalizedNamed(service.Image)
|
img, err := reference.ParseNormalizedNamed(service.Image)
|
||||||
|
|||||||
@ -70,6 +70,21 @@ func ValidateRecipe(args []string, cmdName string) recipe.Recipe {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = chosenRecipe.GetComposeConfig(nil)
|
||||||
|
if err != nil {
|
||||||
|
if cmdName == i18n.G("generate") {
|
||||||
|
if strings.Contains(err.Error(), "missing a compose") {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Warn(err)
|
||||||
|
} else {
|
||||||
|
if strings.Contains(err.Error(), "template_driver is not allowed") {
|
||||||
|
log.Warn(i18n.G("ensure %s recipe compose.* files include \"version: '3.8'\"", recipeName))
|
||||||
|
}
|
||||||
|
log.Fatal(i18n.G("unable to validate recipe: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log.Debug(i18n.G("validated %s as recipe argument", recipeName))
|
log.Debug(i18n.G("validated %s as recipe argument", recipeName))
|
||||||
|
|
||||||
return chosenRecipe
|
return chosenRecipe
|
||||||
|
|||||||
@ -312,11 +312,10 @@ likely to change.
|
|||||||
func GetImageVersions(recipe recipePkg.Recipe) (map[string]string, error) {
|
func GetImageVersions(recipe recipePkg.Recipe) (map[string]string, error) {
|
||||||
services := make(map[string]string)
|
services := make(map[string]string)
|
||||||
|
|
||||||
config, err := recipe.GetComposeConfig()
|
config, err := recipe.GetComposeConfig(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
missingTag := false
|
missingTag := false
|
||||||
for _, service := range config.Services {
|
for _, service := range config.Services {
|
||||||
if service.Image == "" {
|
if service.Image == "" {
|
||||||
|
|||||||
@ -124,7 +124,7 @@ interface.`),
|
|||||||
log.Debug(i18n.G("did not find versions file for %s", recipe.Name))
|
log.Debug(i18n.G("did not find versions file for %s", recipe.Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := recipe.GetComposeConfig()
|
config, err := recipe.GetComposeConfig(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
6
go.mod
6
go.mod
@ -9,7 +9,6 @@ require (
|
|||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/charmbracelet/log v1.0.0
|
github.com/charmbracelet/log v1.0.0
|
||||||
github.com/compose-spec/compose-go/v2 v2.10.1
|
|
||||||
github.com/distribution/reference v0.6.0
|
github.com/distribution/reference v0.6.0
|
||||||
github.com/docker/cli v28.4.0+incompatible
|
github.com/docker/cli v28.4.0+incompatible
|
||||||
github.com/docker/docker v28.5.2+incompatible
|
github.com/docker/docker v28.5.2+incompatible
|
||||||
@ -81,7 +80,6 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.21 // indirect
|
github.com/mattn/go-runewidth v0.0.21 // indirect
|
||||||
github.com/mattn/go-shellwords v1.0.12 // indirect
|
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||||
@ -105,14 +103,12 @@ require (
|
|||||||
github.com/prometheus/procfs v0.20.1 // indirect
|
github.com/prometheus/procfs v0.20.1 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect
|
|
||||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||||
github.com/skeema/knownhosts v1.3.2 // indirect
|
github.com/skeema/knownhosts v1.3.2 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||||
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
|
||||||
@ -128,11 +124,9 @@ require (
|
|||||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect
|
|
||||||
golang.org/x/crypto v0.49.0 // indirect
|
golang.org/x/crypto v0.49.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
|
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
|
||||||
golang.org/x/net v0.52.0 // indirect
|
golang.org/x/net v0.52.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
|
||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
golang.org/x/time v0.15.0 // indirect
|
golang.org/x/time v0.15.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||||
|
|||||||
13
go.sum
13
go.sum
@ -176,8 +176,6 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX
|
|||||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
|
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
|
||||||
github.com/compose-spec/compose-go/v2 v2.10.1 h1:mFbXobojGRFIVi1UknrvaDAZ+PkJfyjqkA1yseh+vAU=
|
|
||||||
github.com/compose-spec/compose-go/v2 v2.10.1/go.mod h1:Ohac1SzhO/4fXXrzWIztIVB6ckmKBv1Nt5Z5mGVESUg=
|
|
||||||
github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE=
|
github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE=
|
||||||
github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU=
|
github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU=
|
||||||
github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=
|
github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=
|
||||||
@ -320,8 +318,6 @@ github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11
|
|||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
|
||||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
|
||||||
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
||||||
github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
|
github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
|
||||||
@ -636,7 +632,6 @@ github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEj
|
|||||||
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||||
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||||
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
|
||||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||||
github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
@ -812,8 +807,6 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
|
|||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
|
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
|
||||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
|
|
||||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
|
||||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
|
github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
|
||||||
github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
|
github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
|
||||||
@ -901,8 +894,6 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:
|
|||||||
github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
|
github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||||
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
|
|
||||||
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
|
|
||||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
@ -957,8 +948,6 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
|||||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
|
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
|
||||||
golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
@ -1073,8 +1062,6 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
|||||||
@ -18,10 +18,10 @@ import (
|
|||||||
"coopcloud.tech/abra/pkg/recipe"
|
"coopcloud.tech/abra/pkg/recipe"
|
||||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||||
composeGoTypes "github.com/compose-spec/compose-go/v2/types"
|
|
||||||
|
|
||||||
"coopcloud.tech/abra/pkg/log"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
loader "coopcloud.tech/abra/pkg/upstream/stack"
|
loader "coopcloud.tech/abra/pkg/upstream/stack"
|
||||||
|
composetypes "github.com/docker/cli/cli/compose/types"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/schollz/progressbar/v3"
|
"github.com/schollz/progressbar/v3"
|
||||||
)
|
)
|
||||||
@ -179,7 +179,8 @@ func (a App) Filters(appendServiceNames, exactMatch bool, services ...string) (f
|
|||||||
return filters, err
|
return filters, err
|
||||||
}
|
}
|
||||||
|
|
||||||
compose, err := GetAppComposeConfig(composeFiles, a.Env)
|
opts := stack.Deploy{Composefiles: composeFiles}
|
||||||
|
compose, err := GetAppComposeConfig(a.Recipe.Name, opts, a.Env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return filters, err
|
return filters, err
|
||||||
}
|
}
|
||||||
@ -332,7 +333,8 @@ func GetAppServiceNames(appName string) ([]string, error) {
|
|||||||
return serviceNames, err
|
return serviceNames, err
|
||||||
}
|
}
|
||||||
|
|
||||||
compose, err := GetAppComposeConfig(composeFiles, app.Env)
|
opts := stack.Deploy{Composefiles: composeFiles}
|
||||||
|
compose, err := GetAppComposeConfig(app.Recipe.Name, opts, app.Env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return serviceNames, err
|
return serviceNames, err
|
||||||
}
|
}
|
||||||
@ -488,18 +490,13 @@ func GetAppStatuses(apps []App, MachineReadable bool) (map[string]map[string]str
|
|||||||
// GetAppComposeConfig retrieves a compose specification for a recipe. This
|
// GetAppComposeConfig retrieves a compose specification for a recipe. This
|
||||||
// specification is the result of a merge of all the compose.**.yml files in
|
// specification is the result of a merge of all the compose.**.yml files in
|
||||||
// the recipe repository.
|
// the recipe repository.
|
||||||
func GetAppComposeConfig(composeFiles []string, appEnv envfile.AppEnv) (*composeGoTypes.Project, error) {
|
func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv envfile.AppEnv) (*composetypes.Config, error) {
|
||||||
compose, err := loader.LoadCompose(loader.LoadConf{ComposeFiles: composeFiles, AppEnv: appEnv})
|
compose, err := loader.LoadComposefile(opts, appEnv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &composeGoTypes.Project{}, err
|
return &composetypes.Config{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
recipeName, exists := appEnv["RECIPE"]
|
log.Debug(i18n.G("retrieved %s for %s", compose.Filename, recipe))
|
||||||
if !exists {
|
|
||||||
recipeName, _ = appEnv["TYPE"]
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug(i18n.G("retrieved %s for %s", compose.Name, recipeName))
|
|
||||||
|
|
||||||
return compose, nil
|
return compose, nil
|
||||||
}
|
}
|
||||||
@ -507,7 +504,7 @@ func GetAppComposeConfig(composeFiles []string, appEnv envfile.AppEnv) (*compose
|
|||||||
// ExposeAllEnv exposes all env variables to the app container
|
// ExposeAllEnv exposes all env variables to the app container
|
||||||
func ExposeAllEnv(
|
func ExposeAllEnv(
|
||||||
stackName string,
|
stackName string,
|
||||||
compose *composeGoTypes.Project,
|
compose *composetypes.Config,
|
||||||
appEnv envfile.AppEnv,
|
appEnv envfile.AppEnv,
|
||||||
toDeployVersion string) {
|
toDeployVersion string) {
|
||||||
for _, service := range compose.Services {
|
for _, service := range compose.Services {
|
||||||
|
|||||||
@ -7,12 +7,12 @@ import (
|
|||||||
|
|
||||||
"coopcloud.tech/abra/pkg/i18n"
|
"coopcloud.tech/abra/pkg/i18n"
|
||||||
"coopcloud.tech/abra/pkg/log"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
composeGoTypes "github.com/compose-spec/compose-go/v2/types"
|
composetypes "github.com/docker/cli/cli/compose/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetRecipeLabel adds the label 'coop-cloud.${STACK_NAME}.recipe=${RECIPE}' to the app container
|
// SetRecipeLabel adds the label 'coop-cloud.${STACK_NAME}.recipe=${RECIPE}' to the app container
|
||||||
// to signal which recipe is connected to the deployed app
|
// to signal which recipe is connected to the deployed app
|
||||||
func SetRecipeLabel(compose *composeGoTypes.Project, stackName string, recipe string) {
|
func SetRecipeLabel(compose *composetypes.Config, stackName string, recipe string) {
|
||||||
for _, service := range compose.Services {
|
for _, service := range compose.Services {
|
||||||
if service.Name == "app" {
|
if service.Name == "app" {
|
||||||
log.Debug(i18n.G("set recipe label 'coop-cloud.%s.recipe' to %s for %s", stackName, recipe, stackName))
|
log.Debug(i18n.G("set recipe label 'coop-cloud.%s.recipe' to %s for %s", stackName, recipe, stackName))
|
||||||
@ -24,7 +24,7 @@ func SetRecipeLabel(compose *composeGoTypes.Project, stackName string, recipe st
|
|||||||
|
|
||||||
// SetChaosLabel adds the label 'coop-cloud.${STACK_NAME}.chaos=true/false' to the app container
|
// SetChaosLabel adds the label 'coop-cloud.${STACK_NAME}.chaos=true/false' to the app container
|
||||||
// to signal if the app is deployed in chaos mode
|
// to signal if the app is deployed in chaos mode
|
||||||
func SetChaosLabel(compose *composeGoTypes.Project, stackName string, chaos bool) {
|
func SetChaosLabel(compose *composetypes.Config, stackName string, chaos bool) {
|
||||||
for _, service := range compose.Services {
|
for _, service := range compose.Services {
|
||||||
if service.Name == "app" {
|
if service.Name == "app" {
|
||||||
log.Debug(i18n.G("set label 'coop-cloud.%s.chaos' to %v for %s", stackName, chaos, stackName))
|
log.Debug(i18n.G("set label 'coop-cloud.%s.chaos' to %v for %s", stackName, chaos, stackName))
|
||||||
@ -35,7 +35,7 @@ func SetChaosLabel(compose *composeGoTypes.Project, stackName string, chaos bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetChaosVersionLabel adds the label 'coop-cloud.${STACK_NAME}.chaos-version=$(GIT_COMMIT)' to the app container
|
// SetChaosVersionLabel adds the label 'coop-cloud.${STACK_NAME}.chaos-version=$(GIT_COMMIT)' to the app container
|
||||||
func SetChaosVersionLabel(compose *composeGoTypes.Project, stackName string, chaosVersion string) {
|
func SetChaosVersionLabel(compose *composetypes.Config, stackName string, chaosVersion string) {
|
||||||
for _, service := range compose.Services {
|
for _, service := range compose.Services {
|
||||||
if service.Name == "app" {
|
if service.Name == "app" {
|
||||||
log.Debug(i18n.G("set label 'coop-cloud.%s.chaos-version' to %v for %s", stackName, chaosVersion, stackName))
|
log.Debug(i18n.G("set label 'coop-cloud.%s.chaos-version' to %v for %s", stackName, chaosVersion, stackName))
|
||||||
@ -45,7 +45,7 @@ func SetChaosVersionLabel(compose *composeGoTypes.Project, stackName string, cha
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetVersionLabel(compose *composeGoTypes.Project, stackName string, version string) {
|
func SetVersionLabel(compose *composetypes.Config, stackName string, version string) {
|
||||||
for _, service := range compose.Services {
|
for _, service := range compose.Services {
|
||||||
if service.Name == "app" {
|
if service.Name == "app" {
|
||||||
log.Debug(i18n.G("set label 'coop-cloud.%s.version' to %v for %s", stackName, version, stackName))
|
log.Debug(i18n.G("set label 'coop-cloud.%s.version' to %v for %s", stackName, version, stackName))
|
||||||
@ -56,7 +56,7 @@ func SetVersionLabel(compose *composeGoTypes.Project, stackName string, version
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetLabel reads docker labels in the format of "coop-cloud.${STACK_NAME}.${LABEL}" from the local compose files
|
// GetLabel reads docker labels in the format of "coop-cloud.${STACK_NAME}.${LABEL}" from the local compose files
|
||||||
func GetLabel(compose *composeGoTypes.Project, stackName string, label string) string {
|
func GetLabel(compose *composetypes.Config, stackName string, label string) string {
|
||||||
for _, service := range compose.Services {
|
for _, service := range compose.Services {
|
||||||
if service.Name == "app" {
|
if service.Name == "app" {
|
||||||
labelKey := fmt.Sprintf("coop-cloud.%s.%s", stackName, label)
|
labelKey := fmt.Sprintf("coop-cloud.%s.%s", stackName, label)
|
||||||
@ -73,7 +73,7 @@ func GetLabel(compose *composeGoTypes.Project, stackName string, label string) s
|
|||||||
// GetTimeoutFromLabel reads the timeout value from docker label
|
// GetTimeoutFromLabel reads the timeout value from docker label
|
||||||
// `coop-cloud.${STACK_NAME}.timeout=...` if present. A value is present if the
|
// `coop-cloud.${STACK_NAME}.timeout=...` if present. A value is present if the
|
||||||
// operator uses a `TIMEOUT=...` in their app env.
|
// operator uses a `TIMEOUT=...` in their app env.
|
||||||
func GetTimeoutFromLabel(compose *composeGoTypes.Project, stackName string) (int, error) {
|
func GetTimeoutFromLabel(compose *composetypes.Config, stackName string) (int, error) {
|
||||||
var timeout int
|
var timeout int
|
||||||
|
|
||||||
if timeoutLabel := GetLabel(compose, stackName, "timeout"); timeoutLabel != "" {
|
if timeoutLabel := GetLabel(compose, stackName, "timeout"); timeoutLabel != "" {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
appPkg "coopcloud.tech/abra/pkg/app"
|
appPkg "coopcloud.tech/abra/pkg/app"
|
||||||
"coopcloud.tech/abra/pkg/test"
|
"coopcloud.tech/abra/pkg/test"
|
||||||
testPkg "coopcloud.tech/abra/pkg/test"
|
testPkg "coopcloud.tech/abra/pkg/test"
|
||||||
|
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@ -39,8 +40,15 @@ func TestGetTimeoutFromLabel(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.Env["STACK_NAME"] = app.StackName()
|
deployOpts := stack.Deploy{
|
||||||
compose, err := appPkg.GetAppComposeConfig(composeFiles, app.Env)
|
Composefiles: composeFiles,
|
||||||
|
Namespace: app.StackName(),
|
||||||
|
Prune: false,
|
||||||
|
ResolveImage: stack.ResolveImageAlways,
|
||||||
|
Detach: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -120,7 +120,7 @@ func CommandNameComplete(appName string) ([]string, cobra.ShellCompDirective) {
|
|||||||
func SecretComplete(recipeName string) ([]string, cobra.ShellCompDirective) {
|
func SecretComplete(recipeName string) ([]string, cobra.ShellCompDirective) {
|
||||||
r := recipe.Get(recipeName)
|
r := recipe.Get(recipeName)
|
||||||
|
|
||||||
config, err := r.GetComposeConfig()
|
config, err := r.GetComposeConfig(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := i18n.G("autocomplete failed: %s", err)
|
err := i18n.G("autocomplete failed: %s", err)
|
||||||
return []string{err}, cobra.ShellCompDirectiveError
|
return []string{err}, cobra.ShellCompDirectiveError
|
||||||
|
|||||||
@ -14,8 +14,8 @@ import (
|
|||||||
"coopcloud.tech/abra/pkg/recipe"
|
"coopcloud.tech/abra/pkg/recipe"
|
||||||
"coopcloud.tech/abra/pkg/secret"
|
"coopcloud.tech/abra/pkg/secret"
|
||||||
|
|
||||||
composeGoTypes "github.com/compose-spec/compose-go/v2/types"
|
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
|
composetypes "github.com/docker/cli/cli/compose/types"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
dockerClient "github.com/docker/docker/client"
|
dockerClient "github.com/docker/docker/client"
|
||||||
)
|
)
|
||||||
@ -229,7 +229,7 @@ func GatherSecretsForDeploy(cl *dockerClient.Client, app appPkg.App, showUnchang
|
|||||||
return secretInfo, nil
|
return secretInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GatherConfigsForDeploy(cl *dockerClient.Client, app appPkg.App, compose *composeGoTypes.Project, abraShEnv map[string]string, showUnchanged bool) ([]string, error) {
|
func GatherConfigsForDeploy(cl *dockerClient.Client, app appPkg.App, compose *composetypes.Config, abraShEnv map[string]string, showUnchanged bool) ([]string, error) {
|
||||||
// Get current configs from existing deployment
|
// Get current configs from existing deployment
|
||||||
currentConfigs, err := GetConfigsForStack(cl, app)
|
currentConfigs, err := GetConfigsForStack(cl, app)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -268,7 +268,7 @@ func GatherConfigsForDeploy(cl *dockerClient.Client, app appPkg.App, compose *co
|
|||||||
return configInfo, nil
|
return configInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GatherImagesForDeploy(cl *dockerClient.Client, app appPkg.App, compose *composeGoTypes.Project, showUnchanged bool) ([]string, error) {
|
func GatherImagesForDeploy(cl *dockerClient.Client, app appPkg.App, compose *composetypes.Config, showUnchanged bool) ([]string, error) {
|
||||||
// Get current images from existing deployment
|
// Get current images from existing deployment
|
||||||
currentImages, err := GetImagesForStack(cl, app)
|
currentImages, err := GetImagesForStack(cl, app)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -62,6 +62,13 @@ func (l LintRule) Skip(recipe recipe.Recipe) bool {
|
|||||||
|
|
||||||
var LintRules = map[string][]LintRule{
|
var LintRules = map[string][]LintRule{
|
||||||
"warn": {
|
"warn": {
|
||||||
|
{
|
||||||
|
Ref: "R001",
|
||||||
|
Level: i18n.G("warn"),
|
||||||
|
Description: i18n.G("compose config has expected version"),
|
||||||
|
HowToResolve: i18n.G("ensure 'version: \"3.8\"' in compose configs"),
|
||||||
|
Function: LintComposeVersion,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Ref: "R002",
|
Ref: "R002",
|
||||||
Level: i18n.G("warn"),
|
Level: i18n.G("warn"),
|
||||||
@ -210,6 +217,18 @@ func LintForErrors(recipe recipe.Recipe) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LintComposeVersion(recipe recipe.Recipe) (bool, error) {
|
||||||
|
config, err := recipe.GetComposeConfig(nil)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if config.Version == "3.8" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
func LintEnvConfigPresent(r recipe.Recipe) (bool, error) {
|
func LintEnvConfigPresent(r recipe.Recipe) (bool, error) {
|
||||||
if _, err := os.Stat(r.SampleEnvPath); !os.IsNotExist(err) {
|
if _, err := os.Stat(r.SampleEnvPath); !os.IsNotExist(err) {
|
||||||
return true, nil
|
return true, nil
|
||||||
@ -219,7 +238,7 @@ func LintEnvConfigPresent(r recipe.Recipe) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func LintAppService(recipe recipe.Recipe) (bool, error) {
|
func LintAppService(recipe recipe.Recipe) (bool, error) {
|
||||||
config, err := recipe.GetComposeConfig()
|
config, err := recipe.GetComposeConfig(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -250,7 +269,7 @@ func LintTraefikEnabledSkipCondition(r recipe.Recipe) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func LintTraefikEnabled(recipe recipe.Recipe) (bool, error) {
|
func LintTraefikEnabled(recipe recipe.Recipe) (bool, error) {
|
||||||
config, err := recipe.GetComposeConfig()
|
config, err := recipe.GetComposeConfig(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -268,7 +287,7 @@ func LintTraefikEnabled(recipe recipe.Recipe) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func LintDeployLabelsPresent(recipe recipe.Recipe) (bool, error) {
|
func LintDeployLabelsPresent(recipe recipe.Recipe) (bool, error) {
|
||||||
config, err := recipe.GetComposeConfig()
|
config, err := recipe.GetComposeConfig(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -283,7 +302,7 @@ func LintDeployLabelsPresent(recipe recipe.Recipe) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func LintHealthchecks(recipe recipe.Recipe) (bool, error) {
|
func LintHealthchecks(recipe recipe.Recipe) (bool, error) {
|
||||||
config, err := recipe.GetComposeConfig()
|
config, err := recipe.GetComposeConfig(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -297,7 +316,7 @@ func LintHealthchecks(recipe recipe.Recipe) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func LintAllImagesTagged(recipe recipe.Recipe) (bool, error) {
|
func LintAllImagesTagged(recipe recipe.Recipe) (bool, error) {
|
||||||
config, err := recipe.GetComposeConfig()
|
config, err := recipe.GetComposeConfig(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -315,7 +334,7 @@ func LintAllImagesTagged(recipe recipe.Recipe) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func LintNoUnstableTags(recipe recipe.Recipe) (bool, error) {
|
func LintNoUnstableTags(recipe recipe.Recipe) (bool, error) {
|
||||||
config, err := recipe.GetComposeConfig()
|
config, err := recipe.GetComposeConfig(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -342,7 +361,7 @@ func LintNoUnstableTags(recipe recipe.Recipe) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func LintSemverLikeTags(recipe recipe.Recipe) (bool, error) {
|
func LintSemverLikeTags(recipe recipe.Recipe) (bool, error) {
|
||||||
config, err := recipe.GetComposeConfig()
|
config, err := recipe.GetComposeConfig(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -369,7 +388,7 @@ func LintSemverLikeTags(recipe recipe.Recipe) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func LintImagePresent(recipe recipe.Recipe) (bool, error) {
|
func LintImagePresent(recipe recipe.Recipe) (bool, error) {
|
||||||
config, err := recipe.GetComposeConfig()
|
config, err := recipe.GetComposeConfig(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -421,7 +440,7 @@ func LintMetadataFilledIn(r recipe.Recipe) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func LintAbraShVendors(recipe recipe.Recipe) (bool, error) {
|
func LintAbraShVendors(recipe recipe.Recipe) (bool, error) {
|
||||||
config, err := recipe.GetComposeConfig()
|
config, err := recipe.GetComposeConfig(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -453,7 +472,7 @@ func LintHasRecipeRepo(recipe recipe.Recipe) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func LintSecretLengths(recipe recipe.Recipe) (bool, error) {
|
func LintSecretLengths(recipe recipe.Recipe) (bool, error) {
|
||||||
config, err := recipe.GetComposeConfig()
|
config, err := recipe.GetComposeConfig(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,9 +11,10 @@ import (
|
|||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
"coopcloud.tech/abra/pkg/i18n"
|
"coopcloud.tech/abra/pkg/i18n"
|
||||||
"coopcloud.tech/abra/pkg/log"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
|
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||||
loader "coopcloud.tech/abra/pkg/upstream/stack"
|
loader "coopcloud.tech/abra/pkg/upstream/stack"
|
||||||
composeGoTypes "github.com/compose-spec/compose-go/v2/types"
|
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
|
composetypes "github.com/docker/cli/cli/compose/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetComposeFiles gets the list of compose files for an app (or recipe if you
|
// GetComposeFiles gets the list of compose files for an app (or recipe if you
|
||||||
@ -60,7 +61,7 @@ func (r Recipe) GetComposeFiles(appEnv map[string]string) ([]string, error) {
|
|||||||
return composeFiles, nil
|
return composeFiles, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Recipe) GetComposeConfig() (*composeGoTypes.Project, error) {
|
func (r Recipe) GetComposeConfig(env map[string]string) (*composetypes.Config, error) {
|
||||||
pattern := fmt.Sprintf("%s/compose**yml", r.Dir)
|
pattern := fmt.Sprintf("%s/compose**yml", r.Dir)
|
||||||
composeFiles, err := filepath.Glob(pattern)
|
composeFiles, err := filepath.Glob(pattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -71,18 +72,25 @@ func (r Recipe) GetComposeConfig() (*composeGoTypes.Project, error) {
|
|||||||
return nil, errors.New(i18n.G("%s is missing a compose.yml or compose.*.yml file?", r.Name))
|
return nil, errors.New(i18n.G("%s is missing a compose.yml or compose.*.yml file?", r.Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := loader.LoadCompose(loader.LoadConf{ComposeFiles: composeFiles})
|
if env == nil {
|
||||||
|
env, err = r.SampleEnv()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := stack.Deploy{Composefiles: composeFiles}
|
||||||
|
config, err := loader.LoadComposefile(opts, env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetVersionLabelLocal retrieves the version label on the local recipe config
|
// GetVersionLabelLocal retrieves the version label on the local recipe config
|
||||||
func (r Recipe) GetVersionLabelLocal() (string, error) {
|
func (r Recipe) GetVersionLabelLocal() (string, error) {
|
||||||
var label string
|
var label string
|
||||||
config, err := r.GetComposeConfig()
|
config, err := r.GetComposeConfig(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@ -115,7 +123,14 @@ func (r Recipe) UpdateTag(image, tag string) (bool, error) {
|
|||||||
log.Debug(i18n.G("considering %s config(s) for tag update", strings.Join(composeFiles, ", ")))
|
log.Debug(i18n.G("considering %s config(s) for tag update", strings.Join(composeFiles, ", ")))
|
||||||
|
|
||||||
for _, composeFile := range composeFiles {
|
for _, composeFile := range composeFiles {
|
||||||
compose, err := loader.LoadCompose(loader.LoadConf{ComposeFiles: []string{composeFile}})
|
opts := stack.Deploy{Composefiles: []string{composeFile}}
|
||||||
|
|
||||||
|
sampleEnv, err := r.SampleEnv()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
compose, err := loader.LoadComposefile(opts, sampleEnv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -153,9 +168,9 @@ func (r Recipe) UpdateTag(image, tag string) (bool, error) {
|
|||||||
new := fmt.Sprintf("%s:%s", composeImage, tag)
|
new := fmt.Sprintf("%s:%s", composeImage, tag)
|
||||||
replacedBytes := strings.Replace(string(bytes), old, new, -1)
|
replacedBytes := strings.Replace(string(bytes), old, new, -1)
|
||||||
|
|
||||||
log.Debug(i18n.G("updating %s to %s in %s", old, new, compose.Name))
|
log.Debug(i18n.G("updating %s to %s in %s", old, new, compose.Filename))
|
||||||
|
|
||||||
if err := os.WriteFile(compose.Name, []byte(replacedBytes), 0o764); err != nil {
|
if err := os.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -176,13 +191,20 @@ func (r Recipe) UpdateLabel(pattern, serviceName, label string) error {
|
|||||||
log.Debug(i18n.G("considering %s config(s) for label update", strings.Join(composeFiles, ", ")))
|
log.Debug(i18n.G("considering %s config(s) for label update", strings.Join(composeFiles, ", ")))
|
||||||
|
|
||||||
for _, composeFile := range composeFiles {
|
for _, composeFile := range composeFiles {
|
||||||
compose, err := loader.LoadCompose(loader.LoadConf{ComposeFiles: []string{composeFile}})
|
opts := stack.Deploy{Composefiles: []string{composeFile}}
|
||||||
|
|
||||||
|
sampleEnv, err := r.SampleEnv()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
compose, err := loader.LoadComposefile(opts, sampleEnv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceExists := false
|
serviceExists := false
|
||||||
var service composeGoTypes.ServiceConfig
|
var service composetypes.ServiceConfig
|
||||||
for _, s := range compose.Services {
|
for _, s := range compose.Services {
|
||||||
if s.Name == serviceName {
|
if s.Name == serviceName {
|
||||||
service = s
|
service = s
|
||||||
@ -212,9 +234,9 @@ func (r Recipe) UpdateLabel(pattern, serviceName, label string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug(i18n.G("updating %s to %s in %s", old, label, compose.Name))
|
log.Debug(i18n.G("updating %s to %s in %s", old, label, compose.Filename))
|
||||||
|
|
||||||
if err := ioutil.WriteFile(compose.Name, []byte(replacedBytes), 0o764); err != nil {
|
if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -416,7 +416,7 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
|
|||||||
|
|
||||||
log.Debug(i18n.G("git checkout: %s in %s", ref.Name(), r.Dir))
|
log.Debug(i18n.G("git checkout: %s in %s", ref.Name(), r.Dir))
|
||||||
|
|
||||||
config, err := r.GetComposeConfig()
|
config, err := r.GetComposeConfig(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug(i18n.G("failed to get compose config for %s: %s", tag, err))
|
log.Debug(i18n.G("failed to get compose config for %s: %s", tag, err))
|
||||||
warnMsg = append(warnMsg, i18n.G("skipping tag %s: invalid compose config: %s", tag, err))
|
warnMsg = append(warnMsg, i18n.G("skipping tag %s: invalid compose config: %s", tag, err))
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import (
|
|||||||
"coopcloud.tech/abra/pkg/envfile"
|
"coopcloud.tech/abra/pkg/envfile"
|
||||||
"coopcloud.tech/abra/pkg/i18n"
|
"coopcloud.tech/abra/pkg/i18n"
|
||||||
"coopcloud.tech/abra/pkg/log"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
|
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||||
loader "coopcloud.tech/abra/pkg/upstream/stack"
|
loader "coopcloud.tech/abra/pkg/upstream/stack"
|
||||||
"github.com/decentral1se/passgen"
|
"github.com/decentral1se/passgen"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
@ -121,13 +122,14 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
|
|||||||
// Set the STACK_NAME to be able to generate the remote name correctly.
|
// Set the STACK_NAME to be able to generate the remote name correctly.
|
||||||
appEnv["STACK_NAME"] = stackName
|
appEnv["STACK_NAME"] = stackName
|
||||||
|
|
||||||
composeConfig, err := loader.LoadCompose(loader.LoadConf{ComposeFiles: composeFiles, AppEnv: appEnv})
|
opts := stack.Deploy{Composefiles: composeFiles}
|
||||||
|
composeConfig, err := loader.LoadComposefile(opts, appEnv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the compose files without injecting environment variables.
|
// Read the compose files without injecting environment variables.
|
||||||
configWithoutEnv, err := loader.LoadCompose(loader.LoadConf{ComposeFiles: composeFiles})
|
configWithoutEnv, err := loader.LoadComposefile(opts, map[string]string{}, loader.SkipInterpolation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,8 +5,6 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||||
"git.coopcloud.tech/toolshed/godotenv"
|
"git.coopcloud.tech/toolshed/godotenv"
|
||||||
@ -14,7 +12,6 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
AppName = "test_app.example.com"
|
AppName = "test_app.example.com"
|
||||||
StackName = "test_app_example_com"
|
|
||||||
ServerName = "test_server"
|
ServerName = "test_server"
|
||||||
RecipeName = "test_recipe"
|
RecipeName = "test_recipe"
|
||||||
|
|
||||||
@ -62,19 +59,13 @@ func Setup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, f, _, ok := runtime.Caller(0)
|
serverSrcDir := os.ExpandEnv("$PWD/../../tests/resources/test_server")
|
||||||
if !ok {
|
|
||||||
log.Fatal("Setup: unable to discover current working directory of file")
|
|
||||||
}
|
|
||||||
pwd := filepath.Dir(f)
|
|
||||||
|
|
||||||
serverSrcDir := filepath.Join(pwd, "/../../tests/resources/test_server")
|
|
||||||
serverDestDir := os.ExpandEnv("$ABRA_DIR/servers/test_server")
|
serverDestDir := os.ExpandEnv("$ABRA_DIR/servers/test_server")
|
||||||
if err := os.CopyFS(serverDestDir, os.DirFS(serverSrcDir)); err != nil {
|
if err := os.CopyFS(serverDestDir, os.DirFS(serverSrcDir)); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
recipeSrcDir := filepath.Join(pwd, "/../../tests/resources/test_recipe")
|
recipeSrcDir := os.ExpandEnv("$PWD/../../tests/resources/test_recipe")
|
||||||
recipeDestDir := os.ExpandEnv("$ABRA_DIR/recipes/test_recipe")
|
recipeDestDir := os.ExpandEnv("$ABRA_DIR/recipes/test_recipe")
|
||||||
if err := os.CopyFS(recipeDestDir, os.DirFS(recipeSrcDir)); err != nil {
|
if err := os.CopyFS(recipeDestDir, os.DirFS(recipeSrcDir)); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
composeGoTypes "github.com/compose-spec/compose-go/v2/types"
|
composetypes "github.com/docker/cli/cli/compose/types"
|
||||||
networktypes "github.com/docker/docker/api/types/network"
|
networktypes "github.com/docker/docker/api/types/network"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
)
|
)
|
||||||
@ -48,17 +48,19 @@ func AddStackLabel(namespace Namespace, labels map[string]string) map[string]str
|
|||||||
return labels
|
return labels
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type networkMap map[string]composetypes.NetworkConfig
|
||||||
|
|
||||||
// Networks from the compose-file type to the engine API type
|
// Networks from the compose-file type to the engine API type
|
||||||
func Networks(namespace Namespace, networks map[string]composeGoTypes.NetworkConfig, servicesNetworks map[string]struct{}) (map[string]networktypes.CreateOptions, []string) {
|
func Networks(namespace Namespace, networks networkMap, servicesNetworks map[string]struct{}) (map[string]networktypes.CreateOptions, []string) {
|
||||||
if networks == nil {
|
if networks == nil {
|
||||||
networks = make(map[string]composeGoTypes.NetworkConfig)
|
networks = make(map[string]composetypes.NetworkConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
externalNetworks := []string{}
|
externalNetworks := []string{}
|
||||||
result := make(map[string]networktypes.CreateOptions)
|
result := make(map[string]networktypes.CreateOptions)
|
||||||
for internalName := range servicesNetworks {
|
for internalName := range servicesNetworks {
|
||||||
network := networks[internalName]
|
network := networks[internalName]
|
||||||
if network.External {
|
if network.External.External {
|
||||||
externalNetworks = append(externalNetworks, network.Name)
|
externalNetworks = append(externalNetworks, network.Name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -96,19 +98,19 @@ func Networks(namespace Namespace, networks map[string]composeGoTypes.NetworkCon
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Secrets converts secrets from the Compose type to the engine API type
|
// Secrets converts secrets from the Compose type to the engine API type
|
||||||
func Secrets(namespace Namespace, secrets map[string]composeGoTypes.SecretConfig) ([]swarm.SecretSpec, error) {
|
func Secrets(namespace Namespace, secrets map[string]composetypes.SecretConfig) ([]swarm.SecretSpec, error) {
|
||||||
result := []swarm.SecretSpec{}
|
result := []swarm.SecretSpec{}
|
||||||
for name, secret := range secrets {
|
for name, secret := range secrets {
|
||||||
if secret.External {
|
if secret.External.External {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var obj swarmFileObject
|
var obj swarmFileObject
|
||||||
var err error
|
var err error
|
||||||
if secret.Driver != "" {
|
if secret.Driver != "" {
|
||||||
obj = driverObjectConfig(namespace, name, composeGoTypes.FileObjectConfig(secret))
|
obj = driverObjectConfig(namespace, name, composetypes.FileObjectConfig(secret))
|
||||||
} else {
|
} else {
|
||||||
obj, err = fileObjectConfig(namespace, name, composeGoTypes.FileObjectConfig(secret))
|
obj, err = fileObjectConfig(namespace, name, composetypes.FileObjectConfig(secret))
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -131,14 +133,14 @@ func Secrets(namespace Namespace, secrets map[string]composeGoTypes.SecretConfig
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Configs converts config objects from the Compose type to the engine API type
|
// Configs converts config objects from the Compose type to the engine API type
|
||||||
func Configs(namespace Namespace, configs map[string]composeGoTypes.ConfigObjConfig) ([]swarm.ConfigSpec, error) {
|
func Configs(namespace Namespace, configs map[string]composetypes.ConfigObjConfig) ([]swarm.ConfigSpec, error) {
|
||||||
result := []swarm.ConfigSpec{}
|
result := []swarm.ConfigSpec{}
|
||||||
for name, config := range configs {
|
for name, config := range configs {
|
||||||
if config.External {
|
if config.External.External {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
obj, err := fileObjectConfig(namespace, name, composeGoTypes.FileObjectConfig(config))
|
obj, err := fileObjectConfig(namespace, name, composetypes.FileObjectConfig(config))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -158,7 +160,7 @@ type swarmFileObject struct {
|
|||||||
Data []byte
|
Data []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func driverObjectConfig(namespace Namespace, name string, obj composeGoTypes.FileObjectConfig) swarmFileObject {
|
func driverObjectConfig(namespace Namespace, name string, obj composetypes.FileObjectConfig) swarmFileObject {
|
||||||
if obj.Name != "" {
|
if obj.Name != "" {
|
||||||
name = obj.Name
|
name = obj.Name
|
||||||
} else {
|
} else {
|
||||||
@ -174,7 +176,7 @@ func driverObjectConfig(namespace Namespace, name string, obj composeGoTypes.Fil
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileObjectConfig(namespace Namespace, name string, obj composeGoTypes.FileObjectConfig) (swarmFileObject, error) {
|
func fileObjectConfig(namespace Namespace, name string, obj composetypes.FileObjectConfig) (swarmFileObject, error) {
|
||||||
data, err := ioutil.ReadFile(obj.File)
|
data, err := ioutil.ReadFile(obj.File)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return swarmFileObject{}, err
|
return swarmFileObject{}, err
|
||||||
|
|||||||
170
pkg/upstream/convert/compose_test.go
Normal file
170
pkg/upstream/convert/compose_test.go
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
package convert // https://github.com/docker/cli/blob/master/cli/compose/convert/compose_test.go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
composetypes "github.com/docker/cli/cli/compose/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]network.CreateOptions{
|
||||||
|
"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))
|
||||||
|
}
|
||||||
@ -5,13 +5,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"coopcloud.tech/abra/pkg/i18n"
|
"coopcloud.tech/abra/pkg/i18n"
|
||||||
composeGoTypes "github.com/compose-spec/compose-go/v2/types"
|
composetypes "github.com/docker/cli/cli/compose/types"
|
||||||
"github.com/docker/cli/opts"
|
"github.com/docker/cli/opts"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
@ -180,7 +178,7 @@ func ParseConfigs(client client.ConfigAPIClient, requestedConfigs []*swarmtypes.
|
|||||||
// Services from compose-file types to engine API types
|
// Services from compose-file types to engine API types
|
||||||
func Services(
|
func Services(
|
||||||
namespace Namespace,
|
namespace Namespace,
|
||||||
config *composeGoTypes.Project,
|
config *composetypes.Config,
|
||||||
client client.CommonAPIClient,
|
client client.CommonAPIClient,
|
||||||
) (map[string]swarm.ServiceSpec, error) {
|
) (map[string]swarm.ServiceSpec, error) {
|
||||||
result := make(map[string]swarm.ServiceSpec)
|
result := make(map[string]swarm.ServiceSpec)
|
||||||
@ -213,17 +211,14 @@ func Services(
|
|||||||
func Service(
|
func Service(
|
||||||
apiVersion string,
|
apiVersion string,
|
||||||
namespace Namespace,
|
namespace Namespace,
|
||||||
service composeGoTypes.ServiceConfig,
|
service composetypes.ServiceConfig,
|
||||||
networkConfigs map[string]composeGoTypes.NetworkConfig,
|
networkConfigs map[string]composetypes.NetworkConfig,
|
||||||
volumes map[string]composeGoTypes.VolumeConfig,
|
volumes map[string]composetypes.VolumeConfig,
|
||||||
secrets []*swarm.SecretReference,
|
secrets []*swarm.SecretReference,
|
||||||
configs []*swarm.ConfigReference,
|
configs []*swarm.ConfigReference,
|
||||||
) (swarm.ServiceSpec, error) {
|
) (swarm.ServiceSpec, error) {
|
||||||
name := namespace.Scope(service.Name)
|
name := namespace.Scope(service.Name)
|
||||||
endpoint, err := convertEndpointSpec(service.Deploy.EndpointMode, service.Ports)
|
endpoint := convertEndpointSpec(service.Deploy.EndpointMode, service.Ports)
|
||||||
if err != nil {
|
|
||||||
return swarm.ServiceSpec{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
mode, err := convertDeployMode(service.Deploy.Mode, service.Deploy.Replicas)
|
mode, err := convertDeployMode(service.Deploy.Mode, service.Deploy.Replicas)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -259,16 +254,9 @@ func Service(
|
|||||||
dnsConfig := convertDNSConfig(service.DNS, service.DNSSearch)
|
dnsConfig := convertDNSConfig(service.DNS, service.DNSSearch)
|
||||||
|
|
||||||
var privileges swarm.Privileges
|
var privileges swarm.Privileges
|
||||||
|
|
||||||
credSpec := service.CredentialSpec
|
|
||||||
if credSpec == nil {
|
|
||||||
credSpec = &composeGoTypes.CredentialSpecConfig{}
|
|
||||||
}
|
|
||||||
|
|
||||||
privileges.CredentialSpec, err = convertCredentialSpec(
|
privileges.CredentialSpec, err = convertCredentialSpec(
|
||||||
namespace, *credSpec, configs,
|
namespace, service.CredentialSpec, configs,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return swarm.ServiceSpec{}, err
|
return swarm.ServiceSpec{}, err
|
||||||
}
|
}
|
||||||
@ -283,11 +271,6 @@ func Service(
|
|||||||
|
|
||||||
capAdd, capDrop := opts.EffectiveCapAddCapDrop(service.CapAdd, service.CapDrop)
|
capAdd, capDrop := opts.EffectiveCapAddCapDrop(service.CapAdd, service.CapDrop)
|
||||||
|
|
||||||
var stopGracePtr time.Duration
|
|
||||||
if service.StopGracePeriod != nil {
|
|
||||||
stopGracePtr = time.Duration(*service.StopGracePeriod)
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceSpec := swarm.ServiceSpec{
|
serviceSpec := swarm.ServiceSpec{
|
||||||
Annotations: swarm.Annotations{
|
Annotations: swarm.Annotations{
|
||||||
Name: name,
|
Name: name,
|
||||||
@ -307,7 +290,7 @@ func Service(
|
|||||||
Dir: service.WorkingDir,
|
Dir: service.WorkingDir,
|
||||||
User: service.User,
|
User: service.User,
|
||||||
Mounts: mounts,
|
Mounts: mounts,
|
||||||
StopGracePeriod: &stopGracePtr,
|
StopGracePeriod: composetypes.ConvertDurationPtr(service.StopGracePeriod),
|
||||||
StopSignal: service.StopSignal,
|
StopSignal: service.StopSignal,
|
||||||
TTY: service.Tty,
|
TTY: service.Tty,
|
||||||
OpenStdin: service.StdinOpen,
|
OpenStdin: service.StdinOpen,
|
||||||
@ -355,7 +338,7 @@ func Service(
|
|||||||
return serviceSpec, nil
|
return serviceSpec, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPlacementPreference(preferences []composeGoTypes.PlacementPreferences) []swarm.PlacementPreference {
|
func getPlacementPreference(preferences []composetypes.PlacementPreferences) []swarm.PlacementPreference {
|
||||||
result := []swarm.PlacementPreference{}
|
result := []swarm.PlacementPreference{}
|
||||||
for _, preference := range preferences {
|
for _, preference := range preferences {
|
||||||
spreadDescriptor := preference.Spread
|
spreadDescriptor := preference.Spread
|
||||||
@ -374,13 +357,13 @@ func sortStrings(strs []string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func convertServiceNetworks(
|
func convertServiceNetworks(
|
||||||
networks map[string]*composeGoTypes.ServiceNetworkConfig,
|
networks map[string]*composetypes.ServiceNetworkConfig,
|
||||||
networkConfigs map[string]composeGoTypes.NetworkConfig,
|
networkConfigs networkMap,
|
||||||
namespace Namespace,
|
namespace Namespace,
|
||||||
name string,
|
name string,
|
||||||
) ([]swarm.NetworkAttachmentConfig, error) {
|
) ([]swarm.NetworkAttachmentConfig, error) {
|
||||||
if len(networks) == 0 {
|
if len(networks) == 0 {
|
||||||
networks = map[string]*composeGoTypes.ServiceNetworkConfig{
|
networks = map[string]*composetypes.ServiceNetworkConfig{
|
||||||
defaultNetwork: {},
|
defaultNetwork: {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -420,20 +403,20 @@ func convertServiceNetworks(
|
|||||||
func convertServiceSecrets(
|
func convertServiceSecrets(
|
||||||
client client.SecretAPIClient,
|
client client.SecretAPIClient,
|
||||||
namespace Namespace,
|
namespace Namespace,
|
||||||
secrets []composeGoTypes.ServiceSecretConfig,
|
secrets []composetypes.ServiceSecretConfig,
|
||||||
secretSpecs map[string]composeGoTypes.SecretConfig,
|
secretSpecs map[string]composetypes.SecretConfig,
|
||||||
) ([]*swarm.SecretReference, error) {
|
) ([]*swarm.SecretReference, error) {
|
||||||
refs := []*swarm.SecretReference{}
|
refs := []*swarm.SecretReference{}
|
||||||
|
|
||||||
lookup := func(key string) (composeGoTypes.FileObjectConfig, error) {
|
lookup := func(key string) (composetypes.FileObjectConfig, error) {
|
||||||
secretSpec, exists := secretSpecs[key]
|
secretSpec, exists := secretSpecs[key]
|
||||||
if !exists {
|
if !exists {
|
||||||
return composeGoTypes.FileObjectConfig{}, errors.New(i18n.G("undefined secret %q", key))
|
return composetypes.FileObjectConfig{}, errors.New(i18n.G("undefined secret %q", key))
|
||||||
}
|
}
|
||||||
return composeGoTypes.FileObjectConfig(secretSpec), nil
|
return composetypes.FileObjectConfig(secretSpec), nil
|
||||||
}
|
}
|
||||||
for _, secret := range secrets {
|
for _, secret := range secrets {
|
||||||
obj, err := convertFileObject(namespace, composeGoTypes.FileReferenceConfig(secret), lookup)
|
obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(secret), lookup)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -468,20 +451,20 @@ func convertServiceSecrets(
|
|||||||
func convertServiceConfigObjs(
|
func convertServiceConfigObjs(
|
||||||
client client.ConfigAPIClient,
|
client client.ConfigAPIClient,
|
||||||
namespace Namespace,
|
namespace Namespace,
|
||||||
service composeGoTypes.ServiceConfig,
|
service composetypes.ServiceConfig,
|
||||||
configSpecs map[string]composeGoTypes.ConfigObjConfig,
|
configSpecs map[string]composetypes.ConfigObjConfig,
|
||||||
) ([]*swarm.ConfigReference, error) {
|
) ([]*swarm.ConfigReference, error) {
|
||||||
refs := []*swarm.ConfigReference{}
|
refs := []*swarm.ConfigReference{}
|
||||||
|
|
||||||
lookup := func(key string) (composeGoTypes.FileObjectConfig, error) {
|
lookup := func(key string) (composetypes.FileObjectConfig, error) {
|
||||||
configSpec, exists := configSpecs[key]
|
configSpec, exists := configSpecs[key]
|
||||||
if !exists {
|
if !exists {
|
||||||
return composeGoTypes.FileObjectConfig{}, errors.New(i18n.G("undefined config %q", key))
|
return composetypes.FileObjectConfig{}, errors.New(i18n.G("undefined config %q", key))
|
||||||
}
|
}
|
||||||
return composeGoTypes.FileObjectConfig(configSpec), nil
|
return composetypes.FileObjectConfig(configSpec), nil
|
||||||
}
|
}
|
||||||
for _, config := range service.Configs {
|
for _, config := range service.Configs {
|
||||||
obj, err := convertFileObject(namespace, composeGoTypes.FileReferenceConfig(config), lookup)
|
obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(config), lookup)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -504,7 +487,7 @@ func convertServiceConfigObjs(
|
|||||||
// if the credSpec uses a config, then we should grab the config name, and
|
// 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
|
// create a config reference for it. A File or Registry-type CredentialSpec
|
||||||
// does not need this operation.
|
// does not need this operation.
|
||||||
if credSpec != nil && credSpec.Config != "" {
|
if credSpec.Config != "" {
|
||||||
// look up the config in the configSpecs.
|
// look up the config in the configSpecs.
|
||||||
obj, err := lookup(credSpec.Config)
|
obj, err := lookup(credSpec.Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -549,8 +532,8 @@ type swarmReferenceObject struct {
|
|||||||
|
|
||||||
func convertFileObject(
|
func convertFileObject(
|
||||||
namespace Namespace,
|
namespace Namespace,
|
||||||
config composeGoTypes.FileReferenceConfig,
|
config composetypes.FileReferenceConfig,
|
||||||
lookup func(key string) (composeGoTypes.FileObjectConfig, error),
|
lookup func(key string) (composetypes.FileObjectConfig, error),
|
||||||
) (swarmReferenceObject, error) {
|
) (swarmReferenceObject, error) {
|
||||||
obj, err := lookup(config.Source)
|
obj, err := lookup(config.Source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -575,37 +558,40 @@ func convertFileObject(
|
|||||||
if gid == "" {
|
if gid == "" {
|
||||||
gid = "0"
|
gid = "0"
|
||||||
}
|
}
|
||||||
|
mode := config.Mode
|
||||||
|
if mode == nil {
|
||||||
|
mode = uint32Ptr(0444)
|
||||||
|
}
|
||||||
|
|
||||||
ref := swarmReferenceObject{
|
return swarmReferenceObject{
|
||||||
File: swarmReferenceTarget{
|
File: swarmReferenceTarget{
|
||||||
Name: target,
|
Name: target,
|
||||||
UID: uid,
|
UID: uid,
|
||||||
GID: gid,
|
GID: gid,
|
||||||
|
Mode: os.FileMode(*mode),
|
||||||
},
|
},
|
||||||
Name: source,
|
Name: source,
|
||||||
}
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
if config.Mode == nil {
|
func uint32Ptr(value uint32) *uint32 {
|
||||||
defaultMode := 0444
|
return &value
|
||||||
ref.File.Mode = os.FileMode(defaultMode)
|
|
||||||
} else {
|
|
||||||
ref.File.Mode = os.FileMode(*config.Mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ref, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertExtraHosts converts <host>:<ip> mappings to SwarmKit notation:
|
// convertExtraHosts converts <host>:<ip> mappings to SwarmKit notation:
|
||||||
// "IP-address hostname(s)". The original order of mappings is preserved.
|
// "IP-address hostname(s)". The original order of mappings is preserved.
|
||||||
func convertExtraHosts(extraHosts composeGoTypes.HostsList) []string {
|
func convertExtraHosts(extraHosts composetypes.HostsList) []string {
|
||||||
hosts := []string{}
|
hosts := []string{}
|
||||||
for hostName, hostIP := range extraHosts {
|
for _, hostIP := range extraHosts {
|
||||||
hosts = append(hosts, fmt.Sprintf("%s %s", hostIP, hostName))
|
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
|
return hosts
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertHealthcheck(healthcheck *composeGoTypes.HealthCheckConfig) (*container.HealthConfig, error) {
|
func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) {
|
||||||
if healthcheck == nil {
|
if healthcheck == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@ -643,7 +629,7 @@ func convertHealthcheck(healthcheck *composeGoTypes.HealthCheckConfig) (*contain
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertRestartPolicy(restart string, source *composeGoTypes.RestartPolicy) (*swarm.RestartPolicy, error) {
|
func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) {
|
||||||
if source == nil {
|
if source == nil {
|
||||||
policy, err := opts.ParseRestartPolicy(restart)
|
policy, err := opts.ParseRestartPolicy(restart)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -667,25 +653,15 @@ func convertRestartPolicy(restart string, source *composeGoTypes.RestartPolicy)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var windowPtr time.Duration
|
|
||||||
if source.Window != nil {
|
|
||||||
windowPtr = time.Duration(*source.Window)
|
|
||||||
}
|
|
||||||
|
|
||||||
var delayPtr time.Duration
|
|
||||||
if source.Delay != nil {
|
|
||||||
delayPtr = time.Duration(*source.Delay)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &swarm.RestartPolicy{
|
return &swarm.RestartPolicy{
|
||||||
Condition: swarm.RestartPolicyCondition(source.Condition),
|
Condition: swarm.RestartPolicyCondition(source.Condition),
|
||||||
Delay: &delayPtr,
|
Delay: composetypes.ConvertDurationPtr(source.Delay),
|
||||||
MaxAttempts: source.MaxAttempts,
|
MaxAttempts: source.MaxAttempts,
|
||||||
Window: &windowPtr,
|
Window: composetypes.ConvertDurationPtr(source.Window),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertUpdateConfig(source *composeGoTypes.UpdateConfig) *swarm.UpdateConfig {
|
func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig {
|
||||||
if source == nil {
|
if source == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -703,13 +679,13 @@ func convertUpdateConfig(source *composeGoTypes.UpdateConfig) *swarm.UpdateConfi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertResources(source composeGoTypes.Resources) (*swarm.ResourceRequirements, error) {
|
func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) {
|
||||||
resources := &swarm.ResourceRequirements{}
|
resources := &swarm.ResourceRequirements{}
|
||||||
var err error
|
var err error
|
||||||
if source.Limits != nil {
|
if source.Limits != nil {
|
||||||
var cpus int64
|
var cpus int64
|
||||||
if source.Limits.NanoCPUs > 0 {
|
if source.Limits.NanoCPUs != "" {
|
||||||
cpus, err = opts.ParseCPUs(fmt.Sprintf("%f", source.Limits.NanoCPUs))
|
cpus, err = opts.ParseCPUs(source.Limits.NanoCPUs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -722,8 +698,8 @@ func convertResources(source composeGoTypes.Resources) (*swarm.ResourceRequireme
|
|||||||
}
|
}
|
||||||
if source.Reservations != nil {
|
if source.Reservations != nil {
|
||||||
var cpus int64
|
var cpus int64
|
||||||
if source.Reservations.NanoCPUs > 0 {
|
if source.Reservations.NanoCPUs != "" {
|
||||||
cpus, err = opts.ParseCPUs(fmt.Sprintf("%f", source.Reservations.NanoCPUs))
|
cpus, err = opts.ParseCPUs(source.Reservations.NanoCPUs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -752,29 +728,13 @@ func convertResources(source composeGoTypes.Resources) (*swarm.ResourceRequireme
|
|||||||
return resources, nil
|
return resources, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func str2uint32(s string) (uint32, error) {
|
func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortConfig) *swarm.EndpointSpec {
|
||||||
var u32 uint32
|
|
||||||
|
|
||||||
u64, err := strconv.ParseUint(s, 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
return u32, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return uint32(u64), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertEndpointSpec(endpointMode string, source []composeGoTypes.ServicePortConfig) (*swarm.EndpointSpec, error) {
|
|
||||||
portConfigs := []swarm.PortConfig{}
|
portConfigs := []swarm.PortConfig{}
|
||||||
for _, port := range source {
|
for _, port := range source {
|
||||||
published, err := str2uint32(port.Published)
|
|
||||||
if err != nil {
|
|
||||||
return &swarm.EndpointSpec{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
portConfig := swarm.PortConfig{
|
portConfig := swarm.PortConfig{
|
||||||
Protocol: swarm.PortConfigProtocol(port.Protocol),
|
Protocol: swarm.PortConfigProtocol(port.Protocol),
|
||||||
TargetPort: port.Target,
|
TargetPort: port.Target,
|
||||||
PublishedPort: published,
|
PublishedPort: port.Published,
|
||||||
PublishMode: swarm.PortConfigPublishMode(port.Mode),
|
PublishMode: swarm.PortConfigPublishMode(port.Mode),
|
||||||
}
|
}
|
||||||
portConfigs = append(portConfigs, portConfig)
|
portConfigs = append(portConfigs, portConfig)
|
||||||
@ -787,7 +747,7 @@ func convertEndpointSpec(endpointMode string, source []composeGoTypes.ServicePor
|
|||||||
return &swarm.EndpointSpec{
|
return &swarm.EndpointSpec{
|
||||||
Mode: swarm.ResolutionMode(strings.ToLower(endpointMode)),
|
Mode: swarm.ResolutionMode(strings.ToLower(endpointMode)),
|
||||||
Ports: portConfigs,
|
Ports: portConfigs,
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertEnvironment(source map[string]*string) []string {
|
func convertEnvironment(source map[string]*string) []string {
|
||||||
@ -805,7 +765,7 @@ func convertEnvironment(source map[string]*string) []string {
|
|||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertDeployMode(mode string, replicas *int) (swarm.ServiceMode, error) {
|
func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) {
|
||||||
serviceMode := swarm.ServiceMode{}
|
serviceMode := swarm.ServiceMode{}
|
||||||
|
|
||||||
switch mode {
|
switch mode {
|
||||||
@ -815,8 +775,7 @@ func convertDeployMode(mode string, replicas *int) (swarm.ServiceMode, error) {
|
|||||||
}
|
}
|
||||||
serviceMode.Global = &swarm.GlobalService{}
|
serviceMode.Global = &swarm.GlobalService{}
|
||||||
case "replicated", "":
|
case "replicated", "":
|
||||||
convReplicas := (*uint64)(unsafe.Pointer(replicas))
|
serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas}
|
||||||
serviceMode.Replicated = &swarm.ReplicatedService{Replicas: convReplicas}
|
|
||||||
default:
|
default:
|
||||||
return serviceMode, errors.New(i18n.G("unknown mode: %s", mode))
|
return serviceMode, errors.New(i18n.G("unknown mode: %s", mode))
|
||||||
}
|
}
|
||||||
@ -833,7 +792,7 @@ func convertDNSConfig(DNS []string, DNSSearch []string) *swarm.DNSConfig {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertCredentialSpec(namespace Namespace, spec composeGoTypes.CredentialSpecConfig, refs []*swarm.ConfigReference) (*swarm.CredentialSpec, error) {
|
func convertCredentialSpec(namespace Namespace, spec composetypes.CredentialSpecConfig, refs []*swarm.ConfigReference) (*swarm.CredentialSpec, error) {
|
||||||
var o []string
|
var o []string
|
||||||
|
|
||||||
// Config was added in API v1.40
|
// Config was added in API v1.40
|
||||||
@ -855,13 +814,7 @@ func convertCredentialSpec(namespace Namespace, spec composeGoTypes.CredentialSp
|
|||||||
case l > 2:
|
case l > 2:
|
||||||
return nil, errors.New(i18n.G("invalid credential spec: cannot specify both %s, and %s", strings.Join(o[:l-1], ", "), o[l-1]))
|
return nil, errors.New(i18n.G("invalid credential spec: cannot specify both %s, and %s", strings.Join(o[:l-1], ", "), o[l-1]))
|
||||||
}
|
}
|
||||||
|
swarmCredSpec := swarm.CredentialSpec(spec)
|
||||||
swarmCredSpec := swarm.CredentialSpec{
|
|
||||||
Config: spec.Config,
|
|
||||||
File: spec.File,
|
|
||||||
Registry: spec.Registry,
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we're using a swarm Config for the credential spec, over-write it
|
// if we're using a swarm Config for the credential spec, over-write it
|
||||||
// here with the config ID
|
// here with the config ID
|
||||||
if swarmCredSpec.Config != "" {
|
if swarmCredSpec.Config != "" {
|
||||||
@ -883,7 +836,7 @@ func convertCredentialSpec(namespace Namespace, spec composeGoTypes.CredentialSp
|
|||||||
return &swarmCredSpec, nil
|
return &swarmCredSpec, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertUlimits(origUlimits map[string]*composeGoTypes.UlimitsConfig) []*units.Ulimit {
|
func convertUlimits(origUlimits map[string]*composetypes.UlimitsConfig) []*units.Ulimit {
|
||||||
newUlimits := make(map[string]*units.Ulimit)
|
newUlimits := make(map[string]*units.Ulimit)
|
||||||
for name, u := range origUlimits {
|
for name, u := range origUlimits {
|
||||||
if u.Single != 0 {
|
if u.Single != 0 {
|
||||||
|
|||||||
678
pkg/upstream/convert/service_test.go
Normal file
678
pkg/upstream/convert/service_test.go
Normal file
@ -0,0 +1,678 @@
|
|||||||
|
package convert // https://github.com/docker/cli/blob/master/cli/compose/convert/service_test.go
|
||||||
|
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,15 +2,15 @@ package convert // https://github.com/docker/cli/blob/master/cli/compose/convert
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"coopcloud.tech/abra/pkg/i18n"
|
"coopcloud.tech/abra/pkg/i18n"
|
||||||
composeGoTypes "github.com/compose-spec/compose-go/v2/types"
|
composetypes "github.com/docker/cli/cli/compose/types"
|
||||||
"github.com/docker/docker/api/types/mount"
|
"github.com/docker/docker/api/types/mount"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type volumes map[string]composeGoTypes.VolumeConfig
|
type volumes map[string]composetypes.VolumeConfig
|
||||||
|
|
||||||
// Volumes from compose-file types to engine api types
|
// Volumes from compose-file types to engine api types
|
||||||
func Volumes(serviceVolumes []composeGoTypes.ServiceVolumeConfig, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) {
|
func Volumes(serviceVolumes []composetypes.ServiceVolumeConfig, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) {
|
||||||
var mounts []mount.Mount
|
var mounts []mount.Mount
|
||||||
|
|
||||||
for _, volumeConfig := range serviceVolumes {
|
for _, volumeConfig := range serviceVolumes {
|
||||||
@ -23,7 +23,7 @@ func Volumes(serviceVolumes []composeGoTypes.ServiceVolumeConfig, stackVolumes v
|
|||||||
return mounts, nil
|
return mounts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createMountFromVolume(volume composeGoTypes.ServiceVolumeConfig) mount.Mount {
|
func createMountFromVolume(volume composetypes.ServiceVolumeConfig) mount.Mount {
|
||||||
return mount.Mount{
|
return mount.Mount{
|
||||||
Type: mount.Type(volume.Type),
|
Type: mount.Type(volume.Type),
|
||||||
Target: volume.Target,
|
Target: volume.Target,
|
||||||
@ -34,7 +34,7 @@ func createMountFromVolume(volume composeGoTypes.ServiceVolumeConfig) mount.Moun
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleVolumeToMount(
|
func handleVolumeToMount(
|
||||||
volume composeGoTypes.ServiceVolumeConfig,
|
volume composetypes.ServiceVolumeConfig,
|
||||||
stackVolumes volumes,
|
stackVolumes volumes,
|
||||||
namespace Namespace,
|
namespace Namespace,
|
||||||
) (mount.Mount, error) {
|
) (mount.Mount, error) {
|
||||||
@ -68,7 +68,7 @@ func handleVolumeToMount(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// External named volumes
|
// External named volumes
|
||||||
if stackVolume.External {
|
if stackVolume.External.External {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ func handleVolumeToMount(
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleBindToMount(volume composeGoTypes.ServiceVolumeConfig) (mount.Mount, error) {
|
func handleBindToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
|
||||||
result := createMountFromVolume(volume)
|
result := createMountFromVolume(volume)
|
||||||
|
|
||||||
if volume.Source == "" {
|
if volume.Source == "" {
|
||||||
@ -103,7 +103,7 @@ func handleBindToMount(volume composeGoTypes.ServiceVolumeConfig) (mount.Mount,
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleTmpfsToMount(volume composeGoTypes.ServiceVolumeConfig) (mount.Mount, error) {
|
func handleTmpfsToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
|
||||||
result := createMountFromVolume(volume)
|
result := createMountFromVolume(volume)
|
||||||
|
|
||||||
if volume.Source != "" {
|
if volume.Source != "" {
|
||||||
@ -117,13 +117,13 @@ func handleTmpfsToMount(volume composeGoTypes.ServiceVolumeConfig) (mount.Mount,
|
|||||||
}
|
}
|
||||||
if volume.Tmpfs != nil {
|
if volume.Tmpfs != nil {
|
||||||
result.TmpfsOptions = &mount.TmpfsOptions{
|
result.TmpfsOptions = &mount.TmpfsOptions{
|
||||||
SizeBytes: int64(volume.Tmpfs.Size),
|
SizeBytes: volume.Tmpfs.Size,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleNpipeToMount(volume composeGoTypes.ServiceVolumeConfig) (mount.Mount, error) {
|
func handleNpipeToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
|
||||||
result := createMountFromVolume(volume)
|
result := createMountFromVolume(volume)
|
||||||
|
|
||||||
if volume.Source == "" {
|
if volume.Source == "" {
|
||||||
@ -144,7 +144,7 @@ func handleNpipeToMount(volume composeGoTypes.ServiceVolumeConfig) (mount.Mount,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func convertVolumeToMount(
|
func convertVolumeToMount(
|
||||||
volume composeGoTypes.ServiceVolumeConfig,
|
volume composetypes.ServiceVolumeConfig,
|
||||||
stackVolumes volumes,
|
stackVolumes volumes,
|
||||||
namespace Namespace,
|
namespace Namespace,
|
||||||
) (mount.Mount, error) {
|
) (mount.Mount, error) {
|
||||||
|
|||||||
361
pkg/upstream/convert/volume_test.go
Normal file
361
pkg/upstream/convert/volume_test.go
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
package convert // https://github.com/docker/cli/blob/master/cli/compose/convert/volume_test.go
|
||||||
|
|
||||||
|
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,7 +1,6 @@
|
|||||||
package stack // https://github.com/docker/cli/blob/master/cli/command/stack/loader/loader.go
|
package stack // https://github.com/docker/cli/blob/master/cli/command/stack/loader/loader.go
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -9,62 +8,58 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"coopcloud.tech/abra/pkg/i18n"
|
"coopcloud.tech/abra/pkg/i18n"
|
||||||
composeGoCli "github.com/compose-spec/compose-go/v2/cli"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
composeGoTypes "github.com/compose-spec/compose-go/v2/types"
|
|
||||||
"github.com/docker/cli/cli/compose/loader"
|
"github.com/docker/cli/cli/compose/loader"
|
||||||
"github.com/docker/cli/cli/compose/schema"
|
"github.com/docker/cli/cli/compose/schema"
|
||||||
composetypes "github.com/docker/cli/cli/compose/types"
|
composetypes "github.com/docker/cli/cli/compose/types"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type LoadConf struct {
|
// DontSkipValidation ensures validation is done for compose file loading
|
||||||
ComposeFiles []string
|
func DontSkipValidation(opts *loader.Options) {
|
||||||
AppEnv map[string]string
|
opts.SkipValidation = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadCompose(conf LoadConf) (*composeGoTypes.Project, error) {
|
// SkipInterpolation skip interpolating environment variables.
|
||||||
var project *composeGoTypes.Project
|
func SkipInterpolation(opts *loader.Options) {
|
||||||
|
opts.SkipInterpolation = true
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE(d1): silence compose-go internal logger
|
// LoadComposefile parse the composefile specified in the cli and returns its Config and version.
|
||||||
logrus.SetOutput(ioutil.Discard)
|
func LoadComposefile(opts Deploy, appEnv map[string]string, options ...func(*loader.Options)) (*composetypes.Config, error) {
|
||||||
|
configDetails, err := getConfigDetails(opts.Composefiles, appEnv)
|
||||||
var projectOptions *composeGoCli.ProjectOptions
|
|
||||||
if len(conf.ComposeFiles) == 0 {
|
|
||||||
return project, errors.New(i18n.G("LoadCompose: provide compose files"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(conf.AppEnv) == 0 {
|
|
||||||
var err error
|
|
||||||
projectOptions, err = composeGoCli.NewProjectOptions(
|
|
||||||
conf.ComposeFiles,
|
|
||||||
composeGoCli.WithInterpolation(false),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return project, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var env []string
|
|
||||||
for k, v := range conf.AppEnv {
|
|
||||||
env = append(env, fmt.Sprintf("%s=%s", k, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
projectOptions, err = composeGoCli.NewProjectOptions(
|
|
||||||
conf.ComposeFiles,
|
|
||||||
composeGoCli.WithEnv(env),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return project, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
project, err := projectOptions.LoadProject(context.Background())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return project, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return project, nil
|
if options == nil {
|
||||||
|
options = []func(*loader.Options){DontSkipValidation}
|
||||||
|
}
|
||||||
|
|
||||||
|
dicts := getDictsFrom(configDetails.ConfigFiles)
|
||||||
|
config, err := loader.Load(configDetails, options...)
|
||||||
|
if err != nil {
|
||||||
|
if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok {
|
||||||
|
return nil, errors.New(i18n.G("compose file contains unsupported options: %s", propertyWarnings(fpe.Properties)))
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
recipeName, exists := appEnv["RECIPE"]
|
||||||
|
if !exists {
|
||||||
|
recipeName, _ = appEnv["TYPE"]
|
||||||
|
}
|
||||||
|
|
||||||
|
unsupportedProperties := loader.GetUnsupportedProperties(dicts...)
|
||||||
|
if len(unsupportedProperties) > 0 {
|
||||||
|
log.Warn(i18n.G("%s: ignoring unsupported options: %s", recipeName, strings.Join(unsupportedProperties, ", ")))
|
||||||
|
}
|
||||||
|
|
||||||
|
deprecatedProperties := loader.GetDeprecatedProperties(dicts...)
|
||||||
|
if len(deprecatedProperties) > 0 {
|
||||||
|
log.Warn(i18n.G("%s: ignoring deprecated options: %s", recipeName, propertyWarnings(deprecatedProperties)))
|
||||||
|
}
|
||||||
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDictsFrom(configFiles []composetypes.ConfigFile) []map[string]interface{} {
|
func getDictsFrom(configFiles []composetypes.ConfigFile) []map[string]interface{} {
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
package stack_test // https://github.com/docker/cli/blob/master/cli/command/stack/loader/loader.go
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"coopcloud.tech/abra/pkg/app"
|
|
||||||
"coopcloud.tech/abra/pkg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSkipInterpolation(t *testing.T) {
|
|
||||||
test.Setup()
|
|
||||||
t.Cleanup(func() { test.Teardown() })
|
|
||||||
|
|
||||||
a, err := app.Get(test.AppName)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = a.Recipe.GetComposeConfig()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: ensure compose has a port with no interpolated value
|
|
||||||
// TODO: ensure compose has port with interpolated value
|
|
||||||
}
|
|
||||||
@ -12,7 +12,6 @@ import (
|
|||||||
stdlibErr "errors"
|
stdlibErr "errors"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
composeGoTypes "github.com/compose-spec/compose-go/v2/types"
|
|
||||||
|
|
||||||
"coopcloud.tech/abra/pkg/config"
|
"coopcloud.tech/abra/pkg/config"
|
||||||
"coopcloud.tech/abra/pkg/i18n"
|
"coopcloud.tech/abra/pkg/i18n"
|
||||||
@ -21,6 +20,7 @@ import (
|
|||||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/stack/formatter"
|
"github.com/docker/cli/cli/command/stack/formatter"
|
||||||
|
composetypes "github.com/docker/cli/cli/compose/types"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
@ -197,7 +197,7 @@ func pruneServices(ctx context.Context, cl *dockerClient.Client, namespace conve
|
|||||||
func RunDeploy(
|
func RunDeploy(
|
||||||
cl *dockerClient.Client,
|
cl *dockerClient.Client,
|
||||||
opts Deploy,
|
opts Deploy,
|
||||||
cfg *composeGoTypes.Project,
|
cfg *composetypes.Config,
|
||||||
appName string,
|
appName string,
|
||||||
serverName string,
|
serverName string,
|
||||||
dontWait bool,
|
dontWait bool,
|
||||||
@ -246,7 +246,7 @@ func deployCompose(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
cl *dockerClient.Client,
|
cl *dockerClient.Client,
|
||||||
opts Deploy,
|
opts Deploy,
|
||||||
config *composeGoTypes.Project,
|
config *composetypes.Config,
|
||||||
appName string,
|
appName string,
|
||||||
serverName string,
|
serverName string,
|
||||||
dontWait bool,
|
dontWait bool,
|
||||||
@ -325,7 +325,7 @@ func deployCompose(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getServicesDeclaredNetworks(serviceConfigs map[string]composeGoTypes.ServiceConfig) map[string]struct{} {
|
func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
|
||||||
serviceNetworks := map[string]struct{}{}
|
serviceNetworks := map[string]struct{}{}
|
||||||
for _, serviceConfig := range serviceConfigs {
|
for _, serviceConfig := range serviceConfigs {
|
||||||
if len(serviceConfig.Networks) == 0 {
|
if len(serviceConfig.Networks) == 0 {
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
|
|
||||||
services:
|
|
||||||
app:
|
|
||||||
ports:
|
|
||||||
- target: 22
|
|
||||||
published: ${PORT}
|
|
||||||
@ -1,4 +1,5 @@
|
|||||||
---
|
---
|
||||||
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
|
|||||||
191
vendor/github.com/compose-spec/compose-go/v2/LICENSE
generated
vendored
191
vendor/github.com/compose-spec/compose-go/v2/LICENSE
generated
vendored
@ -1,191 +0,0 @@
|
|||||||
|
|
||||||
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 2020 The Compose Specification Authors
|
|
||||||
|
|
||||||
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.
|
|
||||||
2
vendor/github.com/compose-spec/compose-go/v2/NOTICE
generated
vendored
2
vendor/github.com/compose-spec/compose-go/v2/NOTICE
generated
vendored
@ -1,2 +0,0 @@
|
|||||||
The Compose Specification
|
|
||||||
Copyright 2020 The Compose Specification Authors
|
|
||||||
590
vendor/github.com/compose-spec/compose-go/v2/cli/options.go
generated
vendored
590
vendor/github.com/compose-spec/compose-go/v2/cli/options.go
generated
vendored
@ -1,590 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"go.yaml.in/yaml/v4"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/consts"
|
|
||||||
"github.com/compose-spec/compose-go/v2/dotenv"
|
|
||||||
"github.com/compose-spec/compose-go/v2/loader"
|
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
|
||||||
"github.com/compose-spec/compose-go/v2/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ProjectOptions provides common configuration for loading a project.
|
|
||||||
type ProjectOptions struct {
|
|
||||||
// Name is a valid Compose project name to be used or empty.
|
|
||||||
//
|
|
||||||
// If empty, the project loader will automatically infer a reasonable
|
|
||||||
// project name if possible.
|
|
||||||
Name string
|
|
||||||
|
|
||||||
// WorkingDir is a file path to use as the project directory or empty.
|
|
||||||
//
|
|
||||||
// If empty, the project loader will automatically infer a reasonable
|
|
||||||
// working directory if possible.
|
|
||||||
WorkingDir string
|
|
||||||
|
|
||||||
// ConfigPaths are file paths to one or more Compose files.
|
|
||||||
//
|
|
||||||
// These are applied in order by the loader following the override logic
|
|
||||||
// as described in the spec.
|
|
||||||
//
|
|
||||||
// The first entry is required and is the primary Compose file.
|
|
||||||
// For convenience, WithConfigFileEnv and WithDefaultConfigPath
|
|
||||||
// are provided to populate this in a predictable manner.
|
|
||||||
ConfigPaths []string
|
|
||||||
|
|
||||||
// Environment are additional environment variables to make available
|
|
||||||
// for interpolation.
|
|
||||||
//
|
|
||||||
// NOTE: For security, the loader does not automatically expose any
|
|
||||||
// process environment variables. For convenience, WithOsEnv can be
|
|
||||||
// used if appropriate.
|
|
||||||
Environment types.Mapping
|
|
||||||
|
|
||||||
// EnvFiles are file paths to ".env" files with additional environment
|
|
||||||
// variable data.
|
|
||||||
//
|
|
||||||
// These are loaded in-order, so it is possible to override variables or
|
|
||||||
// in subsequent files.
|
|
||||||
//
|
|
||||||
// This field is optional, but any file paths that are included here must
|
|
||||||
// exist or an error will be returned during load.
|
|
||||||
EnvFiles []string
|
|
||||||
|
|
||||||
loadOptions []func(*loader.Options)
|
|
||||||
|
|
||||||
// Callbacks to retrieve metadata information during parse defined before
|
|
||||||
// creating the project
|
|
||||||
Listeners []loader.Listener
|
|
||||||
// ResourceLoaders manages support for remote resources
|
|
||||||
ResourceLoaders []loader.ResourceLoader
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProjectOptionsFn func(*ProjectOptions) error
|
|
||||||
|
|
||||||
// NewProjectOptions creates ProjectOptions
|
|
||||||
func NewProjectOptions(configs []string, opts ...ProjectOptionsFn) (*ProjectOptions, error) {
|
|
||||||
options := &ProjectOptions{
|
|
||||||
ConfigPaths: configs,
|
|
||||||
Environment: map[string]string{},
|
|
||||||
Listeners: []loader.Listener{},
|
|
||||||
}
|
|
||||||
for _, o := range opts {
|
|
||||||
err := o(options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return options, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithName defines ProjectOptions' name
|
|
||||||
func WithName(name string) ProjectOptionsFn {
|
|
||||||
return func(o *ProjectOptions) error {
|
|
||||||
// a project (once loaded) cannot have an empty name
|
|
||||||
// however, on the options object, the name is optional: if unset,
|
|
||||||
// a name will be inferred by the loader, so it's legal to set the
|
|
||||||
// name to an empty string here
|
|
||||||
if name != loader.NormalizeProjectName(name) {
|
|
||||||
return loader.InvalidProjectNameErr(name)
|
|
||||||
}
|
|
||||||
o.Name = name
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithWorkingDirectory defines ProjectOptions' working directory
|
|
||||||
func WithWorkingDirectory(wd string) ProjectOptionsFn {
|
|
||||||
return func(o *ProjectOptions) error {
|
|
||||||
if wd == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
abs, err := filepath.Abs(wd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
o.WorkingDir = abs
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithConfigFileEnv allow to set compose config file paths by COMPOSE_FILE environment variable
|
|
||||||
func WithConfigFileEnv(o *ProjectOptions) error {
|
|
||||||
if len(o.ConfigPaths) > 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
sep := o.Environment[consts.ComposePathSeparator]
|
|
||||||
if sep == "" {
|
|
||||||
sep = string(os.PathListSeparator)
|
|
||||||
}
|
|
||||||
f, ok := o.Environment[consts.ComposeFilePath]
|
|
||||||
if ok {
|
|
||||||
paths, err := absolutePaths(strings.Split(f, sep))
|
|
||||||
o.ConfigPaths = paths
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithDefaultConfigPath searches for default config files from working directory
|
|
||||||
func WithDefaultConfigPath(o *ProjectOptions) error {
|
|
||||||
if len(o.ConfigPaths) > 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
pwd, err := o.GetWorkingDir()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
candidates := findFiles(DefaultFileNames, pwd)
|
|
||||||
if len(candidates) > 0 {
|
|
||||||
winner := candidates[0]
|
|
||||||
if len(candidates) > 1 {
|
|
||||||
logrus.Warnf("Found multiple config files with supported names: %s", strings.Join(candidates, ", "))
|
|
||||||
logrus.Warnf("Using %s", winner)
|
|
||||||
}
|
|
||||||
o.ConfigPaths = append(o.ConfigPaths, winner)
|
|
||||||
|
|
||||||
overrides := findFiles(DefaultOverrideFileNames, pwd)
|
|
||||||
if len(overrides) > 0 {
|
|
||||||
if len(overrides) > 1 {
|
|
||||||
logrus.Warnf("Found multiple override files with supported names: %s", strings.Join(overrides, ", "))
|
|
||||||
logrus.Warnf("Using %s", overrides[0])
|
|
||||||
}
|
|
||||||
o.ConfigPaths = append(o.ConfigPaths, overrides[0])
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
parent := filepath.Dir(pwd)
|
|
||||||
if parent == pwd {
|
|
||||||
// no config file found, but that's not a blocker if caller only needs project name
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
pwd = parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithEnv defines a key=value set of variables used for compose file interpolation
|
|
||||||
func WithEnv(env []string) ProjectOptionsFn {
|
|
||||||
return func(o *ProjectOptions) error {
|
|
||||||
for k, v := range utils.GetAsEqualsMap(env) {
|
|
||||||
o.Environment[k] = v
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithDiscardEnvFile sets discards the `env_file` section after resolving to
|
|
||||||
// the `environment` section
|
|
||||||
func WithDiscardEnvFile(o *ProjectOptions) error {
|
|
||||||
o.loadOptions = append(o.loadOptions, loader.WithDiscardEnvFiles)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithLoadOptions provides a hook to control how compose files are loaded
|
|
||||||
func WithLoadOptions(loadOptions ...func(*loader.Options)) ProjectOptionsFn {
|
|
||||||
return func(o *ProjectOptions) error {
|
|
||||||
o.loadOptions = append(o.loadOptions, loadOptions...)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithDefaultProfiles uses the provided profiles (if any), and falls back to
|
|
||||||
// profiles specified via the COMPOSE_PROFILES environment variable otherwise.
|
|
||||||
func WithDefaultProfiles(profiles ...string) ProjectOptionsFn {
|
|
||||||
return func(o *ProjectOptions) error {
|
|
||||||
if len(profiles) == 0 {
|
|
||||||
for _, s := range strings.Split(o.Environment[consts.ComposeProfiles], ",") {
|
|
||||||
profiles = append(profiles, strings.TrimSpace(s))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
o.loadOptions = append(o.loadOptions, loader.WithProfiles(profiles))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithProfiles sets profiles to be activated
|
|
||||||
func WithProfiles(profiles []string) ProjectOptionsFn {
|
|
||||||
return func(o *ProjectOptions) error {
|
|
||||||
o.loadOptions = append(o.loadOptions, loader.WithProfiles(profiles))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithOsEnv imports environment variables from OS
|
|
||||||
func WithOsEnv(o *ProjectOptions) error {
|
|
||||||
for k, v := range utils.GetAsEqualsMap(os.Environ()) {
|
|
||||||
if _, set := o.Environment[k]; set {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
o.Environment[k] = v
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithEnvFile sets an alternate env file.
|
|
||||||
//
|
|
||||||
// Deprecated: use WithEnvFiles instead.
|
|
||||||
func WithEnvFile(file string) ProjectOptionsFn {
|
|
||||||
var files []string
|
|
||||||
if file != "" {
|
|
||||||
files = []string{file}
|
|
||||||
}
|
|
||||||
return WithEnvFiles(files...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithEnvFiles set env file(s) to be loaded to set project environment.
|
|
||||||
// defaults to local .env file if no explicit file is selected, until COMPOSE_DISABLE_ENV_FILE is set
|
|
||||||
func WithEnvFiles(file ...string) ProjectOptionsFn {
|
|
||||||
return func(o *ProjectOptions) error {
|
|
||||||
if len(file) > 0 {
|
|
||||||
o.EnvFiles = file
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if v, ok := os.LookupEnv(consts.ComposeDisableDefaultEnvFile); ok {
|
|
||||||
b, err := strconv.ParseBool(v)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if b {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wd, err := o.GetWorkingDir()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defaultDotEnv := filepath.Join(wd, ".env")
|
|
||||||
|
|
||||||
s, err := os.Stat(defaultDotEnv)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !s.IsDir() {
|
|
||||||
o.EnvFiles = []string{defaultDotEnv}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithDotEnv imports environment variables from .env file
|
|
||||||
func WithDotEnv(o *ProjectOptions) error {
|
|
||||||
envMap, err := dotenv.GetEnvFromFile(o.Environment, o.EnvFiles)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
o.Environment.Merge(envMap)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithInterpolation set ProjectOptions to enable/skip interpolation
|
|
||||||
func WithInterpolation(interpolation bool) ProjectOptionsFn {
|
|
||||||
return func(o *ProjectOptions) error {
|
|
||||||
o.loadOptions = append(o.loadOptions, func(options *loader.Options) {
|
|
||||||
options.SkipInterpolation = !interpolation
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithNormalization set ProjectOptions to enable/skip normalization
|
|
||||||
func WithNormalization(normalization bool) ProjectOptionsFn {
|
|
||||||
return func(o *ProjectOptions) error {
|
|
||||||
o.loadOptions = append(o.loadOptions, func(options *loader.Options) {
|
|
||||||
options.SkipNormalization = !normalization
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithConsistency set ProjectOptions to enable/skip consistency
|
|
||||||
func WithConsistency(consistency bool) ProjectOptionsFn {
|
|
||||||
return func(o *ProjectOptions) error {
|
|
||||||
o.loadOptions = append(o.loadOptions, func(options *loader.Options) {
|
|
||||||
options.SkipConsistencyCheck = !consistency
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithResolvedPaths set ProjectOptions to enable paths resolution
|
|
||||||
func WithResolvedPaths(resolve bool) ProjectOptionsFn {
|
|
||||||
return func(o *ProjectOptions) error {
|
|
||||||
o.loadOptions = append(o.loadOptions, func(options *loader.Options) {
|
|
||||||
options.ResolvePaths = resolve
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithResourceLoader register support for ResourceLoader to manage remote resources
|
|
||||||
func WithResourceLoader(r loader.ResourceLoader) ProjectOptionsFn {
|
|
||||||
return func(o *ProjectOptions) error {
|
|
||||||
o.ResourceLoaders = append(o.ResourceLoaders, r)
|
|
||||||
o.loadOptions = append(o.loadOptions, func(options *loader.Options) {
|
|
||||||
options.ResourceLoaders = o.ResourceLoaders
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithExtension register a know extension `x-*` with the go struct type to decode into
|
|
||||||
func WithExtension(name string, typ any) ProjectOptionsFn {
|
|
||||||
return func(o *ProjectOptions) error {
|
|
||||||
o.loadOptions = append(o.loadOptions, func(options *loader.Options) {
|
|
||||||
if options.KnownExtensions == nil {
|
|
||||||
options.KnownExtensions = map[string]any{}
|
|
||||||
}
|
|
||||||
options.KnownExtensions[name] = typ
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append listener to event
|
|
||||||
func (o *ProjectOptions) WithListeners(listeners ...loader.Listener) {
|
|
||||||
o.Listeners = append(o.Listeners, listeners...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithoutEnvironmentResolution disable environment resolution
|
|
||||||
func WithoutEnvironmentResolution(o *ProjectOptions) error {
|
|
||||||
o.loadOptions = append(o.loadOptions, func(options *loader.Options) {
|
|
||||||
options.SkipResolveEnvironment = true
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultFileNames defines the Compose file names for auto-discovery (in order of preference)
|
|
||||||
var DefaultFileNames = []string{"compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml"}
|
|
||||||
|
|
||||||
// DefaultOverrideFileNames defines the Compose override file names for auto-discovery (in order of preference)
|
|
||||||
var DefaultOverrideFileNames = []string{"compose.override.yml", "compose.override.yaml", "docker-compose.override.yml", "docker-compose.override.yaml"}
|
|
||||||
|
|
||||||
func (o *ProjectOptions) GetWorkingDir() (string, error) {
|
|
||||||
if o.WorkingDir != "" {
|
|
||||||
return filepath.Abs(o.WorkingDir)
|
|
||||||
}
|
|
||||||
PATH:
|
|
||||||
for _, path := range o.ConfigPaths {
|
|
||||||
if path != "-" {
|
|
||||||
for _, l := range o.ResourceLoaders {
|
|
||||||
if l.Accept(path) {
|
|
||||||
break PATH
|
|
||||||
}
|
|
||||||
}
|
|
||||||
absPath, err := filepath.Abs(path)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return filepath.Dir(absPath), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return os.Getwd()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadConfigFiles reads ConfigFiles and populates the content field
|
|
||||||
func (o *ProjectOptions) ReadConfigFiles(ctx context.Context, workingDir string, options *ProjectOptions) (*types.ConfigDetails, error) {
|
|
||||||
config, err := loader.LoadConfigFiles(ctx, options.ConfigPaths, workingDir, options.loadOptions...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
configs := make([][]byte, len(config.ConfigFiles))
|
|
||||||
|
|
||||||
for i, c := range config.ConfigFiles {
|
|
||||||
var err error
|
|
||||||
var b []byte
|
|
||||||
if c.IsStdin() {
|
|
||||||
b, err = io.ReadAll(os.Stdin)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
f, err := filepath.Abs(c.Filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
b, err = os.ReadFile(f)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
configs[i] = b
|
|
||||||
}
|
|
||||||
for i, c := range configs {
|
|
||||||
config.ConfigFiles[i].Content = c
|
|
||||||
}
|
|
||||||
return config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadProject loads compose file according to options and bind to types.Project go structs
|
|
||||||
func (o *ProjectOptions) LoadProject(ctx context.Context) (*types.Project, error) {
|
|
||||||
config, err := o.prepare(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
project, err := loader.LoadWithContext(ctx, types.ConfigDetails{
|
|
||||||
ConfigFiles: config.ConfigFiles,
|
|
||||||
WorkingDir: config.WorkingDir,
|
|
||||||
Environment: o.Environment,
|
|
||||||
}, o.loadOptions...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, config := range config.ConfigFiles {
|
|
||||||
project.ComposeFiles = append(project.ComposeFiles, config.Filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
return project, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadModel loads compose file according to options and returns a raw (yaml tree) model
|
|
||||||
func (o *ProjectOptions) LoadModel(ctx context.Context) (map[string]any, error) {
|
|
||||||
configDetails, err := o.prepare(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return loader.LoadModelWithContext(ctx, *configDetails, o.loadOptions...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepare converts ProjectOptions into loader's types.ConfigDetails and configures default load options
|
|
||||||
func (o *ProjectOptions) prepare(ctx context.Context) (*types.ConfigDetails, error) {
|
|
||||||
defaultDir, err := o.GetWorkingDir()
|
|
||||||
if err != nil {
|
|
||||||
return &types.ConfigDetails{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
configDetails, err := o.ReadConfigFiles(ctx, defaultDir, o)
|
|
||||||
if err != nil {
|
|
||||||
return configDetails, err
|
|
||||||
}
|
|
||||||
|
|
||||||
isNamed := false
|
|
||||||
if o.Name == "" {
|
|
||||||
type named struct {
|
|
||||||
Name string `yaml:"name,omitempty"`
|
|
||||||
}
|
|
||||||
// if any of the compose file is named, this is equivalent to user passing --project-name
|
|
||||||
for _, cfg := range configDetails.ConfigFiles {
|
|
||||||
var n named
|
|
||||||
err = yaml.Unmarshal(cfg.Content, &n)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if n.Name != "" {
|
|
||||||
isNamed = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
o.loadOptions = append(o.loadOptions,
|
|
||||||
withNamePrecedenceLoad(defaultDir, isNamed, o),
|
|
||||||
withConvertWindowsPaths(o),
|
|
||||||
withListeners(o))
|
|
||||||
|
|
||||||
return configDetails, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProjectFromOptions load a compose project based on command line options
|
|
||||||
// Deprecated: use ProjectOptions.LoadProject or ProjectOptions.LoadModel
|
|
||||||
func ProjectFromOptions(ctx context.Context, options *ProjectOptions) (*types.Project, error) {
|
|
||||||
return options.LoadProject(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func withNamePrecedenceLoad(absWorkingDir string, namedInYaml bool, options *ProjectOptions) func(*loader.Options) {
|
|
||||||
return func(opts *loader.Options) {
|
|
||||||
if options.Name != "" {
|
|
||||||
opts.SetProjectName(options.Name, true)
|
|
||||||
} else if nameFromEnv, ok := options.Environment[consts.ComposeProjectName]; ok && nameFromEnv != "" {
|
|
||||||
opts.SetProjectName(nameFromEnv, true)
|
|
||||||
} else if !namedInYaml {
|
|
||||||
dirname := filepath.Base(absWorkingDir)
|
|
||||||
symlink, err := filepath.EvalSymlinks(absWorkingDir)
|
|
||||||
if err == nil && filepath.Base(symlink) != dirname {
|
|
||||||
logrus.Warnf("project has been loaded without an explicit name from a symlink. Using name %q", dirname)
|
|
||||||
}
|
|
||||||
opts.SetProjectName(
|
|
||||||
loader.NormalizeProjectName(dirname),
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func withConvertWindowsPaths(options *ProjectOptions) func(*loader.Options) {
|
|
||||||
return func(o *loader.Options) {
|
|
||||||
if o.ResolvePaths {
|
|
||||||
o.ConvertWindowsPaths = utils.StringToBool(options.Environment["COMPOSE_CONVERT_WINDOWS_PATHS"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// save listeners from ProjectOptions (compose) to loader.Options
|
|
||||||
func withListeners(options *ProjectOptions) func(*loader.Options) {
|
|
||||||
return func(opts *loader.Options) {
|
|
||||||
opts.Listeners = append(opts.Listeners, options.Listeners...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func findFiles(names []string, pwd string) []string {
|
|
||||||
candidates := []string{}
|
|
||||||
for _, n := range names {
|
|
||||||
f := filepath.Join(pwd, n)
|
|
||||||
if _, err := os.Stat(f); err == nil {
|
|
||||||
candidates = append(candidates, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return candidates
|
|
||||||
}
|
|
||||||
|
|
||||||
func absolutePaths(p []string) ([]string, error) {
|
|
||||||
var paths []string
|
|
||||||
for _, f := range p {
|
|
||||||
if f == "-" {
|
|
||||||
paths = append(paths, f)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
abs, err := filepath.Abs(f)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
f = abs
|
|
||||||
if _, err := os.Stat(f); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
paths = append(paths, f)
|
|
||||||
}
|
|
||||||
return paths, nil
|
|
||||||
}
|
|
||||||
29
vendor/github.com/compose-spec/compose-go/v2/consts/consts.go
generated
vendored
29
vendor/github.com/compose-spec/compose-go/v2/consts/consts.go
generated
vendored
@ -1,29 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package consts
|
|
||||||
|
|
||||||
const (
|
|
||||||
ComposeProjectName = "COMPOSE_PROJECT_NAME"
|
|
||||||
ComposePathSeparator = "COMPOSE_PATH_SEPARATOR"
|
|
||||||
ComposeFilePath = "COMPOSE_FILE"
|
|
||||||
ComposeDisableDefaultEnvFile = "COMPOSE_DISABLE_ENV_FILE"
|
|
||||||
ComposeProfiles = "COMPOSE_PROFILES"
|
|
||||||
)
|
|
||||||
|
|
||||||
const Extensions = "#extensions" // Using # prefix, we prevent risk to conflict with an actual yaml key
|
|
||||||
|
|
||||||
type ComposeFileKey struct{}
|
|
||||||
22
vendor/github.com/compose-spec/compose-go/v2/dotenv/LICENSE
generated
vendored
22
vendor/github.com/compose-spec/compose-go/v2/dotenv/LICENSE
generated
vendored
@ -1,22 +0,0 @@
|
|||||||
Copyright (c) 2013 John Barton
|
|
||||||
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
|
||||||
a copy of this software and associated documentation files (the
|
|
||||||
"Software"), to deal in the Software without restriction, including
|
|
||||||
without limitation the rights to use, copy, modify, merge, publish,
|
|
||||||
distribute, sublicense, and/or sell copies of the Software, and to
|
|
||||||
permit persons to whom the Software is furnished to do so, subject to
|
|
||||||
the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be
|
|
||||||
included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
||||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
||||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
||||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
73
vendor/github.com/compose-spec/compose-go/v2/dotenv/env.go
generated
vendored
73
vendor/github.com/compose-spec/compose-go/v2/dotenv/env.go
generated
vendored
@ -1,73 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package dotenv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetEnvFromFile(currentEnv map[string]string, filenames []string) (map[string]string, error) {
|
|
||||||
envMap := make(map[string]string)
|
|
||||||
|
|
||||||
for _, dotEnvFile := range filenames {
|
|
||||||
abs, err := filepath.Abs(dotEnvFile)
|
|
||||||
if err != nil {
|
|
||||||
return envMap, err
|
|
||||||
}
|
|
||||||
dotEnvFile = abs
|
|
||||||
|
|
||||||
s, err := os.Stat(dotEnvFile)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return envMap, fmt.Errorf("couldn't find env file: %s", dotEnvFile)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return envMap, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.IsDir() {
|
|
||||||
if len(filenames) == 0 {
|
|
||||||
return envMap, nil
|
|
||||||
}
|
|
||||||
return envMap, fmt.Errorf("%s is a directory", dotEnvFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := os.ReadFile(dotEnvFile)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil, fmt.Errorf("couldn't read env file: %s", dotEnvFile)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return envMap, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = parseWithLookup(bytes.NewReader(b), envMap, func(k string) (string, bool) {
|
|
||||||
v, ok := currentEnv[k]
|
|
||||||
if ok {
|
|
||||||
return v, true
|
|
||||||
}
|
|
||||||
v, ok = envMap[k]
|
|
||||||
return v, ok
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return envMap, fmt.Errorf("failed to read %s: %w", dotEnvFile, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return envMap, nil
|
|
||||||
}
|
|
||||||
51
vendor/github.com/compose-spec/compose-go/v2/dotenv/format.go
generated
vendored
51
vendor/github.com/compose-spec/compose-go/v2/dotenv/format.go
generated
vendored
@ -1,51 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package dotenv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
const DotEnv = ".env"
|
|
||||||
|
|
||||||
var formats = map[string]Parser{
|
|
||||||
DotEnv: func(r io.Reader, filename string, vars map[string]string, lookup func(key string) (string, bool)) error {
|
|
||||||
err := parseWithLookup(r, vars, lookup)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read %s: %w", filename, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
type Parser func(r io.Reader, filename string, vars map[string]string, lookup func(key string) (string, bool)) error
|
|
||||||
|
|
||||||
func RegisterFormat(format string, p Parser) {
|
|
||||||
formats[format] = p
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseWithFormat(r io.Reader, filename string, vars map[string]string, resolve LookupFn, format string) error {
|
|
||||||
if format == "" {
|
|
||||||
format = DotEnv
|
|
||||||
}
|
|
||||||
fn, ok := formats[format]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("unsupported env_file format %q", format)
|
|
||||||
}
|
|
||||||
return fn(r, filename, vars, resolve)
|
|
||||||
}
|
|
||||||
182
vendor/github.com/compose-spec/compose-go/v2/dotenv/godotenv.go
generated
vendored
182
vendor/github.com/compose-spec/compose-go/v2/dotenv/godotenv.go
generated
vendored
@ -1,182 +0,0 @@
|
|||||||
// Package dotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv)
|
|
||||||
//
|
|
||||||
// Examples/readme can be found on the github page at https://github.com/joho/godotenv
|
|
||||||
//
|
|
||||||
// The TL;DR is that you make a .env file that looks something like
|
|
||||||
//
|
|
||||||
// SOME_ENV_VAR=somevalue
|
|
||||||
//
|
|
||||||
// and then in your go code you can call
|
|
||||||
//
|
|
||||||
// godotenv.Load()
|
|
||||||
//
|
|
||||||
// and all the env vars declared in .env will be available through os.Getenv("SOME_ENV_VAR")
|
|
||||||
package dotenv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/template"
|
|
||||||
)
|
|
||||||
|
|
||||||
var utf8BOM = []byte("\uFEFF")
|
|
||||||
|
|
||||||
var startsWithDigitRegex = regexp.MustCompile(`^\s*\d.*`) // Keys starting with numbers are ignored
|
|
||||||
|
|
||||||
// LookupFn represents a lookup function to resolve variables from
|
|
||||||
type LookupFn func(string) (string, bool)
|
|
||||||
|
|
||||||
var noLookupFn = func(_ string) (string, bool) {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse reads an env file from io.Reader, returning a map of keys and values.
|
|
||||||
func Parse(r io.Reader) (map[string]string, error) {
|
|
||||||
return ParseWithLookup(r, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseWithLookup reads an env file from io.Reader, returning a map of keys and values.
|
|
||||||
func ParseWithLookup(r io.Reader, lookupFn LookupFn) (map[string]string, error) {
|
|
||||||
vars := map[string]string{}
|
|
||||||
err := parseWithLookup(r, vars, lookupFn)
|
|
||||||
return vars, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseWithLookup reads an env file from io.Reader, returning a map of keys and values.
|
|
||||||
func parseWithLookup(r io.Reader, vars map[string]string, lookupFn LookupFn) error {
|
|
||||||
data, err := io.ReadAll(r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// seek past the UTF-8 BOM if it exists (particularly on Windows, some
|
|
||||||
// editors tend to add it, and it'll cause parsing to fail)
|
|
||||||
data = bytes.TrimPrefix(data, utf8BOM)
|
|
||||||
|
|
||||||
return newParser().parse(string(data), vars, lookupFn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load will read your env file(s) and load them into ENV for this process.
|
|
||||||
//
|
|
||||||
// Call this function as close as possible to the start of your program (ideally in main).
|
|
||||||
//
|
|
||||||
// If you call Load without any args it will default to loading .env in the current path.
|
|
||||||
//
|
|
||||||
// You can otherwise tell it which files to load (there can be more than one) like:
|
|
||||||
//
|
|
||||||
// godotenv.Load("fileone", "filetwo")
|
|
||||||
//
|
|
||||||
// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults
|
|
||||||
func Load(filenames ...string) error {
|
|
||||||
return load(false, filenames...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func load(overload bool, filenames ...string) error {
|
|
||||||
filenames = filenamesOrDefault(filenames)
|
|
||||||
for _, filename := range filenames {
|
|
||||||
err := loadFile(filename, overload)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadWithLookup gets all env vars from the files and/or lookup function and return values as
|
|
||||||
// a map rather than automatically writing values into env
|
|
||||||
func ReadWithLookup(lookupFn LookupFn, filenames ...string) (map[string]string, error) {
|
|
||||||
filenames = filenamesOrDefault(filenames)
|
|
||||||
envMap := make(map[string]string)
|
|
||||||
|
|
||||||
for _, filename := range filenames {
|
|
||||||
individualEnvMap, individualErr := ReadFile(filename, lookupFn)
|
|
||||||
|
|
||||||
if individualErr != nil {
|
|
||||||
return envMap, individualErr
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, value := range individualEnvMap {
|
|
||||||
if startsWithDigitRegex.MatchString(key) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
envMap[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return envMap, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read all env (with same file loading semantics as Load) but return values as
|
|
||||||
// a map rather than automatically writing values into env
|
|
||||||
func Read(filenames ...string) (map[string]string, error) {
|
|
||||||
return ReadWithLookup(nil, filenames...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalBytesWithLookup parses env file from byte slice of chars, returning a map of keys and values.
|
|
||||||
func UnmarshalBytesWithLookup(src []byte, lookupFn LookupFn) (map[string]string, error) {
|
|
||||||
return UnmarshalWithLookup(string(src), lookupFn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalWithLookup parses env file from string, returning a map of keys and values.
|
|
||||||
func UnmarshalWithLookup(src string, lookupFn LookupFn) (map[string]string, error) {
|
|
||||||
out := make(map[string]string)
|
|
||||||
err := newParser().parse(src, out, lookupFn)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func filenamesOrDefault(filenames []string) []string {
|
|
||||||
if len(filenames) == 0 {
|
|
||||||
return []string{".env"}
|
|
||||||
}
|
|
||||||
return filenames
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadFile(filename string, overload bool) error {
|
|
||||||
envMap, err := ReadFile(filename, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
currentEnv := map[string]bool{}
|
|
||||||
rawEnv := os.Environ()
|
|
||||||
for _, rawEnvLine := range rawEnv {
|
|
||||||
key := strings.Split(rawEnvLine, "=")[0]
|
|
||||||
currentEnv[key] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, value := range envMap {
|
|
||||||
if !currentEnv[key] || overload {
|
|
||||||
_ = os.Setenv(key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadFile(filename string, lookupFn LookupFn) (map[string]string, error) {
|
|
||||||
file, err := os.Open(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
return ParseWithLookup(file, lookupFn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func expandVariables(value string, envMap map[string]string, lookupFn LookupFn) (string, error) {
|
|
||||||
retVal, err := template.Substitute(value, func(k string) (string, bool) {
|
|
||||||
if v, ok := lookupFn(k); ok {
|
|
||||||
return v, true
|
|
||||||
}
|
|
||||||
v, ok := envMap[k]
|
|
||||||
return v, ok
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return value, err
|
|
||||||
}
|
|
||||||
return retVal, nil
|
|
||||||
}
|
|
||||||
286
vendor/github.com/compose-spec/compose-go/v2/dotenv/parser.go
generated
vendored
286
vendor/github.com/compose-spec/compose-go/v2/dotenv/parser.go
generated
vendored
@ -1,286 +0,0 @@
|
|||||||
package dotenv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
charComment = '#'
|
|
||||||
prefixSingleQuote = '\''
|
|
||||||
prefixDoubleQuote = '"'
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
escapeSeqRegex = regexp.MustCompile(`(\\(?:[abcfnrtv$"\\]|0\d{0,3}))`)
|
|
||||||
exportRegex = regexp.MustCompile(`^export\s+`)
|
|
||||||
)
|
|
||||||
|
|
||||||
type parser struct {
|
|
||||||
line int
|
|
||||||
}
|
|
||||||
|
|
||||||
func newParser() *parser {
|
|
||||||
return &parser{
|
|
||||||
line: 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *parser) parse(src string, out map[string]string, lookupFn LookupFn) error {
|
|
||||||
cutset := src
|
|
||||||
if lookupFn == nil {
|
|
||||||
lookupFn = noLookupFn
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
cutset = p.getStatementStart(cutset)
|
|
||||||
if cutset == "" {
|
|
||||||
// reached end of file
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
key, left, inherited, err := p.locateKeyName(cutset)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if strings.Contains(key, " ") {
|
|
||||||
return fmt.Errorf("line %d: key cannot contain a space", p.line)
|
|
||||||
}
|
|
||||||
|
|
||||||
if inherited {
|
|
||||||
value, ok := lookupFn(key)
|
|
||||||
if ok {
|
|
||||||
out[key] = value
|
|
||||||
}
|
|
||||||
cutset = left
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
value, left, err := p.extractVarValue(left, out, lookupFn)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
out[key] = value
|
|
||||||
cutset = left
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getStatementPosition returns position of statement begin.
|
|
||||||
//
|
|
||||||
// It skips any comment line or non-whitespace character.
|
|
||||||
func (p *parser) getStatementStart(src string) string {
|
|
||||||
pos := p.indexOfNonSpaceChar(src)
|
|
||||||
if pos == -1 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
src = src[pos:]
|
|
||||||
if src[0] != charComment {
|
|
||||||
return src
|
|
||||||
}
|
|
||||||
|
|
||||||
// skip comment section
|
|
||||||
pos = strings.IndexFunc(src, isCharFunc('\n'))
|
|
||||||
if pos == -1 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return p.getStatementStart(src[pos:])
|
|
||||||
}
|
|
||||||
|
|
||||||
// locateKeyName locates and parses key name and returns rest of slice
|
|
||||||
func (p *parser) locateKeyName(src string) (string, string, bool, error) {
|
|
||||||
var key string
|
|
||||||
var inherited bool
|
|
||||||
// trim "export" and space at beginning
|
|
||||||
if exportRegex.MatchString(src) {
|
|
||||||
// we use a `strings.trim` to preserve the pointer to the same underlying memory.
|
|
||||||
// a regexp replace would copy the string.
|
|
||||||
src = strings.TrimLeftFunc(strings.TrimPrefix(src, "export"), isSpace)
|
|
||||||
}
|
|
||||||
|
|
||||||
// locate key name end and validate it in single loop
|
|
||||||
offset := 0
|
|
||||||
loop:
|
|
||||||
for i, rune := range src {
|
|
||||||
if isSpace(rune) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch rune {
|
|
||||||
case '=', ':', '\n':
|
|
||||||
// library also supports yaml-style value declaration
|
|
||||||
key = src[0:i]
|
|
||||||
offset = i + 1
|
|
||||||
inherited = rune == '\n'
|
|
||||||
break loop
|
|
||||||
case '_', '.', '-', '[', ']':
|
|
||||||
default:
|
|
||||||
// variable name should match [A-Za-z0-9_.-]
|
|
||||||
if unicode.IsLetter(rune) || unicode.IsNumber(rune) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", "", inherited, fmt.Errorf(
|
|
||||||
`line %d: unexpected character %q in variable name %q`,
|
|
||||||
p.line, string(rune), strings.Split(src, "\n")[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if src == "" {
|
|
||||||
return "", "", inherited, errors.New("zero length string")
|
|
||||||
}
|
|
||||||
|
|
||||||
if inherited && strings.IndexByte(key, ' ') == -1 {
|
|
||||||
p.line++
|
|
||||||
}
|
|
||||||
|
|
||||||
// trim whitespace
|
|
||||||
key = strings.TrimRightFunc(key, unicode.IsSpace)
|
|
||||||
cutset := strings.TrimLeftFunc(src[offset:], isSpace)
|
|
||||||
return key, cutset, inherited, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractVarValue extracts variable value and returns rest of slice
|
|
||||||
func (p *parser) extractVarValue(src string, envMap map[string]string, lookupFn LookupFn) (string, string, error) {
|
|
||||||
quote, isQuoted := hasQuotePrefix(src)
|
|
||||||
if !isQuoted {
|
|
||||||
// unquoted value - read until new line
|
|
||||||
value, rest, _ := strings.Cut(src, "\n")
|
|
||||||
p.line++
|
|
||||||
|
|
||||||
// Remove inline comments on unquoted lines
|
|
||||||
value, _, _ = strings.Cut(value, " #")
|
|
||||||
value = strings.TrimRightFunc(value, unicode.IsSpace)
|
|
||||||
retVal, err := expandVariables(value, envMap, lookupFn)
|
|
||||||
return retVal, rest, err
|
|
||||||
}
|
|
||||||
|
|
||||||
previousCharIsEscape := false
|
|
||||||
// lookup quoted string terminator
|
|
||||||
var chars []byte
|
|
||||||
for i := 1; i < len(src); i++ {
|
|
||||||
char := src[i]
|
|
||||||
if char == '\n' {
|
|
||||||
p.line++
|
|
||||||
}
|
|
||||||
if char != quote {
|
|
||||||
if !previousCharIsEscape && char == '\\' {
|
|
||||||
previousCharIsEscape = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if previousCharIsEscape {
|
|
||||||
previousCharIsEscape = false
|
|
||||||
chars = append(chars, '\\')
|
|
||||||
}
|
|
||||||
chars = append(chars, char)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// skip escaped quote symbol (\" or \', depends on quote)
|
|
||||||
if previousCharIsEscape {
|
|
||||||
previousCharIsEscape = false
|
|
||||||
chars = append(chars, char)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// trim quotes
|
|
||||||
value := string(chars)
|
|
||||||
if quote == prefixDoubleQuote {
|
|
||||||
// expand standard shell escape sequences & then interpolate
|
|
||||||
// variables on the result
|
|
||||||
retVal, err := expandVariables(expandEscapes(value), envMap, lookupFn)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
value = retVal
|
|
||||||
}
|
|
||||||
|
|
||||||
return value, src[i+1:], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// return formatted error if quoted string is not terminated
|
|
||||||
valEndIndex := strings.IndexFunc(src, isCharFunc('\n'))
|
|
||||||
if valEndIndex == -1 {
|
|
||||||
valEndIndex = len(src)
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", "", fmt.Errorf("line %d: unterminated quoted value %s", p.line, src[:valEndIndex])
|
|
||||||
}
|
|
||||||
|
|
||||||
func expandEscapes(str string) string {
|
|
||||||
out := escapeSeqRegex.ReplaceAllStringFunc(str, func(match string) string {
|
|
||||||
if match == `\$` {
|
|
||||||
// `\$` is not a Go escape sequence, the expansion parser uses
|
|
||||||
// the special `$$` syntax
|
|
||||||
// both `FOO=\$bar` and `FOO=$$bar` are valid in an env file and
|
|
||||||
// will result in FOO w/ literal value of "$bar" (no interpolation)
|
|
||||||
return "$$"
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(match, `\0`) {
|
|
||||||
// octal escape sequences in Go are not prefixed with `\0`, so
|
|
||||||
// rewrite the prefix, e.g. `\0123` -> `\123` -> literal value "S"
|
|
||||||
match = strings.Replace(match, `\0`, `\`, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// use Go to unquote (unescape) the literal
|
|
||||||
// see https://go.dev/ref/spec#Rune_literals
|
|
||||||
//
|
|
||||||
// NOTE: Go supports ADDITIONAL escapes like `\x` & `\u` & `\U`!
|
|
||||||
// These are NOT supported, which is why we use a regex to find
|
|
||||||
// only matches we support and then use `UnquoteChar` instead of a
|
|
||||||
// `Unquote` on the entire value
|
|
||||||
v, _, _, err := strconv.UnquoteChar(match, '"')
|
|
||||||
if err != nil {
|
|
||||||
return match
|
|
||||||
}
|
|
||||||
return string(v)
|
|
||||||
})
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *parser) indexOfNonSpaceChar(src string) int {
|
|
||||||
return strings.IndexFunc(src, func(r rune) bool {
|
|
||||||
if r == '\n' {
|
|
||||||
p.line++
|
|
||||||
}
|
|
||||||
return !unicode.IsSpace(r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// hasQuotePrefix reports whether charset starts with single or double quote and returns quote character
|
|
||||||
func hasQuotePrefix(src string) (byte, bool) {
|
|
||||||
if src == "" {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
switch quote := src[0]; quote {
|
|
||||||
case prefixDoubleQuote, prefixSingleQuote:
|
|
||||||
return quote, true // isQuoted
|
|
||||||
default:
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isCharFunc(char rune) func(rune) bool {
|
|
||||||
return func(v rune) bool {
|
|
||||||
return v == char
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// isSpace reports whether the rune is a space character but not line break character
|
|
||||||
//
|
|
||||||
// this differs from unicode.IsSpace, which also applies line break as space
|
|
||||||
func isSpace(r rune) bool {
|
|
||||||
switch r {
|
|
||||||
case '\t', '\v', '\f', '\r', ' ', 0x85, 0xA0:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
56
vendor/github.com/compose-spec/compose-go/v2/errdefs/errors.go
generated
vendored
56
vendor/github.com/compose-spec/compose-go/v2/errdefs/errors.go
generated
vendored
@ -1,56 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package errdefs
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrNotFound is returned when an object is not found
|
|
||||||
ErrNotFound = errors.New("not found")
|
|
||||||
|
|
||||||
// ErrInvalid is returned when a compose project is invalid
|
|
||||||
ErrInvalid = errors.New("invalid compose project")
|
|
||||||
|
|
||||||
// ErrUnsupported is returned when a compose project uses an unsupported attribute
|
|
||||||
ErrUnsupported = errors.New("unsupported attribute")
|
|
||||||
|
|
||||||
// ErrIncompatible is returned when a compose project uses an incompatible attribute
|
|
||||||
ErrIncompatible = errors.New("incompatible attribute")
|
|
||||||
|
|
||||||
// ErrDisabled is returned when a resource was found in model but is disabled
|
|
||||||
ErrDisabled = errors.New("disabled")
|
|
||||||
)
|
|
||||||
|
|
||||||
// IsNotFoundError returns true if the unwrapped error is ErrNotFound
|
|
||||||
func IsNotFoundError(err error) bool {
|
|
||||||
return errors.Is(err, ErrNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsInvalidError returns true if the unwrapped error is ErrInvalid
|
|
||||||
func IsInvalidError(err error) bool {
|
|
||||||
return errors.Is(err, ErrInvalid)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsUnsupportedError returns true if the unwrapped error is ErrUnsupported
|
|
||||||
func IsUnsupportedError(err error) bool {
|
|
||||||
return errors.Is(err, ErrUnsupported)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsUnsupportedError returns true if the unwrapped error is ErrIncompatible
|
|
||||||
func IsIncompatibleError(err error) bool {
|
|
||||||
return errors.Is(err, ErrIncompatible)
|
|
||||||
}
|
|
||||||
199
vendor/github.com/compose-spec/compose-go/v2/format/volume.go
generated
vendored
199
vendor/github.com/compose-spec/compose-go/v2/format/volume.go
generated
vendored
@ -1,199 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package format
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
"unicode/utf8"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
const endOfSpec = rune(0)
|
|
||||||
|
|
||||||
// ParseVolume parses a volume spec without any knowledge of the target platform
|
|
||||||
func ParseVolume(spec string) (types.ServiceVolumeConfig, error) {
|
|
||||||
volume := types.ServiceVolumeConfig{}
|
|
||||||
|
|
||||||
switch len(spec) {
|
|
||||||
case 0:
|
|
||||||
return volume, errors.New("invalid empty volume spec")
|
|
||||||
case 1, 2:
|
|
||||||
volume.Target = spec
|
|
||||||
volume.Type = types.VolumeTypeVolume
|
|
||||||
return volume, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var buffer []rune
|
|
||||||
var inVarSubstitution int // Track nesting depth of ${...}
|
|
||||||
for i, char := range spec + string(endOfSpec) {
|
|
||||||
// Check if we're entering a variable substitution
|
|
||||||
if char == '$' && i+1 < len(spec) && rune(spec[i+1]) == '{' {
|
|
||||||
inVarSubstitution++
|
|
||||||
buffer = append(buffer, char)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we're exiting a variable substitution
|
|
||||||
if char == '}' && inVarSubstitution > 0 {
|
|
||||||
inVarSubstitution--
|
|
||||||
buffer = append(buffer, char)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case isWindowsDrive(buffer, char):
|
|
||||||
buffer = append(buffer, char)
|
|
||||||
case (char == ':' || char == endOfSpec) && inVarSubstitution == 0:
|
|
||||||
if err := populateFieldFromBuffer(char, buffer, &volume); err != nil {
|
|
||||||
populateType(&volume)
|
|
||||||
return volume, fmt.Errorf("invalid spec: %s: %w", spec, err)
|
|
||||||
}
|
|
||||||
buffer = nil
|
|
||||||
default:
|
|
||||||
buffer = append(buffer, char)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
populateType(&volume)
|
|
||||||
return volume, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isWindowsDrive(buffer []rune, char rune) bool {
|
|
||||||
return char == ':' && len(buffer) == 1 && unicode.IsLetter(buffer[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
func populateFieldFromBuffer(char rune, buffer []rune, volume *types.ServiceVolumeConfig) error {
|
|
||||||
strBuffer := string(buffer)
|
|
||||||
switch {
|
|
||||||
case len(buffer) == 0:
|
|
||||||
return errors.New("empty section between colons")
|
|
||||||
// Anonymous volume
|
|
||||||
case volume.Source == "" && char == endOfSpec:
|
|
||||||
volume.Target = strBuffer
|
|
||||||
return nil
|
|
||||||
case volume.Source == "":
|
|
||||||
volume.Source = strBuffer
|
|
||||||
return nil
|
|
||||||
case volume.Target == "":
|
|
||||||
volume.Target = strBuffer
|
|
||||||
return nil
|
|
||||||
case char == ':':
|
|
||||||
return errors.New("too many colons")
|
|
||||||
}
|
|
||||||
for _, option := range strings.Split(strBuffer, ",") {
|
|
||||||
switch option {
|
|
||||||
case "ro":
|
|
||||||
volume.ReadOnly = true
|
|
||||||
case "rw":
|
|
||||||
volume.ReadOnly = false
|
|
||||||
case "nocopy":
|
|
||||||
volume.Volume = &types.ServiceVolumeVolume{NoCopy: true}
|
|
||||||
default:
|
|
||||||
if isBindOption(option) {
|
|
||||||
setBindOption(volume, option)
|
|
||||||
}
|
|
||||||
// ignore unknown options FIXME why not report an error here?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var Propagations = []string{
|
|
||||||
types.PropagationRPrivate,
|
|
||||||
types.PropagationPrivate,
|
|
||||||
types.PropagationRShared,
|
|
||||||
types.PropagationShared,
|
|
||||||
types.PropagationRSlave,
|
|
||||||
types.PropagationSlave,
|
|
||||||
}
|
|
||||||
|
|
||||||
type setBindOptionFunc func(bind *types.ServiceVolumeBind, option string)
|
|
||||||
|
|
||||||
var bindOptions = map[string]setBindOptionFunc{
|
|
||||||
types.PropagationRPrivate: setBindPropagation,
|
|
||||||
types.PropagationPrivate: setBindPropagation,
|
|
||||||
types.PropagationRShared: setBindPropagation,
|
|
||||||
types.PropagationShared: setBindPropagation,
|
|
||||||
types.PropagationRSlave: setBindPropagation,
|
|
||||||
types.PropagationSlave: setBindPropagation,
|
|
||||||
types.SELinuxShared: setBindSELinux,
|
|
||||||
types.SELinuxPrivate: setBindSELinux,
|
|
||||||
}
|
|
||||||
|
|
||||||
func setBindPropagation(bind *types.ServiceVolumeBind, option string) {
|
|
||||||
bind.Propagation = option
|
|
||||||
}
|
|
||||||
|
|
||||||
func setBindSELinux(bind *types.ServiceVolumeBind, option string) {
|
|
||||||
bind.SELinux = option
|
|
||||||
}
|
|
||||||
|
|
||||||
func isBindOption(option string) bool {
|
|
||||||
_, ok := bindOptions[option]
|
|
||||||
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func setBindOption(volume *types.ServiceVolumeConfig, option string) {
|
|
||||||
if volume.Bind == nil {
|
|
||||||
volume.Bind = &types.ServiceVolumeBind{}
|
|
||||||
}
|
|
||||||
|
|
||||||
bindOptions[option](volume.Bind, option)
|
|
||||||
}
|
|
||||||
|
|
||||||
func populateType(volume *types.ServiceVolumeConfig) {
|
|
||||||
if isFilePath(volume.Source) {
|
|
||||||
volume.Type = types.VolumeTypeBind
|
|
||||||
if volume.Bind == nil {
|
|
||||||
volume.Bind = &types.ServiceVolumeBind{}
|
|
||||||
}
|
|
||||||
// For backward compatibility with docker-compose legacy, using short notation involves
|
|
||||||
// bind will create missing host path
|
|
||||||
volume.Bind.CreateHostPath = true
|
|
||||||
} else {
|
|
||||||
volume.Type = types.VolumeTypeVolume
|
|
||||||
if volume.Volume == nil {
|
|
||||||
volume.Volume = &types.ServiceVolumeVolume{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isFilePath(source string) bool {
|
|
||||||
if source == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
switch source[0] {
|
|
||||||
case '.', '/', '~':
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// windows named pipes
|
|
||||||
if strings.HasPrefix(source, `\\`) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
first, nextIndex := utf8.DecodeRuneInString(source)
|
|
||||||
if len(source) <= nextIndex {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return isWindowsDrive([]rune{first}, rune(source[nextIndex]))
|
|
||||||
}
|
|
||||||
63
vendor/github.com/compose-spec/compose-go/v2/graph/cycle.go
generated
vendored
63
vendor/github.com/compose-spec/compose-go/v2/graph/cycle.go
generated
vendored
@ -1,63 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package graph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
|
||||||
"github.com/compose-spec/compose-go/v2/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CheckCycle analyze project's depends_on relation and report an error on cycle detection
|
|
||||||
func CheckCycle(project *types.Project) error {
|
|
||||||
g, err := newGraph(project)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return g.checkCycle()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *graph[T]) checkCycle() error {
|
|
||||||
// iterate on vertices in a name-order to render a predicable error message
|
|
||||||
// this is required by tests and enforce command reproducibility by user, which otherwise could be confusing
|
|
||||||
names := utils.MapKeys(g.vertices)
|
|
||||||
for _, name := range names {
|
|
||||||
err := searchCycle([]string{name}, g.vertices[name])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func searchCycle[T any](path []string, v *vertex[T]) error {
|
|
||||||
names := utils.MapKeys(v.children)
|
|
||||||
for _, name := range names {
|
|
||||||
if i := slices.Index(path, name); i >= 0 {
|
|
||||||
return fmt.Errorf("dependency cycle detected: %s -> %s", strings.Join(path[i:], " -> "), name)
|
|
||||||
}
|
|
||||||
ch := v.children[name]
|
|
||||||
err := searchCycle(append(path, name), ch)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
75
vendor/github.com/compose-spec/compose-go/v2/graph/graph.go
generated
vendored
75
vendor/github.com/compose-spec/compose-go/v2/graph/graph.go
generated
vendored
@ -1,75 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package graph
|
|
||||||
|
|
||||||
// graph represents project as service dependencies
|
|
||||||
type graph[T any] struct {
|
|
||||||
vertices map[string]*vertex[T]
|
|
||||||
}
|
|
||||||
|
|
||||||
// vertex represents a service in the dependencies structure
|
|
||||||
type vertex[T any] struct {
|
|
||||||
key string
|
|
||||||
service *T
|
|
||||||
children map[string]*vertex[T]
|
|
||||||
parents map[string]*vertex[T]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *graph[T]) addVertex(name string, service T) {
|
|
||||||
g.vertices[name] = &vertex[T]{
|
|
||||||
key: name,
|
|
||||||
service: &service,
|
|
||||||
parents: map[string]*vertex[T]{},
|
|
||||||
children: map[string]*vertex[T]{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *graph[T]) addEdge(src, dest string) {
|
|
||||||
g.vertices[src].children[dest] = g.vertices[dest]
|
|
||||||
g.vertices[dest].parents[src] = g.vertices[src]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *graph[T]) roots() []*vertex[T] {
|
|
||||||
var res []*vertex[T]
|
|
||||||
for _, v := range g.vertices {
|
|
||||||
if len(v.parents) == 0 {
|
|
||||||
res = append(res, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *graph[T]) leaves() []*vertex[T] {
|
|
||||||
var res []*vertex[T]
|
|
||||||
for _, v := range g.vertices {
|
|
||||||
if len(v.children) == 0 {
|
|
||||||
res = append(res, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// descendents return all descendents for a vertex, might contain duplicates
|
|
||||||
func (v *vertex[T]) descendents() []string {
|
|
||||||
var vx []string
|
|
||||||
for _, n := range v.children {
|
|
||||||
vx = append(vx, n.key)
|
|
||||||
vx = append(vx, n.descendents()...)
|
|
||||||
}
|
|
||||||
return vx
|
|
||||||
}
|
|
||||||
80
vendor/github.com/compose-spec/compose-go/v2/graph/services.go
generated
vendored
80
vendor/github.com/compose-spec/compose-go/v2/graph/services.go
generated
vendored
@ -1,80 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package graph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
// InDependencyOrder walk the service graph an invoke VisitorFn in respect to dependency order
|
|
||||||
func InDependencyOrder(ctx context.Context, project *types.Project, fn VisitorFn[types.ServiceConfig], options ...func(*Options)) error {
|
|
||||||
_, err := CollectInDependencyOrder[any](ctx, project, func(ctx context.Context, s string, config types.ServiceConfig) (any, error) {
|
|
||||||
return nil, fn(ctx, s, config)
|
|
||||||
}, options...)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// CollectInDependencyOrder walk the service graph an invoke CollectorFn in respect to dependency order, then return result for each call
|
|
||||||
func CollectInDependencyOrder[T any](ctx context.Context, project *types.Project, fn CollectorFn[types.ServiceConfig, T], options ...func(*Options)) (map[string]T, error) {
|
|
||||||
graph, err := newGraph(project)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
t := newTraversal(fn)
|
|
||||||
for _, option := range options {
|
|
||||||
option(t.Options)
|
|
||||||
}
|
|
||||||
err = walk(ctx, graph, t)
|
|
||||||
return t.results, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// newGraph creates a service graph from project
|
|
||||||
func newGraph(project *types.Project) (*graph[types.ServiceConfig], error) {
|
|
||||||
g := &graph[types.ServiceConfig]{
|
|
||||||
vertices: map[string]*vertex[types.ServiceConfig]{},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, s := range project.Services {
|
|
||||||
g.addVertex(name, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, s := range project.Services {
|
|
||||||
src := g.vertices[name]
|
|
||||||
for dep, condition := range s.DependsOn {
|
|
||||||
dest, ok := g.vertices[dep]
|
|
||||||
if !ok {
|
|
||||||
if condition.Required {
|
|
||||||
if ds, exists := project.DisabledServices[dep]; exists {
|
|
||||||
return nil, fmt.Errorf("service %q is required by %q but is disabled. Can be enabled by profiles %s", dep, name, ds.Profiles)
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("service %q depends on unknown service %q", name, dep)
|
|
||||||
}
|
|
||||||
delete(s.DependsOn, name)
|
|
||||||
project.Services[name] = s
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
src.children[dep] = dest
|
|
||||||
dest.parents[name] = src
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := g.checkCycle()
|
|
||||||
return g, err
|
|
||||||
}
|
|
||||||
211
vendor/github.com/compose-spec/compose-go/v2/graph/traversal.go
generated
vendored
211
vendor/github.com/compose-spec/compose-go/v2/graph/traversal.go
generated
vendored
@ -1,211 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package graph
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"slices"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CollectorFn executes on each graph vertex based on visit order and return associated value
|
|
||||||
type CollectorFn[S any, T any] func(context.Context, string, S) (T, error)
|
|
||||||
|
|
||||||
// VisitorFn executes on each graph nodes based on visit order
|
|
||||||
type VisitorFn[S any] func(context.Context, string, S) error
|
|
||||||
|
|
||||||
type traversal[S any, T any] struct {
|
|
||||||
*Options
|
|
||||||
visitor CollectorFn[S, T]
|
|
||||||
|
|
||||||
mu sync.Mutex
|
|
||||||
status map[string]int
|
|
||||||
results map[string]T
|
|
||||||
}
|
|
||||||
|
|
||||||
type Options struct {
|
|
||||||
// inverse reverse the traversal direction
|
|
||||||
inverse bool
|
|
||||||
// maxConcurrency limit the concurrent execution of visitorFn while walking the graph
|
|
||||||
maxConcurrency int
|
|
||||||
// after marks a set of node as starting points walking the graph
|
|
||||||
after []string
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
vertexEntered = iota
|
|
||||||
vertexVisited
|
|
||||||
)
|
|
||||||
|
|
||||||
func newTraversal[S, T any](fn CollectorFn[S, T]) *traversal[S, T] {
|
|
||||||
return &traversal[S, T]{
|
|
||||||
Options: &Options{},
|
|
||||||
status: map[string]int{},
|
|
||||||
results: map[string]T{},
|
|
||||||
visitor: fn,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithMaxConcurrency configure traversal to limit concurrency walking graph nodes
|
|
||||||
func WithMaxConcurrency(concurrency int) func(*Options) {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.maxConcurrency = concurrency
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InReverseOrder configure traversal to walk the graph in reverse dependency order
|
|
||||||
func InReverseOrder(o *Options) {
|
|
||||||
o.inverse = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithRootNodesAndDown creates a graphTraversal to start from selected nodes
|
|
||||||
func WithRootNodesAndDown(nodes []string) func(*Options) {
|
|
||||||
return func(o *Options) {
|
|
||||||
o.after = nodes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func walk[S, T any](ctx context.Context, g *graph[S], t *traversal[S, T]) error {
|
|
||||||
expect := len(g.vertices)
|
|
||||||
if expect == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// nodeCh need to allow n=expect writers while reader goroutine could have returned after ctx.Done
|
|
||||||
nodeCh := make(chan *vertex[S], expect)
|
|
||||||
defer close(nodeCh)
|
|
||||||
|
|
||||||
eg, ctx := errgroup.WithContext(ctx)
|
|
||||||
if t.maxConcurrency > 0 {
|
|
||||||
eg.SetLimit(t.maxConcurrency + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
eg.Go(func() error {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil
|
|
||||||
case node := <-nodeCh:
|
|
||||||
expect--
|
|
||||||
if expect == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, adj := range t.adjacentNodes(node) {
|
|
||||||
t.visit(ctx, eg, adj, nodeCh)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// select nodes to start walking the graph based on traversal.direction
|
|
||||||
for _, node := range t.extremityNodes(g) {
|
|
||||||
t.visit(ctx, eg, node, nodeCh)
|
|
||||||
}
|
|
||||||
|
|
||||||
return eg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *traversal[S, T]) visit(ctx context.Context, eg *errgroup.Group, node *vertex[S], nodeCh chan *vertex[S]) {
|
|
||||||
if !t.ready(node) {
|
|
||||||
// don't visit this service yet as dependencies haven't been visited
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !t.enter(node) {
|
|
||||||
// another worker already acquired this node
|
|
||||||
return
|
|
||||||
}
|
|
||||||
eg.Go(func() error {
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
result T
|
|
||||||
)
|
|
||||||
if !t.skip(node) {
|
|
||||||
result, err = t.visitor(ctx, node.key, *node.service)
|
|
||||||
}
|
|
||||||
t.done(node, result)
|
|
||||||
nodeCh <- node
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *traversal[S, T]) extremityNodes(g *graph[S]) []*vertex[S] {
|
|
||||||
if t.inverse {
|
|
||||||
return g.roots()
|
|
||||||
}
|
|
||||||
return g.leaves()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *traversal[S, T]) adjacentNodes(v *vertex[S]) map[string]*vertex[S] {
|
|
||||||
if t.inverse {
|
|
||||||
return v.children
|
|
||||||
}
|
|
||||||
return v.parents
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *traversal[S, T]) ready(v *vertex[S]) bool {
|
|
||||||
t.mu.Lock()
|
|
||||||
defer t.mu.Unlock()
|
|
||||||
|
|
||||||
depends := v.children
|
|
||||||
if t.inverse {
|
|
||||||
depends = v.parents
|
|
||||||
}
|
|
||||||
for name := range depends {
|
|
||||||
if t.status[name] != vertexVisited {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *traversal[S, T]) enter(v *vertex[S]) bool {
|
|
||||||
t.mu.Lock()
|
|
||||||
defer t.mu.Unlock()
|
|
||||||
|
|
||||||
if _, ok := t.status[v.key]; ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
t.status[v.key] = vertexEntered
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *traversal[S, T]) done(v *vertex[S], result T) {
|
|
||||||
t.mu.Lock()
|
|
||||||
defer t.mu.Unlock()
|
|
||||||
t.status[v.key] = vertexVisited
|
|
||||||
t.results[v.key] = result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *traversal[S, T]) skip(node *vertex[S]) bool {
|
|
||||||
if len(t.after) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if slices.Contains(t.after, node.key) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// is none of our starting node is a descendent, skip visit
|
|
||||||
ancestors := node.descendents()
|
|
||||||
for _, name := range t.after {
|
|
||||||
if slices.Contains(ancestors, name) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
137
vendor/github.com/compose-spec/compose-go/v2/interpolation/interpolation.go
generated
vendored
137
vendor/github.com/compose-spec/compose-go/v2/interpolation/interpolation.go
generated
vendored
@ -1,137 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package interpolation
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/template"
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Options supported by Interpolate
|
|
||||||
type Options struct {
|
|
||||||
// LookupValue from a key
|
|
||||||
LookupValue LookupValue
|
|
||||||
// TypeCastMapping maps key paths to functions to cast to a type
|
|
||||||
TypeCastMapping map[tree.Path]Cast
|
|
||||||
// Substitution function to use
|
|
||||||
Substitute func(string, template.Mapping) (string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LookupValue is a function which maps from variable names to values.
|
|
||||||
// Returns the value as a string and a bool indicating whether
|
|
||||||
// the value is present, to distinguish between an empty string
|
|
||||||
// and the absence of a value.
|
|
||||||
type LookupValue func(key string) (string, bool)
|
|
||||||
|
|
||||||
// Cast a value to a new type, or return an error if the value can't be cast
|
|
||||||
type Cast func(value string) (interface{}, error)
|
|
||||||
|
|
||||||
// Interpolate replaces variables in a string with the values from a mapping
|
|
||||||
func Interpolate(config map[string]interface{}, opts Options) (map[string]interface{}, error) {
|
|
||||||
if opts.LookupValue == nil {
|
|
||||||
opts.LookupValue = os.LookupEnv
|
|
||||||
}
|
|
||||||
if opts.TypeCastMapping == nil {
|
|
||||||
opts.TypeCastMapping = make(map[tree.Path]Cast)
|
|
||||||
}
|
|
||||||
if opts.Substitute == nil {
|
|
||||||
opts.Substitute = template.Substitute
|
|
||||||
}
|
|
||||||
|
|
||||||
out := map[string]interface{}{}
|
|
||||||
|
|
||||||
for key, value := range config {
|
|
||||||
interpolatedValue, err := recursiveInterpolate(value, tree.NewPath(key), opts)
|
|
||||||
if err != nil {
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
out[key] = interpolatedValue
|
|
||||||
}
|
|
||||||
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func recursiveInterpolate(value interface{}, path tree.Path, opts Options) (interface{}, error) {
|
|
||||||
switch value := value.(type) {
|
|
||||||
case string:
|
|
||||||
newValue, err := opts.Substitute(value, template.Mapping(opts.LookupValue))
|
|
||||||
if err != nil {
|
|
||||||
return value, newPathError(path, err)
|
|
||||||
}
|
|
||||||
caster, ok := opts.getCasterForPath(path)
|
|
||||||
if !ok {
|
|
||||||
return newValue, nil
|
|
||||||
}
|
|
||||||
casted, err := caster(newValue)
|
|
||||||
if err != nil {
|
|
||||||
return casted, newPathError(path, fmt.Errorf("failed to cast to expected type: %w", err))
|
|
||||||
}
|
|
||||||
return casted, nil
|
|
||||||
|
|
||||||
case map[string]interface{}:
|
|
||||||
out := map[string]interface{}{}
|
|
||||||
for key, elem := range value {
|
|
||||||
interpolatedElem, err := recursiveInterpolate(elem, path.Next(key), opts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out[key] = interpolatedElem
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
|
|
||||||
case []interface{}:
|
|
||||||
out := make([]interface{}, len(value))
|
|
||||||
for i, elem := range value {
|
|
||||||
interpolatedElem, err := recursiveInterpolate(elem, path.Next(tree.PathMatchList), opts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out[i] = interpolatedElem
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPathError(path tree.Path, err error) error {
|
|
||||||
var ite *template.InvalidTemplateError
|
|
||||||
switch {
|
|
||||||
case err == nil:
|
|
||||||
return nil
|
|
||||||
case errors.As(err, &ite):
|
|
||||||
return fmt.Errorf(
|
|
||||||
"invalid interpolation format for %s.\nYou may need to escape any $ with another $.\n%s",
|
|
||||||
path, ite.Template)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("error while interpolating %s: %w", path, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o Options) getCasterForPath(path tree.Path) (Cast, bool) {
|
|
||||||
for pattern, caster := range o.TypeCastMapping {
|
|
||||||
if path.Matches(pattern) {
|
|
||||||
return caster, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
110
vendor/github.com/compose-spec/compose-go/v2/loader/environment.go
generated
vendored
110
vendor/github.com/compose-spec/compose-go/v2/loader/environment.go
generated
vendored
@ -1,110 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package loader
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ResolveEnvironment update the environment variables for the format {- VAR} (without interpolation)
|
|
||||||
func ResolveEnvironment(dict map[string]any, environment types.Mapping) {
|
|
||||||
resolveServicesEnvironment(dict, environment)
|
|
||||||
resolveSecretsEnvironment(dict, environment)
|
|
||||||
resolveConfigsEnvironment(dict, environment)
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveServicesEnvironment(dict map[string]any, environment types.Mapping) {
|
|
||||||
services, ok := dict["services"].(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for service, cfg := range services {
|
|
||||||
serviceConfig, ok := cfg.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
serviceEnv, ok := serviceConfig["environment"].([]any)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
envs := []any{}
|
|
||||||
for _, env := range serviceEnv {
|
|
||||||
varEnv, ok := env.(string)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if found, ok := environment[varEnv]; ok {
|
|
||||||
envs = append(envs, fmt.Sprintf("%s=%s", varEnv, found))
|
|
||||||
} else {
|
|
||||||
// either does not exist or it was already resolved in interpolation
|
|
||||||
envs = append(envs, varEnv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
serviceConfig["environment"] = envs
|
|
||||||
services[service] = serviceConfig
|
|
||||||
}
|
|
||||||
dict["services"] = services
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveSecretsEnvironment(dict map[string]any, environment types.Mapping) {
|
|
||||||
secrets, ok := dict["secrets"].(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, cfg := range secrets {
|
|
||||||
secret, ok := cfg.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
env, ok := secret["environment"].(string)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if found, ok := environment[env]; ok {
|
|
||||||
secret[types.SecretConfigXValue] = found
|
|
||||||
}
|
|
||||||
secrets[name] = secret
|
|
||||||
}
|
|
||||||
dict["secrets"] = secrets
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveConfigsEnvironment(dict map[string]any, environment types.Mapping) {
|
|
||||||
configs, ok := dict["configs"].(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, cfg := range configs {
|
|
||||||
config, ok := cfg.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
env, ok := config["environment"].(string)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if found, ok := environment[env]; ok {
|
|
||||||
config["content"] = found
|
|
||||||
}
|
|
||||||
configs[name] = config
|
|
||||||
}
|
|
||||||
dict["configs"] = configs
|
|
||||||
}
|
|
||||||
10
vendor/github.com/compose-spec/compose-go/v2/loader/example1.env
generated
vendored
10
vendor/github.com/compose-spec/compose-go/v2/loader/example1.env
generated
vendored
@ -1,10 +0,0 @@
|
|||||||
# passed through
|
|
||||||
FOO=foo_from_env_file
|
|
||||||
ENV.WITH.DOT=ok
|
|
||||||
ENV_WITH_UNDERSCORE=ok
|
|
||||||
|
|
||||||
# overridden in example2.env
|
|
||||||
BAR=bar_from_env_file
|
|
||||||
|
|
||||||
# overridden in full-example.yml
|
|
||||||
BAZ=baz_from_env_file
|
|
||||||
10
vendor/github.com/compose-spec/compose-go/v2/loader/example1.label
generated
vendored
10
vendor/github.com/compose-spec/compose-go/v2/loader/example1.label
generated
vendored
@ -1,10 +0,0 @@
|
|||||||
# passed through
|
|
||||||
FOO=foo_from_label_file
|
|
||||||
LABEL.WITH.DOT=ok
|
|
||||||
LABEL_WITH_UNDERSCORE=ok
|
|
||||||
|
|
||||||
# overridden in example2.label
|
|
||||||
BAR=bar_from_label_file
|
|
||||||
|
|
||||||
# overridden in full-example.yml
|
|
||||||
BAZ=baz_from_label_file
|
|
||||||
4
vendor/github.com/compose-spec/compose-go/v2/loader/example2.env
generated
vendored
4
vendor/github.com/compose-spec/compose-go/v2/loader/example2.env
generated
vendored
@ -1,4 +0,0 @@
|
|||||||
BAR=bar_from_env_file_2
|
|
||||||
|
|
||||||
# overridden in configDetails.Environment
|
|
||||||
QUX=quz_from_env_file_2
|
|
||||||
4
vendor/github.com/compose-spec/compose-go/v2/loader/example2.label
generated
vendored
4
vendor/github.com/compose-spec/compose-go/v2/loader/example2.label
generated
vendored
@ -1,4 +0,0 @@
|
|||||||
BAR=bar_from_label_file_2
|
|
||||||
|
|
||||||
# overridden in configDetails.Labels
|
|
||||||
QUX=quz_from_label_file_2
|
|
||||||
221
vendor/github.com/compose-spec/compose-go/v2/loader/extends.go
generated
vendored
221
vendor/github.com/compose-spec/compose-go/v2/loader/extends.go
generated
vendored
@ -1,221 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package loader
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/consts"
|
|
||||||
"github.com/compose-spec/compose-go/v2/override"
|
|
||||||
"github.com/compose-spec/compose-go/v2/paths"
|
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ApplyExtends(ctx context.Context, dict map[string]any, opts *Options, tracker *cycleTracker, post PostProcessor) error {
|
|
||||||
a, ok := dict["services"]
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
services, ok := a.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("services must be a mapping")
|
|
||||||
}
|
|
||||||
for name := range services {
|
|
||||||
merged, err := applyServiceExtends(ctx, name, services, opts, tracker, post)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
services[name] = merged
|
|
||||||
}
|
|
||||||
dict["services"] = services
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyServiceExtends(ctx context.Context, name string, services map[string]any, opts *Options, tracker *cycleTracker, post PostProcessor) (any, error) {
|
|
||||||
s := services[name]
|
|
||||||
if s == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
service, ok := s.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("services.%s must be a mapping", name)
|
|
||||||
}
|
|
||||||
extends, ok := service["extends"]
|
|
||||||
if !ok {
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
filename := ctx.Value(consts.ComposeFileKey{}).(string)
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
ref string
|
|
||||||
file any
|
|
||||||
)
|
|
||||||
switch v := extends.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
ref, ok = v["service"].(string)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("extends.%s.service is required", name)
|
|
||||||
}
|
|
||||||
file = v["file"]
|
|
||||||
opts.ProcessEvent("extends", v)
|
|
||||||
case string:
|
|
||||||
ref = v
|
|
||||||
opts.ProcessEvent("extends", map[string]any{"service": ref})
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
base any
|
|
||||||
processor = post
|
|
||||||
)
|
|
||||||
|
|
||||||
if file != nil {
|
|
||||||
refFilename := file.(string)
|
|
||||||
services, processor, err = getExtendsBaseFromFile(ctx, name, ref, filename, refFilename, opts, tracker)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
filename = refFilename
|
|
||||||
} else {
|
|
||||||
_, ok := services[ref]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("cannot extend service %q in %s: service %q not found", name, filename, ref)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tracker, err = tracker.Add(filename, name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// recursively apply `extends`
|
|
||||||
base, err = applyServiceExtends(ctx, ref, services, opts, tracker, processor)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if base == nil {
|
|
||||||
return service, nil
|
|
||||||
}
|
|
||||||
source := deepClone(base).(map[string]any)
|
|
||||||
|
|
||||||
err = post.Apply(map[string]any{
|
|
||||||
"services": map[string]any{
|
|
||||||
name: source,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
merged, err := override.ExtendService(source, service)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(merged, "extends")
|
|
||||||
services[name] = merged
|
|
||||||
return merged, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getExtendsBaseFromFile(
|
|
||||||
ctx context.Context,
|
|
||||||
name, ref string,
|
|
||||||
path, refPath string,
|
|
||||||
opts *Options,
|
|
||||||
ct *cycleTracker,
|
|
||||||
) (map[string]any, PostProcessor, error) {
|
|
||||||
for _, loader := range opts.ResourceLoaders {
|
|
||||||
if !loader.Accept(refPath) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
local, err := loader.Load(ctx, refPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
localdir := filepath.Dir(local)
|
|
||||||
relworkingdir := loader.Dir(refPath)
|
|
||||||
|
|
||||||
extendsOpts := opts.clone()
|
|
||||||
// replace localResourceLoader with a new flavour, using extended file base path
|
|
||||||
extendsOpts.ResourceLoaders = append(opts.RemoteResourceLoaders(), localResourceLoader{
|
|
||||||
WorkingDir: localdir,
|
|
||||||
})
|
|
||||||
extendsOpts.ResolvePaths = false // we do relative path resolution after file has been loaded
|
|
||||||
extendsOpts.SkipNormalization = true
|
|
||||||
extendsOpts.SkipConsistencyCheck = true
|
|
||||||
extendsOpts.SkipInclude = true
|
|
||||||
extendsOpts.SkipExtends = true // we manage extends recursively based on raw service definition
|
|
||||||
extendsOpts.SkipValidation = true // we validate the merge result
|
|
||||||
extendsOpts.SkipDefaultValues = true
|
|
||||||
source, processor, err := loadYamlFile(ctx, types.ConfigFile{Filename: local},
|
|
||||||
extendsOpts, relworkingdir, nil, ct, map[string]any{}, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
m, ok := source["services"]
|
|
||||||
if !ok {
|
|
||||||
return nil, nil, fmt.Errorf("cannot extend service %q in %s: no services section", name, local)
|
|
||||||
}
|
|
||||||
services, ok := m.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return nil, nil, fmt.Errorf("cannot extend service %q in %s: services must be a mapping", name, local)
|
|
||||||
}
|
|
||||||
_, ok = services[ref]
|
|
||||||
if !ok {
|
|
||||||
return nil, nil, fmt.Errorf(
|
|
||||||
"cannot extend service %q in %s: service %q not found in %s",
|
|
||||||
name,
|
|
||||||
path,
|
|
||||||
ref,
|
|
||||||
refPath,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var remotes []paths.RemoteResource
|
|
||||||
for _, loader := range opts.RemoteResourceLoaders() {
|
|
||||||
remotes = append(remotes, loader.Accept)
|
|
||||||
}
|
|
||||||
err = paths.ResolveRelativePaths(source, relworkingdir, remotes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return services, processor, nil
|
|
||||||
}
|
|
||||||
return nil, nil, fmt.Errorf("cannot read %s", refPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func deepClone(value any) any {
|
|
||||||
switch v := value.(type) {
|
|
||||||
case []any:
|
|
||||||
cp := make([]any, len(v))
|
|
||||||
for i, e := range v {
|
|
||||||
cp[i] = deepClone(e)
|
|
||||||
}
|
|
||||||
return cp
|
|
||||||
case map[string]any:
|
|
||||||
cp := make(map[string]any, len(v))
|
|
||||||
for k, e := range v {
|
|
||||||
cp[k] = deepClone(e)
|
|
||||||
}
|
|
||||||
return cp
|
|
||||||
default:
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
36
vendor/github.com/compose-spec/compose-go/v2/loader/fix.go
generated
vendored
36
vendor/github.com/compose-spec/compose-go/v2/loader/fix.go
generated
vendored
@ -1,36 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package loader
|
|
||||||
|
|
||||||
// fixEmptyNotNull is a workaround for https://github.com/xeipuuv/gojsonschema/issues/141
|
|
||||||
// as go-yaml `[]` will load as a `[]any(nil)`, which is not the same as an empty array
|
|
||||||
func fixEmptyNotNull(value any) interface{} {
|
|
||||||
switch v := value.(type) {
|
|
||||||
case []any:
|
|
||||||
if v == nil {
|
|
||||||
return []any{}
|
|
||||||
}
|
|
||||||
for i, e := range v {
|
|
||||||
v[i] = fixEmptyNotNull(e)
|
|
||||||
}
|
|
||||||
case map[string]any:
|
|
||||||
for k, e := range v {
|
|
||||||
v[k] = fixEmptyNotNull(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
461
vendor/github.com/compose-spec/compose-go/v2/loader/full-example.yml
generated
vendored
461
vendor/github.com/compose-spec/compose-go/v2/loader/full-example.yml
generated
vendored
@ -1,461 +0,0 @@
|
|||||||
name: full_example_project_name
|
|
||||||
services:
|
|
||||||
|
|
||||||
bar:
|
|
||||||
build:
|
|
||||||
dockerfile_inline: |
|
|
||||||
FROM alpine
|
|
||||||
RUN echo "hello" > /world.txt
|
|
||||||
|
|
||||||
foo:
|
|
||||||
annotations:
|
|
||||||
- com.example.foo=bar
|
|
||||||
build:
|
|
||||||
context: ./dir
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
args:
|
|
||||||
foo: bar
|
|
||||||
ssh:
|
|
||||||
- default
|
|
||||||
target: foo
|
|
||||||
network: foo
|
|
||||||
cache_from:
|
|
||||||
- foo
|
|
||||||
- bar
|
|
||||||
labels: [FOO=BAR]
|
|
||||||
additional_contexts:
|
|
||||||
foo: ./bar
|
|
||||||
secrets:
|
|
||||||
- source: secret1
|
|
||||||
target: /run/secrets/secret1
|
|
||||||
- source: secret2
|
|
||||||
target: my_secret
|
|
||||||
uid: '103'
|
|
||||||
gid: '103'
|
|
||||||
mode: 0440
|
|
||||||
tags:
|
|
||||||
- foo:v1.0.0
|
|
||||||
- docker.io/username/foo:my-other-tag
|
|
||||||
- ${COMPOSE_PROJECT_NAME}:1.0.0
|
|
||||||
platforms:
|
|
||||||
- linux/amd64
|
|
||||||
- linux/arm64
|
|
||||||
|
|
||||||
|
|
||||||
cap_add:
|
|
||||||
- ALL
|
|
||||||
|
|
||||||
cap_drop:
|
|
||||||
- NET_ADMIN
|
|
||||||
- SYS_ADMIN
|
|
||||||
|
|
||||||
cgroup_parent: m-executor-abcd
|
|
||||||
|
|
||||||
# String or list
|
|
||||||
command: bundle exec thin -p 3000
|
|
||||||
# command: ["bundle", "exec", "thin", "-p", "3000"]
|
|
||||||
|
|
||||||
configs:
|
|
||||||
- config1
|
|
||||||
- source: config2
|
|
||||||
target: /my_config
|
|
||||||
uid: '103'
|
|
||||||
gid: '103'
|
|
||||||
mode: 0440
|
|
||||||
|
|
||||||
container_name: my-web-container
|
|
||||||
|
|
||||||
depends_on:
|
|
||||||
- db
|
|
||||||
- redis
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
mode: replicated
|
|
||||||
replicas: 6
|
|
||||||
labels: [FOO=BAR]
|
|
||||||
rollback_config:
|
|
||||||
parallelism: 3
|
|
||||||
delay: 10s
|
|
||||||
failure_action: continue
|
|
||||||
monitor: 60s
|
|
||||||
max_failure_ratio: 0.3
|
|
||||||
order: start-first
|
|
||||||
update_config:
|
|
||||||
parallelism: 3
|
|
||||||
delay: 10s
|
|
||||||
failure_action: continue
|
|
||||||
monitor: 60s
|
|
||||||
max_failure_ratio: 0.3
|
|
||||||
order: start-first
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: '0.001'
|
|
||||||
memory: 50M
|
|
||||||
reservations:
|
|
||||||
cpus: '0.0001'
|
|
||||||
memory: 20M
|
|
||||||
generic_resources:
|
|
||||||
- discrete_resource_spec:
|
|
||||||
kind: 'gpu'
|
|
||||||
value: 2
|
|
||||||
- discrete_resource_spec:
|
|
||||||
kind: 'ssd'
|
|
||||||
value: 1
|
|
||||||
restart_policy:
|
|
||||||
condition: on-failure
|
|
||||||
delay: 5s
|
|
||||||
max_attempts: 3
|
|
||||||
window: 120s
|
|
||||||
placement:
|
|
||||||
constraints: [node=foo]
|
|
||||||
max_replicas_per_node: 5
|
|
||||||
preferences:
|
|
||||||
- spread: node.labels.az
|
|
||||||
endpoint_mode: dnsrr
|
|
||||||
|
|
||||||
device_cgroup_rules:
|
|
||||||
- "c 1:3 mr"
|
|
||||||
- "a 7:* rmw"
|
|
||||||
|
|
||||||
devices:
|
|
||||||
- source: /dev/ttyUSB0
|
|
||||||
target: /dev/ttyUSB0
|
|
||||||
permissions: rwm
|
|
||||||
|
|
||||||
# String or list
|
|
||||||
# dns: 8.8.8.8
|
|
||||||
dns:
|
|
||||||
- 8.8.8.8
|
|
||||||
- 9.9.9.9
|
|
||||||
|
|
||||||
# String or list
|
|
||||||
# dns_search: example.com
|
|
||||||
dns_search:
|
|
||||||
- dc1.example.com
|
|
||||||
- dc2.example.com
|
|
||||||
|
|
||||||
domainname: foo.com
|
|
||||||
|
|
||||||
# String or list
|
|
||||||
# entrypoint: /code/entrypoint.sh -p 3000
|
|
||||||
entrypoint: ["/code/entrypoint.sh", "-p", "3000"]
|
|
||||||
|
|
||||||
# String or list
|
|
||||||
# env_file: .env
|
|
||||||
env_file:
|
|
||||||
- ./example1.env
|
|
||||||
- path: ./example2.env
|
|
||||||
required: false
|
|
||||||
|
|
||||||
# Mapping or list
|
|
||||||
# Mapping values can be strings, numbers or null
|
|
||||||
# Booleans are not allowed - must be quoted
|
|
||||||
environment:
|
|
||||||
BAZ: baz_from_service_def
|
|
||||||
QUX:
|
|
||||||
# environment:
|
|
||||||
# - RACK_ENV=development
|
|
||||||
# - SHOW=true
|
|
||||||
# - SESSION_SECRET
|
|
||||||
|
|
||||||
# Items can be strings or numbers
|
|
||||||
expose:
|
|
||||||
- "3000"
|
|
||||||
- 8000
|
|
||||||
|
|
||||||
external_links:
|
|
||||||
- redis_1
|
|
||||||
- project_db_1:mysql
|
|
||||||
- project_db_1:postgresql
|
|
||||||
|
|
||||||
# Mapping or list
|
|
||||||
# Mapping values must be strings
|
|
||||||
# extra_hosts:
|
|
||||||
# somehost: "162.242.195.82"
|
|
||||||
# otherhost: "50.31.209.229"
|
|
||||||
extra_hosts:
|
|
||||||
- "otherhost:50.31.209.229"
|
|
||||||
- "somehost:162.242.195.82"
|
|
||||||
|
|
||||||
hostname: foo
|
|
||||||
|
|
||||||
healthcheck:
|
|
||||||
test: echo "hello world"
|
|
||||||
interval: 10s
|
|
||||||
timeout: 1s
|
|
||||||
retries: 5
|
|
||||||
start_period: 15s
|
|
||||||
start_interval: 5s
|
|
||||||
|
|
||||||
# Any valid image reference - repo, tag, id, sha
|
|
||||||
image: redis
|
|
||||||
# image: ubuntu:14.04
|
|
||||||
# image: tutum/influxdb
|
|
||||||
# image: example-registry.com:4000/postgresql
|
|
||||||
# image: a4bc65fd
|
|
||||||
# image: busybox@sha256:38a203e1986cf79639cfb9b2e1d6e773de84002feea2d4eb006b52004ee8502d
|
|
||||||
|
|
||||||
ipc: host
|
|
||||||
|
|
||||||
uts: host
|
|
||||||
|
|
||||||
# Mapping or list
|
|
||||||
# Mapping values can be strings, numbers or null
|
|
||||||
labels:
|
|
||||||
com.example.description: "Accounting webapp"
|
|
||||||
com.example.number: 42
|
|
||||||
com.example.empty-label:
|
|
||||||
# labels:
|
|
||||||
# - "com.example.description=Accounting webapp"
|
|
||||||
# - "com.example.number=42"
|
|
||||||
# - "com.example.empty-label"
|
|
||||||
|
|
||||||
label_file:
|
|
||||||
- ./example1.label
|
|
||||||
- ./example2.label
|
|
||||||
|
|
||||||
links:
|
|
||||||
- db
|
|
||||||
- db:database
|
|
||||||
- redis
|
|
||||||
|
|
||||||
logging:
|
|
||||||
driver: syslog
|
|
||||||
options:
|
|
||||||
syslog-address: "tcp://192.168.0.42:123"
|
|
||||||
|
|
||||||
mac_address: 02:42:ac:11:65:43
|
|
||||||
|
|
||||||
# network_mode: "bridge"
|
|
||||||
# network_mode: "host"
|
|
||||||
# network_mode: "none"
|
|
||||||
# Use the network mode of an arbitrary container from another service
|
|
||||||
# network_mode: "service:db"
|
|
||||||
# Use the network mode of another container, specified by name or id
|
|
||||||
# network_mode: "container:some-container"
|
|
||||||
network_mode: "container:0cfeab0f748b9a743dc3da582046357c6ef497631c1a016d28d2bf9b4f899f7b"
|
|
||||||
|
|
||||||
networks:
|
|
||||||
some-network:
|
|
||||||
aliases:
|
|
||||||
- alias1
|
|
||||||
- alias3
|
|
||||||
other-network:
|
|
||||||
ipv4_address: 172.16.238.10
|
|
||||||
ipv6_address: 2001:3984:3989::10
|
|
||||||
mac_address: 02:42:72:98:65:08
|
|
||||||
other-other-network:
|
|
||||||
|
|
||||||
pid: "host"
|
|
||||||
|
|
||||||
ports:
|
|
||||||
- 3000
|
|
||||||
- "3001-3005"
|
|
||||||
- "8000:8000"
|
|
||||||
- "9090-9091:8080-8081"
|
|
||||||
- "49100:22"
|
|
||||||
- "127.0.0.1:8001:8001"
|
|
||||||
- "127.0.0.1:5000-5010:5000-5010"
|
|
||||||
|
|
||||||
privileged: true
|
|
||||||
|
|
||||||
read_only: true
|
|
||||||
|
|
||||||
restart: always
|
|
||||||
|
|
||||||
secrets:
|
|
||||||
- source: secret1
|
|
||||||
target: /run/secrets/secret1
|
|
||||||
- source: secret2
|
|
||||||
target: my_secret
|
|
||||||
uid: '103'
|
|
||||||
gid: '103'
|
|
||||||
mode: 0440
|
|
||||||
|
|
||||||
security_opt:
|
|
||||||
- label=level:s0:c100,c200
|
|
||||||
- label=type:svirt_apache_t
|
|
||||||
|
|
||||||
stdin_open: true
|
|
||||||
|
|
||||||
stop_grace_period: 20s
|
|
||||||
|
|
||||||
stop_signal: SIGUSR1
|
|
||||||
storage_opt:
|
|
||||||
size: "20G"
|
|
||||||
sysctls:
|
|
||||||
net.core.somaxconn: 1024
|
|
||||||
net.ipv4.tcp_syncookies: 0
|
|
||||||
|
|
||||||
# String or list
|
|
||||||
# tmpfs: /run
|
|
||||||
tmpfs:
|
|
||||||
- /run
|
|
||||||
- /tmp
|
|
||||||
|
|
||||||
tty: true
|
|
||||||
|
|
||||||
ulimits:
|
|
||||||
# Single number or mapping with soft + hard limits
|
|
||||||
nproc: 65535
|
|
||||||
nofile:
|
|
||||||
soft: 20000
|
|
||||||
hard: 40000
|
|
||||||
|
|
||||||
user: someone
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
# Just specify a path and let the Engine create a volume
|
|
||||||
- /var/lib/anonymous
|
|
||||||
# Specify an absolute path mapping
|
|
||||||
- /opt/data:/var/lib/data
|
|
||||||
# Path on the host, relative to the Compose file
|
|
||||||
- .:/code
|
|
||||||
- ./static:/var/www/html
|
|
||||||
# User-relative path
|
|
||||||
- ~/configs:/etc/configs:ro
|
|
||||||
# Named volume
|
|
||||||
- datavolume:/var/lib/volume
|
|
||||||
- type: bind
|
|
||||||
source: ./opt
|
|
||||||
target: /opt/cached
|
|
||||||
consistency: cached
|
|
||||||
- type: tmpfs
|
|
||||||
target: /opt/tmpfs
|
|
||||||
tmpfs:
|
|
||||||
size: 10000
|
|
||||||
|
|
||||||
working_dir: /code
|
|
||||||
x-bar: baz
|
|
||||||
x-foo: bar
|
|
||||||
|
|
||||||
networks:
|
|
||||||
# Entries can be null, which specifies simply that a network
|
|
||||||
# called "{project name}_some-network" should be created and
|
|
||||||
# use the default driver
|
|
||||||
some-network:
|
|
||||||
|
|
||||||
other-network:
|
|
||||||
driver: overlay
|
|
||||||
|
|
||||||
driver_opts:
|
|
||||||
# Values can be strings or numbers
|
|
||||||
foo: "bar"
|
|
||||||
baz: 1
|
|
||||||
|
|
||||||
ipam:
|
|
||||||
driver: overlay
|
|
||||||
# driver_opts:
|
|
||||||
# # Values can be strings or numbers
|
|
||||||
# com.docker.network.enable_ipv6: "true"
|
|
||||||
# com.docker.network.numeric_value: 1
|
|
||||||
config:
|
|
||||||
- subnet: 172.28.0.0/16
|
|
||||||
ip_range: 172.28.5.0/24
|
|
||||||
gateway: 172.28.5.254
|
|
||||||
aux_addresses:
|
|
||||||
host1: 172.28.1.5
|
|
||||||
host2: 172.28.1.6
|
|
||||||
host3: 172.28.1.7
|
|
||||||
- subnet: 2001:3984:3989::/64
|
|
||||||
gateway: 2001:3984:3989::1
|
|
||||||
|
|
||||||
labels:
|
|
||||||
foo: bar
|
|
||||||
|
|
||||||
external-network:
|
|
||||||
# Specifies that a pre-existing network called "external-network"
|
|
||||||
# can be referred to within this file as "external-network"
|
|
||||||
external: true
|
|
||||||
|
|
||||||
other-external-network:
|
|
||||||
# Specifies that a pre-existing network called "my-cool-network"
|
|
||||||
# can be referred to within this file as "other-external-network"
|
|
||||||
external:
|
|
||||||
name: my-cool-network
|
|
||||||
x-bar: baz
|
|
||||||
x-foo: bar
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
# Entries can be null, which specifies simply that a volume
|
|
||||||
# called "{project name}_some-volume" should be created and
|
|
||||||
# use the default driver
|
|
||||||
some-volume:
|
|
||||||
|
|
||||||
other-volume:
|
|
||||||
driver: flocker
|
|
||||||
|
|
||||||
driver_opts:
|
|
||||||
# Values can be strings or numbers
|
|
||||||
foo: "bar"
|
|
||||||
baz: 1
|
|
||||||
labels:
|
|
||||||
foo: bar
|
|
||||||
|
|
||||||
another-volume:
|
|
||||||
name: "user_specified_name"
|
|
||||||
driver: vsphere
|
|
||||||
|
|
||||||
driver_opts:
|
|
||||||
# Values can be strings or numbers
|
|
||||||
foo: "bar"
|
|
||||||
baz: 1
|
|
||||||
|
|
||||||
external-volume:
|
|
||||||
# Specifies that a pre-existing volume called "external-volume"
|
|
||||||
# can be referred to within this file as "external-volume"
|
|
||||||
external: true
|
|
||||||
|
|
||||||
other-external-volume:
|
|
||||||
# Specifies that a pre-existing volume called "my-cool-volume"
|
|
||||||
# can be referred to within this file as "other-external-volume"
|
|
||||||
# This example uses the deprecated "volume.external.name" (replaced by "volume.name")
|
|
||||||
external:
|
|
||||||
name: my-cool-volume
|
|
||||||
|
|
||||||
external-volume3:
|
|
||||||
# Specifies that a pre-existing volume called "this-is-volume3"
|
|
||||||
# can be referred to within this file as "external-volume3"
|
|
||||||
name: this-is-volume3
|
|
||||||
external: true
|
|
||||||
x-bar: baz
|
|
||||||
x-foo: bar
|
|
||||||
|
|
||||||
configs:
|
|
||||||
config1:
|
|
||||||
file: ./config_data
|
|
||||||
labels:
|
|
||||||
foo: bar
|
|
||||||
config2:
|
|
||||||
external:
|
|
||||||
name: my_config
|
|
||||||
config3:
|
|
||||||
external: true
|
|
||||||
config4:
|
|
||||||
name: foo
|
|
||||||
file: ~/config_data
|
|
||||||
x-bar: baz
|
|
||||||
x-foo: bar
|
|
||||||
|
|
||||||
secrets:
|
|
||||||
secret1:
|
|
||||||
file: ./secret_data
|
|
||||||
labels:
|
|
||||||
foo: bar
|
|
||||||
secret2:
|
|
||||||
external:
|
|
||||||
name: my_secret
|
|
||||||
secret3:
|
|
||||||
external: true
|
|
||||||
secret4:
|
|
||||||
name: bar
|
|
||||||
environment: BAR
|
|
||||||
x-bar: baz
|
|
||||||
x-foo: bar
|
|
||||||
secret5:
|
|
||||||
file: /abs/secret_data
|
|
||||||
x-bar: baz
|
|
||||||
x-foo: bar
|
|
||||||
x-nested:
|
|
||||||
bar: baz
|
|
||||||
foo: bar
|
|
||||||
223
vendor/github.com/compose-spec/compose-go/v2/loader/include.go
generated
vendored
223
vendor/github.com/compose-spec/compose-go/v2/loader/include.go
generated
vendored
@ -1,223 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package loader
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/dotenv"
|
|
||||||
interp "github.com/compose-spec/compose-go/v2/interpolation"
|
|
||||||
"github.com/compose-spec/compose-go/v2/override"
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
// loadIncludeConfig parse the required config from raw yaml
|
|
||||||
func loadIncludeConfig(source any) ([]types.IncludeConfig, error) {
|
|
||||||
if source == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
configs, ok := source.([]any)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("`include` must be a list, got %s", source)
|
|
||||||
}
|
|
||||||
for i, config := range configs {
|
|
||||||
if v, ok := config.(string); ok {
|
|
||||||
configs[i] = map[string]any{
|
|
||||||
"path": v,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var requires []types.IncludeConfig
|
|
||||||
err := Transform(source, &requires)
|
|
||||||
return requires, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func ApplyInclude(ctx context.Context, workingDir string, environment types.Mapping, model map[string]any, options *Options, included []string, processor PostProcessor) error {
|
|
||||||
includeConfig, err := loadIncludeConfig(model["include"])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, r := range includeConfig {
|
|
||||||
for _, listener := range options.Listeners {
|
|
||||||
listener("include", map[string]any{
|
|
||||||
"path": r.Path,
|
|
||||||
"workingdir": workingDir,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var relworkingdir string
|
|
||||||
for i, p := range r.Path {
|
|
||||||
for _, loader := range options.ResourceLoaders {
|
|
||||||
if !loader.Accept(p) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
path, err := loader.Load(ctx, p)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p = path
|
|
||||||
|
|
||||||
if i == 0 { // This is the "main" file, used to define project-directory. Others are overrides
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case r.ProjectDirectory == "":
|
|
||||||
relworkingdir = loader.Dir(path)
|
|
||||||
r.ProjectDirectory = filepath.Dir(path)
|
|
||||||
case !filepath.IsAbs(r.ProjectDirectory):
|
|
||||||
relworkingdir = loader.Dir(r.ProjectDirectory)
|
|
||||||
r.ProjectDirectory = filepath.Join(workingDir, r.ProjectDirectory)
|
|
||||||
|
|
||||||
default:
|
|
||||||
relworkingdir = r.ProjectDirectory
|
|
||||||
|
|
||||||
}
|
|
||||||
for _, f := range included {
|
|
||||||
if f == path {
|
|
||||||
included = append(included, path)
|
|
||||||
return fmt.Errorf("include cycle detected:\n%s\n include %s", included[0], strings.Join(included[1:], "\n include "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
r.Path[i] = p
|
|
||||||
}
|
|
||||||
|
|
||||||
loadOptions := options.clone()
|
|
||||||
loadOptions.ResolvePaths = true
|
|
||||||
loadOptions.SkipNormalization = true
|
|
||||||
loadOptions.SkipConsistencyCheck = true
|
|
||||||
loadOptions.ResourceLoaders = append(loadOptions.RemoteResourceLoaders(), localResourceLoader{
|
|
||||||
WorkingDir: r.ProjectDirectory,
|
|
||||||
})
|
|
||||||
|
|
||||||
if len(r.EnvFile) == 0 {
|
|
||||||
f := filepath.Join(r.ProjectDirectory, ".env")
|
|
||||||
if s, err := os.Stat(f); err == nil && !s.IsDir() {
|
|
||||||
r.EnvFile = types.StringList{f}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
envFile := []string{}
|
|
||||||
for _, f := range r.EnvFile {
|
|
||||||
if f == "/dev/null" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !filepath.IsAbs(f) {
|
|
||||||
f = filepath.Join(workingDir, f)
|
|
||||||
s, err := os.Stat(f)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if s.IsDir() {
|
|
||||||
return fmt.Errorf("%s is not a file", f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
envFile = append(envFile, f)
|
|
||||||
}
|
|
||||||
r.EnvFile = envFile
|
|
||||||
}
|
|
||||||
|
|
||||||
envFromFile, err := dotenv.GetEnvFromFile(environment, r.EnvFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
config := types.ConfigDetails{
|
|
||||||
WorkingDir: relworkingdir,
|
|
||||||
ConfigFiles: types.ToConfigFiles(r.Path),
|
|
||||||
Environment: environment.Clone().Merge(envFromFile),
|
|
||||||
}
|
|
||||||
loadOptions.Interpolate = &interp.Options{
|
|
||||||
Substitute: options.Interpolate.Substitute,
|
|
||||||
LookupValue: config.LookupEnv,
|
|
||||||
TypeCastMapping: options.Interpolate.TypeCastMapping,
|
|
||||||
}
|
|
||||||
imported, err := loadYamlModel(ctx, config, loadOptions, &cycleTracker{}, included)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = importResources(imported, model, processor)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete(model, "include")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// importResources import into model all resources defined by imported, and report error on conflict
|
|
||||||
func importResources(source map[string]any, target map[string]any, processor PostProcessor) error {
|
|
||||||
if err := importResource(source, target, "services", processor); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := importResource(source, target, "volumes", processor); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := importResource(source, target, "networks", processor); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := importResource(source, target, "secrets", processor); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := importResource(source, target, "configs", processor); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := importResource(source, target, "models", processor); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func importResource(source map[string]any, target map[string]any, key string, processor PostProcessor) error {
|
|
||||||
from := source[key]
|
|
||||||
if from != nil {
|
|
||||||
var to map[string]any
|
|
||||||
if v, ok := target[key]; ok {
|
|
||||||
to = v.(map[string]any)
|
|
||||||
} else {
|
|
||||||
to = map[string]any{}
|
|
||||||
}
|
|
||||||
for name, a := range from.(map[string]any) {
|
|
||||||
conflict, ok := to[name]
|
|
||||||
if !ok {
|
|
||||||
to[name] = a
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err := processor.Apply(map[string]any{
|
|
||||||
key: map[string]any{
|
|
||||||
name: a,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
merged, err := override.MergeYaml(a, conflict, tree.NewPath(key, name))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
to[name] = merged
|
|
||||||
}
|
|
||||||
target[key] = to
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
118
vendor/github.com/compose-spec/compose-go/v2/loader/interpolate.go
generated
vendored
118
vendor/github.com/compose-spec/compose-go/v2/loader/interpolate.go
generated
vendored
@ -1,118 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package loader
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
interp "github.com/compose-spec/compose-go/v2/interpolation"
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
var interpolateTypeCastMapping = map[tree.Path]interp.Cast{
|
|
||||||
servicePath("cpu_count"): toInt64,
|
|
||||||
servicePath("cpu_percent"): toFloat,
|
|
||||||
servicePath("cpu_period"): toInt64,
|
|
||||||
servicePath("cpu_quota"): toInt64,
|
|
||||||
servicePath("cpu_rt_period"): toInt64,
|
|
||||||
servicePath("cpu_rt_runtime"): toInt64,
|
|
||||||
servicePath("cpus"): toFloat32,
|
|
||||||
servicePath("cpu_shares"): toInt64,
|
|
||||||
servicePath("init"): toBoolean,
|
|
||||||
servicePath("depends_on", tree.PathMatchAll, "required"): toBoolean,
|
|
||||||
servicePath("depends_on", tree.PathMatchAll, "restart"): toBoolean,
|
|
||||||
servicePath("deploy", "replicas"): toInt,
|
|
||||||
servicePath("deploy", "update_config", "parallelism"): toInt,
|
|
||||||
servicePath("deploy", "update_config", "max_failure_ratio"): toFloat,
|
|
||||||
servicePath("deploy", "rollback_config", "parallelism"): toInt,
|
|
||||||
servicePath("deploy", "rollback_config", "max_failure_ratio"): toFloat,
|
|
||||||
servicePath("deploy", "restart_policy", "max_attempts"): toInt,
|
|
||||||
servicePath("deploy", "placement", "max_replicas_per_node"): toInt,
|
|
||||||
servicePath("healthcheck", "retries"): toInt,
|
|
||||||
servicePath("healthcheck", "disable"): toBoolean,
|
|
||||||
servicePath("oom_kill_disable"): toBoolean,
|
|
||||||
servicePath("oom_score_adj"): toInt64,
|
|
||||||
servicePath("pids_limit"): toInt64,
|
|
||||||
servicePath("ports", tree.PathMatchList, "target"): toInt,
|
|
||||||
servicePath("privileged"): toBoolean,
|
|
||||||
servicePath("read_only"): toBoolean,
|
|
||||||
servicePath("scale"): toInt,
|
|
||||||
servicePath("stdin_open"): toBoolean,
|
|
||||||
servicePath("tty"): toBoolean,
|
|
||||||
servicePath("ulimits", tree.PathMatchAll): toInt,
|
|
||||||
servicePath("ulimits", tree.PathMatchAll, "hard"): toInt,
|
|
||||||
servicePath("ulimits", tree.PathMatchAll, "soft"): toInt,
|
|
||||||
servicePath("volumes", tree.PathMatchList, "read_only"): toBoolean,
|
|
||||||
servicePath("volumes", tree.PathMatchList, "volume", "nocopy"): toBoolean,
|
|
||||||
iPath("networks", tree.PathMatchAll, "external"): toBoolean,
|
|
||||||
iPath("networks", tree.PathMatchAll, "internal"): toBoolean,
|
|
||||||
iPath("networks", tree.PathMatchAll, "attachable"): toBoolean,
|
|
||||||
iPath("networks", tree.PathMatchAll, "enable_ipv4"): toBoolean,
|
|
||||||
iPath("networks", tree.PathMatchAll, "enable_ipv6"): toBoolean,
|
|
||||||
iPath("volumes", tree.PathMatchAll, "external"): toBoolean,
|
|
||||||
iPath("secrets", tree.PathMatchAll, "external"): toBoolean,
|
|
||||||
iPath("configs", tree.PathMatchAll, "external"): toBoolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
func iPath(parts ...string) tree.Path {
|
|
||||||
return tree.NewPath(parts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func servicePath(parts ...string) tree.Path {
|
|
||||||
return iPath(append([]string{"services", tree.PathMatchAll}, parts...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func toInt(value string) (interface{}, error) {
|
|
||||||
return strconv.Atoi(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func toInt64(value string) (interface{}, error) {
|
|
||||||
return strconv.ParseInt(value, 10, 64)
|
|
||||||
}
|
|
||||||
|
|
||||||
func toFloat(value string) (interface{}, error) {
|
|
||||||
return strconv.ParseFloat(value, 64)
|
|
||||||
}
|
|
||||||
|
|
||||||
func toFloat32(value string) (interface{}, error) {
|
|
||||||
f, err := strconv.ParseFloat(value, 32)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return float32(f), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// should match http://yaml.org/type/bool.html
|
|
||||||
func toBoolean(value string) (interface{}, error) {
|
|
||||||
switch strings.ToLower(value) {
|
|
||||||
case "true":
|
|
||||||
return true, nil
|
|
||||||
case "false":
|
|
||||||
return false, nil
|
|
||||||
case "y", "yes", "on":
|
|
||||||
logrus.Warnf("%q for boolean is not supported by YAML 1.2, please use `true`", value)
|
|
||||||
return true, nil
|
|
||||||
case "n", "no", "off":
|
|
||||||
logrus.Warnf("%q for boolean is not supported by YAML 1.2, please use `false`", value)
|
|
||||||
return false, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("invalid boolean: %s", value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
899
vendor/github.com/compose-spec/compose-go/v2/loader/loader.go
generated
vendored
899
vendor/github.com/compose-spec/compose-go/v2/loader/loader.go
generated
vendored
@ -1,899 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package loader
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
|
||||||
"regexp"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/consts"
|
|
||||||
"github.com/compose-spec/compose-go/v2/errdefs"
|
|
||||||
interp "github.com/compose-spec/compose-go/v2/interpolation"
|
|
||||||
"github.com/compose-spec/compose-go/v2/override"
|
|
||||||
"github.com/compose-spec/compose-go/v2/paths"
|
|
||||||
"github.com/compose-spec/compose-go/v2/schema"
|
|
||||||
"github.com/compose-spec/compose-go/v2/template"
|
|
||||||
"github.com/compose-spec/compose-go/v2/transform"
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
|
||||||
"github.com/compose-spec/compose-go/v2/validation"
|
|
||||||
"github.com/go-viper/mapstructure/v2"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"go.yaml.in/yaml/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Options supported by Load
|
|
||||||
type Options struct {
|
|
||||||
// Skip schema validation
|
|
||||||
SkipValidation bool
|
|
||||||
// Skip interpolation
|
|
||||||
SkipInterpolation bool
|
|
||||||
// Skip normalization
|
|
||||||
SkipNormalization bool
|
|
||||||
// Resolve path
|
|
||||||
ResolvePaths bool
|
|
||||||
// Convert Windows path
|
|
||||||
ConvertWindowsPaths bool
|
|
||||||
// Skip consistency check
|
|
||||||
SkipConsistencyCheck bool
|
|
||||||
// Skip extends
|
|
||||||
SkipExtends bool
|
|
||||||
// SkipInclude will ignore `include` and only load model from file(s) set by ConfigDetails
|
|
||||||
SkipInclude bool
|
|
||||||
// SkipResolveEnvironment will ignore computing `environment` for services
|
|
||||||
SkipResolveEnvironment bool
|
|
||||||
// SkipDefaultValues will ignore missing required attributes
|
|
||||||
SkipDefaultValues bool
|
|
||||||
// Interpolation options
|
|
||||||
Interpolate *interp.Options
|
|
||||||
// Discard 'env_file' entries after resolving to 'environment' section
|
|
||||||
discardEnvFiles bool
|
|
||||||
// Set project projectName
|
|
||||||
projectName string
|
|
||||||
// Indicates when the projectName was imperatively set or guessed from path
|
|
||||||
projectNameImperativelySet bool
|
|
||||||
// Profiles set profiles to enable
|
|
||||||
Profiles []string
|
|
||||||
// ResourceLoaders manages support for remote resources
|
|
||||||
ResourceLoaders []ResourceLoader
|
|
||||||
// KnownExtensions manages x-* attribute we know and the corresponding go structs
|
|
||||||
KnownExtensions map[string]any
|
|
||||||
// Metada for telemetry
|
|
||||||
Listeners []Listener
|
|
||||||
}
|
|
||||||
|
|
||||||
var versionWarning []string
|
|
||||||
|
|
||||||
func (o *Options) warnObsoleteVersion(file string) {
|
|
||||||
if !slices.Contains(versionWarning, file) {
|
|
||||||
logrus.Warning(fmt.Sprintf("%s: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion", file))
|
|
||||||
}
|
|
||||||
versionWarning = append(versionWarning, file)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Listener = func(event string, metadata map[string]any)
|
|
||||||
|
|
||||||
// Invoke all listeners for an event
|
|
||||||
func (o *Options) ProcessEvent(event string, metadata map[string]any) {
|
|
||||||
for _, l := range o.Listeners {
|
|
||||||
l(event, metadata)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResourceLoader is a plugable remote resource resolver
|
|
||||||
type ResourceLoader interface {
|
|
||||||
// Accept returns `true` is the resource reference matches ResourceLoader supported protocol(s)
|
|
||||||
Accept(path string) bool
|
|
||||||
// Load returns the path to a local copy of remote resource identified by `path`.
|
|
||||||
Load(ctx context.Context, path string) (string, error)
|
|
||||||
// Dir computes path to resource"s parent folder, made relative if possible
|
|
||||||
Dir(path string) string
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoteResourceLoaders excludes localResourceLoader from ResourceLoaders
|
|
||||||
func (o Options) RemoteResourceLoaders() []ResourceLoader {
|
|
||||||
var loaders []ResourceLoader
|
|
||||||
for i, loader := range o.ResourceLoaders {
|
|
||||||
if _, ok := loader.(localResourceLoader); ok {
|
|
||||||
if i != len(o.ResourceLoaders)-1 {
|
|
||||||
logrus.Warning("misconfiguration of ResourceLoaders: localResourceLoader should be last")
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
loaders = append(loaders, loader)
|
|
||||||
}
|
|
||||||
return loaders
|
|
||||||
}
|
|
||||||
|
|
||||||
type localResourceLoader struct {
|
|
||||||
WorkingDir string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l localResourceLoader) abs(p string) string {
|
|
||||||
if filepath.IsAbs(p) {
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
return filepath.Join(l.WorkingDir, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l localResourceLoader) Accept(_ string) bool {
|
|
||||||
// LocalResourceLoader is the last loader tested so it always should accept the config and try to get the content.
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l localResourceLoader) Load(_ context.Context, p string) (string, error) {
|
|
||||||
return l.abs(p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l localResourceLoader) Dir(originalPath string) string {
|
|
||||||
path := l.abs(originalPath)
|
|
||||||
if !l.isDir(path) {
|
|
||||||
path = l.abs(filepath.Dir(originalPath))
|
|
||||||
}
|
|
||||||
rel, err := filepath.Rel(l.WorkingDir, path)
|
|
||||||
if err != nil {
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
return rel
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l localResourceLoader) isDir(path string) bool {
|
|
||||||
fileInfo, err := os.Stat(path)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return fileInfo.IsDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Options) clone() *Options {
|
|
||||||
return &Options{
|
|
||||||
SkipValidation: o.SkipValidation,
|
|
||||||
SkipInterpolation: o.SkipInterpolation,
|
|
||||||
SkipNormalization: o.SkipNormalization,
|
|
||||||
ResolvePaths: o.ResolvePaths,
|
|
||||||
ConvertWindowsPaths: o.ConvertWindowsPaths,
|
|
||||||
SkipConsistencyCheck: o.SkipConsistencyCheck,
|
|
||||||
SkipExtends: o.SkipExtends,
|
|
||||||
SkipInclude: o.SkipInclude,
|
|
||||||
Interpolate: o.Interpolate,
|
|
||||||
discardEnvFiles: o.discardEnvFiles,
|
|
||||||
projectName: o.projectName,
|
|
||||||
projectNameImperativelySet: o.projectNameImperativelySet,
|
|
||||||
Profiles: o.Profiles,
|
|
||||||
ResourceLoaders: o.ResourceLoaders,
|
|
||||||
KnownExtensions: o.KnownExtensions,
|
|
||||||
Listeners: o.Listeners,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Options) SetProjectName(name string, imperativelySet bool) {
|
|
||||||
o.projectName = name
|
|
||||||
o.projectNameImperativelySet = imperativelySet
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o Options) GetProjectName() (string, bool) {
|
|
||||||
return o.projectName, o.projectNameImperativelySet
|
|
||||||
}
|
|
||||||
|
|
||||||
// serviceRef identifies a reference to a service. It's used to detect cyclic
|
|
||||||
// references in "extends".
|
|
||||||
type serviceRef struct {
|
|
||||||
filename string
|
|
||||||
service string
|
|
||||||
}
|
|
||||||
|
|
||||||
type cycleTracker struct {
|
|
||||||
loaded []serviceRef
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ct *cycleTracker) Add(filename, service string) (*cycleTracker, error) {
|
|
||||||
toAdd := serviceRef{filename: filename, service: service}
|
|
||||||
for _, loaded := range ct.loaded {
|
|
||||||
if toAdd == loaded {
|
|
||||||
// Create an error message of the form:
|
|
||||||
// Circular reference:
|
|
||||||
// service-a in docker-compose.yml
|
|
||||||
// extends service-b in docker-compose.yml
|
|
||||||
// extends service-a in docker-compose.yml
|
|
||||||
errLines := []string{
|
|
||||||
"Circular reference:",
|
|
||||||
fmt.Sprintf(" %s in %s", ct.loaded[0].service, ct.loaded[0].filename),
|
|
||||||
}
|
|
||||||
for _, service := range append(ct.loaded[1:], toAdd) {
|
|
||||||
errLines = append(errLines, fmt.Sprintf(" extends %s in %s", service.service, service.filename))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errors.New(strings.Join(errLines, "\n"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var branch []serviceRef
|
|
||||||
branch = append(branch, ct.loaded...)
|
|
||||||
branch = append(branch, toAdd)
|
|
||||||
return &cycleTracker{
|
|
||||||
loaded: branch,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithDiscardEnvFiles sets the Options to discard the `env_file` section after resolving to
|
|
||||||
// the `environment` section
|
|
||||||
func WithDiscardEnvFiles(opts *Options) {
|
|
||||||
opts.discardEnvFiles = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithSkipValidation sets the Options to skip validation when loading sections
|
|
||||||
func WithSkipValidation(opts *Options) {
|
|
||||||
opts.SkipValidation = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithProfiles sets profiles to be activated
|
|
||||||
func WithProfiles(profiles []string) func(*Options) {
|
|
||||||
return func(opts *Options) {
|
|
||||||
opts.Profiles = profiles
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PostProcessor is used to tweak compose model based on metadata extracted during yaml Unmarshal phase
|
|
||||||
// that hardly can be implemented using go-yaml and mapstructure
|
|
||||||
type PostProcessor interface {
|
|
||||||
// Apply changes to compose model based on recorder metadata
|
|
||||||
Apply(interface{}) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type NoopPostProcessor struct{}
|
|
||||||
|
|
||||||
func (NoopPostProcessor) Apply(interface{}) error { return nil }
|
|
||||||
|
|
||||||
// LoadConfigFiles ingests config files with ResourceLoader and returns config details with paths to local copies
|
|
||||||
func LoadConfigFiles(ctx context.Context, configFiles []string, workingDir string, options ...func(*Options)) (*types.ConfigDetails, error) {
|
|
||||||
if len(configFiles) < 1 {
|
|
||||||
return &types.ConfigDetails{}, fmt.Errorf("no configuration file provided: %w", errdefs.ErrNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := &Options{}
|
|
||||||
config := &types.ConfigDetails{
|
|
||||||
ConfigFiles: make([]types.ConfigFile, len(configFiles)),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, op := range options {
|
|
||||||
op(opts)
|
|
||||||
}
|
|
||||||
opts.ResourceLoaders = append(opts.ResourceLoaders, localResourceLoader{})
|
|
||||||
|
|
||||||
for i, p := range configFiles {
|
|
||||||
if p == "-" {
|
|
||||||
config.ConfigFiles[i] = types.ConfigFile{
|
|
||||||
Filename: p,
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, loader := range opts.ResourceLoaders {
|
|
||||||
_, isLocalResourceLoader := loader.(localResourceLoader)
|
|
||||||
if !loader.Accept(p) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
local, err := loader.Load(ctx, p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if config.WorkingDir == "" && !isLocalResourceLoader {
|
|
||||||
config.WorkingDir = filepath.Dir(local)
|
|
||||||
}
|
|
||||||
abs, err := filepath.Abs(local)
|
|
||||||
if err != nil {
|
|
||||||
abs = local
|
|
||||||
}
|
|
||||||
config.ConfigFiles[i] = types.ConfigFile{
|
|
||||||
Filename: abs,
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if config.WorkingDir == "" {
|
|
||||||
config.WorkingDir = workingDir
|
|
||||||
}
|
|
||||||
return config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadWithContext reads a ConfigDetails and returns a fully loaded configuration as a compose-go Project
|
|
||||||
func LoadWithContext(ctx context.Context, configDetails types.ConfigDetails, options ...func(*Options)) (*types.Project, error) {
|
|
||||||
opts := ToOptions(&configDetails, options)
|
|
||||||
dict, err := loadModelWithContext(ctx, &configDetails, opts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return ModelToProject(dict, opts, configDetails)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadModelWithContext reads a ConfigDetails and returns a fully loaded configuration as a yaml dictionary
|
|
||||||
func LoadModelWithContext(ctx context.Context, configDetails types.ConfigDetails, options ...func(*Options)) (map[string]any, error) {
|
|
||||||
opts := ToOptions(&configDetails, options)
|
|
||||||
return loadModelWithContext(ctx, &configDetails, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadModelWithContext reads a ConfigDetails and returns a fully loaded configuration as a yaml dictionary
|
|
||||||
func loadModelWithContext(ctx context.Context, configDetails *types.ConfigDetails, opts *Options) (map[string]any, error) {
|
|
||||||
if len(configDetails.ConfigFiles) < 1 {
|
|
||||||
return nil, errors.New("no compose file specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := projectName(configDetails, opts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return load(ctx, *configDetails, opts, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToOptions(configDetails *types.ConfigDetails, options []func(*Options)) *Options {
|
|
||||||
opts := &Options{
|
|
||||||
Interpolate: &interp.Options{
|
|
||||||
Substitute: template.Substitute,
|
|
||||||
LookupValue: configDetails.LookupEnv,
|
|
||||||
TypeCastMapping: interpolateTypeCastMapping,
|
|
||||||
},
|
|
||||||
ResolvePaths: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, op := range options {
|
|
||||||
op(opts)
|
|
||||||
}
|
|
||||||
opts.ResourceLoaders = append(opts.ResourceLoaders, localResourceLoader{configDetails.WorkingDir})
|
|
||||||
return opts
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadYamlModel(ctx context.Context, config types.ConfigDetails, opts *Options, ct *cycleTracker, included []string) (map[string]interface{}, error) {
|
|
||||||
var (
|
|
||||||
dict = map[string]interface{}{}
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
workingDir, environment := config.WorkingDir, config.Environment
|
|
||||||
|
|
||||||
for _, file := range config.ConfigFiles {
|
|
||||||
dict, _, err = loadYamlFile(ctx, file, opts, workingDir, environment, ct, dict, included)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !opts.SkipDefaultValues {
|
|
||||||
dict, err = transform.SetDefaultValues(dict)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !opts.SkipValidation {
|
|
||||||
if err := validation.Validate(dict); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.ResolvePaths {
|
|
||||||
var remotes []paths.RemoteResource
|
|
||||||
for _, loader := range opts.RemoteResourceLoaders() {
|
|
||||||
remotes = append(remotes, loader.Accept)
|
|
||||||
}
|
|
||||||
err = paths.ResolveRelativePaths(dict, config.WorkingDir, remotes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ResolveEnvironment(dict, config.Environment)
|
|
||||||
|
|
||||||
return dict, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadYamlFile(ctx context.Context,
|
|
||||||
file types.ConfigFile,
|
|
||||||
opts *Options,
|
|
||||||
workingDir string,
|
|
||||||
environment types.Mapping,
|
|
||||||
ct *cycleTracker,
|
|
||||||
dict map[string]interface{},
|
|
||||||
included []string,
|
|
||||||
) (map[string]interface{}, PostProcessor, error) {
|
|
||||||
ctx = context.WithValue(ctx, consts.ComposeFileKey{}, file.Filename)
|
|
||||||
if file.Content == nil && file.Config == nil {
|
|
||||||
content, err := os.ReadFile(file.Filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
file.Content = content
|
|
||||||
}
|
|
||||||
|
|
||||||
processRawYaml := func(raw interface{}, processor PostProcessor) error {
|
|
||||||
converted, err := convertToStringKeysRecursive(raw, "")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
cfg, ok := converted.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return errors.New("top-level object must be a mapping")
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.Interpolate != nil && !opts.SkipInterpolation {
|
|
||||||
cfg, err = interp.Interpolate(cfg, *opts.Interpolate)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fixEmptyNotNull(cfg)
|
|
||||||
|
|
||||||
if !opts.SkipExtends {
|
|
||||||
err = ApplyExtends(ctx, cfg, opts, ct, processor)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := processor.Apply(dict); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !opts.SkipInclude {
|
|
||||||
included = append(included, file.Filename)
|
|
||||||
err = ApplyInclude(ctx, workingDir, environment, cfg, opts, included, processor)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dict, err = override.Merge(dict, cfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
dict, err = override.EnforceUnicity(dict)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !opts.SkipValidation {
|
|
||||||
if err := schema.Validate(dict); err != nil {
|
|
||||||
return fmt.Errorf("validating %s: %w", file.Filename, err)
|
|
||||||
}
|
|
||||||
if _, ok := dict["version"]; ok {
|
|
||||||
opts.warnObsoleteVersion(file.Filename)
|
|
||||||
delete(dict, "version")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dict, err = transform.Canonical(dict, opts.SkipInterpolation)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
dict = OmitEmpty(dict)
|
|
||||||
|
|
||||||
// Canonical transformation can reveal duplicates, typically as ports can be a range and conflict with an override
|
|
||||||
dict, err = override.EnforceUnicity(dict)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var processor PostProcessor
|
|
||||||
if file.Config == nil {
|
|
||||||
r := bytes.NewReader(file.Content)
|
|
||||||
decoder := yaml.NewDecoder(r)
|
|
||||||
for {
|
|
||||||
var raw interface{}
|
|
||||||
reset := &ResetProcessor{target: &raw}
|
|
||||||
err := decoder.Decode(reset)
|
|
||||||
if err != nil && errors.Is(err, io.EOF) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("failed to parse %s: %w", file.Filename, err)
|
|
||||||
}
|
|
||||||
processor = reset
|
|
||||||
if err := processRawYaml(raw, processor); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := processRawYaml(file.Config, NoopPostProcessor{}); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dict, processor, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func load(ctx context.Context, configDetails types.ConfigDetails, opts *Options, loaded []string) (map[string]interface{}, error) {
|
|
||||||
mainFile := configDetails.ConfigFiles[0].Filename
|
|
||||||
for _, f := range loaded {
|
|
||||||
if f == mainFile {
|
|
||||||
loaded = append(loaded, mainFile)
|
|
||||||
return nil, fmt.Errorf("include cycle detected:\n%s\n include %s", loaded[0], strings.Join(loaded[1:], "\n include "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dict, err := loadYamlModel(ctx, configDetails, opts, &cycleTracker{}, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(dict) == 0 {
|
|
||||||
return nil, errors.New("empty compose file")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !opts.SkipValidation && opts.projectName == "" {
|
|
||||||
return nil, errors.New("project name must not be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !opts.SkipNormalization {
|
|
||||||
dict["name"] = opts.projectName
|
|
||||||
dict, err = Normalize(dict, configDetails.Environment)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dict, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ModelToProject binds a canonical yaml dict into compose-go structs
|
|
||||||
func ModelToProject(dict map[string]interface{}, opts *Options, configDetails types.ConfigDetails) (*types.Project, error) {
|
|
||||||
project := &types.Project{
|
|
||||||
Name: opts.projectName,
|
|
||||||
WorkingDir: configDetails.WorkingDir,
|
|
||||||
Environment: configDetails.Environment,
|
|
||||||
}
|
|
||||||
delete(dict, "name") // project name set by yaml must be identified by caller as opts.projectName
|
|
||||||
|
|
||||||
var err error
|
|
||||||
dict, err = processExtensions(dict, tree.NewPath(), opts.KnownExtensions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = Transform(dict, project)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.ConvertWindowsPaths {
|
|
||||||
for i, service := range project.Services {
|
|
||||||
for j, volume := range service.Volumes {
|
|
||||||
service.Volumes[j] = convertVolumePath(volume)
|
|
||||||
}
|
|
||||||
project.Services[i] = service
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if project, err = project.WithProfiles(opts.Profiles); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !opts.SkipConsistencyCheck {
|
|
||||||
err := checkConsistency(project)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !opts.SkipResolveEnvironment {
|
|
||||||
project, err = project.WithServicesEnvironmentResolved(opts.discardEnvFiles)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
project, err = project.WithServicesLabelsResolved(opts.discardEnvFiles)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return project, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func InvalidProjectNameErr(v string) error {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"invalid project name %q: must consist only of lowercase alphanumeric characters, hyphens, and underscores as well as start with a letter or number",
|
|
||||||
v,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// projectName determines the canonical name to use for the project considering
|
|
||||||
// the loader Options as well as `name` fields in Compose YAML fields (which
|
|
||||||
// also support interpolation).
|
|
||||||
func projectName(details *types.ConfigDetails, opts *Options) error {
|
|
||||||
defer func() {
|
|
||||||
if details.Environment == nil {
|
|
||||||
details.Environment = map[string]string{}
|
|
||||||
}
|
|
||||||
details.Environment[consts.ComposeProjectName] = opts.projectName
|
|
||||||
}()
|
|
||||||
|
|
||||||
if opts.projectNameImperativelySet {
|
|
||||||
if NormalizeProjectName(opts.projectName) != opts.projectName {
|
|
||||||
return InvalidProjectNameErr(opts.projectName)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type named struct {
|
|
||||||
Name string `yaml:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// if user did NOT provide a name explicitly, then see if one is defined
|
|
||||||
// in any of the config files
|
|
||||||
var pjNameFromConfigFile string
|
|
||||||
for _, configFile := range details.ConfigFiles {
|
|
||||||
content := configFile.Content
|
|
||||||
if content == nil {
|
|
||||||
// This can be hit when Filename is set but Content is not. One
|
|
||||||
// example is when using ToConfigFiles().
|
|
||||||
d, err := os.ReadFile(configFile.Filename)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read file %q: %w", configFile.Filename, err)
|
|
||||||
}
|
|
||||||
content = d
|
|
||||||
configFile.Content = d
|
|
||||||
}
|
|
||||||
var n named
|
|
||||||
r := bytes.NewReader(content)
|
|
||||||
decoder := yaml.NewDecoder(r)
|
|
||||||
for {
|
|
||||||
err := decoder.Decode(&n)
|
|
||||||
if err != nil && errors.Is(err, io.EOF) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
// HACK: the way that loading is currently structured, this is
|
|
||||||
// a duplicative parse just for the `name`. if it fails, we
|
|
||||||
// give up but don't return the error, knowing that it'll get
|
|
||||||
// caught downstream for us
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if n.Name != "" {
|
|
||||||
pjNameFromConfigFile = n.Name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !opts.SkipInterpolation {
|
|
||||||
interpolated, err := interp.Interpolate(
|
|
||||||
map[string]interface{}{"name": pjNameFromConfigFile},
|
|
||||||
*opts.Interpolate,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
pjNameFromConfigFile = interpolated["name"].(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !opts.SkipNormalization {
|
|
||||||
pjNameFromConfigFile = NormalizeProjectName(pjNameFromConfigFile)
|
|
||||||
}
|
|
||||||
if pjNameFromConfigFile != "" {
|
|
||||||
opts.projectName = pjNameFromConfigFile
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NormalizeProjectName(s string) string {
|
|
||||||
r := regexp.MustCompile("[a-z0-9_-]")
|
|
||||||
s = strings.ToLower(s)
|
|
||||||
s = strings.Join(r.FindAllString(s, -1), "")
|
|
||||||
return strings.TrimLeft(s, "_-")
|
|
||||||
}
|
|
||||||
|
|
||||||
var userDefinedKeys = []tree.Path{
|
|
||||||
"services",
|
|
||||||
"services.*.depends_on",
|
|
||||||
"volumes",
|
|
||||||
"networks",
|
|
||||||
"secrets",
|
|
||||||
"configs",
|
|
||||||
}
|
|
||||||
|
|
||||||
func processExtensions(dict map[string]any, p tree.Path, extensions map[string]any) (map[string]interface{}, error) {
|
|
||||||
extras := map[string]any{}
|
|
||||||
var err error
|
|
||||||
for key, value := range dict {
|
|
||||||
skip := false
|
|
||||||
for _, uk := range userDefinedKeys {
|
|
||||||
if p.Matches(uk) {
|
|
||||||
skip = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !skip && strings.HasPrefix(key, "x-") {
|
|
||||||
extras[key] = value
|
|
||||||
delete(dict, key)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch v := value.(type) {
|
|
||||||
case map[string]interface{}:
|
|
||||||
dict[key], err = processExtensions(v, p.Next(key), extensions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
case []interface{}:
|
|
||||||
for i, e := range v {
|
|
||||||
if m, ok := e.(map[string]interface{}); ok {
|
|
||||||
v[i], err = processExtensions(m, p.Next(strconv.Itoa(i)), extensions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for name, val := range extras {
|
|
||||||
if typ, ok := extensions[name]; ok {
|
|
||||||
target := reflect.New(reflect.TypeOf(typ)).Elem().Interface()
|
|
||||||
err = Transform(val, &target)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
extras[name] = target
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(extras) > 0 {
|
|
||||||
dict[consts.Extensions] = extras
|
|
||||||
}
|
|
||||||
return dict, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transform converts the source into the target struct with compose types transformer
|
|
||||||
// and the specified transformers if any.
|
|
||||||
func Transform(source interface{}, target interface{}) error {
|
|
||||||
data := mapstructure.Metadata{}
|
|
||||||
config := &mapstructure.DecoderConfig{
|
|
||||||
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
|
||||||
nameServices,
|
|
||||||
decoderHook,
|
|
||||||
cast,
|
|
||||||
secretConfigDecoderHook,
|
|
||||||
),
|
|
||||||
Result: target,
|
|
||||||
TagName: "yaml",
|
|
||||||
Metadata: &data,
|
|
||||||
}
|
|
||||||
decoder, err := mapstructure.NewDecoder(config)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return decoder.Decode(source)
|
|
||||||
}
|
|
||||||
|
|
||||||
// nameServices create implicit `name` key for convenience accessing service
|
|
||||||
func nameServices(from reflect.Value, to reflect.Value) (interface{}, error) {
|
|
||||||
if to.Type() == reflect.TypeOf(types.Services{}) {
|
|
||||||
nameK := reflect.ValueOf("name")
|
|
||||||
iter := from.MapRange()
|
|
||||||
for iter.Next() {
|
|
||||||
name := iter.Key()
|
|
||||||
elem := iter.Value()
|
|
||||||
elem.Elem().SetMapIndex(nameK, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return from.Interface(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func secretConfigDecoderHook(from, to reflect.Type, data interface{}) (interface{}, error) {
|
|
||||||
// Check if the input is a map and we're decoding into a SecretConfig
|
|
||||||
if from.Kind() == reflect.Map && to == reflect.TypeOf(types.SecretConfig{}) {
|
|
||||||
if v, ok := data.(map[string]interface{}); ok {
|
|
||||||
if ext, ok := v[consts.Extensions].(map[string]interface{}); ok {
|
|
||||||
if val, ok := ext[types.SecretConfigXValue].(string); ok {
|
|
||||||
// Return a map with the Content field populated
|
|
||||||
v["Content"] = val
|
|
||||||
delete(ext, types.SecretConfigXValue)
|
|
||||||
|
|
||||||
if len(ext) == 0 {
|
|
||||||
delete(v, consts.Extensions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the original data so the rest is handled by default mapstructure logic
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// keys need to be converted to strings for jsonschema
|
|
||||||
func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interface{}, error) {
|
|
||||||
if mapping, ok := value.(map[string]interface{}); ok {
|
|
||||||
for key, entry := range mapping {
|
|
||||||
var newKeyPrefix string
|
|
||||||
if keyPrefix == "" {
|
|
||||||
newKeyPrefix = key
|
|
||||||
} else {
|
|
||||||
newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, key)
|
|
||||||
}
|
|
||||||
convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
mapping[key] = convertedEntry
|
|
||||||
}
|
|
||||||
return mapping, nil
|
|
||||||
}
|
|
||||||
if mapping, ok := value.(map[interface{}]interface{}); ok {
|
|
||||||
dict := make(map[string]interface{})
|
|
||||||
for key, entry := range mapping {
|
|
||||||
str, ok := key.(string)
|
|
||||||
if !ok {
|
|
||||||
return nil, formatInvalidKeyError(keyPrefix, key)
|
|
||||||
}
|
|
||||||
var newKeyPrefix string
|
|
||||||
if keyPrefix == "" {
|
|
||||||
newKeyPrefix = str
|
|
||||||
} else {
|
|
||||||
newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, str)
|
|
||||||
}
|
|
||||||
convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
dict[str] = convertedEntry
|
|
||||||
}
|
|
||||||
return dict, nil
|
|
||||||
}
|
|
||||||
if list, ok := value.([]interface{}); ok {
|
|
||||||
var convertedList []interface{}
|
|
||||||
for index, entry := range list {
|
|
||||||
newKeyPrefix := fmt.Sprintf("%s[%d]", keyPrefix, index)
|
|
||||||
convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
convertedList = append(convertedList, convertedEntry)
|
|
||||||
}
|
|
||||||
return convertedList, nil
|
|
||||||
}
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatInvalidKeyError(keyPrefix string, key interface{}) error {
|
|
||||||
var location string
|
|
||||||
if keyPrefix == "" {
|
|
||||||
location = "at top level"
|
|
||||||
} else {
|
|
||||||
location = fmt.Sprintf("in %s", keyPrefix)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("non-string key %s: %#v", location, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Windows path, c:\\my\\path\\shiny, need to be changed to be compatible with
|
|
||||||
// the Engine. Volume path are expected to be linux style /c/my/path/shiny/
|
|
||||||
func convertVolumePath(volume types.ServiceVolumeConfig) types.ServiceVolumeConfig {
|
|
||||||
volumeName := strings.ToLower(filepath.VolumeName(volume.Source))
|
|
||||||
if len(volumeName) != 2 {
|
|
||||||
return volume
|
|
||||||
}
|
|
||||||
|
|
||||||
convertedSource := fmt.Sprintf("/%c%s", volumeName[0], volume.Source[len(volumeName):])
|
|
||||||
convertedSource = strings.ReplaceAll(convertedSource, "\\", "/")
|
|
||||||
|
|
||||||
volume.Source = convertedSource
|
|
||||||
return volume
|
|
||||||
}
|
|
||||||
79
vendor/github.com/compose-spec/compose-go/v2/loader/mapstructure.go
generated
vendored
79
vendor/github.com/compose-spec/compose-go/v2/loader/mapstructure.go
generated
vendored
@ -1,79 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package loader
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// comparable to yaml.Unmarshaler, decoder allow a type to define it's own custom logic to convert value
|
|
||||||
// see https://github.com/mitchellh/mapstructure/pull/294
|
|
||||||
type decoder interface {
|
|
||||||
DecodeMapstructure(interface{}) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// see https://github.com/mitchellh/mapstructure/issues/115#issuecomment-735287466
|
|
||||||
// adapted to support types derived from built-in types, as DecodeMapstructure would not be able to mutate internal
|
|
||||||
// value, so need to invoke DecodeMapstructure defined by pointer to type
|
|
||||||
func decoderHook(from reflect.Value, to reflect.Value) (interface{}, error) {
|
|
||||||
// If the destination implements the decoder interface
|
|
||||||
u, ok := to.Interface().(decoder)
|
|
||||||
if !ok {
|
|
||||||
// for non-struct types we need to invoke func (*type) DecodeMapstructure()
|
|
||||||
if to.CanAddr() {
|
|
||||||
pto := to.Addr()
|
|
||||||
u, ok = pto.Interface().(decoder)
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
return from.Interface(), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If it is nil and a pointer, create and assign the target value first
|
|
||||||
if to.Type().Kind() == reflect.Ptr && to.IsNil() {
|
|
||||||
to.Set(reflect.New(to.Type().Elem()))
|
|
||||||
u = to.Interface().(decoder)
|
|
||||||
}
|
|
||||||
// Call the custom DecodeMapstructure method
|
|
||||||
if err := u.DecodeMapstructure(from.Interface()); err != nil {
|
|
||||||
return to.Interface(), err
|
|
||||||
}
|
|
||||||
return to.Interface(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func cast(from reflect.Value, to reflect.Value) (interface{}, error) {
|
|
||||||
switch from.Type().Kind() {
|
|
||||||
case reflect.String:
|
|
||||||
switch to.Kind() {
|
|
||||||
case reflect.Bool:
|
|
||||||
return toBoolean(from.String())
|
|
||||||
case reflect.Int:
|
|
||||||
return toInt(from.String())
|
|
||||||
case reflect.Int64:
|
|
||||||
return toInt64(from.String())
|
|
||||||
case reflect.Float32:
|
|
||||||
return toFloat32(from.String())
|
|
||||||
case reflect.Float64:
|
|
||||||
return toFloat(from.String())
|
|
||||||
}
|
|
||||||
case reflect.Int:
|
|
||||||
if to.Kind() == reflect.String {
|
|
||||||
return strconv.FormatInt(from.Int(), 10), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return from.Interface(), nil
|
|
||||||
}
|
|
||||||
266
vendor/github.com/compose-spec/compose-go/v2/loader/normalize.go
generated
vendored
266
vendor/github.com/compose-spec/compose-go/v2/loader/normalize.go
generated
vendored
@ -1,266 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package loader
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Normalize compose project by moving deprecated attributes to their canonical position and injecting implicit defaults
|
|
||||||
func Normalize(dict map[string]any, env types.Mapping) (map[string]any, error) {
|
|
||||||
normalizeNetworks(dict)
|
|
||||||
|
|
||||||
if d, ok := dict["services"]; ok {
|
|
||||||
services := d.(map[string]any)
|
|
||||||
for name, s := range services {
|
|
||||||
service := s.(map[string]any)
|
|
||||||
|
|
||||||
if service["pull_policy"] == types.PullPolicyIfNotPresent {
|
|
||||||
service["pull_policy"] = types.PullPolicyMissing
|
|
||||||
}
|
|
||||||
|
|
||||||
fn := func(s string) (string, bool) {
|
|
||||||
v, ok := env[s]
|
|
||||||
return v, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
if b, ok := service["build"]; ok {
|
|
||||||
build := b.(map[string]any)
|
|
||||||
if build["context"] == nil {
|
|
||||||
build["context"] = "."
|
|
||||||
}
|
|
||||||
if build["dockerfile"] == nil && build["dockerfile_inline"] == nil {
|
|
||||||
build["dockerfile"] = "Dockerfile"
|
|
||||||
}
|
|
||||||
|
|
||||||
if a, ok := build["args"]; ok {
|
|
||||||
build["args"], _ = resolve(a, fn, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
service["build"] = build
|
|
||||||
}
|
|
||||||
|
|
||||||
if e, ok := service["environment"]; ok {
|
|
||||||
service["environment"], _ = resolve(e, fn, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
var dependsOn map[string]any
|
|
||||||
if d, ok := service["depends_on"]; ok {
|
|
||||||
dependsOn = d.(map[string]any)
|
|
||||||
} else {
|
|
||||||
dependsOn = map[string]any{}
|
|
||||||
}
|
|
||||||
if l, ok := service["links"]; ok {
|
|
||||||
links := l.([]any)
|
|
||||||
for _, e := range links {
|
|
||||||
link := e.(string)
|
|
||||||
parts := strings.Split(link, ":")
|
|
||||||
if len(parts) == 2 {
|
|
||||||
link = parts[0]
|
|
||||||
}
|
|
||||||
if _, ok := dependsOn[link]; !ok {
|
|
||||||
dependsOn[link] = map[string]any{
|
|
||||||
"condition": types.ServiceConditionStarted,
|
|
||||||
"restart": true,
|
|
||||||
"required": true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, namespace := range []string{"network_mode", "ipc", "pid", "uts", "cgroup"} {
|
|
||||||
if n, ok := service[namespace]; ok {
|
|
||||||
ref := n.(string)
|
|
||||||
if strings.HasPrefix(ref, types.ServicePrefix) {
|
|
||||||
shared := ref[len(types.ServicePrefix):]
|
|
||||||
if _, ok := dependsOn[shared]; !ok {
|
|
||||||
dependsOn[shared] = map[string]any{
|
|
||||||
"condition": types.ServiceConditionStarted,
|
|
||||||
"restart": true,
|
|
||||||
"required": true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, ok := service["volumes"]; ok {
|
|
||||||
volumes := v.([]any)
|
|
||||||
for i, volume := range volumes {
|
|
||||||
vol := volume.(map[string]any)
|
|
||||||
target := vol["target"].(string)
|
|
||||||
vol["target"] = path.Clean(target)
|
|
||||||
volumes[i] = vol
|
|
||||||
}
|
|
||||||
service["volumes"] = volumes
|
|
||||||
}
|
|
||||||
|
|
||||||
if n, ok := service["volumes_from"]; ok {
|
|
||||||
volumesFrom := n.([]any)
|
|
||||||
for _, v := range volumesFrom {
|
|
||||||
vol := v.(string)
|
|
||||||
if !strings.HasPrefix(vol, types.ContainerPrefix) {
|
|
||||||
spec := strings.Split(vol, ":")
|
|
||||||
if _, ok := dependsOn[spec[0]]; !ok {
|
|
||||||
dependsOn[spec[0]] = map[string]any{
|
|
||||||
"condition": types.ServiceConditionStarted,
|
|
||||||
"restart": false,
|
|
||||||
"required": true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(dependsOn) > 0 {
|
|
||||||
service["depends_on"] = dependsOn
|
|
||||||
}
|
|
||||||
services[name] = service
|
|
||||||
}
|
|
||||||
|
|
||||||
dict["services"] = services
|
|
||||||
}
|
|
||||||
setNameFromKey(dict)
|
|
||||||
|
|
||||||
return dict, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeNetworks(dict map[string]any) {
|
|
||||||
var networks map[string]any
|
|
||||||
if n, ok := dict["networks"]; ok {
|
|
||||||
networks = n.(map[string]any)
|
|
||||||
} else {
|
|
||||||
networks = map[string]any{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// implicit `default` network must be introduced only if actually used by some service
|
|
||||||
usesDefaultNetwork := false
|
|
||||||
|
|
||||||
if s, ok := dict["services"]; ok {
|
|
||||||
services := s.(map[string]any)
|
|
||||||
for name, se := range services {
|
|
||||||
service := se.(map[string]any)
|
|
||||||
if _, ok := service["provider"]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := service["network_mode"]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if n, ok := service["networks"]; !ok {
|
|
||||||
// If none explicitly declared, service is connected to default network
|
|
||||||
service["networks"] = map[string]any{"default": nil}
|
|
||||||
usesDefaultNetwork = true
|
|
||||||
} else {
|
|
||||||
net := n.(map[string]any)
|
|
||||||
if len(net) == 0 {
|
|
||||||
// networks section declared but empty (corner case)
|
|
||||||
service["networks"] = map[string]any{"default": nil}
|
|
||||||
usesDefaultNetwork = true
|
|
||||||
} else if _, ok := net["default"]; ok {
|
|
||||||
usesDefaultNetwork = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
services[name] = service
|
|
||||||
}
|
|
||||||
dict["services"] = services
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := networks["default"]; !ok && usesDefaultNetwork {
|
|
||||||
// If not declared explicitly, Compose model involves an implicit "default" network
|
|
||||||
networks["default"] = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(networks) > 0 {
|
|
||||||
dict["networks"] = networks
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolve(a any, fn func(s string) (string, bool), keepEmpty bool) (any, bool) {
|
|
||||||
switch v := a.(type) {
|
|
||||||
case []any:
|
|
||||||
var resolved []any
|
|
||||||
for _, val := range v {
|
|
||||||
if r, ok := resolve(val, fn, keepEmpty); ok {
|
|
||||||
resolved = append(resolved, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return resolved, true
|
|
||||||
case map[string]any:
|
|
||||||
resolved := map[string]any{}
|
|
||||||
for key, val := range v {
|
|
||||||
if val != nil {
|
|
||||||
resolved[key] = val
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if s, ok := fn(key); ok {
|
|
||||||
resolved[key] = s
|
|
||||||
} else if keepEmpty {
|
|
||||||
resolved[key] = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return resolved, true
|
|
||||||
case string:
|
|
||||||
if !strings.Contains(v, "=") {
|
|
||||||
if val, ok := fn(v); ok {
|
|
||||||
return fmt.Sprintf("%s=%s", v, val), true
|
|
||||||
}
|
|
||||||
if keepEmpty {
|
|
||||||
return v, true
|
|
||||||
}
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
return v, true
|
|
||||||
default:
|
|
||||||
return v, false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resources with no explicit name are actually named by their key in map
|
|
||||||
func setNameFromKey(dict map[string]any) {
|
|
||||||
for _, r := range []string{"networks", "volumes", "configs", "secrets"} {
|
|
||||||
a, ok := dict[r]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
toplevel := a.(map[string]any)
|
|
||||||
for key, r := range toplevel {
|
|
||||||
var resource map[string]any
|
|
||||||
if r != nil {
|
|
||||||
resource = r.(map[string]any)
|
|
||||||
} else {
|
|
||||||
resource = map[string]any{}
|
|
||||||
}
|
|
||||||
if resource["name"] == nil {
|
|
||||||
if x, ok := resource["external"]; ok && isTrue(x) {
|
|
||||||
resource["name"] = key
|
|
||||||
} else {
|
|
||||||
resource["name"] = fmt.Sprintf("%s_%s", dict["name"], key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toplevel[key] = resource
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isTrue(x any) bool {
|
|
||||||
parseBool, _ := strconv.ParseBool(fmt.Sprint(x))
|
|
||||||
return parseBool
|
|
||||||
}
|
|
||||||
75
vendor/github.com/compose-spec/compose-go/v2/loader/omitEmpty.go
generated
vendored
75
vendor/github.com/compose-spec/compose-go/v2/loader/omitEmpty.go
generated
vendored
@ -1,75 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package loader
|
|
||||||
|
|
||||||
import "github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
|
|
||||||
var omitempty = []tree.Path{
|
|
||||||
"services.*.dns",
|
|
||||||
}
|
|
||||||
|
|
||||||
// OmitEmpty removes empty attributes which are irrelevant when unset
|
|
||||||
func OmitEmpty(yaml map[string]any) map[string]any {
|
|
||||||
cleaned := omitEmpty(yaml, tree.NewPath())
|
|
||||||
return cleaned.(map[string]any)
|
|
||||||
}
|
|
||||||
|
|
||||||
func omitEmpty(data any, p tree.Path) any {
|
|
||||||
switch v := data.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
for k, e := range v {
|
|
||||||
if isEmpty(e) && mustOmit(p) {
|
|
||||||
delete(v, k)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
v[k] = omitEmpty(e, p.Next(k))
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
case []any:
|
|
||||||
var c []any
|
|
||||||
for _, e := range v {
|
|
||||||
if isEmpty(e) && mustOmit(p) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
c = append(c, omitEmpty(e, p.Next("[]")))
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
default:
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustOmit(p tree.Path) bool {
|
|
||||||
for _, pattern := range omitempty {
|
|
||||||
if p.Matches(pattern) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isEmpty(e any) bool {
|
|
||||||
if e == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if v, ok := e.(string); ok && v == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
50
vendor/github.com/compose-spec/compose-go/v2/loader/paths.go
generated
vendored
50
vendor/github.com/compose-spec/compose-go/v2/loader/paths.go
generated
vendored
@ -1,50 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package loader
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ResolveRelativePaths resolves relative paths based on project WorkingDirectory
|
|
||||||
func ResolveRelativePaths(project *types.Project) error {
|
|
||||||
absWorkingDir, err := filepath.Abs(project.WorkingDir)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
project.WorkingDir = absWorkingDir
|
|
||||||
|
|
||||||
absComposeFiles, err := absComposeFiles(project.ComposeFiles)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
project.ComposeFiles = absComposeFiles
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func absComposeFiles(composeFiles []string) ([]string, error) {
|
|
||||||
for i, composeFile := range composeFiles {
|
|
||||||
absComposefile, err := filepath.Abs(composeFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
composeFiles[i] = absComposefile
|
|
||||||
}
|
|
||||||
return composeFiles, nil
|
|
||||||
}
|
|
||||||
196
vendor/github.com/compose-spec/compose-go/v2/loader/reset.go
generated
vendored
196
vendor/github.com/compose-spec/compose-go/v2/loader/reset.go
generated
vendored
@ -1,196 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package loader
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
"go.yaml.in/yaml/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ResetProcessor struct {
|
|
||||||
target interface{}
|
|
||||||
paths []tree.Path
|
|
||||||
visitedNodes map[*yaml.Node][]string
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalYAML implement yaml.Unmarshaler
|
|
||||||
func (p *ResetProcessor) UnmarshalYAML(value *yaml.Node) error {
|
|
||||||
p.visitedNodes = make(map[*yaml.Node][]string)
|
|
||||||
resolved, err := p.resolveReset(value, tree.NewPath())
|
|
||||||
p.visitedNodes = nil
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return resolved.Decode(p.target)
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolveReset detects `!reset` tag being set on yaml nodes and record position in the yaml tree
|
|
||||||
func (p *ResetProcessor) resolveReset(node *yaml.Node, path tree.Path) (*yaml.Node, error) {
|
|
||||||
pathStr := path.String()
|
|
||||||
// If the path contains "<<", removing the "<<" element and merging the path
|
|
||||||
if strings.Contains(pathStr, ".<<") {
|
|
||||||
path = tree.NewPath(strings.Replace(pathStr, ".<<", "", 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the node is an alias, We need to process the alias field in order to consider the !override and !reset tags
|
|
||||||
if node.Kind == yaml.AliasNode {
|
|
||||||
if err := p.checkForCycle(node.Alias, path); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.resolveReset(node.Alias, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
if node.Tag == "!reset" {
|
|
||||||
p.paths = append(p.paths, path)
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
if node.Tag == "!override" {
|
|
||||||
p.paths = append(p.paths, path)
|
|
||||||
return node, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
keys := map[string]int{}
|
|
||||||
switch node.Kind {
|
|
||||||
case yaml.SequenceNode:
|
|
||||||
var nodes []*yaml.Node
|
|
||||||
for idx, v := range node.Content {
|
|
||||||
next := path.Next(strconv.Itoa(idx))
|
|
||||||
resolved, err := p.resolveReset(v, next)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resolved != nil {
|
|
||||||
nodes = append(nodes, resolved)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
node.Content = nodes
|
|
||||||
case yaml.MappingNode:
|
|
||||||
var key string
|
|
||||||
var nodes []*yaml.Node
|
|
||||||
for idx, v := range node.Content {
|
|
||||||
if idx%2 == 0 {
|
|
||||||
key = v.Value
|
|
||||||
if line, seen := keys[key]; seen {
|
|
||||||
return nil, fmt.Errorf("line %d: mapping key %#v already defined at line %d", v.Line, key, line)
|
|
||||||
}
|
|
||||||
keys[key] = v.Line
|
|
||||||
} else {
|
|
||||||
resolved, err := p.resolveReset(v, path.Next(key))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resolved != nil {
|
|
||||||
nodes = append(nodes, node.Content[idx-1], resolved)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
node.Content = nodes
|
|
||||||
}
|
|
||||||
return node, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply finds the go attributes matching recorded paths and reset them to zero value
|
|
||||||
func (p *ResetProcessor) Apply(target any) error {
|
|
||||||
return p.applyNullOverrides(target, tree.NewPath())
|
|
||||||
}
|
|
||||||
|
|
||||||
// applyNullOverrides set val to Zero if it matches any of the recorded paths
|
|
||||||
func (p *ResetProcessor) applyNullOverrides(target any, path tree.Path) error {
|
|
||||||
switch v := target.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
KEYS:
|
|
||||||
for k, e := range v {
|
|
||||||
next := path.Next(k)
|
|
||||||
for _, pattern := range p.paths {
|
|
||||||
if next.Matches(pattern) {
|
|
||||||
delete(v, k)
|
|
||||||
continue KEYS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err := p.applyNullOverrides(e, next)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case []any:
|
|
||||||
ITER:
|
|
||||||
for i, e := range v {
|
|
||||||
next := path.Next(fmt.Sprintf("[%d]", i))
|
|
||||||
for _, pattern := range p.paths {
|
|
||||||
if next.Matches(pattern) {
|
|
||||||
continue ITER
|
|
||||||
// TODO(ndeloof) support removal from sequence
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err := p.applyNullOverrides(e, next)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ResetProcessor) checkForCycle(node *yaml.Node, path tree.Path) error {
|
|
||||||
paths := p.visitedNodes[node]
|
|
||||||
pathStr := path.String()
|
|
||||||
|
|
||||||
for _, prevPath := range paths {
|
|
||||||
// If we're visiting the exact same path, it's not a cycle
|
|
||||||
if pathStr == prevPath {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// If either path is using a merge key, it's legitimate YAML merging
|
|
||||||
if strings.Contains(prevPath, "<<") || strings.Contains(pathStr, "<<") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only consider it a cycle if one path is contained within the other
|
|
||||||
// and they're not in different service definitions
|
|
||||||
if (strings.HasPrefix(pathStr, prevPath+".") ||
|
|
||||||
strings.HasPrefix(prevPath, pathStr+".")) &&
|
|
||||||
!areInDifferentServices(pathStr, prevPath) {
|
|
||||||
return fmt.Errorf("cycle detected: node at path %s references node at path %s", pathStr, prevPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p.visitedNodes[node] = append(paths, pathStr)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// areInDifferentServices checks if two paths are in different service definitions
|
|
||||||
func areInDifferentServices(path1, path2 string) bool {
|
|
||||||
// Split paths into components
|
|
||||||
parts1 := strings.Split(path1, ".")
|
|
||||||
parts2 := strings.Split(path2, ".")
|
|
||||||
|
|
||||||
// Look for the services component and compare the service names
|
|
||||||
for i := 0; i < len(parts1) && i < len(parts2); i++ {
|
|
||||||
if parts1[i] == "services" && i+1 < len(parts1) &&
|
|
||||||
parts2[i] == "services" && i+1 < len(parts2) {
|
|
||||||
// If they're different services, it's not a cycle
|
|
||||||
return parts1[i+1] != parts2[i+1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
218
vendor/github.com/compose-spec/compose-go/v2/loader/validate.go
generated
vendored
218
vendor/github.com/compose-spec/compose-go/v2/loader/validate.go
generated
vendored
@ -1,218 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package loader
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/errdefs"
|
|
||||||
"github.com/compose-spec/compose-go/v2/graph"
|
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
// checkConsistency validate a compose model is consistent
|
|
||||||
func checkConsistency(project *types.Project) error { //nolint:gocyclo
|
|
||||||
for name, s := range project.Services {
|
|
||||||
if s.Build == nil && s.Image == "" && s.Provider == nil {
|
|
||||||
return fmt.Errorf("service %q has neither an image nor a build context specified: %w", s.Name, errdefs.ErrInvalid)
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.Build != nil {
|
|
||||||
if s.Build.DockerfileInline != "" && s.Build.Dockerfile != "" {
|
|
||||||
return fmt.Errorf("service %q declares mutualy exclusive dockerfile and dockerfile_inline: %w", s.Name, errdefs.ErrInvalid)
|
|
||||||
}
|
|
||||||
|
|
||||||
for add, c := range s.Build.AdditionalContexts {
|
|
||||||
if target, ok := strings.CutPrefix(c, types.ServicePrefix); ok {
|
|
||||||
t, err := project.GetService(target)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("service %q declares unknown service %q as additional contexts %s", name, target, add)
|
|
||||||
}
|
|
||||||
if t.Build == nil {
|
|
||||||
return fmt.Errorf("service %q declares non-buildable service %q as additional contexts %s", name, target, add)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(s.Build.Platforms) > 0 && s.Platform != "" {
|
|
||||||
var found bool
|
|
||||||
for _, platform := range s.Build.Platforms {
|
|
||||||
if platform == s.Platform {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return fmt.Errorf("service.build.platforms MUST include service.platform %q: %w", s.Platform, errdefs.ErrInvalid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.NetworkMode != "" && len(s.Networks) > 0 {
|
|
||||||
return fmt.Errorf("service %s declares mutually exclusive `network_mode` and `networks`: %w", s.Name, errdefs.ErrInvalid)
|
|
||||||
}
|
|
||||||
for network := range s.Networks {
|
|
||||||
if _, ok := project.Networks[network]; !ok {
|
|
||||||
return fmt.Errorf("service %q refers to undefined network %s: %w", s.Name, network, errdefs.ErrInvalid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.HealthCheck != nil && len(s.HealthCheck.Test) > 0 {
|
|
||||||
switch s.HealthCheck.Test[0] {
|
|
||||||
case "CMD", "CMD-SHELL", "NONE":
|
|
||||||
default:
|
|
||||||
return errors.New(`healthcheck.test must start either by "CMD", "CMD-SHELL" or "NONE"`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for dependedService, cfg := range s.DependsOn {
|
|
||||||
if _, err := project.GetService(dependedService); err != nil {
|
|
||||||
if errors.Is(err, errdefs.ErrDisabled) && !cfg.Required {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return fmt.Errorf("service %q depends on undefined service %q: %w", s.Name, dependedService, errdefs.ErrInvalid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(s.NetworkMode, types.ServicePrefix) {
|
|
||||||
serviceName := s.NetworkMode[len(types.ServicePrefix):]
|
|
||||||
if _, err := project.GetServices(serviceName); err != nil {
|
|
||||||
return fmt.Errorf("service %q not found for network_mode 'service:%s'", serviceName, serviceName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, volume := range s.Volumes {
|
|
||||||
if volume.Type == types.VolumeTypeVolume && volume.Source != "" { // non anonymous volumes
|
|
||||||
if _, ok := project.Volumes[volume.Source]; !ok {
|
|
||||||
return fmt.Errorf("service %q refers to undefined volume %s: %w", s.Name, volume.Source, errdefs.ErrInvalid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if s.Build != nil {
|
|
||||||
for _, secret := range s.Build.Secrets {
|
|
||||||
if _, ok := project.Secrets[secret.Source]; !ok {
|
|
||||||
return fmt.Errorf("service %q refers to undefined build secret %s: %w", s.Name, secret.Source, errdefs.ErrInvalid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, config := range s.Configs {
|
|
||||||
if _, ok := project.Configs[config.Source]; !ok {
|
|
||||||
return fmt.Errorf("service %q refers to undefined config %s: %w", s.Name, config.Source, errdefs.ErrInvalid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for model := range s.Models {
|
|
||||||
if _, ok := project.Models[model]; !ok {
|
|
||||||
return fmt.Errorf("service %q refers to undefined model %s: %w", s.Name, model, errdefs.ErrInvalid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, secret := range s.Secrets {
|
|
||||||
if _, ok := project.Secrets[secret.Source]; !ok {
|
|
||||||
return fmt.Errorf("service %q refers to undefined secret %s: %w", s.Name, secret.Source, errdefs.ErrInvalid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.Scale != nil && s.Deploy != nil {
|
|
||||||
if s.Deploy.Replicas != nil && *s.Scale != *s.Deploy.Replicas {
|
|
||||||
return fmt.Errorf("services.%s: can't set distinct values on 'scale' and 'deploy.replicas': %w",
|
|
||||||
s.Name, errdefs.ErrInvalid)
|
|
||||||
}
|
|
||||||
s.Deploy.Replicas = s.Scale
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.Scale != nil && *s.Scale < 0 {
|
|
||||||
return fmt.Errorf("services.%s.scale: must be greater than or equal to 0", s.Name)
|
|
||||||
}
|
|
||||||
if s.Deploy != nil && s.Deploy.Replicas != nil && *s.Deploy.Replicas < 0 {
|
|
||||||
return fmt.Errorf("services.%s.deploy.replicas: must be greater than or equal to 0", s.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.CPUS != 0 && s.Deploy != nil {
|
|
||||||
if s.Deploy.Resources.Limits != nil && s.Deploy.Resources.Limits.NanoCPUs.Value() != s.CPUS {
|
|
||||||
return fmt.Errorf("services.%s: can't set distinct values on 'cpus' and 'deploy.resources.limits.cpus': %w",
|
|
||||||
s.Name, errdefs.ErrInvalid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if s.MemLimit != 0 && s.Deploy != nil {
|
|
||||||
if s.Deploy.Resources.Limits != nil && s.Deploy.Resources.Limits.MemoryBytes != s.MemLimit {
|
|
||||||
return fmt.Errorf("services.%s: can't set distinct values on 'mem_limit' and 'deploy.resources.limits.memory': %w",
|
|
||||||
s.Name, errdefs.ErrInvalid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if s.MemReservation != 0 && s.Deploy != nil {
|
|
||||||
if s.Deploy.Resources.Reservations != nil && s.Deploy.Resources.Reservations.MemoryBytes != s.MemReservation {
|
|
||||||
return fmt.Errorf("services.%s: can't set distinct values on 'mem_reservation' and 'deploy.resources.reservations.memory': %w",
|
|
||||||
s.Name, errdefs.ErrInvalid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if s.PidsLimit != 0 && s.Deploy != nil {
|
|
||||||
if s.Deploy.Resources.Limits != nil && s.Deploy.Resources.Limits.Pids != s.PidsLimit {
|
|
||||||
return fmt.Errorf("services.%s: can't set distinct values on 'pids_limit' and 'deploy.resources.limits.pids': %w",
|
|
||||||
s.Name, errdefs.ErrInvalid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.GetScale() > 1 && s.ContainerName != "" {
|
|
||||||
attr := "scale"
|
|
||||||
if s.Scale == nil {
|
|
||||||
attr = "deploy.replicas"
|
|
||||||
}
|
|
||||||
return fmt.Errorf("services.%s: can't set container_name and %s as container name must be unique: %w", attr,
|
|
||||||
s.Name, errdefs.ErrInvalid)
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.Develop != nil && s.Develop.Watch != nil {
|
|
||||||
for _, watch := range s.Develop.Watch {
|
|
||||||
if watch.Target == "" && watch.Action != types.WatchActionRebuild && watch.Action != types.WatchActionRestart {
|
|
||||||
return fmt.Errorf("services.%s.develop.watch: target is required for non-rebuild actions: %w", s.Name, errdefs.ErrInvalid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mounts := map[string]string{}
|
|
||||||
for i, tmpfs := range s.Tmpfs {
|
|
||||||
loc := fmt.Sprintf("services.%s.tmpfs[%d]", s.Name, i)
|
|
||||||
path, _, _ := strings.Cut(tmpfs, ":")
|
|
||||||
if p, ok := mounts[path]; ok {
|
|
||||||
return fmt.Errorf("%s: target %s already mounted as %s", loc, path, p)
|
|
||||||
}
|
|
||||||
mounts[path] = loc
|
|
||||||
}
|
|
||||||
for i, volume := range s.Volumes {
|
|
||||||
loc := fmt.Sprintf("services.%s.volumes[%d]", s.Name, i)
|
|
||||||
if p, ok := mounts[volume.Target]; ok {
|
|
||||||
return fmt.Errorf("%s: target %s already mounted as %s", loc, volume.Target, p)
|
|
||||||
}
|
|
||||||
mounts[volume.Target] = loc
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, secret := range project.Secrets {
|
|
||||||
if secret.External {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if secret.File == "" && secret.Environment == "" {
|
|
||||||
return fmt.Errorf("secret %q must declare either `file` or `environment`: %w", name, errdefs.ErrInvalid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return graph.CheckCycle(project)
|
|
||||||
}
|
|
||||||
27
vendor/github.com/compose-spec/compose-go/v2/override/extends.go
generated
vendored
27
vendor/github.com/compose-spec/compose-go/v2/override/extends.go
generated
vendored
@ -1,27 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package override
|
|
||||||
|
|
||||||
import "github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
|
|
||||||
func ExtendService(base, override map[string]any) (map[string]any, error) {
|
|
||||||
yaml, err := MergeYaml(base, override, tree.NewPath("services.x"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return yaml.(map[string]any), nil
|
|
||||||
}
|
|
||||||
307
vendor/github.com/compose-spec/compose-go/v2/override/merge.go
generated
vendored
307
vendor/github.com/compose-spec/compose-go/v2/override/merge.go
generated
vendored
@ -1,307 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package override
|
|
||||||
|
|
||||||
import (
|
|
||||||
"cmp"
|
|
||||||
"fmt"
|
|
||||||
"slices"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Merge applies overrides to a config model
|
|
||||||
func Merge(right, left map[string]any) (map[string]any, error) {
|
|
||||||
merged, err := MergeYaml(right, left, tree.NewPath())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return merged.(map[string]any), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type merger func(any, any, tree.Path) (any, error)
|
|
||||||
|
|
||||||
// mergeSpecials defines the custom rules applied by compose when merging yaml trees
|
|
||||||
var mergeSpecials = map[tree.Path]merger{}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
mergeSpecials["networks.*.ipam.config"] = mergeIPAMConfig
|
|
||||||
mergeSpecials["networks.*.labels"] = mergeToSequence
|
|
||||||
mergeSpecials["volumes.*.labels"] = mergeToSequence
|
|
||||||
mergeSpecials["services.*.annotations"] = mergeToSequence
|
|
||||||
mergeSpecials["services.*.build"] = mergeBuild
|
|
||||||
mergeSpecials["services.*.build.args"] = mergeToSequence
|
|
||||||
mergeSpecials["services.*.build.additional_contexts"] = mergeToSequence
|
|
||||||
mergeSpecials["services.*.build.extra_hosts"] = mergeExtraHosts
|
|
||||||
mergeSpecials["services.*.build.labels"] = mergeToSequence
|
|
||||||
mergeSpecials["services.*.command"] = override
|
|
||||||
mergeSpecials["services.*.depends_on"] = mergeDependsOn
|
|
||||||
mergeSpecials["services.*.deploy.labels"] = mergeToSequence
|
|
||||||
mergeSpecials["services.*.dns"] = mergeToSequence
|
|
||||||
mergeSpecials["services.*.dns_opt"] = mergeToSequence
|
|
||||||
mergeSpecials["services.*.dns_search"] = mergeToSequence
|
|
||||||
mergeSpecials["services.*.entrypoint"] = override
|
|
||||||
mergeSpecials["services.*.env_file"] = mergeToSequence
|
|
||||||
mergeSpecials["services.*.label_file"] = mergeToSequence
|
|
||||||
mergeSpecials["services.*.environment"] = mergeToSequence
|
|
||||||
mergeSpecials["services.*.extra_hosts"] = mergeExtraHosts
|
|
||||||
mergeSpecials["services.*.healthcheck.test"] = override
|
|
||||||
mergeSpecials["services.*.labels"] = mergeToSequence
|
|
||||||
mergeSpecials["services.*.volumes.*.volume.labels"] = mergeToSequence
|
|
||||||
mergeSpecials["services.*.logging"] = mergeLogging
|
|
||||||
mergeSpecials["services.*.models"] = mergeModels
|
|
||||||
mergeSpecials["services.*.networks"] = mergeNetworks
|
|
||||||
mergeSpecials["services.*.sysctls"] = mergeToSequence
|
|
||||||
mergeSpecials["services.*.tmpfs"] = mergeToSequence
|
|
||||||
mergeSpecials["services.*.ulimits.*"] = mergeUlimit
|
|
||||||
}
|
|
||||||
|
|
||||||
// MergeYaml merges map[string]any yaml trees handling special rules
|
|
||||||
func MergeYaml(e any, o any, p tree.Path) (any, error) {
|
|
||||||
for pattern, merger := range mergeSpecials {
|
|
||||||
if p.Matches(pattern) {
|
|
||||||
merged, err := merger(e, o, p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return merged, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if o == nil {
|
|
||||||
return e, nil
|
|
||||||
}
|
|
||||||
switch value := e.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
other, ok := o.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("cannot override %s", p)
|
|
||||||
}
|
|
||||||
return mergeMappings(value, other, p)
|
|
||||||
case []any:
|
|
||||||
other, ok := o.([]any)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("cannot override %s", p)
|
|
||||||
}
|
|
||||||
return append(value, other...), nil
|
|
||||||
default:
|
|
||||||
return o, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeMappings(mapping map[string]any, other map[string]any, p tree.Path) (map[string]any, error) {
|
|
||||||
for k, v := range other {
|
|
||||||
e, ok := mapping[k]
|
|
||||||
if !ok {
|
|
||||||
mapping[k] = v
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
next := p.Next(k)
|
|
||||||
merged, err := MergeYaml(e, v, next)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
mapping[k] = merged
|
|
||||||
}
|
|
||||||
return mapping, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// logging driver options are merged only when both compose file define the same driver
|
|
||||||
func mergeLogging(c any, o any, p tree.Path) (any, error) {
|
|
||||||
config := c.(map[string]any)
|
|
||||||
other := o.(map[string]any)
|
|
||||||
// we override logging config if source and override have the same driver set, or none
|
|
||||||
d, ok1 := other["driver"]
|
|
||||||
o, ok2 := config["driver"]
|
|
||||||
if d == o || !ok1 || !ok2 {
|
|
||||||
return mergeMappings(config, other, p)
|
|
||||||
}
|
|
||||||
return other, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeBuild(c any, o any, path tree.Path) (any, error) {
|
|
||||||
toBuild := func(c any) map[string]any {
|
|
||||||
switch v := c.(type) {
|
|
||||||
case string:
|
|
||||||
return map[string]any{
|
|
||||||
"context": v,
|
|
||||||
}
|
|
||||||
case map[string]any:
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return mergeMappings(toBuild(c), toBuild(o), path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeDependsOn(c any, o any, path tree.Path) (any, error) {
|
|
||||||
right := convertIntoMapping(c, map[string]any{
|
|
||||||
"condition": "service_started",
|
|
||||||
"required": true,
|
|
||||||
})
|
|
||||||
left := convertIntoMapping(o, map[string]any{
|
|
||||||
"condition": "service_started",
|
|
||||||
"required": true,
|
|
||||||
})
|
|
||||||
return mergeMappings(right, left, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeModels(c any, o any, path tree.Path) (any, error) {
|
|
||||||
right := convertIntoMapping(c, nil)
|
|
||||||
left := convertIntoMapping(o, nil)
|
|
||||||
return mergeMappings(right, left, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeNetworks(c any, o any, path tree.Path) (any, error) {
|
|
||||||
right := convertIntoMapping(c, nil)
|
|
||||||
left := convertIntoMapping(o, nil)
|
|
||||||
return mergeMappings(right, left, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeExtraHosts(c any, o any, _ tree.Path) (any, error) {
|
|
||||||
right := convertIntoSequence(c)
|
|
||||||
left := convertIntoSequence(o)
|
|
||||||
// Rewrite content of left slice to remove duplicate elements
|
|
||||||
i := 0
|
|
||||||
for _, v := range left {
|
|
||||||
if !slices.Contains(right, v) {
|
|
||||||
left[i] = v
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// keep only not duplicated elements from left slice
|
|
||||||
left = left[:i]
|
|
||||||
return append(right, left...), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeToSequence(c any, o any, _ tree.Path) (any, error) {
|
|
||||||
right := convertIntoSequence(c)
|
|
||||||
left := convertIntoSequence(o)
|
|
||||||
return append(right, left...), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertIntoSequence(value any) []any {
|
|
||||||
switch v := value.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
var seq []any
|
|
||||||
for k, val := range v {
|
|
||||||
if val == nil {
|
|
||||||
seq = append(seq, k)
|
|
||||||
} else {
|
|
||||||
switch vl := val.(type) {
|
|
||||||
// if val is an array we need to add the key with each value one by one
|
|
||||||
case []any:
|
|
||||||
for _, vlv := range vl {
|
|
||||||
seq = append(seq, fmt.Sprintf("%s=%v", k, vlv))
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
seq = append(seq, fmt.Sprintf("%s=%v", k, val))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
slices.SortFunc(seq, func(a, b any) int {
|
|
||||||
return cmp.Compare(a.(string), b.(string))
|
|
||||||
})
|
|
||||||
return seq
|
|
||||||
case []any:
|
|
||||||
return v
|
|
||||||
case string:
|
|
||||||
return []any{v}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeUlimit(_ any, o any, p tree.Path) (any, error) {
|
|
||||||
over, ismapping := o.(map[string]any)
|
|
||||||
if base, ok := o.(map[string]any); ok && ismapping {
|
|
||||||
return mergeMappings(base, over, p)
|
|
||||||
}
|
|
||||||
return o, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeIPAMConfig(c any, o any, path tree.Path) (any, error) {
|
|
||||||
var ipamConfigs []any
|
|
||||||
configs, ok := c.([]any)
|
|
||||||
if !ok {
|
|
||||||
return o, fmt.Errorf("%s: unexpected type %T", path, c)
|
|
||||||
}
|
|
||||||
overrides, ok := o.([]any)
|
|
||||||
if !ok {
|
|
||||||
return o, fmt.Errorf("%s: unexpected type %T", path, c)
|
|
||||||
}
|
|
||||||
for _, original := range configs {
|
|
||||||
right := convertIntoMapping(original, nil)
|
|
||||||
for _, override := range overrides {
|
|
||||||
left := convertIntoMapping(override, nil)
|
|
||||||
if left["subnet"] != right["subnet"] {
|
|
||||||
// check if left is already in ipamConfigs, add it if not and continue with the next config
|
|
||||||
if !slices.ContainsFunc(ipamConfigs, func(a any) bool {
|
|
||||||
return a.(map[string]any)["subnet"] == left["subnet"]
|
|
||||||
}) {
|
|
||||||
ipamConfigs = append(ipamConfigs, left)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
merged, err := mergeMappings(right, left, path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// find index of potential previous config with the same subnet in ipamConfigs
|
|
||||||
indexIfExist := slices.IndexFunc(ipamConfigs, func(a any) bool {
|
|
||||||
return a.(map[string]any)["subnet"] == merged["subnet"]
|
|
||||||
})
|
|
||||||
// if a previous config is already in ipamConfigs, replace it
|
|
||||||
if indexIfExist >= 0 {
|
|
||||||
ipamConfigs[indexIfExist] = merged
|
|
||||||
} else {
|
|
||||||
// or add the new config to ipamConfigs
|
|
||||||
ipamConfigs = append(ipamConfigs, merged)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ipamConfigs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertIntoMapping(a any, defaultValue map[string]any) map[string]any {
|
|
||||||
switch v := a.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
return v
|
|
||||||
case []any:
|
|
||||||
converted := map[string]any{}
|
|
||||||
for _, s := range v {
|
|
||||||
if defaultValue == nil {
|
|
||||||
converted[s.(string)] = nil
|
|
||||||
} else {
|
|
||||||
// Create a new map for each key
|
|
||||||
converted[s.(string)] = copyMap(defaultValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return converted
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyMap(m map[string]any) map[string]any {
|
|
||||||
c := make(map[string]any)
|
|
||||||
for k, v := range m {
|
|
||||||
c[k] = v
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func override(_ any, other any, _ tree.Path) (any, error) {
|
|
||||||
return other, nil
|
|
||||||
}
|
|
||||||
229
vendor/github.com/compose-spec/compose-go/v2/override/uncity.go
generated
vendored
229
vendor/github.com/compose-spec/compose-go/v2/override/uncity.go
generated
vendored
@ -1,229 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package override
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/format"
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
)
|
|
||||||
|
|
||||||
type indexer func(any, tree.Path) (string, error)
|
|
||||||
|
|
||||||
// mergeSpecials defines the custom rules applied by compose when merging yaml trees
|
|
||||||
var unique = map[tree.Path]indexer{}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
unique["networks.*.labels"] = keyValueIndexer
|
|
||||||
unique["networks.*.ipam.options"] = keyValueIndexer
|
|
||||||
unique["services.*.annotations"] = keyValueIndexer
|
|
||||||
unique["services.*.build.args"] = keyValueIndexer
|
|
||||||
unique["services.*.build.additional_contexts"] = keyValueIndexer
|
|
||||||
unique["services.*.build.platform"] = keyValueIndexer
|
|
||||||
unique["services.*.build.tags"] = keyValueIndexer
|
|
||||||
unique["services.*.build.labels"] = keyValueIndexer
|
|
||||||
unique["services.*.cap_add"] = keyValueIndexer
|
|
||||||
unique["services.*.cap_drop"] = keyValueIndexer
|
|
||||||
unique["services.*.devices"] = volumeIndexer
|
|
||||||
unique["services.*.configs"] = mountIndexer("")
|
|
||||||
unique["services.*.deploy.labels"] = keyValueIndexer
|
|
||||||
unique["services.*.dns"] = keyValueIndexer
|
|
||||||
unique["services.*.dns_opt"] = keyValueIndexer
|
|
||||||
unique["services.*.dns_search"] = keyValueIndexer
|
|
||||||
unique["services.*.environment"] = keyValueIndexer
|
|
||||||
unique["services.*.env_file"] = envFileIndexer
|
|
||||||
unique["services.*.expose"] = exposeIndexer
|
|
||||||
unique["services.*.labels"] = keyValueIndexer
|
|
||||||
unique["services.*.links"] = keyValueIndexer
|
|
||||||
unique["services.*.networks.*.aliases"] = keyValueIndexer
|
|
||||||
unique["services.*.networks.*.link_local_ips"] = keyValueIndexer
|
|
||||||
unique["services.*.ports"] = portIndexer
|
|
||||||
unique["services.*.profiles"] = keyValueIndexer
|
|
||||||
unique["services.*.secrets"] = mountIndexer("/run/secrets")
|
|
||||||
unique["services.*.sysctls"] = keyValueIndexer
|
|
||||||
unique["services.*.tmpfs"] = keyValueIndexer
|
|
||||||
unique["services.*.volumes"] = volumeIndexer
|
|
||||||
unique["services.*.devices"] = deviceMappingIndexer
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnforceUnicity removes redefinition of elements declared in a sequence
|
|
||||||
func EnforceUnicity(value map[string]any) (map[string]any, error) {
|
|
||||||
uniq, err := enforceUnicity(value, tree.NewPath())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return uniq.(map[string]any), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func enforceUnicity(value any, p tree.Path) (any, error) {
|
|
||||||
switch v := value.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
for k, e := range v {
|
|
||||||
u, err := enforceUnicity(e, p.Next(k))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
v[k] = u
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
case []any:
|
|
||||||
for pattern, indexer := range unique {
|
|
||||||
if p.Matches(pattern) {
|
|
||||||
seq := []any{}
|
|
||||||
keys := map[string]int{}
|
|
||||||
for i, entry := range v {
|
|
||||||
key, err := indexer(entry, p.Next(fmt.Sprintf("[%d]", i)))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if j, ok := keys[key]; ok {
|
|
||||||
seq[j] = entry
|
|
||||||
} else {
|
|
||||||
seq = append(seq, entry)
|
|
||||||
keys[key] = len(seq) - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return seq, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func keyValueIndexer(v any, p tree.Path) (string, error) {
|
|
||||||
switch value := v.(type) {
|
|
||||||
case string:
|
|
||||||
key, _, found := strings.Cut(value, "=")
|
|
||||||
if found {
|
|
||||||
return key, nil
|
|
||||||
}
|
|
||||||
return value, nil
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("%s: unexpected type %T", p, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func volumeIndexer(y any, p tree.Path) (string, error) {
|
|
||||||
switch value := y.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
target, ok := value["target"].(string)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("service volume %s is missing a mount target", p)
|
|
||||||
}
|
|
||||||
return target, nil
|
|
||||||
case string:
|
|
||||||
volume, err := format.ParseVolume(value)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return volume.Target, nil
|
|
||||||
}
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func deviceMappingIndexer(y any, p tree.Path) (string, error) {
|
|
||||||
switch value := y.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
target, ok := value["target"].(string)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("service device %s is missing a mount target", p)
|
|
||||||
}
|
|
||||||
return target, nil
|
|
||||||
case string:
|
|
||||||
arr := strings.Split(value, ":")
|
|
||||||
if len(arr) == 1 {
|
|
||||||
return arr[0], nil
|
|
||||||
}
|
|
||||||
return arr[1], nil
|
|
||||||
}
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func exposeIndexer(a any, path tree.Path) (string, error) {
|
|
||||||
switch v := a.(type) {
|
|
||||||
case string:
|
|
||||||
return v, nil
|
|
||||||
case int:
|
|
||||||
return strconv.Itoa(v), nil
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("%s: unsupported expose value %s", path, a)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mountIndexer(defaultPath string) indexer {
|
|
||||||
return func(a any, path tree.Path) (string, error) {
|
|
||||||
switch v := a.(type) {
|
|
||||||
case string:
|
|
||||||
return fmt.Sprintf("%s/%s", defaultPath, v), nil
|
|
||||||
case map[string]any:
|
|
||||||
t, ok := v["target"]
|
|
||||||
if ok {
|
|
||||||
return t.(string), nil
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s/%s", defaultPath, v["source"]), nil
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("%s: unsupported expose value %s", path, a)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func portIndexer(y any, p tree.Path) (string, error) {
|
|
||||||
switch value := y.(type) {
|
|
||||||
case int:
|
|
||||||
return strconv.Itoa(value), nil
|
|
||||||
case map[string]any:
|
|
||||||
target, ok := value["target"]
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("service ports %s is missing a target port", p)
|
|
||||||
}
|
|
||||||
published, ok := value["published"]
|
|
||||||
if !ok {
|
|
||||||
// try to parse it as an int
|
|
||||||
if pub, ok := value["published"]; ok {
|
|
||||||
published = fmt.Sprintf("%d", pub)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
host, ok := value["host_ip"]
|
|
||||||
if !ok {
|
|
||||||
host = "0.0.0.0"
|
|
||||||
}
|
|
||||||
protocol, ok := value["protocol"]
|
|
||||||
if !ok {
|
|
||||||
protocol = "tcp"
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s:%s:%d/%s", host, published, target, protocol), nil
|
|
||||||
case string:
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func envFileIndexer(y any, p tree.Path) (string, error) {
|
|
||||||
switch value := y.(type) {
|
|
||||||
case string:
|
|
||||||
return value, nil
|
|
||||||
case map[string]any:
|
|
||||||
if pathValue, ok := value["path"]; ok {
|
|
||||||
return pathValue.(string), nil
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("environment path attribute %s is missing", p)
|
|
||||||
}
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
51
vendor/github.com/compose-spec/compose-go/v2/paths/context.go
generated
vendored
51
vendor/github.com/compose-spec/compose-go/v2/paths/context.go
generated
vendored
@ -1,51 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package paths
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (r *relativePathsResolver) absContextPath(value any) (any, error) {
|
|
||||||
v := value.(string)
|
|
||||||
if strings.Contains(v, "://") { // `docker-image://` or any builder specific context type
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(v, types.ServicePrefix) { // `docker-image://` or any builder specific context type
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
if isRemoteContext(v) {
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
return r.absPath(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// isRemoteContext returns true if the value is a Git reference or HTTP(S) URL.
|
|
||||||
//
|
|
||||||
// Any other value is assumed to be a local filesystem path and returns false.
|
|
||||||
//
|
|
||||||
// See: https://github.com/moby/buildkit/blob/18fc875d9bfd6e065cd8211abc639434ba65aa56/frontend/dockerui/context.go#L76-L79
|
|
||||||
func isRemoteContext(maybeURL string) bool {
|
|
||||||
for _, prefix := range []string{"https://", "http://", "git://", "ssh://", "github.com/", "git@"} {
|
|
||||||
if strings.HasPrefix(maybeURL, prefix) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
25
vendor/github.com/compose-spec/compose-go/v2/paths/extends.go
generated
vendored
25
vendor/github.com/compose-spec/compose-go/v2/paths/extends.go
generated
vendored
@ -1,25 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package paths
|
|
||||||
|
|
||||||
func (r *relativePathsResolver) absExtendsPath(value any) (any, error) {
|
|
||||||
v := value.(string)
|
|
||||||
if r.isRemoteResource(v) {
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
return r.absPath(v)
|
|
||||||
}
|
|
||||||
37
vendor/github.com/compose-spec/compose-go/v2/paths/home.go
generated
vendored
37
vendor/github.com/compose-spec/compose-go/v2/paths/home.go
generated
vendored
@ -1,37 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package paths
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ExpandUser(p string) string {
|
|
||||||
if strings.HasPrefix(p, "~") {
|
|
||||||
home, err := os.UserHomeDir()
|
|
||||||
if err != nil {
|
|
||||||
logrus.Warn("cannot expand '~', because the environment lacks HOME")
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
return filepath.Join(home, p[1:])
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
169
vendor/github.com/compose-spec/compose-go/v2/paths/resolve.go
generated
vendored
169
vendor/github.com/compose-spec/compose-go/v2/paths/resolve.go
generated
vendored
@ -1,169 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package paths
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
type resolver func(any) (any, error)
|
|
||||||
|
|
||||||
// ResolveRelativePaths make relative paths absolute
|
|
||||||
func ResolveRelativePaths(project map[string]any, base string, remotes []RemoteResource) error {
|
|
||||||
r := relativePathsResolver{
|
|
||||||
workingDir: base,
|
|
||||||
remotes: remotes,
|
|
||||||
}
|
|
||||||
r.resolvers = map[tree.Path]resolver{
|
|
||||||
"services.*.build.context": r.absContextPath,
|
|
||||||
"services.*.build.additional_contexts.*": r.absContextPath,
|
|
||||||
"services.*.build.ssh.*": r.maybeUnixPath,
|
|
||||||
"services.*.env_file.*.path": r.absPath,
|
|
||||||
"services.*.label_file.*": r.absPath,
|
|
||||||
"services.*.extends.file": r.absExtendsPath,
|
|
||||||
"services.*.develop.watch.*.path": r.absSymbolicLink,
|
|
||||||
"services.*.volumes.*": r.absVolumeMount,
|
|
||||||
"configs.*.file": r.maybeUnixPath,
|
|
||||||
"secrets.*.file": r.maybeUnixPath,
|
|
||||||
"include.path": r.absPath,
|
|
||||||
"include.project_directory": r.absPath,
|
|
||||||
"include.env_file": r.absPath,
|
|
||||||
"volumes.*": r.volumeDriverOpts,
|
|
||||||
}
|
|
||||||
_, err := r.resolveRelativePaths(project, tree.NewPath())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
type RemoteResource func(path string) bool
|
|
||||||
|
|
||||||
type relativePathsResolver struct {
|
|
||||||
workingDir string
|
|
||||||
remotes []RemoteResource
|
|
||||||
resolvers map[tree.Path]resolver
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *relativePathsResolver) isRemoteResource(path string) bool {
|
|
||||||
for _, remote := range r.remotes {
|
|
||||||
if remote(path) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *relativePathsResolver) resolveRelativePaths(value any, p tree.Path) (any, error) {
|
|
||||||
for pattern, resolver := range r.resolvers {
|
|
||||||
if p.Matches(pattern) {
|
|
||||||
return resolver(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch v := value.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
for k, e := range v {
|
|
||||||
resolved, err := r.resolveRelativePaths(e, p.Next(k))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
v[k] = resolved
|
|
||||||
}
|
|
||||||
case []any:
|
|
||||||
for i, e := range v {
|
|
||||||
resolved, err := r.resolveRelativePaths(e, p.Next("[]"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
v[i] = resolved
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *relativePathsResolver) absPath(value any) (any, error) {
|
|
||||||
switch v := value.(type) {
|
|
||||||
case []any:
|
|
||||||
for i, s := range v {
|
|
||||||
abs, err := r.absPath(s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
v[i] = abs
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
case string:
|
|
||||||
v = ExpandUser(v)
|
|
||||||
if filepath.IsAbs(v) {
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
if v != "" {
|
|
||||||
return filepath.Join(r.workingDir, v), nil
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("unexpected type %T", value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *relativePathsResolver) absVolumeMount(a any) (any, error) {
|
|
||||||
switch vol := a.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
if vol["type"] != types.VolumeTypeBind {
|
|
||||||
return vol, nil
|
|
||||||
}
|
|
||||||
src, ok := vol["source"]
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New(`invalid mount config for type "bind": field Source must not be empty`)
|
|
||||||
}
|
|
||||||
abs, err := r.maybeUnixPath(src.(string))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
vol["source"] = abs
|
|
||||||
return vol, nil
|
|
||||||
default:
|
|
||||||
// not using canonical format, skip
|
|
||||||
return a, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *relativePathsResolver) volumeDriverOpts(a any) (any, error) {
|
|
||||||
if a == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
vol := a.(map[string]any)
|
|
||||||
if vol["driver"] != "local" {
|
|
||||||
return vol, nil
|
|
||||||
}
|
|
||||||
do, ok := vol["driver_opts"]
|
|
||||||
if !ok {
|
|
||||||
return vol, nil
|
|
||||||
}
|
|
||||||
opts := do.(map[string]any)
|
|
||||||
if dev, ok := opts["device"]; opts["o"] == "bind" && ok {
|
|
||||||
// This is actually a bind mount
|
|
||||||
path, err := r.maybeUnixPath(dev)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
opts["device"] = path
|
|
||||||
}
|
|
||||||
return vol, nil
|
|
||||||
}
|
|
||||||
57
vendor/github.com/compose-spec/compose-go/v2/paths/unix.go
generated
vendored
57
vendor/github.com/compose-spec/compose-go/v2/paths/unix.go
generated
vendored
@ -1,57 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package paths
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (r *relativePathsResolver) maybeUnixPath(a any) (any, error) {
|
|
||||||
p, ok := a.(string)
|
|
||||||
if !ok {
|
|
||||||
return a, nil
|
|
||||||
}
|
|
||||||
p = ExpandUser(p)
|
|
||||||
// Check if source is an absolute path (either Unix or Windows), to
|
|
||||||
// handle a Windows client with a Unix daemon or vice-versa.
|
|
||||||
//
|
|
||||||
// Note that this is not required for Docker for Windows when specifying
|
|
||||||
// a local Windows path, because Docker for Windows translates the Windows
|
|
||||||
// path into a valid path within the VM.
|
|
||||||
if !path.IsAbs(p) && !IsWindowsAbs(p) {
|
|
||||||
if filepath.IsAbs(p) {
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
return filepath.Join(r.workingDir, p), nil
|
|
||||||
}
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *relativePathsResolver) absSymbolicLink(value any) (any, error) {
|
|
||||||
abs, err := r.absPath(value)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
str, ok := abs.(string)
|
|
||||||
if !ok {
|
|
||||||
return abs, nil
|
|
||||||
}
|
|
||||||
return utils.ResolveSymbolicLink(str)
|
|
||||||
}
|
|
||||||
233
vendor/github.com/compose-spec/compose-go/v2/paths/windows_path.go
generated
vendored
233
vendor/github.com/compose-spec/compose-go/v2/paths/windows_path.go
generated
vendored
@ -1,233 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package paths
|
|
||||||
|
|
||||||
// This file contains utilities to check for Windows absolute paths on Linux.
|
|
||||||
// The code in this file was largely copied from the Golang filepath package
|
|
||||||
// https://github.com/golang/go/blob/master/src/internal/filepathlite/path_windows.go
|
|
||||||
|
|
||||||
import "slices"
|
|
||||||
|
|
||||||
// Copyright 2010 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
// https://github.com/golang/go/blob/master/LICENSE
|
|
||||||
|
|
||||||
func IsPathSeparator(c uint8) bool {
|
|
||||||
return c == '\\' || c == '/'
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsWindowsAbs reports whether the path is absolute.
|
|
||||||
// copied from IsAbs(path string) (b bool) from internal.filetpathlite
|
|
||||||
func IsWindowsAbs(path string) (b bool) {
|
|
||||||
l := volumeNameLen(path)
|
|
||||||
if l == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// If the volume name starts with a double slash, this is an absolute path.
|
|
||||||
if IsPathSeparator(path[0]) && IsPathSeparator(path[1]) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
path = path[l:]
|
|
||||||
if path == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return IsPathSeparator(path[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
// volumeNameLen returns length of the leading volume name on Windows.
|
|
||||||
// It returns 0 elsewhere.
|
|
||||||
//
|
|
||||||
// See:
|
|
||||||
// https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
|
|
||||||
// https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
|
|
||||||
func volumeNameLen(path string) int {
|
|
||||||
switch {
|
|
||||||
case len(path) >= 2 && path[1] == ':':
|
|
||||||
// Path starts with a drive letter.
|
|
||||||
//
|
|
||||||
// Not all Windows functions necessarily enforce the requirement that
|
|
||||||
// drive letters be in the set A-Z, and we don't try to here.
|
|
||||||
//
|
|
||||||
// We don't handle the case of a path starting with a non-ASCII character,
|
|
||||||
// in which case the "drive letter" might be multiple bytes long.
|
|
||||||
return 2
|
|
||||||
|
|
||||||
case len(path) == 0 || !IsPathSeparator(path[0]):
|
|
||||||
// Path does not have a volume component.
|
|
||||||
return 0
|
|
||||||
|
|
||||||
case pathHasPrefixFold(path, `\\.\UNC`):
|
|
||||||
// We're going to treat the UNC host and share as part of the volume
|
|
||||||
// prefix for historical reasons, but this isn't really principled;
|
|
||||||
// Windows's own GetFullPathName will happily remove the first
|
|
||||||
// component of the path in this space, converting
|
|
||||||
// \\.\unc\a\b\..\c into \\.\unc\a\c.
|
|
||||||
return uncLen(path, len(`\\.\UNC\`))
|
|
||||||
|
|
||||||
case pathHasPrefixFold(path, `\\.`) ||
|
|
||||||
pathHasPrefixFold(path, `\\?`) || pathHasPrefixFold(path, `\??`):
|
|
||||||
// Path starts with \\.\, and is a Local Device path; or
|
|
||||||
// path starts with \\?\ or \??\ and is a Root Local Device path.
|
|
||||||
//
|
|
||||||
// We treat the next component after the \\.\ prefix as
|
|
||||||
// part of the volume name, which means Clean(`\\?\c:\`)
|
|
||||||
// won't remove the trailing \. (See #64028.)
|
|
||||||
if len(path) == 3 {
|
|
||||||
return 3 // exactly \\.
|
|
||||||
}
|
|
||||||
_, rest, ok := cutPath(path[4:])
|
|
||||||
if !ok {
|
|
||||||
return len(path)
|
|
||||||
}
|
|
||||||
return len(path) - len(rest) - 1
|
|
||||||
|
|
||||||
case len(path) >= 2 && IsPathSeparator(path[1]):
|
|
||||||
// Path starts with \\, and is a UNC path.
|
|
||||||
return uncLen(path, 2)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// pathHasPrefixFold tests whether the path s begins with prefix,
|
|
||||||
// ignoring case and treating all path separators as equivalent.
|
|
||||||
// If s is longer than prefix, then s[len(prefix)] must be a path separator.
|
|
||||||
func pathHasPrefixFold(s, prefix string) bool {
|
|
||||||
if len(s) < len(prefix) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for i := 0; i < len(prefix); i++ {
|
|
||||||
if IsPathSeparator(prefix[i]) {
|
|
||||||
if !IsPathSeparator(s[i]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else if toUpper(prefix[i]) != toUpper(s[i]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(s) > len(prefix) && !IsPathSeparator(s[len(prefix)]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// uncLen returns the length of the volume prefix of a UNC path.
|
|
||||||
// prefixLen is the prefix prior to the start of the UNC host;
|
|
||||||
// for example, for "//host/share", the prefixLen is len("//")==2.
|
|
||||||
func uncLen(path string, prefixLen int) int {
|
|
||||||
count := 0
|
|
||||||
for i := prefixLen; i < len(path); i++ {
|
|
||||||
if IsPathSeparator(path[i]) {
|
|
||||||
count++
|
|
||||||
if count == 2 {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return len(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// cutPath slices path around the first path separator.
|
|
||||||
func cutPath(path string) (before, after string, found bool) {
|
|
||||||
for i := range path {
|
|
||||||
if IsPathSeparator(path[i]) {
|
|
||||||
return path[:i], path[i+1:], true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return path, "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
// postClean adjusts the results of Clean to avoid turning a relative path
|
|
||||||
// into an absolute or rooted one.
|
|
||||||
func postClean(out *lazybuf) {
|
|
||||||
if out.volLen != 0 || out.buf == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// If a ':' appears in the path element at the start of a path,
|
|
||||||
// insert a .\ at the beginning to avoid converting relative paths
|
|
||||||
// like a/../c: into c:.
|
|
||||||
for _, c := range out.buf {
|
|
||||||
if IsPathSeparator(c) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if c == ':' {
|
|
||||||
out.prepend('.', Separator)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If a path begins with \??\, insert a \. at the beginning
|
|
||||||
// to avoid converting paths like \a\..\??\c:\x into \??\c:\x
|
|
||||||
// (equivalent to c:\x).
|
|
||||||
if len(out.buf) >= 3 && IsPathSeparator(out.buf[0]) && out.buf[1] == '?' && out.buf[2] == '?' {
|
|
||||||
out.prepend(Separator, '.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func toUpper(c byte) byte {
|
|
||||||
if 'a' <= c && c <= 'z' {
|
|
||||||
return c - ('a' - 'A')
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
Separator = '\\' // OS-specific path separator
|
|
||||||
)
|
|
||||||
|
|
||||||
// A lazybuf is a lazily constructed path buffer.
|
|
||||||
// It supports append, reading previously appended bytes,
|
|
||||||
// and retrieving the final string. It does not allocate a buffer
|
|
||||||
// to hold the output until that output diverges from s.
|
|
||||||
type lazybuf struct {
|
|
||||||
path string
|
|
||||||
buf []byte
|
|
||||||
w int
|
|
||||||
volAndPath string
|
|
||||||
volLen int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *lazybuf) index(i int) byte {
|
|
||||||
if b.buf != nil {
|
|
||||||
return b.buf[i]
|
|
||||||
}
|
|
||||||
return b.path[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *lazybuf) append(c byte) {
|
|
||||||
if b.buf == nil {
|
|
||||||
if b.w < len(b.path) && b.path[b.w] == c {
|
|
||||||
b.w++
|
|
||||||
return
|
|
||||||
}
|
|
||||||
b.buf = make([]byte, len(b.path))
|
|
||||||
copy(b.buf, b.path[:b.w])
|
|
||||||
}
|
|
||||||
b.buf[b.w] = c
|
|
||||||
b.w++
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *lazybuf) prepend(prefix ...byte) {
|
|
||||||
b.buf = slices.Insert(b.buf, 0, prefix...)
|
|
||||||
b.w += len(prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *lazybuf) string() string {
|
|
||||||
if b.buf == nil {
|
|
||||||
return b.volAndPath[:b.volLen+b.w]
|
|
||||||
}
|
|
||||||
return b.volAndPath[:b.volLen] + string(b.buf[:b.w])
|
|
||||||
}
|
|
||||||
1912
vendor/github.com/compose-spec/compose-go/v2/schema/compose-spec.json
generated
vendored
1912
vendor/github.com/compose-spec/compose-go/v2/schema/compose-spec.json
generated
vendored
File diff suppressed because it is too large
Load Diff
149
vendor/github.com/compose-spec/compose-go/v2/schema/schema.go
generated
vendored
149
vendor/github.com/compose-spec/compose-go/v2/schema/schema.go
generated
vendored
@ -1,149 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package schema
|
|
||||||
|
|
||||||
import (
|
|
||||||
// Enable support for embedded static resources
|
|
||||||
_ "embed"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/santhosh-tekuri/jsonschema/v6"
|
|
||||||
"github.com/santhosh-tekuri/jsonschema/v6/kind"
|
|
||||||
"golang.org/x/text/language"
|
|
||||||
"golang.org/x/text/message"
|
|
||||||
)
|
|
||||||
|
|
||||||
func durationFormatChecker(input any) error {
|
|
||||||
value, ok := input.(string)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("expected string")
|
|
||||||
}
|
|
||||||
_, err := time.ParseDuration(value)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schema is the compose-spec JSON schema
|
|
||||||
//
|
|
||||||
//go:embed compose-spec.json
|
|
||||||
var Schema string
|
|
||||||
|
|
||||||
// Validate uses the jsonschema to validate the configuration
|
|
||||||
func Validate(config map[string]interface{}) error {
|
|
||||||
compiler := jsonschema.NewCompiler()
|
|
||||||
shema, err := jsonschema.UnmarshalJSON(strings.NewReader(Schema))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = compiler.AddResource("compose-spec.json", shema)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
compiler.RegisterFormat(&jsonschema.Format{
|
|
||||||
Name: "duration",
|
|
||||||
Validate: durationFormatChecker,
|
|
||||||
})
|
|
||||||
schema := compiler.MustCompile("compose-spec.json")
|
|
||||||
|
|
||||||
// santhosh-tekuri doesn't allow derived types
|
|
||||||
// see https://github.com/santhosh-tekuri/jsonschema/pull/240
|
|
||||||
marshaled, err := json.Marshal(config)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var raw map[string]interface{}
|
|
||||||
err = json.Unmarshal(marshaled, &raw)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = schema.Validate(raw)
|
|
||||||
var verr *jsonschema.ValidationError
|
|
||||||
if ok := errors.As(err, &verr); ok {
|
|
||||||
return validationError{getMostSpecificError(verr)}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
type validationError struct {
|
|
||||||
err *jsonschema.ValidationError
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e validationError) Error() string {
|
|
||||||
path := strings.Join(e.err.InstanceLocation, ".")
|
|
||||||
p := message.NewPrinter(language.English)
|
|
||||||
switch k := e.err.ErrorKind.(type) {
|
|
||||||
case *kind.Type:
|
|
||||||
return fmt.Sprintf("%s must be a %s", path, humanReadableType(k.Want...))
|
|
||||||
case *kind.Minimum:
|
|
||||||
return fmt.Sprintf("%s must be greater than or equal to %s", path, k.Want.Num())
|
|
||||||
case *kind.Maximum:
|
|
||||||
return fmt.Sprintf("%s must be less than or equal to %s", path, k.Want.Num())
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s %s", path, e.err.ErrorKind.LocalizedString(p))
|
|
||||||
}
|
|
||||||
|
|
||||||
func humanReadableType(want ...string) string {
|
|
||||||
if len(want) == 1 {
|
|
||||||
switch want[0] {
|
|
||||||
case "object":
|
|
||||||
return "mapping"
|
|
||||||
default:
|
|
||||||
return want[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, s := range want {
|
|
||||||
want[i] = humanReadableType(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
slices.Sort(want)
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"%s or %s",
|
|
||||||
strings.Join(want[0:len(want)-1], ", "),
|
|
||||||
want[len(want)-1],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMostSpecificError(err *jsonschema.ValidationError) *jsonschema.ValidationError {
|
|
||||||
var mostSpecificError *jsonschema.ValidationError
|
|
||||||
if len(err.Causes) == 0 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, cause := range err.Causes {
|
|
||||||
cause = getMostSpecificError(cause)
|
|
||||||
if specificity(cause) > specificity(mostSpecificError) {
|
|
||||||
mostSpecificError = cause
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mostSpecificError
|
|
||||||
}
|
|
||||||
|
|
||||||
func specificity(err *jsonschema.ValidationError) int {
|
|
||||||
if err == nil {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if _, ok := err.ErrorKind.(*kind.AdditionalProperties); ok {
|
|
||||||
return len(err.InstanceLocation) + 1
|
|
||||||
}
|
|
||||||
return len(err.InstanceLocation)
|
|
||||||
}
|
|
||||||
123
vendor/github.com/compose-spec/compose-go/v2/schema/using-variables.yaml
generated
vendored
123
vendor/github.com/compose-spec/compose-go/v2/schema/using-variables.yaml
generated
vendored
@ -1,123 +0,0 @@
|
|||||||
name: ${VARIABLE}
|
|
||||||
services:
|
|
||||||
foo:
|
|
||||||
deploy:
|
|
||||||
mode: ${VARIABLE}
|
|
||||||
replicas: ${VARIABLE}
|
|
||||||
rollback_config:
|
|
||||||
parallelism: ${VARIABLE}
|
|
||||||
delay: ${VARIABLE}
|
|
||||||
failure_action: ${VARIABLE}
|
|
||||||
monitor: ${VARIABLE}
|
|
||||||
max_failure_ratio: ${VARIABLE}
|
|
||||||
update_config:
|
|
||||||
parallelism: ${VARIABLE}
|
|
||||||
delay: ${VARIABLE}
|
|
||||||
failure_action: ${VARIABLE}
|
|
||||||
monitor: ${VARIABLE}
|
|
||||||
max_failure_ratio: ${VARIABLE}
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: ${VARIABLE}
|
|
||||||
reservations:
|
|
||||||
memory: ${VARIABLE}
|
|
||||||
generic_resources:
|
|
||||||
- discrete_resource_spec:
|
|
||||||
kind: ${VARIABLE}
|
|
||||||
value: ${VARIABLE}
|
|
||||||
- discrete_resource_spec:
|
|
||||||
kind: ${VARIABLE}
|
|
||||||
value: ${VARIABLE}
|
|
||||||
restart_policy:
|
|
||||||
condition: ${VARIABLE}
|
|
||||||
delay: ${VARIABLE}
|
|
||||||
max_attempts: ${VARIABLE}
|
|
||||||
window: ${VARIABLE}
|
|
||||||
placement:
|
|
||||||
max_replicas_per_node: ${VARIABLE}
|
|
||||||
preferences:
|
|
||||||
- spread: ${VARIABLE}
|
|
||||||
endpoint_mode: ${VARIABLE}
|
|
||||||
expose:
|
|
||||||
- ${VARIABLE}
|
|
||||||
external_links:
|
|
||||||
- ${VARIABLE}
|
|
||||||
extra_hosts:
|
|
||||||
- ${VARIABLE}
|
|
||||||
hostname: ${VARIABLE}
|
|
||||||
|
|
||||||
healthcheck:
|
|
||||||
test: ${VARIABLE}
|
|
||||||
interval: ${VARIABLE}
|
|
||||||
timeout: ${VARIABLE}
|
|
||||||
retries: ${VARIABLE}
|
|
||||||
start_period: ${VARIABLE}
|
|
||||||
start_interval: ${VARIABLE}
|
|
||||||
image: ${VARIABLE}
|
|
||||||
mac_address: ${VARIABLE}
|
|
||||||
networks:
|
|
||||||
some-network:
|
|
||||||
aliases:
|
|
||||||
- ${VARIABLE}
|
|
||||||
other-network:
|
|
||||||
ipv4_address: ${VARIABLE}
|
|
||||||
ipv6_address: ${VARIABLE}
|
|
||||||
mac_address: ${VARIABLE}
|
|
||||||
ports:
|
|
||||||
- ${VARIABLE}
|
|
||||||
privileged: ${VARIABLE}
|
|
||||||
read_only: ${VARIABLE}
|
|
||||||
restart: ${VARIABLE}
|
|
||||||
secrets:
|
|
||||||
- source: ${VARIABLE}
|
|
||||||
target: ${VARIABLE}
|
|
||||||
uid: ${VARIABLE}
|
|
||||||
gid: ${VARIABLE}
|
|
||||||
mode: ${VARIABLE}
|
|
||||||
stdin_open: ${VARIABLE}
|
|
||||||
stop_grace_period: ${VARIABLE}
|
|
||||||
stop_signal: ${VARIABLE}
|
|
||||||
storage_opt:
|
|
||||||
size: ${VARIABLE}
|
|
||||||
sysctls:
|
|
||||||
net.core.somaxconn: ${VARIABLE}
|
|
||||||
tmpfs:
|
|
||||||
- ${VARIABLE}
|
|
||||||
tty: ${VARIABLE}
|
|
||||||
ulimits:
|
|
||||||
nproc: ${VARIABLE}
|
|
||||||
nofile:
|
|
||||||
soft: ${VARIABLE}
|
|
||||||
hard: ${VARIABLE}
|
|
||||||
user: ${VARIABLE}
|
|
||||||
volumes:
|
|
||||||
- ${VARIABLE}:${VARIABLE}
|
|
||||||
- type: tmpfs
|
|
||||||
target: ${VARIABLE}
|
|
||||||
tmpfs:
|
|
||||||
size: ${VARIABLE}
|
|
||||||
|
|
||||||
networks:
|
|
||||||
network:
|
|
||||||
ipam:
|
|
||||||
driver: ${VARIABLE}
|
|
||||||
config:
|
|
||||||
- subnet: ${VARIABLE}
|
|
||||||
ip_range: ${VARIABLE}
|
|
||||||
gateway: ${VARIABLE}
|
|
||||||
aux_addresses:
|
|
||||||
host1: ${VARIABLE}
|
|
||||||
external-network:
|
|
||||||
external: ${VARIABLE}
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
external-volume:
|
|
||||||
external: ${VARIABLE}
|
|
||||||
|
|
||||||
configs:
|
|
||||||
config1:
|
|
||||||
external: ${VARIABLE}
|
|
||||||
|
|
||||||
secrets:
|
|
||||||
secret1:
|
|
||||||
external: ${VARIABLE}
|
|
||||||
380
vendor/github.com/compose-spec/compose-go/v2/template/template.go
generated
vendored
380
vendor/github.com/compose-spec/compose-go/v2/template/template.go
generated
vendored
@ -1,380 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package template
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
delimiter = "\\$"
|
|
||||||
substitutionNamed = "[_a-z][_a-z0-9]*"
|
|
||||||
substitutionBraced = "[_a-z][_a-z0-9]*(?::?[-+?](.*))?"
|
|
||||||
groupEscaped = "escaped"
|
|
||||||
groupNamed = "named"
|
|
||||||
groupBraced = "braced"
|
|
||||||
groupInvalid = "invalid"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
patternString = fmt.Sprintf(
|
|
||||||
"%s(?i:(?P<%s>%s)|(?P<%s>%s)|{(?:(?P<%s>%s)}|(?P<%s>)))",
|
|
||||||
delimiter,
|
|
||||||
groupEscaped, delimiter,
|
|
||||||
groupNamed, substitutionNamed,
|
|
||||||
groupBraced, substitutionBraced,
|
|
||||||
groupInvalid,
|
|
||||||
)
|
|
||||||
|
|
||||||
DefaultPattern = regexp.MustCompile(patternString)
|
|
||||||
)
|
|
||||||
|
|
||||||
// InvalidTemplateError is returned when a variable template is not in a valid
|
|
||||||
// format
|
|
||||||
type InvalidTemplateError struct {
|
|
||||||
Template string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e InvalidTemplateError) Error() string {
|
|
||||||
return fmt.Sprintf("Invalid template: %#v", e.Template)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MissingRequiredError is returned when a variable template is missing
|
|
||||||
type MissingRequiredError struct {
|
|
||||||
Variable string
|
|
||||||
Reason string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e MissingRequiredError) Error() string {
|
|
||||||
if e.Reason != "" {
|
|
||||||
return fmt.Sprintf("required variable %s is missing a value: %s", e.Variable, e.Reason)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("required variable %s is missing a value", e.Variable)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mapping is a user-supplied function which maps from variable names to values.
|
|
||||||
// Returns the value as a string and a bool indicating whether
|
|
||||||
// the value is present, to distinguish between an empty string
|
|
||||||
// and the absence of a value.
|
|
||||||
type Mapping func(string) (string, bool)
|
|
||||||
|
|
||||||
// SubstituteFunc is a user-supplied function that apply substitution.
|
|
||||||
// Returns the value as a string, a bool indicating if the function could apply
|
|
||||||
// the substitution and an error.
|
|
||||||
type SubstituteFunc func(string, Mapping) (string, bool, error)
|
|
||||||
|
|
||||||
// ReplacementFunc is a user-supplied function that is apply to the matching
|
|
||||||
// substring. Returns the value as a string and an error.
|
|
||||||
type ReplacementFunc func(string, Mapping, *Config) (string, error)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
pattern *regexp.Regexp
|
|
||||||
substituteFunc SubstituteFunc
|
|
||||||
replacementFunc ReplacementFunc
|
|
||||||
logging bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type Option func(*Config)
|
|
||||||
|
|
||||||
func WithPattern(pattern *regexp.Regexp) Option {
|
|
||||||
return func(cfg *Config) {
|
|
||||||
cfg.pattern = pattern
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithSubstitutionFunction(subsFunc SubstituteFunc) Option {
|
|
||||||
return func(cfg *Config) {
|
|
||||||
cfg.substituteFunc = subsFunc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithReplacementFunction(replacementFunc ReplacementFunc) Option {
|
|
||||||
return func(cfg *Config) {
|
|
||||||
cfg.replacementFunc = replacementFunc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithoutLogging(cfg *Config) {
|
|
||||||
cfg.logging = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubstituteWithOptions substitute variables in the string with their values.
|
|
||||||
// It accepts additional options such as a custom function or pattern.
|
|
||||||
func SubstituteWithOptions(template string, mapping Mapping, options ...Option) (string, error) {
|
|
||||||
var returnErr error
|
|
||||||
|
|
||||||
cfg := &Config{
|
|
||||||
pattern: DefaultPattern,
|
|
||||||
replacementFunc: DefaultReplacementFunc,
|
|
||||||
logging: true,
|
|
||||||
}
|
|
||||||
for _, o := range options {
|
|
||||||
o(cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := cfg.pattern.ReplaceAllStringFunc(template, func(substring string) string {
|
|
||||||
replacement, err := cfg.replacementFunc(substring, mapping, cfg)
|
|
||||||
if err != nil {
|
|
||||||
// Add the template for template errors
|
|
||||||
var tmplErr *InvalidTemplateError
|
|
||||||
if errors.As(err, &tmplErr) {
|
|
||||||
if tmplErr.Template == "" {
|
|
||||||
tmplErr.Template = template
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Save the first error to be returned
|
|
||||||
if returnErr == nil {
|
|
||||||
returnErr = err
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return replacement
|
|
||||||
})
|
|
||||||
|
|
||||||
return result, returnErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultReplacementFunc(substring string, mapping Mapping, cfg *Config) (string, error) {
|
|
||||||
value, _, err := DefaultReplacementAppliedFunc(substring, mapping, cfg)
|
|
||||||
return value, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultReplacementAppliedFunc(substring string, mapping Mapping, cfg *Config) (string, bool, error) {
|
|
||||||
pattern := cfg.pattern
|
|
||||||
subsFunc := cfg.substituteFunc
|
|
||||||
if subsFunc == nil {
|
|
||||||
_, subsFunc = getSubstitutionFunctionForTemplate(substring)
|
|
||||||
}
|
|
||||||
|
|
||||||
closingBraceIndex := getFirstBraceClosingIndex(substring)
|
|
||||||
rest := ""
|
|
||||||
if closingBraceIndex > -1 {
|
|
||||||
rest = substring[closingBraceIndex+1:]
|
|
||||||
substring = substring[0 : closingBraceIndex+1]
|
|
||||||
}
|
|
||||||
|
|
||||||
matches := pattern.FindStringSubmatch(substring)
|
|
||||||
groups := matchGroups(matches, pattern)
|
|
||||||
if escaped := groups[groupEscaped]; escaped != "" {
|
|
||||||
return escaped, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
braced := false
|
|
||||||
substitution := groups[groupNamed]
|
|
||||||
if substitution == "" {
|
|
||||||
substitution = groups[groupBraced]
|
|
||||||
braced = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if substitution == "" {
|
|
||||||
return "", false, &InvalidTemplateError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if braced {
|
|
||||||
value, applied, err := subsFunc(substitution, mapping)
|
|
||||||
if err != nil {
|
|
||||||
return "", false, err
|
|
||||||
}
|
|
||||||
if applied {
|
|
||||||
interpolatedNested, err := SubstituteWith(rest, mapping, pattern)
|
|
||||||
if err != nil {
|
|
||||||
return "", false, err
|
|
||||||
}
|
|
||||||
return value + interpolatedNested, true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
value, ok := mapping(substitution)
|
|
||||||
if !ok && cfg.logging {
|
|
||||||
logrus.Warnf("The %q variable is not set. Defaulting to a blank string.", substitution)
|
|
||||||
}
|
|
||||||
|
|
||||||
return value, ok, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubstituteWith substitute variables in the string with their values.
|
|
||||||
// It accepts additional substitute function.
|
|
||||||
func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, subsFuncs ...SubstituteFunc) (string, error) {
|
|
||||||
options := []Option{
|
|
||||||
WithPattern(pattern),
|
|
||||||
}
|
|
||||||
if len(subsFuncs) > 0 {
|
|
||||||
options = append(options, WithSubstitutionFunction(subsFuncs[0]))
|
|
||||||
}
|
|
||||||
|
|
||||||
return SubstituteWithOptions(template, mapping, options...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSubstitutionFunctionForTemplate(template string) (string, SubstituteFunc) {
|
|
||||||
interpolationMapping := []struct {
|
|
||||||
string
|
|
||||||
SubstituteFunc
|
|
||||||
}{
|
|
||||||
{":?", requiredErrorWhenEmptyOrUnset},
|
|
||||||
{"?", requiredErrorWhenUnset},
|
|
||||||
{":-", defaultWhenEmptyOrUnset},
|
|
||||||
{"-", defaultWhenUnset},
|
|
||||||
{":+", defaultWhenNotEmpty},
|
|
||||||
{"+", defaultWhenSet},
|
|
||||||
}
|
|
||||||
sort.Slice(interpolationMapping, func(i, j int) bool {
|
|
||||||
idxI := strings.Index(template, interpolationMapping[i].string)
|
|
||||||
idxJ := strings.Index(template, interpolationMapping[j].string)
|
|
||||||
if idxI < 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if idxJ < 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return idxI < idxJ
|
|
||||||
})
|
|
||||||
|
|
||||||
return interpolationMapping[0].string, interpolationMapping[0].SubstituteFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFirstBraceClosingIndex(s string) int {
|
|
||||||
openVariableBraces := 0
|
|
||||||
for i := 0; i < len(s); i++ {
|
|
||||||
if s[i] == '}' {
|
|
||||||
openVariableBraces--
|
|
||||||
if openVariableBraces == 0 {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if s[i] == '{' {
|
|
||||||
openVariableBraces++
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Substitute variables in the string with their values
|
|
||||||
func Substitute(template string, mapping Mapping) (string, error) {
|
|
||||||
return SubstituteWith(template, mapping, DefaultPattern)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Soft default (fall back if unset or empty)
|
|
||||||
func defaultWhenEmptyOrUnset(substitution string, mapping Mapping) (string, bool, error) {
|
|
||||||
return withDefaultWhenAbsence(substitution, mapping, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hard default (fall back if-and-only-if empty)
|
|
||||||
func defaultWhenUnset(substitution string, mapping Mapping) (string, bool, error) {
|
|
||||||
return withDefaultWhenAbsence(substitution, mapping, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultWhenNotEmpty(substitution string, mapping Mapping) (string, bool, error) {
|
|
||||||
return withDefaultWhenPresence(substitution, mapping, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultWhenSet(substitution string, mapping Mapping) (string, bool, error) {
|
|
||||||
return withDefaultWhenPresence(substitution, mapping, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func requiredErrorWhenEmptyOrUnset(substitution string, mapping Mapping) (string, bool, error) {
|
|
||||||
return withRequired(substitution, mapping, ":?", func(v string) bool { return v != "" })
|
|
||||||
}
|
|
||||||
|
|
||||||
func requiredErrorWhenUnset(substitution string, mapping Mapping) (string, bool, error) {
|
|
||||||
return withRequired(substitution, mapping, "?", func(_ string) bool { return true })
|
|
||||||
}
|
|
||||||
|
|
||||||
func withDefaultWhenPresence(substitution string, mapping Mapping, notEmpty bool) (string, bool, error) {
|
|
||||||
sep := "+"
|
|
||||||
if notEmpty {
|
|
||||||
sep = ":+"
|
|
||||||
}
|
|
||||||
if !strings.Contains(substitution, sep) {
|
|
||||||
return "", false, nil
|
|
||||||
}
|
|
||||||
name, defaultValue := partition(substitution, sep)
|
|
||||||
value, ok := mapping(name)
|
|
||||||
if ok && (!notEmpty || (notEmpty && value != "")) {
|
|
||||||
defaultValue, err := Substitute(defaultValue, mapping)
|
|
||||||
if err != nil {
|
|
||||||
return "", false, err
|
|
||||||
}
|
|
||||||
return defaultValue, true, nil
|
|
||||||
}
|
|
||||||
return value, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func withDefaultWhenAbsence(substitution string, mapping Mapping, emptyOrUnset bool) (string, bool, error) {
|
|
||||||
sep := "-"
|
|
||||||
if emptyOrUnset {
|
|
||||||
sep = ":-"
|
|
||||||
}
|
|
||||||
if !strings.Contains(substitution, sep) {
|
|
||||||
return "", false, nil
|
|
||||||
}
|
|
||||||
name, defaultValue := partition(substitution, sep)
|
|
||||||
value, ok := mapping(name)
|
|
||||||
if !ok || (emptyOrUnset && value == "") {
|
|
||||||
defaultValue, err := Substitute(defaultValue, mapping)
|
|
||||||
if err != nil {
|
|
||||||
return "", false, err
|
|
||||||
}
|
|
||||||
return defaultValue, true, nil
|
|
||||||
}
|
|
||||||
return value, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func withRequired(substitution string, mapping Mapping, sep string, valid func(string) bool) (string, bool, error) {
|
|
||||||
if !strings.Contains(substitution, sep) {
|
|
||||||
return "", false, nil
|
|
||||||
}
|
|
||||||
name, errorMessage := partition(substitution, sep)
|
|
||||||
value, ok := mapping(name)
|
|
||||||
if !ok || !valid(value) {
|
|
||||||
errorMessage, err := Substitute(errorMessage, mapping)
|
|
||||||
if err != nil {
|
|
||||||
return "", false, err
|
|
||||||
}
|
|
||||||
return "", true, &MissingRequiredError{
|
|
||||||
Reason: errorMessage,
|
|
||||||
Variable: name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func matchGroups(matches []string, pattern *regexp.Regexp) map[string]string {
|
|
||||||
groups := make(map[string]string)
|
|
||||||
for i, name := range pattern.SubexpNames()[1:] {
|
|
||||||
groups[name] = matches[i+1]
|
|
||||||
}
|
|
||||||
return groups
|
|
||||||
}
|
|
||||||
|
|
||||||
// Split the string at the first occurrence of sep, and return the part before the separator,
|
|
||||||
// and the part after the separator.
|
|
||||||
//
|
|
||||||
// If the separator is not found, return the string itself, followed by an empty string.
|
|
||||||
func partition(s, sep string) (string, string) {
|
|
||||||
if strings.Contains(s, sep) {
|
|
||||||
parts := strings.SplitN(s, sep, 2)
|
|
||||||
return parts[0], parts[1]
|
|
||||||
}
|
|
||||||
return s, ""
|
|
||||||
}
|
|
||||||
157
vendor/github.com/compose-spec/compose-go/v2/template/variables.go
generated
vendored
157
vendor/github.com/compose-spec/compose-go/v2/template/variables.go
generated
vendored
@ -1,157 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package template
|
|
||||||
|
|
||||||
import (
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Variable struct {
|
|
||||||
Name string
|
|
||||||
DefaultValue string
|
|
||||||
PresenceValue string
|
|
||||||
Required bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtractVariables returns a map of all the variables defined in the specified
|
|
||||||
// compose file (dict representation) and their default value if any.
|
|
||||||
func ExtractVariables(configDict map[string]interface{}, pattern *regexp.Regexp) map[string]Variable {
|
|
||||||
if pattern == nil {
|
|
||||||
pattern = DefaultPattern
|
|
||||||
}
|
|
||||||
return recurseExtract(configDict, pattern)
|
|
||||||
}
|
|
||||||
|
|
||||||
func recurseExtract(value interface{}, pattern *regexp.Regexp) map[string]Variable {
|
|
||||||
m := map[string]Variable{}
|
|
||||||
|
|
||||||
switch value := value.(type) {
|
|
||||||
case string:
|
|
||||||
if values, is := extractVariable(value, pattern); is {
|
|
||||||
for _, v := range values {
|
|
||||||
m[v.Name] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case map[string]interface{}:
|
|
||||||
for _, elem := range value {
|
|
||||||
submap := recurseExtract(elem, pattern)
|
|
||||||
for key, value := range submap {
|
|
||||||
m[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case []interface{}:
|
|
||||||
for _, elem := range value {
|
|
||||||
submap := recurseExtract(elem, pattern)
|
|
||||||
for key, value := range submap {
|
|
||||||
m[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractVariable(value interface{}, pattern *regexp.Regexp) ([]Variable, bool) {
|
|
||||||
sValue, ok := value.(string)
|
|
||||||
if !ok {
|
|
||||||
return []Variable{}, false
|
|
||||||
}
|
|
||||||
matches := pattern.FindAllStringSubmatch(sValue, -1)
|
|
||||||
if len(matches) == 0 {
|
|
||||||
return []Variable{}, false
|
|
||||||
}
|
|
||||||
values := []Variable{}
|
|
||||||
for _, match := range matches {
|
|
||||||
groups := matchGroups(match, pattern)
|
|
||||||
if escaped := groups[groupEscaped]; escaped != "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
val := groups[groupNamed]
|
|
||||||
if val == "" {
|
|
||||||
val = groups[groupBraced]
|
|
||||||
s := match[0]
|
|
||||||
i := getFirstBraceClosingIndex(s)
|
|
||||||
if i > 0 {
|
|
||||||
val = s[2:i]
|
|
||||||
if len(s) > i {
|
|
||||||
if v, b := extractVariable(s[i+1:], pattern); b {
|
|
||||||
values = append(values, v...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
name := val
|
|
||||||
var defaultValue string
|
|
||||||
var presenceValue string
|
|
||||||
var required bool
|
|
||||||
i := strings.IndexFunc(val, func(r rune) bool {
|
|
||||||
if r >= 'a' && r <= 'z' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if r >= 'A' && r <= 'Z' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if r >= '0' && r <= '9' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if r == '_' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
if i > 0 {
|
|
||||||
name = val[:i]
|
|
||||||
rest := val[i:]
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(rest, ":?"):
|
|
||||||
required = true
|
|
||||||
case strings.HasPrefix(rest, "?"):
|
|
||||||
required = true
|
|
||||||
case strings.HasPrefix(rest, ":-"):
|
|
||||||
defaultValue = rest[2:]
|
|
||||||
case strings.HasPrefix(rest, "-"):
|
|
||||||
defaultValue = rest[1:]
|
|
||||||
case strings.HasPrefix(rest, ":+"):
|
|
||||||
presenceValue = rest[2:]
|
|
||||||
case strings.HasPrefix(rest, "+"):
|
|
||||||
presenceValue = rest[1:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
values = append(values, Variable{
|
|
||||||
Name: name,
|
|
||||||
DefaultValue: defaultValue,
|
|
||||||
PresenceValue: presenceValue,
|
|
||||||
Required: required,
|
|
||||||
})
|
|
||||||
|
|
||||||
if defaultValue != "" {
|
|
||||||
if v, b := extractVariable(defaultValue, pattern); b {
|
|
||||||
values = append(values, v...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if presenceValue != "" {
|
|
||||||
if v, b := extractVariable(presenceValue, pattern); b {
|
|
||||||
values = append(values, v...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return values, len(values) > 0
|
|
||||||
}
|
|
||||||
48
vendor/github.com/compose-spec/compose-go/v2/transform/build.go
generated
vendored
48
vendor/github.com/compose-spec/compose-go/v2/transform/build.go
generated
vendored
@ -1,48 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package transform
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
)
|
|
||||||
|
|
||||||
func transformBuild(data any, p tree.Path, ignoreParseError bool) (any, error) {
|
|
||||||
switch v := data.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
return transformMapping(v, p, ignoreParseError)
|
|
||||||
case string:
|
|
||||||
return map[string]any{
|
|
||||||
"context": v,
|
|
||||||
}, nil
|
|
||||||
default:
|
|
||||||
return data, fmt.Errorf("%s: invalid type %T for build", p, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultBuildContext(data any, _ tree.Path, _ bool) (any, error) {
|
|
||||||
switch v := data.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
if _, ok := v["context"]; !ok {
|
|
||||||
v["context"] = "."
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
default:
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
137
vendor/github.com/compose-spec/compose-go/v2/transform/canonical.go
generated
vendored
137
vendor/github.com/compose-spec/compose-go/v2/transform/canonical.go
generated
vendored
@ -1,137 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package transform
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Func is a function that can transform data at a specific path
|
|
||||||
type Func func(data any, p tree.Path, ignoreParseError bool) (any, error)
|
|
||||||
|
|
||||||
var transformers = map[tree.Path]Func{}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
transformers["services.*"] = transformService
|
|
||||||
transformers["services.*.build.secrets.*"] = transformFileMount
|
|
||||||
transformers["services.*.build.provenance"] = transformStringOrX
|
|
||||||
transformers["services.*.build.sbom"] = transformStringOrX
|
|
||||||
transformers["services.*.build.additional_contexts"] = transformKeyValue
|
|
||||||
transformers["services.*.depends_on"] = transformDependsOn
|
|
||||||
transformers["services.*.env_file"] = transformEnvFile
|
|
||||||
transformers["services.*.label_file"] = transformStringOrList
|
|
||||||
transformers["services.*.extends"] = transformExtends
|
|
||||||
transformers["services.*.gpus"] = transformGpus
|
|
||||||
transformers["services.*.networks"] = transformStringSliceToMap
|
|
||||||
transformers["services.*.models"] = transformStringSliceToMap
|
|
||||||
transformers["services.*.volumes.*"] = transformVolumeMount
|
|
||||||
transformers["services.*.dns"] = transformStringOrList
|
|
||||||
transformers["services.*.devices.*"] = transformDeviceMapping
|
|
||||||
transformers["services.*.secrets.*"] = transformFileMount
|
|
||||||
transformers["services.*.configs.*"] = transformFileMount
|
|
||||||
transformers["services.*.ports"] = transformPorts
|
|
||||||
transformers["services.*.build"] = transformBuild
|
|
||||||
transformers["services.*.build.ssh"] = transformSSH
|
|
||||||
transformers["services.*.ulimits.*"] = transformUlimits
|
|
||||||
transformers["services.*.build.ulimits.*"] = transformUlimits
|
|
||||||
transformers["services.*.develop.watch.*.ignore"] = transformStringOrList
|
|
||||||
transformers["services.*.develop.watch.*.include"] = transformStringOrList
|
|
||||||
transformers["volumes.*"] = transformMaybeExternal
|
|
||||||
transformers["networks.*"] = transformMaybeExternal
|
|
||||||
transformers["secrets.*"] = transformMaybeExternal
|
|
||||||
transformers["configs.*"] = transformMaybeExternal
|
|
||||||
transformers["include.*"] = transformInclude
|
|
||||||
}
|
|
||||||
|
|
||||||
func transformStringOrList(data any, _ tree.Path, _ bool) (any, error) {
|
|
||||||
switch t := data.(type) {
|
|
||||||
case string:
|
|
||||||
return []any{t}, nil
|
|
||||||
default:
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Canonical transforms a compose model into canonical syntax
|
|
||||||
func Canonical(yaml map[string]any, ignoreParseError bool) (map[string]any, error) {
|
|
||||||
canonical, err := transform(yaml, tree.NewPath(), ignoreParseError)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return canonical.(map[string]any), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func transform(data any, p tree.Path, ignoreParseError bool) (any, error) {
|
|
||||||
for pattern, transformer := range transformers {
|
|
||||||
if p.Matches(pattern) {
|
|
||||||
t, err := transformer(data, p, ignoreParseError)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch v := data.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
a, err := transformMapping(v, p, ignoreParseError)
|
|
||||||
if err != nil {
|
|
||||||
return a, err
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
case []any:
|
|
||||||
a, err := transformSequence(v, p, ignoreParseError)
|
|
||||||
if err != nil {
|
|
||||||
return a, err
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
default:
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func transformSequence(v []any, p tree.Path, ignoreParseError bool) ([]any, error) {
|
|
||||||
for i, e := range v {
|
|
||||||
t, err := transform(e, p.Next("[]"), ignoreParseError)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
v[i] = t
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func transformMapping(v map[string]any, p tree.Path, ignoreParseError bool) (map[string]any, error) {
|
|
||||||
for k, e := range v {
|
|
||||||
t, err := transform(e, p.Next(k), ignoreParseError)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
v[k] = t
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func transformStringOrX(data any, _ tree.Path, _ bool) (any, error) {
|
|
||||||
switch v := data.(type) {
|
|
||||||
case string:
|
|
||||||
return v, nil
|
|
||||||
default:
|
|
||||||
return fmt.Sprint(v), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
97
vendor/github.com/compose-spec/compose-go/v2/transform/defaults.go
generated
vendored
97
vendor/github.com/compose-spec/compose-go/v2/transform/defaults.go
generated
vendored
@ -1,97 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package transform
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DefaultValues contains the default value transformers for compose fields
|
|
||||||
var DefaultValues = map[tree.Path]Func{}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
DefaultValues["services.*.build"] = defaultBuildContext
|
|
||||||
DefaultValues["services.*.secrets.*"] = defaultSecretMount
|
|
||||||
DefaultValues["services.*.ports.*"] = portDefaults
|
|
||||||
DefaultValues["services.*.deploy.resources.reservations.devices.*"] = deviceRequestDefaults
|
|
||||||
DefaultValues["services.*.gpus.*"] = deviceRequestDefaults
|
|
||||||
DefaultValues["services.*.volumes.*.bind"] = defaultVolumeBind
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterDefaultValue registers a custom transformer for the given path pattern
|
|
||||||
func RegisterDefaultValue(path string, transformer Func) {
|
|
||||||
DefaultValues[tree.Path(path)] = transformer
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDefaultValues transforms a compose model to set default values to missing attributes
|
|
||||||
func SetDefaultValues(yaml map[string]any) (map[string]any, error) {
|
|
||||||
result, err := setDefaults(yaml, tree.NewPath())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return result.(map[string]any), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setDefaults(data any, p tree.Path) (any, error) {
|
|
||||||
for pattern, transformer := range DefaultValues {
|
|
||||||
if p.Matches(pattern) {
|
|
||||||
t, err := transformer(data, p, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch v := data.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
a, err := setDefaultsMapping(v, p)
|
|
||||||
if err != nil {
|
|
||||||
return a, err
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
case []any:
|
|
||||||
a, err := setDefaultsSequence(v, p)
|
|
||||||
if err != nil {
|
|
||||||
return a, err
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
default:
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setDefaultsSequence(v []any, p tree.Path) ([]any, error) {
|
|
||||||
for i, e := range v {
|
|
||||||
t, err := setDefaults(e, p.Next("[]"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
v[i] = t
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setDefaultsMapping(v map[string]any, p tree.Path) (map[string]any, error) {
|
|
||||||
for k, e := range v {
|
|
||||||
t, err := setDefaults(e, p.Next(k))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
v[k] = t
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
53
vendor/github.com/compose-spec/compose-go/v2/transform/dependson.go
generated
vendored
53
vendor/github.com/compose-spec/compose-go/v2/transform/dependson.go
generated
vendored
@ -1,53 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package transform
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
)
|
|
||||||
|
|
||||||
func transformDependsOn(data any, p tree.Path, _ bool) (any, error) {
|
|
||||||
switch v := data.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
for i, e := range v {
|
|
||||||
d, ok := e.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("%s.%s: unsupported value %s", p, i, v)
|
|
||||||
}
|
|
||||||
if _, ok := d["condition"]; !ok {
|
|
||||||
d["condition"] = "service_started"
|
|
||||||
}
|
|
||||||
if _, ok := d["required"]; !ok {
|
|
||||||
d["required"] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
case []any:
|
|
||||||
d := map[string]any{}
|
|
||||||
for _, k := range v {
|
|
||||||
d[k.(string)] = map[string]any{
|
|
||||||
"condition": "service_started",
|
|
||||||
"required": true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return d, nil
|
|
||||||
default:
|
|
||||||
return data, fmt.Errorf("%s: invalid type %T for depend_on", p, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
60
vendor/github.com/compose-spec/compose-go/v2/transform/device.go
generated
vendored
60
vendor/github.com/compose-spec/compose-go/v2/transform/device.go
generated
vendored
@ -1,60 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package transform
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
)
|
|
||||||
|
|
||||||
func transformDeviceMapping(data any, p tree.Path, ignoreParseError bool) (any, error) {
|
|
||||||
switch v := data.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
return v, nil
|
|
||||||
case string:
|
|
||||||
src := ""
|
|
||||||
dst := ""
|
|
||||||
permissions := "rwm"
|
|
||||||
arr := strings.Split(v, ":")
|
|
||||||
switch len(arr) {
|
|
||||||
case 3:
|
|
||||||
permissions = arr[2]
|
|
||||||
fallthrough
|
|
||||||
case 2:
|
|
||||||
dst = arr[1]
|
|
||||||
fallthrough
|
|
||||||
case 1:
|
|
||||||
src = arr[0]
|
|
||||||
default:
|
|
||||||
if !ignoreParseError {
|
|
||||||
return nil, fmt.Errorf("confusing device mapping, please use long syntax: %s", v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if dst == "" {
|
|
||||||
dst = src
|
|
||||||
}
|
|
||||||
return map[string]any{
|
|
||||||
"source": src,
|
|
||||||
"target": dst,
|
|
||||||
"permissions": permissions,
|
|
||||||
}, nil
|
|
||||||
default:
|
|
||||||
return data, fmt.Errorf("%s: invalid type %T for service volume mount", p, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
36
vendor/github.com/compose-spec/compose-go/v2/transform/devices.go
generated
vendored
36
vendor/github.com/compose-spec/compose-go/v2/transform/devices.go
generated
vendored
@ -1,36 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package transform
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
)
|
|
||||||
|
|
||||||
func deviceRequestDefaults(data any, p tree.Path, _ bool) (any, error) {
|
|
||||||
v, ok := data.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return data, fmt.Errorf("%s: invalid type %T for device request", p, v)
|
|
||||||
}
|
|
||||||
_, hasCount := v["count"]
|
|
||||||
_, hasIDs := v["device_ids"]
|
|
||||||
if !hasCount && !hasIDs {
|
|
||||||
v["count"] = "all"
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
55
vendor/github.com/compose-spec/compose-go/v2/transform/envfile.go
generated
vendored
55
vendor/github.com/compose-spec/compose-go/v2/transform/envfile.go
generated
vendored
@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package transform
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
)
|
|
||||||
|
|
||||||
func transformEnvFile(data any, p tree.Path, _ bool) (any, error) {
|
|
||||||
switch v := data.(type) {
|
|
||||||
case string:
|
|
||||||
return []any{
|
|
||||||
transformEnvFileValue(v),
|
|
||||||
}, nil
|
|
||||||
case []any:
|
|
||||||
for i, e := range v {
|
|
||||||
v[i] = transformEnvFileValue(e)
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("%s: invalid type %T for env_file", p, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func transformEnvFileValue(data any) any {
|
|
||||||
switch v := data.(type) {
|
|
||||||
case string:
|
|
||||||
return map[string]any{
|
|
||||||
"path": v,
|
|
||||||
"required": true,
|
|
||||||
}
|
|
||||||
case map[string]any:
|
|
||||||
if _, ok := v["required"]; !ok {
|
|
||||||
v["required"] = true
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
36
vendor/github.com/compose-spec/compose-go/v2/transform/extends.go
generated
vendored
36
vendor/github.com/compose-spec/compose-go/v2/transform/extends.go
generated
vendored
@ -1,36 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package transform
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
)
|
|
||||||
|
|
||||||
func transformExtends(data any, p tree.Path, ignoreParseError bool) (any, error) {
|
|
||||||
switch v := data.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
return transformMapping(v, p, ignoreParseError)
|
|
||||||
case string:
|
|
||||||
return map[string]any{
|
|
||||||
"service": v,
|
|
||||||
}, nil
|
|
||||||
default:
|
|
||||||
return data, fmt.Errorf("%s: invalid type %T for extends", p, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
54
vendor/github.com/compose-spec/compose-go/v2/transform/external.go
generated
vendored
54
vendor/github.com/compose-spec/compose-go/v2/transform/external.go
generated
vendored
@ -1,54 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package transform
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func transformMaybeExternal(data any, p tree.Path, ignoreParseError bool) (any, error) {
|
|
||||||
if data == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
resource, err := transformMapping(data.(map[string]any), p, ignoreParseError)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if ext, ok := resource["external"]; ok {
|
|
||||||
name, named := resource["name"]
|
|
||||||
if external, ok := ext.(map[string]any); ok {
|
|
||||||
resource["external"] = true
|
|
||||||
if extname, extNamed := external["name"]; extNamed {
|
|
||||||
logrus.Warnf("%s: external.name is deprecated. Please set name and external: true", p)
|
|
||||||
if named && extname != name {
|
|
||||||
return nil, fmt.Errorf("%s: name and external.name conflict; only use name", p)
|
|
||||||
}
|
|
||||||
if !named {
|
|
||||||
// adopt (deprecated) external.name if set
|
|
||||||
resource["name"] = extname
|
|
||||||
return resource, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resource, nil
|
|
||||||
}
|
|
||||||
38
vendor/github.com/compose-spec/compose-go/v2/transform/gpus.go
generated
vendored
38
vendor/github.com/compose-spec/compose-go/v2/transform/gpus.go
generated
vendored
@ -1,38 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package transform
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
)
|
|
||||||
|
|
||||||
func transformGpus(data any, p tree.Path, ignoreParseError bool) (any, error) {
|
|
||||||
switch v := data.(type) {
|
|
||||||
case []any:
|
|
||||||
return transformSequence(v, p, ignoreParseError)
|
|
||||||
case string:
|
|
||||||
return []any{
|
|
||||||
map[string]any{
|
|
||||||
"count": "all",
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
default:
|
|
||||||
return data, fmt.Errorf("%s: invalid type %T for gpus", p, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
36
vendor/github.com/compose-spec/compose-go/v2/transform/include.go
generated
vendored
36
vendor/github.com/compose-spec/compose-go/v2/transform/include.go
generated
vendored
@ -1,36 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package transform
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
)
|
|
||||||
|
|
||||||
func transformInclude(data any, p tree.Path, _ bool) (any, error) {
|
|
||||||
switch v := data.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
return v, nil
|
|
||||||
case string:
|
|
||||||
return map[string]any{
|
|
||||||
"path": v,
|
|
||||||
}, nil
|
|
||||||
default:
|
|
||||||
return data, fmt.Errorf("%s: invalid type %T for external", p, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
46
vendor/github.com/compose-spec/compose-go/v2/transform/mapping.go
generated
vendored
46
vendor/github.com/compose-spec/compose-go/v2/transform/mapping.go
generated
vendored
@ -1,46 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package transform
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
)
|
|
||||||
|
|
||||||
func transformKeyValue(data any, p tree.Path, ignoreParseError bool) (any, error) {
|
|
||||||
switch v := data.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
return v, nil
|
|
||||||
case []any:
|
|
||||||
mapping := map[string]any{}
|
|
||||||
for _, e := range v {
|
|
||||||
before, after, found := strings.Cut(e.(string), "=")
|
|
||||||
if !found {
|
|
||||||
if ignoreParseError {
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("%s: invalid value %s, expected key=value", p, e)
|
|
||||||
}
|
|
||||||
mapping[before] = after
|
|
||||||
}
|
|
||||||
return mapping, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("%s: invalid type %T", p, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
104
vendor/github.com/compose-spec/compose-go/v2/transform/ports.go
generated
vendored
104
vendor/github.com/compose-spec/compose-go/v2/transform/ports.go
generated
vendored
@ -1,104 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package transform
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
"github.com/compose-spec/compose-go/v2/types"
|
|
||||||
"github.com/go-viper/mapstructure/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func transformPorts(data any, p tree.Path, ignoreParseError bool) (any, error) {
|
|
||||||
switch entries := data.(type) {
|
|
||||||
case []any:
|
|
||||||
// We process the list instead of individual items here.
|
|
||||||
// The reason is that one entry might be mapped to multiple ServicePortConfig.
|
|
||||||
// Therefore we take an input of a list and return an output of a list.
|
|
||||||
var ports []any
|
|
||||||
for _, entry := range entries {
|
|
||||||
switch value := entry.(type) {
|
|
||||||
case int:
|
|
||||||
parsed, err := types.ParsePortConfig(fmt.Sprint(value))
|
|
||||||
if err != nil {
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
for _, v := range parsed {
|
|
||||||
m, err := encode(v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ports = append(ports, m)
|
|
||||||
}
|
|
||||||
case string:
|
|
||||||
parsed, err := types.ParsePortConfig(value)
|
|
||||||
if err != nil {
|
|
||||||
if ignoreParseError {
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, v := range parsed {
|
|
||||||
m, err := encode(v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ports = append(ports, m)
|
|
||||||
}
|
|
||||||
case map[string]any:
|
|
||||||
ports = append(ports, value)
|
|
||||||
default:
|
|
||||||
return data, fmt.Errorf("%s: invalid type %T for port", p, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ports, nil
|
|
||||||
default:
|
|
||||||
return data, fmt.Errorf("%s: invalid type %T for port", p, entries)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func encode(v any) (map[string]any, error) {
|
|
||||||
m := map[string]any{}
|
|
||||||
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
|
||||||
Result: &m,
|
|
||||||
TagName: "yaml",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
err = decoder.Decode(v)
|
|
||||||
return m, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func portDefaults(data any, _ tree.Path, _ bool) (any, error) {
|
|
||||||
switch v := data.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
if _, ok := v["protocol"]; !ok {
|
|
||||||
v["protocol"] = "tcp"
|
|
||||||
}
|
|
||||||
if _, ok := v["mode"]; !ok {
|
|
||||||
v["mode"] = "ingress"
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
default:
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
49
vendor/github.com/compose-spec/compose-go/v2/transform/secrets.go
generated
vendored
49
vendor/github.com/compose-spec/compose-go/v2/transform/secrets.go
generated
vendored
@ -1,49 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package transform
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
)
|
|
||||||
|
|
||||||
func transformFileMount(data any, p tree.Path, _ bool) (any, error) {
|
|
||||||
switch v := data.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
return data, nil
|
|
||||||
case string:
|
|
||||||
return map[string]any{
|
|
||||||
"source": v,
|
|
||||||
}, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("%s: unsupported type %T", p, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultSecretMount(data any, p tree.Path, _ bool) (any, error) {
|
|
||||||
switch v := data.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
source := v["source"]
|
|
||||||
if _, ok := v["target"]; !ok {
|
|
||||||
v["target"] = fmt.Sprintf("/run/secrets/%s", source)
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("%s: unsupported type %T", p, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
41
vendor/github.com/compose-spec/compose-go/v2/transform/services.go
generated
vendored
41
vendor/github.com/compose-spec/compose-go/v2/transform/services.go
generated
vendored
@ -1,41 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package transform
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
)
|
|
||||||
|
|
||||||
func transformService(data any, p tree.Path, ignoreParseError bool) (any, error) {
|
|
||||||
switch value := data.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
return transformMapping(value, p, ignoreParseError)
|
|
||||||
default:
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func transformStringSliceToMap(data any, _ tree.Path, _ bool) (any, error) {
|
|
||||||
if slice, ok := data.([]any); ok {
|
|
||||||
mapping := make(map[string]any, len(slice))
|
|
||||||
for _, net := range slice {
|
|
||||||
mapping[net.(string)] = nil
|
|
||||||
}
|
|
||||||
return mapping, nil
|
|
||||||
}
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
51
vendor/github.com/compose-spec/compose-go/v2/transform/ssh.go
generated
vendored
51
vendor/github.com/compose-spec/compose-go/v2/transform/ssh.go
generated
vendored
@ -1,51 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 The Compose Specification Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package transform
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/v2/tree"
|
|
||||||
)
|
|
||||||
|
|
||||||
func transformSSH(data any, p tree.Path, _ bool) (any, error) {
|
|
||||||
switch v := data.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
return v, nil
|
|
||||||
case []any:
|
|
||||||
result := make(map[string]any, len(v))
|
|
||||||
for _, e := range v {
|
|
||||||
s, ok := e.(string)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("invalid ssh key type %T", e)
|
|
||||||
}
|
|
||||||
id, path, ok := strings.Cut(s, "=")
|
|
||||||
if !ok {
|
|
||||||
if id != "default" {
|
|
||||||
return nil, fmt.Errorf("invalid ssh key %q", s)
|
|
||||||
}
|
|
||||||
result[id] = nil
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result[id] = path
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
default:
|
|
||||||
return data, fmt.Errorf("%s: invalid type %T for ssh", p, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user