Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
94624bb16d
|
@ -157,7 +157,8 @@ 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"
|
||||||
composetypes "github.com/docker/cli/cli/compose/types"
|
composeGoTypes "github.com/compose-spec/compose-go/v2/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(app.Env)
|
config, err := app.Recipe.GetComposeConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var localLabelKeys []string
|
var localLabelKeys []string
|
||||||
var appServiceConfig composetypes.ServiceConfig
|
var appServiceConfig composeGoTypes.ServiceConfig
|
||||||
for _, service := range config.Services {
|
for _, service := range config.Services {
|
||||||
if service.Name == "app" {
|
if service.Name == "app" {
|
||||||
appServiceConfig = service
|
appServiceConfig = service
|
||||||
|
|||||||
@ -262,8 +262,7 @@ func getAppResources(cl *dockerclient.Client, app app.App) (*AppResources, error
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := stack.Deploy{Composefiles: composeFiles, Namespace: app.StackName()}
|
compose, err := appPkg.GetAppComposeConfig(composeFiles, app.Env)
|
||||||
compose, err := appPkg.GetAppComposeConfig(app.Name, opts, app.Env)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"maps"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
@ -87,26 +88,19 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
deployOpts := stack.Deploy{
|
compose, err := appPkg.GetAppComposeConfig(composeFiles, app.Env)
|
||||||
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 _, service := range services {
|
for _, serviceName := range slices.Sorted(maps.Keys(compose.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(app.Name, deployOpts, app.Env)
|
compose, err := appPkg.GetAppComposeConfig(composeFiles, app.Env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -89,8 +89,7 @@ Passing "--prune/-p" does not remove those volumes.`),
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := stack.Deploy{Composefiles: composeFiles, Namespace: stackName}
|
compose, err := appPkg.GetAppComposeConfig(composeFiles, app.Env)
|
||||||
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(app.Name, deployOpts, app.Env)
|
compose, err := appPkg.GetAppComposeConfig(composeFiles, 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,
|
||||||
i18n.G("no release notes for upgrading from %s to %s", deployMeta.Version, chosenUpgrade),
|
fmt.Sprintf("no release notes available for %s", chosenUpgrade),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -92,10 +92,11 @@ 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(nil)
|
config, err := recipe.GetComposeConfig()
|
||||||
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,21 +70,6 @@ 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,10 +312,11 @@ 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(nil)
|
config, err := recipe.GetComposeConfig()
|
||||||
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(nil)
|
config, err := recipe.GetComposeConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
6
go.mod
6
go.mod
@ -9,6 +9,7 @@ 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
|
||||||
@ -80,6 +81,7 @@ 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
|
||||||
@ -103,12 +105,14 @@ 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
|
||||||
@ -124,9 +128,11 @@ 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,6 +176,8 @@ 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=
|
||||||
@ -318,6 +320,8 @@ 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=
|
||||||
@ -632,6 +636,7 @@ 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=
|
||||||
@ -807,6 +812,8 @@ 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=
|
||||||
@ -894,6 +901,8 @@ 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=
|
||||||
@ -948,6 +957,8 @@ 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=
|
||||||
@ -1062,6 +1073,8 @@ 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,8 +179,7 @@ func (a App) Filters(appendServiceNames, exactMatch bool, services ...string) (f
|
|||||||
return filters, err
|
return filters, err
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := stack.Deploy{Composefiles: composeFiles}
|
compose, err := GetAppComposeConfig(composeFiles, a.Env)
|
||||||
compose, err := GetAppComposeConfig(a.Recipe.Name, opts, a.Env)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return filters, err
|
return filters, err
|
||||||
}
|
}
|
||||||
@ -333,8 +332,7 @@ func GetAppServiceNames(appName string) ([]string, error) {
|
|||||||
return serviceNames, err
|
return serviceNames, err
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := stack.Deploy{Composefiles: composeFiles}
|
compose, err := GetAppComposeConfig(composeFiles, app.Env)
|
||||||
compose, err := GetAppComposeConfig(app.Recipe.Name, opts, app.Env)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return serviceNames, err
|
return serviceNames, err
|
||||||
}
|
}
|
||||||
@ -490,13 +488,18 @@ 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(recipe string, opts stack.Deploy, appEnv envfile.AppEnv) (*composetypes.Config, error) {
|
func GetAppComposeConfig(composeFiles []string, appEnv envfile.AppEnv) (*composeGoTypes.Project, error) {
|
||||||
compose, err := loader.LoadComposefile(opts, appEnv)
|
compose, err := loader.LoadCompose(loader.LoadConf{ComposeFiles: composeFiles, AppEnv: appEnv})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &composetypes.Config{}, err
|
return &composeGoTypes.Project{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug(i18n.G("retrieved %s for %s", compose.Filename, recipe))
|
recipeName, exists := appEnv["RECIPE"]
|
||||||
|
if !exists {
|
||||||
|
recipeName, _ = appEnv["TYPE"]
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug(i18n.G("retrieved %s for %s", compose.Name, recipeName))
|
||||||
|
|
||||||
return compose, nil
|
return compose, nil
|
||||||
}
|
}
|
||||||
@ -504,7 +507,7 @@ func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv envfile.AppEnv
|
|||||||
// 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 *composetypes.Config,
|
compose *composeGoTypes.Project,
|
||||||
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"
|
||||||
composetypes "github.com/docker/cli/cli/compose/types"
|
composeGoTypes "github.com/compose-spec/compose-go/v2/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 *composetypes.Config, stackName string, recipe string) {
|
func SetRecipeLabel(compose *composeGoTypes.Project, 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 *composetypes.Config, stackName string, recipe strin
|
|||||||
|
|
||||||
// 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 *composetypes.Config, stackName string, chaos bool) {
|
func SetChaosLabel(compose *composeGoTypes.Project, 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 *composetypes.Config, 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 *composetypes.Config, stackName string, chaosVersion string) {
|
func SetChaosVersionLabel(compose *composeGoTypes.Project, 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 *composetypes.Config, stackName string, chaosV
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetVersionLabel(compose *composetypes.Config, stackName string, version string) {
|
func SetVersionLabel(compose *composeGoTypes.Project, 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 *composetypes.Config, stackName string, version str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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 *composetypes.Config, stackName string, label string) string {
|
func GetLabel(compose *composeGoTypes.Project, 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 *composetypes.Config, stackName string, label string) stri
|
|||||||
// 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 *composetypes.Config, stackName string) (int, error) {
|
func GetTimeoutFromLabel(compose *composeGoTypes.Project, 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,7 +6,6 @@ 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"
|
||||||
)
|
)
|
||||||
@ -40,15 +39,8 @@ func TestGetTimeoutFromLabel(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
deployOpts := stack.Deploy{
|
app.Env["STACK_NAME"] = app.StackName()
|
||||||
Composefiles: composeFiles,
|
compose, err := appPkg.GetAppComposeConfig(composeFiles, app.Env)
|
||||||
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(nil)
|
config, err := r.GetComposeConfig()
|
||||||
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 *composetypes.Config, abraShEnv map[string]string, showUnchanged bool) ([]string, error) {
|
func GatherConfigsForDeploy(cl *dockerClient.Client, app appPkg.App, compose *composeGoTypes.Project, 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 *composetypes.Config, showUnchanged bool) ([]string, error) {
|
func GatherImagesForDeploy(cl *dockerClient.Client, app appPkg.App, compose *composeGoTypes.Project, 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,13 +62,6 @@ 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"),
|
||||||
@ -217,18 +210,6 @@ 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
|
||||||
@ -238,7 +219,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(nil)
|
config, err := recipe.GetComposeConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -269,7 +250,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(nil)
|
config, err := recipe.GetComposeConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -287,7 +268,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(nil)
|
config, err := recipe.GetComposeConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -302,7 +283,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(nil)
|
config, err := recipe.GetComposeConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -316,7 +297,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(nil)
|
config, err := recipe.GetComposeConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -334,7 +315,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(nil)
|
config, err := recipe.GetComposeConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -361,7 +342,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(nil)
|
config, err := recipe.GetComposeConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -388,7 +369,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(nil)
|
config, err := recipe.GetComposeConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -440,7 +421,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(nil)
|
config, err := recipe.GetComposeConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -472,7 +453,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(nil)
|
config, err := recipe.GetComposeConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,10 +11,9 @@ 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
|
||||||
@ -61,7 +60,7 @@ func (r Recipe) GetComposeFiles(appEnv map[string]string) ([]string, error) {
|
|||||||
return composeFiles, nil
|
return composeFiles, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Recipe) GetComposeConfig(env map[string]string) (*composetypes.Config, error) {
|
func (r Recipe) GetComposeConfig() (*composeGoTypes.Project, 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 {
|
||||||
@ -72,25 +71,18 @@ func (r Recipe) GetComposeConfig(env map[string]string) (*composetypes.Config, e
|
|||||||
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
if env == nil {
|
config, err := loader.LoadCompose(loader.LoadConf{ComposeFiles: composeFiles})
|
||||||
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(nil)
|
config, err := r.GetComposeConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@ -123,14 +115,7 @@ 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 {
|
||||||
opts := stack.Deploy{Composefiles: []string{composeFile}}
|
compose, err := loader.LoadCompose(loader.LoadConf{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
|
||||||
}
|
}
|
||||||
@ -168,9 +153,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.Filename))
|
log.Debug(i18n.G("updating %s to %s in %s", old, new, compose.Name))
|
||||||
|
|
||||||
if err := os.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil {
|
if err := os.WriteFile(compose.Name, []byte(replacedBytes), 0o764); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -191,20 +176,13 @@ 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 {
|
||||||
opts := stack.Deploy{Composefiles: []string{composeFile}}
|
compose, err := loader.LoadCompose(loader.LoadConf{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 composetypes.ServiceConfig
|
var service composeGoTypes.ServiceConfig
|
||||||
for _, s := range compose.Services {
|
for _, s := range compose.Services {
|
||||||
if s.Name == serviceName {
|
if s.Name == serviceName {
|
||||||
service = s
|
service = s
|
||||||
@ -234,9 +212,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.Filename))
|
log.Debug(i18n.G("updating %s to %s in %s", old, label, compose.Name))
|
||||||
|
|
||||||
if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil {
|
if err := ioutil.WriteFile(compose.Name, []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(nil)
|
config, err := r.GetComposeConfig()
|
||||||
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,7 +20,6 @@ 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"
|
||||||
@ -122,14 +121,13 @@ 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
|
||||||
|
|
||||||
opts := stack.Deploy{Composefiles: composeFiles}
|
composeConfig, err := loader.LoadCompose(loader.LoadConf{ComposeFiles: composeFiles, AppEnv: appEnv})
|
||||||
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.LoadComposefile(opts, map[string]string{}, loader.SkipInterpolation)
|
configWithoutEnv, err := loader.LoadCompose(loader.LoadConf{ComposeFiles: composeFiles})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,8 @@ 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"
|
||||||
@ -12,6 +14,7 @@ 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"
|
||||||
|
|
||||||
@ -59,13 +62,19 @@ func Setup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serverSrcDir := os.ExpandEnv("$PWD/../../tests/resources/test_server")
|
_, f, _, ok := runtime.Caller(0)
|
||||||
|
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 := os.ExpandEnv("$PWD/../../tests/resources/test_recipe")
|
recipeSrcDir := filepath.Join(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"
|
||||||
|
|
||||||
composetypes "github.com/docker/cli/cli/compose/types"
|
composeGoTypes "github.com/compose-spec/compose-go/v2/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,19 +48,17 @@ 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 networkMap, servicesNetworks map[string]struct{}) (map[string]networktypes.CreateOptions, []string) {
|
func Networks(namespace Namespace, networks map[string]composeGoTypes.NetworkConfig, servicesNetworks map[string]struct{}) (map[string]networktypes.CreateOptions, []string) {
|
||||||
if networks == nil {
|
if networks == nil {
|
||||||
networks = make(map[string]composetypes.NetworkConfig)
|
networks = make(map[string]composeGoTypes.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.External {
|
if network.External {
|
||||||
externalNetworks = append(externalNetworks, network.Name)
|
externalNetworks = append(externalNetworks, network.Name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -98,19 +96,19 @@ func Networks(namespace Namespace, networks networkMap, servicesNetworks map[str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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]composetypes.SecretConfig) ([]swarm.SecretSpec, error) {
|
func Secrets(namespace Namespace, secrets map[string]composeGoTypes.SecretConfig) ([]swarm.SecretSpec, error) {
|
||||||
result := []swarm.SecretSpec{}
|
result := []swarm.SecretSpec{}
|
||||||
for name, secret := range secrets {
|
for name, secret := range secrets {
|
||||||
if secret.External.External {
|
if secret.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, composetypes.FileObjectConfig(secret))
|
obj = driverObjectConfig(namespace, name, composeGoTypes.FileObjectConfig(secret))
|
||||||
} else {
|
} else {
|
||||||
obj, err = fileObjectConfig(namespace, name, composetypes.FileObjectConfig(secret))
|
obj, err = fileObjectConfig(namespace, name, composeGoTypes.FileObjectConfig(secret))
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -133,14 +131,14 @@ func Secrets(namespace Namespace, secrets map[string]composetypes.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]composetypes.ConfigObjConfig) ([]swarm.ConfigSpec, error) {
|
func Configs(namespace Namespace, configs map[string]composeGoTypes.ConfigObjConfig) ([]swarm.ConfigSpec, error) {
|
||||||
result := []swarm.ConfigSpec{}
|
result := []swarm.ConfigSpec{}
|
||||||
for name, config := range configs {
|
for name, config := range configs {
|
||||||
if config.External.External {
|
if config.External {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
obj, err := fileObjectConfig(namespace, name, composetypes.FileObjectConfig(config))
|
obj, err := fileObjectConfig(namespace, name, composeGoTypes.FileObjectConfig(config))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -160,7 +158,7 @@ type swarmFileObject struct {
|
|||||||
Data []byte
|
Data []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func driverObjectConfig(namespace Namespace, name string, obj composetypes.FileObjectConfig) swarmFileObject {
|
func driverObjectConfig(namespace Namespace, name string, obj composeGoTypes.FileObjectConfig) swarmFileObject {
|
||||||
if obj.Name != "" {
|
if obj.Name != "" {
|
||||||
name = obj.Name
|
name = obj.Name
|
||||||
} else {
|
} else {
|
||||||
@ -176,7 +174,7 @@ func driverObjectConfig(namespace Namespace, name string, obj composetypes.FileO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileObjectConfig(namespace Namespace, name string, obj composetypes.FileObjectConfig) (swarmFileObject, error) {
|
func fileObjectConfig(namespace Namespace, name string, obj composeGoTypes.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
|
||||||
|
|||||||
@ -1,170 +0,0 @@
|
|||||||
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,11 +5,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
"coopcloud.tech/abra/pkg/i18n"
|
"coopcloud.tech/abra/pkg/i18n"
|
||||||
composetypes "github.com/docker/cli/cli/compose/types"
|
composeGoTypes "github.com/compose-spec/compose-go/v2/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"
|
||||||
@ -178,7 +180,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 *composetypes.Config,
|
config *composeGoTypes.Project,
|
||||||
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)
|
||||||
@ -211,14 +213,17 @@ func Services(
|
|||||||
func Service(
|
func Service(
|
||||||
apiVersion string,
|
apiVersion string,
|
||||||
namespace Namespace,
|
namespace Namespace,
|
||||||
service composetypes.ServiceConfig,
|
service composeGoTypes.ServiceConfig,
|
||||||
networkConfigs map[string]composetypes.NetworkConfig,
|
networkConfigs map[string]composeGoTypes.NetworkConfig,
|
||||||
volumes map[string]composetypes.VolumeConfig,
|
volumes map[string]composeGoTypes.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 := convertEndpointSpec(service.Deploy.EndpointMode, service.Ports)
|
endpoint, err := 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 {
|
||||||
@ -254,9 +259,16 @@ 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, service.CredentialSpec, configs,
|
namespace, *credSpec, configs,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return swarm.ServiceSpec{}, err
|
return swarm.ServiceSpec{}, err
|
||||||
}
|
}
|
||||||
@ -271,6 +283,11 @@ 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,
|
||||||
@ -290,7 +307,7 @@ func Service(
|
|||||||
Dir: service.WorkingDir,
|
Dir: service.WorkingDir,
|
||||||
User: service.User,
|
User: service.User,
|
||||||
Mounts: mounts,
|
Mounts: mounts,
|
||||||
StopGracePeriod: composetypes.ConvertDurationPtr(service.StopGracePeriod),
|
StopGracePeriod: &stopGracePtr,
|
||||||
StopSignal: service.StopSignal,
|
StopSignal: service.StopSignal,
|
||||||
TTY: service.Tty,
|
TTY: service.Tty,
|
||||||
OpenStdin: service.StdinOpen,
|
OpenStdin: service.StdinOpen,
|
||||||
@ -338,7 +355,7 @@ func Service(
|
|||||||
return serviceSpec, nil
|
return serviceSpec, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPlacementPreference(preferences []composetypes.PlacementPreferences) []swarm.PlacementPreference {
|
func getPlacementPreference(preferences []composeGoTypes.PlacementPreferences) []swarm.PlacementPreference {
|
||||||
result := []swarm.PlacementPreference{}
|
result := []swarm.PlacementPreference{}
|
||||||
for _, preference := range preferences {
|
for _, preference := range preferences {
|
||||||
spreadDescriptor := preference.Spread
|
spreadDescriptor := preference.Spread
|
||||||
@ -357,13 +374,13 @@ func sortStrings(strs []string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func convertServiceNetworks(
|
func convertServiceNetworks(
|
||||||
networks map[string]*composetypes.ServiceNetworkConfig,
|
networks map[string]*composeGoTypes.ServiceNetworkConfig,
|
||||||
networkConfigs networkMap,
|
networkConfigs map[string]composeGoTypes.NetworkConfig,
|
||||||
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]*composetypes.ServiceNetworkConfig{
|
networks = map[string]*composeGoTypes.ServiceNetworkConfig{
|
||||||
defaultNetwork: {},
|
defaultNetwork: {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -403,20 +420,20 @@ func convertServiceNetworks(
|
|||||||
func convertServiceSecrets(
|
func convertServiceSecrets(
|
||||||
client client.SecretAPIClient,
|
client client.SecretAPIClient,
|
||||||
namespace Namespace,
|
namespace Namespace,
|
||||||
secrets []composetypes.ServiceSecretConfig,
|
secrets []composeGoTypes.ServiceSecretConfig,
|
||||||
secretSpecs map[string]composetypes.SecretConfig,
|
secretSpecs map[string]composeGoTypes.SecretConfig,
|
||||||
) ([]*swarm.SecretReference, error) {
|
) ([]*swarm.SecretReference, error) {
|
||||||
refs := []*swarm.SecretReference{}
|
refs := []*swarm.SecretReference{}
|
||||||
|
|
||||||
lookup := func(key string) (composetypes.FileObjectConfig, error) {
|
lookup := func(key string) (composeGoTypes.FileObjectConfig, error) {
|
||||||
secretSpec, exists := secretSpecs[key]
|
secretSpec, exists := secretSpecs[key]
|
||||||
if !exists {
|
if !exists {
|
||||||
return composetypes.FileObjectConfig{}, errors.New(i18n.G("undefined secret %q", key))
|
return composeGoTypes.FileObjectConfig{}, errors.New(i18n.G("undefined secret %q", key))
|
||||||
}
|
}
|
||||||
return composetypes.FileObjectConfig(secretSpec), nil
|
return composeGoTypes.FileObjectConfig(secretSpec), nil
|
||||||
}
|
}
|
||||||
for _, secret := range secrets {
|
for _, secret := range secrets {
|
||||||
obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(secret), lookup)
|
obj, err := convertFileObject(namespace, composeGoTypes.FileReferenceConfig(secret), lookup)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -451,20 +468,20 @@ func convertServiceSecrets(
|
|||||||
func convertServiceConfigObjs(
|
func convertServiceConfigObjs(
|
||||||
client client.ConfigAPIClient,
|
client client.ConfigAPIClient,
|
||||||
namespace Namespace,
|
namespace Namespace,
|
||||||
service composetypes.ServiceConfig,
|
service composeGoTypes.ServiceConfig,
|
||||||
configSpecs map[string]composetypes.ConfigObjConfig,
|
configSpecs map[string]composeGoTypes.ConfigObjConfig,
|
||||||
) ([]*swarm.ConfigReference, error) {
|
) ([]*swarm.ConfigReference, error) {
|
||||||
refs := []*swarm.ConfigReference{}
|
refs := []*swarm.ConfigReference{}
|
||||||
|
|
||||||
lookup := func(key string) (composetypes.FileObjectConfig, error) {
|
lookup := func(key string) (composeGoTypes.FileObjectConfig, error) {
|
||||||
configSpec, exists := configSpecs[key]
|
configSpec, exists := configSpecs[key]
|
||||||
if !exists {
|
if !exists {
|
||||||
return composetypes.FileObjectConfig{}, errors.New(i18n.G("undefined config %q", key))
|
return composeGoTypes.FileObjectConfig{}, errors.New(i18n.G("undefined config %q", key))
|
||||||
}
|
}
|
||||||
return composetypes.FileObjectConfig(configSpec), nil
|
return composeGoTypes.FileObjectConfig(configSpec), nil
|
||||||
}
|
}
|
||||||
for _, config := range service.Configs {
|
for _, config := range service.Configs {
|
||||||
obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(config), lookup)
|
obj, err := convertFileObject(namespace, composeGoTypes.FileReferenceConfig(config), lookup)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -487,7 +504,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.Config != "" {
|
if credSpec != nil && 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 {
|
||||||
@ -532,8 +549,8 @@ type swarmReferenceObject struct {
|
|||||||
|
|
||||||
func convertFileObject(
|
func convertFileObject(
|
||||||
namespace Namespace,
|
namespace Namespace,
|
||||||
config composetypes.FileReferenceConfig,
|
config composeGoTypes.FileReferenceConfig,
|
||||||
lookup func(key string) (composetypes.FileObjectConfig, error),
|
lookup func(key string) (composeGoTypes.FileObjectConfig, error),
|
||||||
) (swarmReferenceObject, error) {
|
) (swarmReferenceObject, error) {
|
||||||
obj, err := lookup(config.Source)
|
obj, err := lookup(config.Source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -558,40 +575,37 @@ func convertFileObject(
|
|||||||
if gid == "" {
|
if gid == "" {
|
||||||
gid = "0"
|
gid = "0"
|
||||||
}
|
}
|
||||||
mode := config.Mode
|
|
||||||
if mode == nil {
|
|
||||||
mode = uint32Ptr(0444)
|
|
||||||
}
|
|
||||||
|
|
||||||
return swarmReferenceObject{
|
ref := 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
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func uint32Ptr(value uint32) *uint32 {
|
if config.Mode == nil {
|
||||||
return &value
|
defaultMode := 0444
|
||||||
|
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 composetypes.HostsList) []string {
|
func convertExtraHosts(extraHosts composeGoTypes.HostsList) []string {
|
||||||
hosts := []string{}
|
hosts := []string{}
|
||||||
for _, hostIP := range extraHosts {
|
for hostName, hostIP := range extraHosts {
|
||||||
if v := strings.SplitN(hostIP, ":", 2); len(v) == 2 {
|
hosts = append(hosts, fmt.Sprintf("%s %s", hostIP, hostName))
|
||||||
// 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 *composetypes.HealthCheckConfig) (*container.HealthConfig, error) {
|
func convertHealthcheck(healthcheck *composeGoTypes.HealthCheckConfig) (*container.HealthConfig, error) {
|
||||||
if healthcheck == nil {
|
if healthcheck == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@ -629,7 +643,7 @@ func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) {
|
func convertRestartPolicy(restart string, source *composeGoTypes.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 {
|
||||||
@ -653,15 +667,25 @@ func convertRestartPolicy(restart string, source *composetypes.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: composetypes.ConvertDurationPtr(source.Delay),
|
Delay: &delayPtr,
|
||||||
MaxAttempts: source.MaxAttempts,
|
MaxAttempts: source.MaxAttempts,
|
||||||
Window: composetypes.ConvertDurationPtr(source.Window),
|
Window: &windowPtr,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig {
|
func convertUpdateConfig(source *composeGoTypes.UpdateConfig) *swarm.UpdateConfig {
|
||||||
if source == nil {
|
if source == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -679,13 +703,13 @@ func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) {
|
func convertResources(source composeGoTypes.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 != "" {
|
if source.Limits.NanoCPUs > 0 {
|
||||||
cpus, err = opts.ParseCPUs(source.Limits.NanoCPUs)
|
cpus, err = opts.ParseCPUs(fmt.Sprintf("%f", source.Limits.NanoCPUs))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -698,8 +722,8 @@ func convertResources(source composetypes.Resources) (*swarm.ResourceRequirement
|
|||||||
}
|
}
|
||||||
if source.Reservations != nil {
|
if source.Reservations != nil {
|
||||||
var cpus int64
|
var cpus int64
|
||||||
if source.Reservations.NanoCPUs != "" {
|
if source.Reservations.NanoCPUs > 0 {
|
||||||
cpus, err = opts.ParseCPUs(source.Reservations.NanoCPUs)
|
cpus, err = opts.ParseCPUs(fmt.Sprintf("%f", source.Reservations.NanoCPUs))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -728,13 +752,29 @@ func convertResources(source composetypes.Resources) (*swarm.ResourceRequirement
|
|||||||
return resources, nil
|
return resources, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortConfig) *swarm.EndpointSpec {
|
func str2uint32(s string) (uint32, error) {
|
||||||
|
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: port.Published,
|
PublishedPort: published,
|
||||||
PublishMode: swarm.PortConfigPublishMode(port.Mode),
|
PublishMode: swarm.PortConfigPublishMode(port.Mode),
|
||||||
}
|
}
|
||||||
portConfigs = append(portConfigs, portConfig)
|
portConfigs = append(portConfigs, portConfig)
|
||||||
@ -747,7 +787,7 @@ func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortC
|
|||||||
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 {
|
||||||
@ -765,7 +805,7 @@ func convertEnvironment(source map[string]*string) []string {
|
|||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) {
|
func convertDeployMode(mode string, replicas *int) (swarm.ServiceMode, error) {
|
||||||
serviceMode := swarm.ServiceMode{}
|
serviceMode := swarm.ServiceMode{}
|
||||||
|
|
||||||
switch mode {
|
switch mode {
|
||||||
@ -775,7 +815,8 @@ func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error)
|
|||||||
}
|
}
|
||||||
serviceMode.Global = &swarm.GlobalService{}
|
serviceMode.Global = &swarm.GlobalService{}
|
||||||
case "replicated", "":
|
case "replicated", "":
|
||||||
serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas}
|
convReplicas := (*uint64)(unsafe.Pointer(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))
|
||||||
}
|
}
|
||||||
@ -792,7 +833,7 @@ func convertDNSConfig(DNS []string, DNSSearch []string) *swarm.DNSConfig {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertCredentialSpec(namespace Namespace, spec composetypes.CredentialSpecConfig, refs []*swarm.ConfigReference) (*swarm.CredentialSpec, error) {
|
func convertCredentialSpec(namespace Namespace, spec composeGoTypes.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
|
||||||
@ -814,7 +855,13 @@ func convertCredentialSpec(namespace Namespace, spec composetypes.CredentialSpec
|
|||||||
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 != "" {
|
||||||
@ -836,7 +883,7 @@ func convertCredentialSpec(namespace Namespace, spec composetypes.CredentialSpec
|
|||||||
return &swarmCredSpec, nil
|
return &swarmCredSpec, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertUlimits(origUlimits map[string]*composetypes.UlimitsConfig) []*units.Ulimit {
|
func convertUlimits(origUlimits map[string]*composeGoTypes.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 {
|
||||||
|
|||||||
@ -1,678 +0,0 @@
|
|||||||
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"
|
||||||
composetypes "github.com/docker/cli/cli/compose/types"
|
composeGoTypes "github.com/compose-spec/compose-go/v2/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]composetypes.VolumeConfig
|
type volumes map[string]composeGoTypes.VolumeConfig
|
||||||
|
|
||||||
// Volumes from compose-file types to engine api types
|
// Volumes from compose-file types to engine api types
|
||||||
func Volumes(serviceVolumes []composetypes.ServiceVolumeConfig, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) {
|
func Volumes(serviceVolumes []composeGoTypes.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 []composetypes.ServiceVolumeConfig, stackVolumes vol
|
|||||||
return mounts, nil
|
return mounts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createMountFromVolume(volume composetypes.ServiceVolumeConfig) mount.Mount {
|
func createMountFromVolume(volume composeGoTypes.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 composetypes.ServiceVolumeConfig) mount.Mount
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleVolumeToMount(
|
func handleVolumeToMount(
|
||||||
volume composetypes.ServiceVolumeConfig,
|
volume composeGoTypes.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.External {
|
if stackVolume.External {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ func handleVolumeToMount(
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleBindToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
|
func handleBindToMount(volume composeGoTypes.ServiceVolumeConfig) (mount.Mount, error) {
|
||||||
result := createMountFromVolume(volume)
|
result := createMountFromVolume(volume)
|
||||||
|
|
||||||
if volume.Source == "" {
|
if volume.Source == "" {
|
||||||
@ -103,7 +103,7 @@ func handleBindToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, er
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleTmpfsToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
|
func handleTmpfsToMount(volume composeGoTypes.ServiceVolumeConfig) (mount.Mount, error) {
|
||||||
result := createMountFromVolume(volume)
|
result := createMountFromVolume(volume)
|
||||||
|
|
||||||
if volume.Source != "" {
|
if volume.Source != "" {
|
||||||
@ -117,13 +117,13 @@ func handleTmpfsToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, e
|
|||||||
}
|
}
|
||||||
if volume.Tmpfs != nil {
|
if volume.Tmpfs != nil {
|
||||||
result.TmpfsOptions = &mount.TmpfsOptions{
|
result.TmpfsOptions = &mount.TmpfsOptions{
|
||||||
SizeBytes: volume.Tmpfs.Size,
|
SizeBytes: int64(volume.Tmpfs.Size),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleNpipeToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
|
func handleNpipeToMount(volume composeGoTypes.ServiceVolumeConfig) (mount.Mount, error) {
|
||||||
result := createMountFromVolume(volume)
|
result := createMountFromVolume(volume)
|
||||||
|
|
||||||
if volume.Source == "" {
|
if volume.Source == "" {
|
||||||
@ -144,7 +144,7 @@ func handleNpipeToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
func convertVolumeToMount(
|
func convertVolumeToMount(
|
||||||
volume composetypes.ServiceVolumeConfig,
|
volume composeGoTypes.ServiceVolumeConfig,
|
||||||
stackVolumes volumes,
|
stackVolumes volumes,
|
||||||
namespace Namespace,
|
namespace Namespace,
|
||||||
) (mount.Mount, error) {
|
) (mount.Mount, error) {
|
||||||
|
|||||||
@ -1,361 +0,0 @@
|
|||||||
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,6 +1,7 @@
|
|||||||
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"
|
||||||
@ -8,58 +9,62 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"coopcloud.tech/abra/pkg/i18n"
|
"coopcloud.tech/abra/pkg/i18n"
|
||||||
"coopcloud.tech/abra/pkg/log"
|
composeGoCli "github.com/compose-spec/compose-go/v2/cli"
|
||||||
|
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DontSkipValidation ensures validation is done for compose file loading
|
type LoadConf struct {
|
||||||
func DontSkipValidation(opts *loader.Options) {
|
ComposeFiles []string
|
||||||
opts.SkipValidation = false
|
AppEnv map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// SkipInterpolation skip interpolating environment variables.
|
func LoadCompose(conf LoadConf) (*composeGoTypes.Project, error) {
|
||||||
func SkipInterpolation(opts *loader.Options) {
|
var project *composeGoTypes.Project
|
||||||
opts.SkipInterpolation = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadComposefile parse the composefile specified in the cli and returns its Config and version.
|
// NOTE(d1): silence compose-go internal logger
|
||||||
func LoadComposefile(opts Deploy, appEnv map[string]string, options ...func(*loader.Options)) (*composetypes.Config, error) {
|
logrus.SetOutput(ioutil.Discard)
|
||||||
configDetails, err := getConfigDetails(opts.Composefiles, appEnv)
|
|
||||||
if err != nil {
|
var projectOptions *composeGoCli.ProjectOptions
|
||||||
return nil, err
|
if len(conf.ComposeFiles) == 0 {
|
||||||
|
return project, errors.New(i18n.G("LoadCompose: provide compose files"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if options == nil {
|
if len(conf.AppEnv) == 0 {
|
||||||
options = []func(*loader.Options){DontSkipValidation}
|
var err error
|
||||||
}
|
projectOptions, err = composeGoCli.NewProjectOptions(
|
||||||
|
conf.ComposeFiles,
|
||||||
dicts := getDictsFrom(configDetails.ConfigFiles)
|
composeGoCli.WithInterpolation(false),
|
||||||
config, err := loader.Load(configDetails, options...)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok {
|
return project, err
|
||||||
return nil, errors.New(i18n.G("compose file contains unsupported options: %s", propertyWarnings(fpe.Properties)))
|
}
|
||||||
|
} 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
|
||||||
}
|
}
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
recipeName, exists := appEnv["RECIPE"]
|
project, err := projectOptions.LoadProject(context.Background())
|
||||||
if !exists {
|
if err != nil {
|
||||||
recipeName, _ = appEnv["TYPE"]
|
return project, err
|
||||||
}
|
}
|
||||||
|
|
||||||
unsupportedProperties := loader.GetUnsupportedProperties(dicts...)
|
return project, nil
|
||||||
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{} {
|
||||||
|
|||||||
26
pkg/upstream/stack/loader_test.go
Normal file
26
pkg/upstream/stack/loader_test.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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,6 +12,7 @@ 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"
|
||||||
@ -20,7 +21,6 @@ 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 *composetypes.Config,
|
cfg *composeGoTypes.Project,
|
||||||
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 *composetypes.Config,
|
config *composeGoTypes.Project,
|
||||||
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 []composetypes.ServiceConfig) map[string]struct{} {
|
func getServicesDeclaredNetworks(serviceConfigs map[string]composeGoTypes.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 {
|
||||||
|
|||||||
7
tests/resources/test_recipe/compose.interpolate.yml
Normal file
7
tests/resources/test_recipe/compose.interpolate.yml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
ports:
|
||||||
|
- target: 22
|
||||||
|
published: ${PORT}
|
||||||
@ -1,5 +1,4 @@
|
|||||||
---
|
---
|
||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
|
|||||||
191
vendor/github.com/compose-spec/compose-go/v2/LICENSE
generated
vendored
Normal file
191
vendor/github.com/compose-spec/compose-go/v2/LICENSE
generated
vendored
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
https://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
Copyright 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
Normal file
2
vendor/github.com/compose-spec/compose-go/v2/NOTICE
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
The Compose Specification
|
||||||
|
Copyright 2020 The Compose Specification Authors
|
||||||
590
vendor/github.com/compose-spec/compose-go/v2/cli/options.go
generated
vendored
Normal file
590
vendor/github.com/compose-spec/compose-go/v2/cli/options.go
generated
vendored
Normal file
@ -0,0 +1,590 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
29
vendor/github.com/compose-spec/compose-go/v2/consts/consts.go
generated
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
22
vendor/github.com/compose-spec/compose-go/v2/dotenv/LICENSE
generated
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
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
Normal file
73
vendor/github.com/compose-spec/compose-go/v2/dotenv/env.go
generated
vendored
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
51
vendor/github.com/compose-spec/compose-go/v2/dotenv/format.go
generated
vendored
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
182
vendor/github.com/compose-spec/compose-go/v2/dotenv/godotenv.go
generated
vendored
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
// 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
Normal file
286
vendor/github.com/compose-spec/compose-go/v2/dotenv/parser.go
generated
vendored
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
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
Normal file
56
vendor/github.com/compose-spec/compose-go/v2/errdefs/errors.go
generated
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
199
vendor/github.com/compose-spec/compose-go/v2/format/volume.go
generated
vendored
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
63
vendor/github.com/compose-spec/compose-go/v2/graph/cycle.go
generated
vendored
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
75
vendor/github.com/compose-spec/compose-go/v2/graph/graph.go
generated
vendored
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
80
vendor/github.com/compose-spec/compose-go/v2/graph/services.go
generated
vendored
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
211
vendor/github.com/compose-spec/compose-go/v2/graph/traversal.go
generated
vendored
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
137
vendor/github.com/compose-spec/compose-go/v2/interpolation/interpolation.go
generated
vendored
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
110
vendor/github.com/compose-spec/compose-go/v2/loader/environment.go
generated
vendored
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
10
vendor/github.com/compose-spec/compose-go/v2/loader/example1.env
generated
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# 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
Normal file
10
vendor/github.com/compose-spec/compose-go/v2/loader/example1.label
generated
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# 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
Normal file
4
vendor/github.com/compose-spec/compose-go/v2/loader/example2.env
generated
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
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
Normal file
4
vendor/github.com/compose-spec/compose-go/v2/loader/example2.label
generated
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
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
Normal file
221
vendor/github.com/compose-spec/compose-go/v2/loader/extends.go
generated
vendored
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
36
vendor/github.com/compose-spec/compose-go/v2/loader/fix.go
generated
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
461
vendor/github.com/compose-spec/compose-go/v2/loader/full-example.yml
generated
vendored
Normal file
@ -0,0 +1,461 @@
|
|||||||
|
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
Normal file
223
vendor/github.com/compose-spec/compose-go/v2/loader/include.go
generated
vendored
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
118
vendor/github.com/compose-spec/compose-go/v2/loader/interpolate.go
generated
vendored
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
899
vendor/github.com/compose-spec/compose-go/v2/loader/loader.go
generated
vendored
Normal file
@ -0,0 +1,899 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
79
vendor/github.com/compose-spec/compose-go/v2/loader/mapstructure.go
generated
vendored
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
266
vendor/github.com/compose-spec/compose-go/v2/loader/normalize.go
generated
vendored
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
75
vendor/github.com/compose-spec/compose-go/v2/loader/omitEmpty.go
generated
vendored
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
50
vendor/github.com/compose-spec/compose-go/v2/loader/paths.go
generated
vendored
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
196
vendor/github.com/compose-spec/compose-go/v2/loader/reset.go
generated
vendored
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
218
vendor/github.com/compose-spec/compose-go/v2/loader/validate.go
generated
vendored
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
27
vendor/github.com/compose-spec/compose-go/v2/override/extends.go
generated
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
307
vendor/github.com/compose-spec/compose-go/v2/override/merge.go
generated
vendored
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
229
vendor/github.com/compose-spec/compose-go/v2/override/uncity.go
generated
vendored
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
51
vendor/github.com/compose-spec/compose-go/v2/paths/context.go
generated
vendored
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
25
vendor/github.com/compose-spec/compose-go/v2/paths/extends.go
generated
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
37
vendor/github.com/compose-spec/compose-go/v2/paths/home.go
generated
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
169
vendor/github.com/compose-spec/compose-go/v2/paths/resolve.go
generated
vendored
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
57
vendor/github.com/compose-spec/compose-go/v2/paths/unix.go
generated
vendored
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
233
vendor/github.com/compose-spec/compose-go/v2/paths/windows_path.go
generated
vendored
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
1912
vendor/github.com/compose-spec/compose-go/v2/schema/compose-spec.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
149
vendor/github.com/compose-spec/compose-go/v2/schema/schema.go
generated
vendored
Normal file
149
vendor/github.com/compose-spec/compose-go/v2/schema/schema.go
generated
vendored
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
123
vendor/github.com/compose-spec/compose-go/v2/schema/using-variables.yaml
generated
vendored
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
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
Normal file
380
vendor/github.com/compose-spec/compose-go/v2/template/template.go
generated
vendored
Normal file
@ -0,0 +1,380 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
157
vendor/github.com/compose-spec/compose-go/v2/template/variables.go
generated
vendored
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
48
vendor/github.com/compose-spec/compose-go/v2/transform/build.go
generated
vendored
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
137
vendor/github.com/compose-spec/compose-go/v2/transform/canonical.go
generated
vendored
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
97
vendor/github.com/compose-spec/compose-go/v2/transform/defaults.go
generated
vendored
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
53
vendor/github.com/compose-spec/compose-go/v2/transform/dependson.go
generated
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
60
vendor/github.com/compose-spec/compose-go/v2/transform/device.go
generated
vendored
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
36
vendor/github.com/compose-spec/compose-go/v2/transform/devices.go
generated
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
55
vendor/github.com/compose-spec/compose-go/v2/transform/envfile.go
generated
vendored
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
36
vendor/github.com/compose-spec/compose-go/v2/transform/extends.go
generated
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
54
vendor/github.com/compose-spec/compose-go/v2/transform/external.go
generated
vendored
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
38
vendor/github.com/compose-spec/compose-go/v2/transform/gpus.go
generated
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
36
vendor/github.com/compose-spec/compose-go/v2/transform/include.go
generated
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
46
vendor/github.com/compose-spec/compose-go/v2/transform/mapping.go
generated
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
104
vendor/github.com/compose-spec/compose-go/v2/transform/ports.go
generated
vendored
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
49
vendor/github.com/compose-spec/compose-go/v2/transform/secrets.go
generated
vendored
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
41
vendor/github.com/compose-spec/compose-go/v2/transform/services.go
generated
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
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
Normal file
51
vendor/github.com/compose-spec/compose-go/v2/transform/ssh.go
generated
vendored
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
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