See #492
This commit is contained in:
@ -157,7 +157,8 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
||||
ResolveImage: stack.ResolveImageAlways,
|
||||
Detach: false,
|
||||
}
|
||||
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
||||
|
||||
compose, err := appPkg.GetAppComposeConfig(composeFiles, app.Env)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ import (
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"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/filters"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
@ -80,13 +80,13 @@ var AppLabelsCommand = &cobra.Command{
|
||||
|
||||
rows = append(rows, []string{i18n.G("RECIPE LABELS"), "---"})
|
||||
|
||||
config, err := app.Recipe.GetComposeConfig(app.Env)
|
||||
config, err := app.Recipe.GetComposeConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var localLabelKeys []string
|
||||
var appServiceConfig composetypes.ServiceConfig
|
||||
var appServiceConfig composeGoTypes.ServiceConfig
|
||||
for _, service := range config.Services {
|
||||
if service.Name == "app" {
|
||||
appServiceConfig = service
|
||||
|
||||
@ -262,8 +262,7 @@ func getAppResources(cl *dockerclient.Client, app app.App) (*AppResources, error
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts := stack.Deploy{Composefiles: composeFiles, Namespace: app.StackName()}
|
||||
compose, err := appPkg.GetAppComposeConfig(app.Name, opts, app.Env)
|
||||
compose, err := appPkg.GetAppComposeConfig(composeFiles, app.Env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -4,7 +4,8 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
@ -87,26 +88,19 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
|
||||
return
|
||||
}
|
||||
|
||||
deployOpts := stack.Deploy{
|
||||
Composefiles: composeFiles,
|
||||
Namespace: app.StackName(),
|
||||
Prune: false,
|
||||
ResolveImage: stack.ResolveImageAlways,
|
||||
}
|
||||
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
||||
compose, err := appPkg.GetAppComposeConfig(composeFiles, app.Env)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
services := compose.Services
|
||||
sort.Slice(services, func(i, j int) bool {
|
||||
return services[i].Name < services[j].Name
|
||||
})
|
||||
|
||||
var rows [][]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.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service.Name))
|
||||
|
||||
|
||||
@ -173,7 +173,7 @@ beforehand. See "abra app backup" for more.`),
|
||||
Detach: false,
|
||||
}
|
||||
|
||||
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
||||
compose, err := appPkg.GetAppComposeConfig(composeFiles, app.Env)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -89,8 +89,7 @@ Passing "--prune/-p" does not remove those volumes.`),
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
opts := stack.Deploy{Composefiles: composeFiles, Namespace: stackName}
|
||||
compose, err := appPkg.GetAppComposeConfig(app.Name, opts, app.Env)
|
||||
compose, err := appPkg.GetAppComposeConfig(composeFiles, app.Env)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -185,7 +185,7 @@ beforehand. See "abra app backup" for more.`),
|
||||
Detach: false,
|
||||
}
|
||||
|
||||
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
||||
compose, err := appPkg.GetAppComposeConfig(composeFiles, app.Env)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -92,10 +92,11 @@ func SetBumpType(bumpType string) {
|
||||
func GetMainAppImage(recipe recipe.Recipe) (string, error) {
|
||||
var path string
|
||||
|
||||
config, err := recipe.GetComposeConfig(nil)
|
||||
config, err := recipe.GetComposeConfig()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, service := range config.Services {
|
||||
if service.Name == "app" {
|
||||
img, err := reference.ParseNormalizedNamed(service.Image)
|
||||
|
||||
@ -70,21 +70,6 @@ func ValidateRecipe(args []string, cmdName string) recipe.Recipe {
|
||||
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))
|
||||
|
||||
return chosenRecipe
|
||||
|
||||
@ -312,10 +312,11 @@ likely to change.
|
||||
func GetImageVersions(recipe recipePkg.Recipe) (map[string]string, error) {
|
||||
services := make(map[string]string)
|
||||
|
||||
config, err := recipe.GetComposeConfig(nil)
|
||||
config, err := recipe.GetComposeConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
missingTag := false
|
||||
for _, service := range config.Services {
|
||||
if service.Image == "" {
|
||||
|
||||
@ -124,7 +124,7 @@ interface.`),
|
||||
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 {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
6
go.mod
6
go.mod
@ -9,6 +9,7 @@ require (
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.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/docker/cli v28.4.0+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-localereader v0.0.1 // 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/miekg/pkcs11 v1.1.1 // 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/rivo/uniseg v0.4.7 // 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/skeema/knownhosts v1.3.2 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // 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
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // 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.yaml.in/yaml/v2 v2.4.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/exp v0.0.0-20260312153236-7ab1446f8b90 // 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/time v0.15.0 // 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/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/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-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU=
|
||||
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/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/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/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=
|
||||
@ -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-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.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||
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/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/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
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/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
|
||||
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 v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
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/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=
|
||||
@ -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/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
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-20180904163835-0709b304e793/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-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.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-20180905080454-ebe1bf3edb33/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/upstream/convert"
|
||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||
composeGoTypes "github.com/compose-spec/compose-go/v2/types"
|
||||
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
loader "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/schollz/progressbar/v3"
|
||||
)
|
||||
@ -179,8 +179,7 @@ func (a App) Filters(appendServiceNames, exactMatch bool, services ...string) (f
|
||||
return filters, err
|
||||
}
|
||||
|
||||
opts := stack.Deploy{Composefiles: composeFiles}
|
||||
compose, err := GetAppComposeConfig(a.Recipe.Name, opts, a.Env)
|
||||
compose, err := GetAppComposeConfig(composeFiles, a.Env)
|
||||
if err != nil {
|
||||
return filters, err
|
||||
}
|
||||
@ -333,8 +332,7 @@ func GetAppServiceNames(appName string) ([]string, error) {
|
||||
return serviceNames, err
|
||||
}
|
||||
|
||||
opts := stack.Deploy{Composefiles: composeFiles}
|
||||
compose, err := GetAppComposeConfig(app.Recipe.Name, opts, app.Env)
|
||||
compose, err := GetAppComposeConfig(composeFiles, app.Env)
|
||||
if err != nil {
|
||||
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
|
||||
// specification is the result of a merge of all the compose.**.yml files in
|
||||
// the recipe repository.
|
||||
func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv envfile.AppEnv) (*composetypes.Config, error) {
|
||||
compose, err := loader.LoadComposefile(opts, appEnv)
|
||||
func GetAppComposeConfig(composeFiles []string, appEnv envfile.AppEnv) (*composeGoTypes.Project, error) {
|
||||
compose, err := loader.LoadCompose(loader.LoadConf{ComposeFiles: composeFiles, AppEnv: appEnv})
|
||||
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
|
||||
}
|
||||
@ -504,7 +507,7 @@ func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv envfile.AppEnv
|
||||
// ExposeAllEnv exposes all env variables to the app container
|
||||
func ExposeAllEnv(
|
||||
stackName string,
|
||||
compose *composetypes.Config,
|
||||
compose *composeGoTypes.Project,
|
||||
appEnv envfile.AppEnv,
|
||||
toDeployVersion string) {
|
||||
for _, service := range compose.Services {
|
||||
|
||||
@ -7,12 +7,12 @@ import (
|
||||
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"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
|
||||
// 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 {
|
||||
if service.Name == "app" {
|
||||
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
|
||||
// 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 {
|
||||
if service.Name == "app" {
|
||||
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
|
||||
func SetChaosVersionLabel(compose *composetypes.Config, stackName string, chaosVersion string) {
|
||||
func SetChaosVersionLabel(compose *composeGoTypes.Project, stackName string, chaosVersion string) {
|
||||
for _, service := range compose.Services {
|
||||
if service.Name == "app" {
|
||||
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 {
|
||||
if service.Name == "app" {
|
||||
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
|
||||
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 {
|
||||
if service.Name == "app" {
|
||||
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
|
||||
// `coop-cloud.${STACK_NAME}.timeout=...` if present. A value is present if the
|
||||
// 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
|
||||
|
||||
if timeoutLabel := GetLabel(compose, stackName, "timeout"); timeoutLabel != "" {
|
||||
|
||||
@ -6,7 +6,6 @@ import (
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/test"
|
||||
testPkg "coopcloud.tech/abra/pkg/test"
|
||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@ -40,15 +39,8 @@ func TestGetTimeoutFromLabel(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
deployOpts := stack.Deploy{
|
||||
Composefiles: composeFiles,
|
||||
Namespace: app.StackName(),
|
||||
Prune: false,
|
||||
ResolveImage: stack.ResolveImageAlways,
|
||||
Detach: false,
|
||||
}
|
||||
|
||||
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
||||
app.Env["STACK_NAME"] = app.StackName()
|
||||
compose, err := appPkg.GetAppComposeConfig(composeFiles, app.Env)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@ -120,7 +120,7 @@ func CommandNameComplete(appName string) ([]string, cobra.ShellCompDirective) {
|
||||
func SecretComplete(recipeName string) ([]string, cobra.ShellCompDirective) {
|
||||
r := recipe.Get(recipeName)
|
||||
|
||||
config, err := r.GetComposeConfig(nil)
|
||||
config, err := r.GetComposeConfig()
|
||||
if err != nil {
|
||||
err := i18n.G("autocomplete failed: %s", err)
|
||||
return []string{err}, cobra.ShellCompDirectiveError
|
||||
|
||||
@ -14,8 +14,8 @@ import (
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/secret"
|
||||
|
||||
composeGoTypes "github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/distribution/reference"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
)
|
||||
@ -229,7 +229,7 @@ func GatherSecretsForDeploy(cl *dockerClient.Client, app appPkg.App, showUnchang
|
||||
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
|
||||
currentConfigs, err := GetConfigsForStack(cl, app)
|
||||
if err != nil {
|
||||
@ -268,7 +268,7 @@ func GatherConfigsForDeploy(cl *dockerClient.Client, app appPkg.App, compose *co
|
||||
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
|
||||
currentImages, err := GetImagesForStack(cl, app)
|
||||
if err != nil {
|
||||
|
||||
@ -62,13 +62,6 @@ func (l LintRule) Skip(recipe recipe.Recipe) bool {
|
||||
|
||||
var LintRules = map[string][]LintRule{
|
||||
"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",
|
||||
Level: i18n.G("warn"),
|
||||
@ -217,18 +210,6 @@ func LintForErrors(recipe recipe.Recipe) error {
|
||||
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) {
|
||||
if _, err := os.Stat(r.SampleEnvPath); !os.IsNotExist(err) {
|
||||
return true, nil
|
||||
@ -238,7 +219,7 @@ func LintEnvConfigPresent(r recipe.Recipe) (bool, error) {
|
||||
}
|
||||
|
||||
func LintAppService(recipe recipe.Recipe) (bool, error) {
|
||||
config, err := recipe.GetComposeConfig(nil)
|
||||
config, err := recipe.GetComposeConfig()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -269,7 +250,7 @@ func LintTraefikEnabledSkipCondition(r recipe.Recipe) (bool, error) {
|
||||
}
|
||||
|
||||
func LintTraefikEnabled(recipe recipe.Recipe) (bool, error) {
|
||||
config, err := recipe.GetComposeConfig(nil)
|
||||
config, err := recipe.GetComposeConfig()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -287,7 +268,7 @@ func LintTraefikEnabled(recipe recipe.Recipe) (bool, error) {
|
||||
}
|
||||
|
||||
func LintDeployLabelsPresent(recipe recipe.Recipe) (bool, error) {
|
||||
config, err := recipe.GetComposeConfig(nil)
|
||||
config, err := recipe.GetComposeConfig()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -302,7 +283,7 @@ func LintDeployLabelsPresent(recipe recipe.Recipe) (bool, error) {
|
||||
}
|
||||
|
||||
func LintHealthchecks(recipe recipe.Recipe) (bool, error) {
|
||||
config, err := recipe.GetComposeConfig(nil)
|
||||
config, err := recipe.GetComposeConfig()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -316,7 +297,7 @@ func LintHealthchecks(recipe recipe.Recipe) (bool, error) {
|
||||
}
|
||||
|
||||
func LintAllImagesTagged(recipe recipe.Recipe) (bool, error) {
|
||||
config, err := recipe.GetComposeConfig(nil)
|
||||
config, err := recipe.GetComposeConfig()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -334,7 +315,7 @@ func LintAllImagesTagged(recipe recipe.Recipe) (bool, error) {
|
||||
}
|
||||
|
||||
func LintNoUnstableTags(recipe recipe.Recipe) (bool, error) {
|
||||
config, err := recipe.GetComposeConfig(nil)
|
||||
config, err := recipe.GetComposeConfig()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -361,7 +342,7 @@ func LintNoUnstableTags(recipe recipe.Recipe) (bool, error) {
|
||||
}
|
||||
|
||||
func LintSemverLikeTags(recipe recipe.Recipe) (bool, error) {
|
||||
config, err := recipe.GetComposeConfig(nil)
|
||||
config, err := recipe.GetComposeConfig()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -388,7 +369,7 @@ func LintSemverLikeTags(recipe recipe.Recipe) (bool, error) {
|
||||
}
|
||||
|
||||
func LintImagePresent(recipe recipe.Recipe) (bool, error) {
|
||||
config, err := recipe.GetComposeConfig(nil)
|
||||
config, err := recipe.GetComposeConfig()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -440,7 +421,7 @@ func LintMetadataFilledIn(r recipe.Recipe) (bool, error) {
|
||||
}
|
||||
|
||||
func LintAbraShVendors(recipe recipe.Recipe) (bool, error) {
|
||||
config, err := recipe.GetComposeConfig(nil)
|
||||
config, err := recipe.GetComposeConfig()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -472,7 +453,7 @@ func LintHasRecipeRepo(recipe recipe.Recipe) (bool, error) {
|
||||
}
|
||||
|
||||
func LintSecretLengths(recipe recipe.Recipe) (bool, error) {
|
||||
config, err := recipe.GetComposeConfig(nil)
|
||||
config, err := recipe.GetComposeConfig()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@ -11,10 +11,9 @@ import (
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"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"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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)
|
||||
composeFiles, err := filepath.Glob(pattern)
|
||||
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))
|
||||
}
|
||||
|
||||
if env == nil {
|
||||
env, err = r.SampleEnv()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
opts := stack.Deploy{Composefiles: composeFiles}
|
||||
config, err := loader.LoadComposefile(opts, env)
|
||||
config, err := loader.LoadCompose(loader.LoadConf{ComposeFiles: composeFiles})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// GetVersionLabelLocal retrieves the version label on the local recipe config
|
||||
func (r Recipe) GetVersionLabelLocal() (string, error) {
|
||||
var label string
|
||||
config, err := r.GetComposeConfig(nil)
|
||||
config, err := r.GetComposeConfig()
|
||||
if err != nil {
|
||||
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, ", ")))
|
||||
|
||||
for _, composeFile := range composeFiles {
|
||||
opts := stack.Deploy{Composefiles: []string{composeFile}}
|
||||
|
||||
sampleEnv, err := r.SampleEnv()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
compose, err := loader.LoadComposefile(opts, sampleEnv)
|
||||
compose, err := loader.LoadCompose(loader.LoadConf{ComposeFiles: []string{composeFile}})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -168,9 +153,9 @@ func (r Recipe) UpdateTag(image, tag string) (bool, error) {
|
||||
new := fmt.Sprintf("%s:%s", composeImage, tag)
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -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, ", ")))
|
||||
|
||||
for _, composeFile := range composeFiles {
|
||||
opts := stack.Deploy{Composefiles: []string{composeFile}}
|
||||
|
||||
sampleEnv, err := r.SampleEnv()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
compose, err := loader.LoadComposefile(opts, sampleEnv)
|
||||
compose, err := loader.LoadCompose(loader.LoadConf{ComposeFiles: []string{composeFile}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serviceExists := false
|
||||
var service composetypes.ServiceConfig
|
||||
var service composeGoTypes.ServiceConfig
|
||||
for _, s := range compose.Services {
|
||||
if s.Name == serviceName {
|
||||
service = s
|
||||
@ -234,9 +212,9 @@ func (r Recipe) UpdateLabel(pattern, serviceName, label string) error {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@ -416,7 +416,7 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
|
||||
|
||||
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 {
|
||||
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))
|
||||
|
||||
@ -20,7 +20,6 @@ import (
|
||||
"coopcloud.tech/abra/pkg/envfile"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||
loader "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"github.com/decentral1se/passgen"
|
||||
"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.
|
||||
appEnv["STACK_NAME"] = stackName
|
||||
|
||||
opts := stack.Deploy{Composefiles: composeFiles}
|
||||
composeConfig, err := loader.LoadComposefile(opts, appEnv)
|
||||
composeConfig, err := loader.LoadCompose(loader.LoadConf{ComposeFiles: composeFiles, AppEnv: appEnv})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -5,6 +5,8 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||
"git.coopcloud.tech/toolshed/godotenv"
|
||||
@ -12,6 +14,7 @@ import (
|
||||
|
||||
var (
|
||||
AppName = "test_app.example.com"
|
||||
StackName = "test_app_example_com"
|
||||
ServerName = "test_server"
|
||||
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")
|
||||
if err := os.CopyFS(serverDestDir, os.DirFS(serverSrcDir)); err != nil {
|
||||
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")
|
||||
if err := os.CopyFS(recipeDestDir, os.DirFS(recipeSrcDir)); err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@ -4,7 +4,7 @@ import (
|
||||
"io/ioutil"
|
||||
"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"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
)
|
||||
@ -48,19 +48,17 @@ func AddStackLabel(namespace Namespace, labels map[string]string) map[string]str
|
||||
return labels
|
||||
}
|
||||
|
||||
type networkMap map[string]composetypes.NetworkConfig
|
||||
|
||||
// Networks from the compose-file type to the engine API type
|
||||
func Networks(namespace Namespace, networks networkMap, servicesNetworks map[string]struct{}) (map[string]networktypes.CreateOptions, []string) {
|
||||
func Networks(namespace Namespace, networks map[string]composeGoTypes.NetworkConfig, servicesNetworks map[string]struct{}) (map[string]networktypes.CreateOptions, []string) {
|
||||
if networks == nil {
|
||||
networks = make(map[string]composetypes.NetworkConfig)
|
||||
networks = make(map[string]composeGoTypes.NetworkConfig)
|
||||
}
|
||||
|
||||
externalNetworks := []string{}
|
||||
result := make(map[string]networktypes.CreateOptions)
|
||||
for internalName := range servicesNetworks {
|
||||
network := networks[internalName]
|
||||
if network.External.External {
|
||||
if network.External {
|
||||
externalNetworks = append(externalNetworks, network.Name)
|
||||
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
|
||||
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{}
|
||||
for name, secret := range secrets {
|
||||
if secret.External.External {
|
||||
if secret.External {
|
||||
continue
|
||||
}
|
||||
|
||||
var obj swarmFileObject
|
||||
var err error
|
||||
if secret.Driver != "" {
|
||||
obj = driverObjectConfig(namespace, name, composetypes.FileObjectConfig(secret))
|
||||
obj = driverObjectConfig(namespace, name, composeGoTypes.FileObjectConfig(secret))
|
||||
} else {
|
||||
obj, err = fileObjectConfig(namespace, name, composetypes.FileObjectConfig(secret))
|
||||
obj, err = fileObjectConfig(namespace, name, composeGoTypes.FileObjectConfig(secret))
|
||||
}
|
||||
if err != nil {
|
||||
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
|
||||
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{}
|
||||
for name, config := range configs {
|
||||
if config.External.External {
|
||||
if config.External {
|
||||
continue
|
||||
}
|
||||
|
||||
obj, err := fileObjectConfig(namespace, name, composetypes.FileObjectConfig(config))
|
||||
obj, err := fileObjectConfig(namespace, name, composeGoTypes.FileObjectConfig(config))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -160,7 +158,7 @@ type swarmFileObject struct {
|
||||
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 != "" {
|
||||
name = obj.Name
|
||||
} 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)
|
||||
if err != nil {
|
||||
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"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"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/docker/api/types"
|
||||
"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
|
||||
func Services(
|
||||
namespace Namespace,
|
||||
config *composetypes.Config,
|
||||
config *composeGoTypes.Project,
|
||||
client client.CommonAPIClient,
|
||||
) (map[string]swarm.ServiceSpec, error) {
|
||||
result := make(map[string]swarm.ServiceSpec)
|
||||
@ -211,14 +213,17 @@ func Services(
|
||||
func Service(
|
||||
apiVersion string,
|
||||
namespace Namespace,
|
||||
service composetypes.ServiceConfig,
|
||||
networkConfigs map[string]composetypes.NetworkConfig,
|
||||
volumes map[string]composetypes.VolumeConfig,
|
||||
service composeGoTypes.ServiceConfig,
|
||||
networkConfigs map[string]composeGoTypes.NetworkConfig,
|
||||
volumes map[string]composeGoTypes.VolumeConfig,
|
||||
secrets []*swarm.SecretReference,
|
||||
configs []*swarm.ConfigReference,
|
||||
) (swarm.ServiceSpec, error) {
|
||||
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)
|
||||
if err != nil {
|
||||
@ -254,9 +259,16 @@ func Service(
|
||||
dnsConfig := convertDNSConfig(service.DNS, service.DNSSearch)
|
||||
|
||||
var privileges swarm.Privileges
|
||||
|
||||
credSpec := service.CredentialSpec
|
||||
if credSpec == nil {
|
||||
credSpec = &composeGoTypes.CredentialSpecConfig{}
|
||||
}
|
||||
|
||||
privileges.CredentialSpec, err = convertCredentialSpec(
|
||||
namespace, service.CredentialSpec, configs,
|
||||
namespace, *credSpec, configs,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return swarm.ServiceSpec{}, err
|
||||
}
|
||||
@ -271,6 +283,11 @@ func Service(
|
||||
|
||||
capAdd, capDrop := opts.EffectiveCapAddCapDrop(service.CapAdd, service.CapDrop)
|
||||
|
||||
var stopGracePtr time.Duration
|
||||
if service.StopGracePeriod != nil {
|
||||
stopGracePtr = time.Duration(*service.StopGracePeriod)
|
||||
}
|
||||
|
||||
serviceSpec := swarm.ServiceSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: name,
|
||||
@ -290,7 +307,7 @@ func Service(
|
||||
Dir: service.WorkingDir,
|
||||
User: service.User,
|
||||
Mounts: mounts,
|
||||
StopGracePeriod: composetypes.ConvertDurationPtr(service.StopGracePeriod),
|
||||
StopGracePeriod: &stopGracePtr,
|
||||
StopSignal: service.StopSignal,
|
||||
TTY: service.Tty,
|
||||
OpenStdin: service.StdinOpen,
|
||||
@ -338,7 +355,7 @@ func Service(
|
||||
return serviceSpec, nil
|
||||
}
|
||||
|
||||
func getPlacementPreference(preferences []composetypes.PlacementPreferences) []swarm.PlacementPreference {
|
||||
func getPlacementPreference(preferences []composeGoTypes.PlacementPreferences) []swarm.PlacementPreference {
|
||||
result := []swarm.PlacementPreference{}
|
||||
for _, preference := range preferences {
|
||||
spreadDescriptor := preference.Spread
|
||||
@ -357,13 +374,13 @@ func sortStrings(strs []string) []string {
|
||||
}
|
||||
|
||||
func convertServiceNetworks(
|
||||
networks map[string]*composetypes.ServiceNetworkConfig,
|
||||
networkConfigs networkMap,
|
||||
networks map[string]*composeGoTypes.ServiceNetworkConfig,
|
||||
networkConfigs map[string]composeGoTypes.NetworkConfig,
|
||||
namespace Namespace,
|
||||
name string,
|
||||
) ([]swarm.NetworkAttachmentConfig, error) {
|
||||
if len(networks) == 0 {
|
||||
networks = map[string]*composetypes.ServiceNetworkConfig{
|
||||
networks = map[string]*composeGoTypes.ServiceNetworkConfig{
|
||||
defaultNetwork: {},
|
||||
}
|
||||
}
|
||||
@ -403,20 +420,20 @@ func convertServiceNetworks(
|
||||
func convertServiceSecrets(
|
||||
client client.SecretAPIClient,
|
||||
namespace Namespace,
|
||||
secrets []composetypes.ServiceSecretConfig,
|
||||
secretSpecs map[string]composetypes.SecretConfig,
|
||||
secrets []composeGoTypes.ServiceSecretConfig,
|
||||
secretSpecs map[string]composeGoTypes.SecretConfig,
|
||||
) ([]*swarm.SecretReference, error) {
|
||||
refs := []*swarm.SecretReference{}
|
||||
|
||||
lookup := func(key string) (composetypes.FileObjectConfig, error) {
|
||||
lookup := func(key string) (composeGoTypes.FileObjectConfig, error) {
|
||||
secretSpec, exists := secretSpecs[key]
|
||||
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 {
|
||||
obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(secret), lookup)
|
||||
obj, err := convertFileObject(namespace, composeGoTypes.FileReferenceConfig(secret), lookup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -451,20 +468,20 @@ func convertServiceSecrets(
|
||||
func convertServiceConfigObjs(
|
||||
client client.ConfigAPIClient,
|
||||
namespace Namespace,
|
||||
service composetypes.ServiceConfig,
|
||||
configSpecs map[string]composetypes.ConfigObjConfig,
|
||||
service composeGoTypes.ServiceConfig,
|
||||
configSpecs map[string]composeGoTypes.ConfigObjConfig,
|
||||
) ([]*swarm.ConfigReference, error) {
|
||||
refs := []*swarm.ConfigReference{}
|
||||
|
||||
lookup := func(key string) (composetypes.FileObjectConfig, error) {
|
||||
lookup := func(key string) (composeGoTypes.FileObjectConfig, error) {
|
||||
configSpec, exists := configSpecs[key]
|
||||
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 {
|
||||
obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(config), lookup)
|
||||
obj, err := convertFileObject(namespace, composeGoTypes.FileReferenceConfig(config), lookup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -487,7 +504,7 @@ func convertServiceConfigObjs(
|
||||
// if the credSpec uses a config, then we should grab the config name, and
|
||||
// create a config reference for it. A File or Registry-type CredentialSpec
|
||||
// does not need this operation.
|
||||
if credSpec.Config != "" {
|
||||
if credSpec != nil && credSpec.Config != "" {
|
||||
// look up the config in the configSpecs.
|
||||
obj, err := lookup(credSpec.Config)
|
||||
if err != nil {
|
||||
@ -532,8 +549,8 @@ type swarmReferenceObject struct {
|
||||
|
||||
func convertFileObject(
|
||||
namespace Namespace,
|
||||
config composetypes.FileReferenceConfig,
|
||||
lookup func(key string) (composetypes.FileObjectConfig, error),
|
||||
config composeGoTypes.FileReferenceConfig,
|
||||
lookup func(key string) (composeGoTypes.FileObjectConfig, error),
|
||||
) (swarmReferenceObject, error) {
|
||||
obj, err := lookup(config.Source)
|
||||
if err != nil {
|
||||
@ -558,40 +575,37 @@ func convertFileObject(
|
||||
if gid == "" {
|
||||
gid = "0"
|
||||
}
|
||||
mode := config.Mode
|
||||
if mode == nil {
|
||||
mode = uint32Ptr(0444)
|
||||
}
|
||||
|
||||
return swarmReferenceObject{
|
||||
ref := swarmReferenceObject{
|
||||
File: swarmReferenceTarget{
|
||||
Name: target,
|
||||
UID: uid,
|
||||
GID: gid,
|
||||
Mode: os.FileMode(*mode),
|
||||
},
|
||||
Name: source,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func uint32Ptr(value uint32) *uint32 {
|
||||
return &value
|
||||
if config.Mode == nil {
|
||||
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:
|
||||
// "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{}
|
||||
for _, hostIP := range extraHosts {
|
||||
if v := strings.SplitN(hostIP, ":", 2); len(v) == 2 {
|
||||
// Convert to SwarmKit notation: IP-address hostname(s)
|
||||
hosts = append(hosts, fmt.Sprintf("%s %s", v[1], v[0]))
|
||||
}
|
||||
for hostName, hostIP := range extraHosts {
|
||||
hosts = append(hosts, fmt.Sprintf("%s %s", hostIP, hostName))
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
|
||||
func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) {
|
||||
func convertHealthcheck(healthcheck *composeGoTypes.HealthCheckConfig) (*container.HealthConfig, error) {
|
||||
if healthcheck == nil {
|
||||
return nil, nil
|
||||
}
|
||||
@ -629,7 +643,7 @@ func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container
|
||||
}, nil
|
||||
}
|
||||
|
||||
func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) {
|
||||
func convertRestartPolicy(restart string, source *composeGoTypes.RestartPolicy) (*swarm.RestartPolicy, error) {
|
||||
if source == nil {
|
||||
policy, err := opts.ParseRestartPolicy(restart)
|
||||
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{
|
||||
Condition: swarm.RestartPolicyCondition(source.Condition),
|
||||
Delay: composetypes.ConvertDurationPtr(source.Delay),
|
||||
Delay: &delayPtr,
|
||||
MaxAttempts: source.MaxAttempts,
|
||||
Window: composetypes.ConvertDurationPtr(source.Window),
|
||||
Window: &windowPtr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig {
|
||||
func convertUpdateConfig(source *composeGoTypes.UpdateConfig) *swarm.UpdateConfig {
|
||||
if source == 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{}
|
||||
var err error
|
||||
if source.Limits != nil {
|
||||
var cpus int64
|
||||
if source.Limits.NanoCPUs != "" {
|
||||
cpus, err = opts.ParseCPUs(source.Limits.NanoCPUs)
|
||||
if source.Limits.NanoCPUs > 0 {
|
||||
cpus, err = opts.ParseCPUs(fmt.Sprintf("%f", source.Limits.NanoCPUs))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -698,8 +722,8 @@ func convertResources(source composetypes.Resources) (*swarm.ResourceRequirement
|
||||
}
|
||||
if source.Reservations != nil {
|
||||
var cpus int64
|
||||
if source.Reservations.NanoCPUs != "" {
|
||||
cpus, err = opts.ParseCPUs(source.Reservations.NanoCPUs)
|
||||
if source.Reservations.NanoCPUs > 0 {
|
||||
cpus, err = opts.ParseCPUs(fmt.Sprintf("%f", source.Reservations.NanoCPUs))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -728,13 +752,29 @@ func convertResources(source composetypes.Resources) (*swarm.ResourceRequirement
|
||||
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{}
|
||||
for _, port := range source {
|
||||
published, err := str2uint32(port.Published)
|
||||
if err != nil {
|
||||
return &swarm.EndpointSpec{}, err
|
||||
}
|
||||
|
||||
portConfig := swarm.PortConfig{
|
||||
Protocol: swarm.PortConfigProtocol(port.Protocol),
|
||||
TargetPort: port.Target,
|
||||
PublishedPort: port.Published,
|
||||
PublishedPort: published,
|
||||
PublishMode: swarm.PortConfigPublishMode(port.Mode),
|
||||
}
|
||||
portConfigs = append(portConfigs, portConfig)
|
||||
@ -747,7 +787,7 @@ func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortC
|
||||
return &swarm.EndpointSpec{
|
||||
Mode: swarm.ResolutionMode(strings.ToLower(endpointMode)),
|
||||
Ports: portConfigs,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func convertEnvironment(source map[string]*string) []string {
|
||||
@ -765,7 +805,7 @@ func convertEnvironment(source map[string]*string) []string {
|
||||
return output
|
||||
}
|
||||
|
||||
func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) {
|
||||
func convertDeployMode(mode string, replicas *int) (swarm.ServiceMode, error) {
|
||||
serviceMode := swarm.ServiceMode{}
|
||||
|
||||
switch mode {
|
||||
@ -775,7 +815,8 @@ func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error)
|
||||
}
|
||||
serviceMode.Global = &swarm.GlobalService{}
|
||||
case "replicated", "":
|
||||
serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas}
|
||||
convReplicas := (*uint64)(unsafe.Pointer(replicas))
|
||||
serviceMode.Replicated = &swarm.ReplicatedService{Replicas: convReplicas}
|
||||
default:
|
||||
return serviceMode, errors.New(i18n.G("unknown mode: %s", mode))
|
||||
}
|
||||
@ -792,7 +833,7 @@ func convertDNSConfig(DNS []string, DNSSearch []string) *swarm.DNSConfig {
|
||||
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
|
||||
|
||||
// Config was added in API v1.40
|
||||
@ -814,7 +855,13 @@ func convertCredentialSpec(namespace Namespace, spec composetypes.CredentialSpec
|
||||
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]))
|
||||
}
|
||||
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
|
||||
// here with the config ID
|
||||
if swarmCredSpec.Config != "" {
|
||||
@ -836,7 +883,7 @@ func convertCredentialSpec(namespace Namespace, spec composetypes.CredentialSpec
|
||||
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)
|
||||
for name, u := range origUlimits {
|
||||
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 (
|
||||
"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/pkg/errors"
|
||||
)
|
||||
|
||||
type volumes map[string]composetypes.VolumeConfig
|
||||
type volumes map[string]composeGoTypes.VolumeConfig
|
||||
|
||||
// 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
|
||||
|
||||
for _, volumeConfig := range serviceVolumes {
|
||||
@ -23,7 +23,7 @@ func Volumes(serviceVolumes []composetypes.ServiceVolumeConfig, stackVolumes vol
|
||||
return mounts, nil
|
||||
}
|
||||
|
||||
func createMountFromVolume(volume composetypes.ServiceVolumeConfig) mount.Mount {
|
||||
func createMountFromVolume(volume composeGoTypes.ServiceVolumeConfig) mount.Mount {
|
||||
return mount.Mount{
|
||||
Type: mount.Type(volume.Type),
|
||||
Target: volume.Target,
|
||||
@ -34,7 +34,7 @@ func createMountFromVolume(volume composetypes.ServiceVolumeConfig) mount.Mount
|
||||
}
|
||||
|
||||
func handleVolumeToMount(
|
||||
volume composetypes.ServiceVolumeConfig,
|
||||
volume composeGoTypes.ServiceVolumeConfig,
|
||||
stackVolumes volumes,
|
||||
namespace Namespace,
|
||||
) (mount.Mount, error) {
|
||||
@ -68,7 +68,7 @@ func handleVolumeToMount(
|
||||
}
|
||||
|
||||
// External named volumes
|
||||
if stackVolume.External.External {
|
||||
if stackVolume.External {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@ -83,7 +83,7 @@ func handleVolumeToMount(
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func handleBindToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
|
||||
func handleBindToMount(volume composeGoTypes.ServiceVolumeConfig) (mount.Mount, error) {
|
||||
result := createMountFromVolume(volume)
|
||||
|
||||
if volume.Source == "" {
|
||||
@ -103,7 +103,7 @@ func handleBindToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, er
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func handleTmpfsToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
|
||||
func handleTmpfsToMount(volume composeGoTypes.ServiceVolumeConfig) (mount.Mount, error) {
|
||||
result := createMountFromVolume(volume)
|
||||
|
||||
if volume.Source != "" {
|
||||
@ -117,13 +117,13 @@ func handleTmpfsToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, e
|
||||
}
|
||||
if volume.Tmpfs != nil {
|
||||
result.TmpfsOptions = &mount.TmpfsOptions{
|
||||
SizeBytes: volume.Tmpfs.Size,
|
||||
SizeBytes: int64(volume.Tmpfs.Size),
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func handleNpipeToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
|
||||
func handleNpipeToMount(volume composeGoTypes.ServiceVolumeConfig) (mount.Mount, error) {
|
||||
result := createMountFromVolume(volume)
|
||||
|
||||
if volume.Source == "" {
|
||||
@ -144,7 +144,7 @@ func handleNpipeToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, e
|
||||
}
|
||||
|
||||
func convertVolumeToMount(
|
||||
volume composetypes.ServiceVolumeConfig,
|
||||
volume composeGoTypes.ServiceVolumeConfig,
|
||||
stackVolumes volumes,
|
||||
namespace Namespace,
|
||||
) (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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
@ -8,58 +9,58 @@ import (
|
||||
"strings"
|
||||
|
||||
"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/schema"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// DontSkipValidation ensures validation is done for compose file loading
|
||||
func DontSkipValidation(opts *loader.Options) {
|
||||
opts.SkipValidation = false
|
||||
type LoadConf struct {
|
||||
ComposeFiles []string
|
||||
AppEnv map[string]string
|
||||
}
|
||||
|
||||
// SkipInterpolation skip interpolating environment variables.
|
||||
func SkipInterpolation(opts *loader.Options) {
|
||||
opts.SkipInterpolation = true
|
||||
}
|
||||
func LoadCompose(conf LoadConf) (*composeGoTypes.Project, error) {
|
||||
var project *composeGoTypes.Project
|
||||
|
||||
// LoadComposefile parse the composefile specified in the cli and returns its Config and version.
|
||||
func LoadComposefile(opts Deploy, appEnv map[string]string, options ...func(*loader.Options)) (*composetypes.Config, error) {
|
||||
configDetails, err := getConfigDetails(opts.Composefiles, appEnv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var projectOptions *composeGoCli.ProjectOptions
|
||||
if len(conf.ComposeFiles) == 0 {
|
||||
return project, errors.New(i18n.G("LoadCompose: provide compose files"))
|
||||
}
|
||||
|
||||
if options == nil {
|
||||
options = []func(*loader.Options){DontSkipValidation}
|
||||
}
|
||||
|
||||
dicts := getDictsFrom(configDetails.ConfigFiles)
|
||||
config, err := loader.Load(configDetails, options...)
|
||||
if err != nil {
|
||||
if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok {
|
||||
return nil, errors.New(i18n.G("compose file contains unsupported options: %s", propertyWarnings(fpe.Properties)))
|
||||
if len(conf.AppEnv) == 0 {
|
||||
var err error
|
||||
projectOptions, err = composeGoCli.NewProjectOptions(
|
||||
conf.ComposeFiles,
|
||||
composeGoCli.WithInterpolation(false),
|
||||
)
|
||||
if err != nil {
|
||||
return project, err
|
||||
}
|
||||
} else {
|
||||
var env []string
|
||||
for k, v := range conf.AppEnv {
|
||||
env = append(env, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
|
||||
var err error
|
||||
projectOptions, err = composeGoCli.NewProjectOptions(
|
||||
conf.ComposeFiles,
|
||||
composeGoCli.WithEnv(env),
|
||||
)
|
||||
if err != nil {
|
||||
return project, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
recipeName, exists := appEnv["RECIPE"]
|
||||
if !exists {
|
||||
recipeName, _ = appEnv["TYPE"]
|
||||
project, err := projectOptions.LoadProject(context.Background())
|
||||
if err != nil {
|
||||
return project, err
|
||||
}
|
||||
|
||||
unsupportedProperties := loader.GetUnsupportedProperties(dicts...)
|
||||
if len(unsupportedProperties) > 0 {
|
||||
log.Warn(i18n.G("%s: ignoring unsupported options: %s", recipeName, strings.Join(unsupportedProperties, ", ")))
|
||||
}
|
||||
|
||||
deprecatedProperties := loader.GetDeprecatedProperties(dicts...)
|
||||
if len(deprecatedProperties) > 0 {
|
||||
log.Warn(i18n.G("%s: ignoring deprecated options: %s", recipeName, propertyWarnings(deprecatedProperties)))
|
||||
}
|
||||
return config, nil
|
||||
return project, nil
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
composeGoTypes "github.com/compose-spec/compose-go/v2/types"
|
||||
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
@ -20,7 +21,6 @@ import (
|
||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"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/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
@ -197,7 +197,7 @@ func pruneServices(ctx context.Context, cl *dockerClient.Client, namespace conve
|
||||
func RunDeploy(
|
||||
cl *dockerClient.Client,
|
||||
opts Deploy,
|
||||
cfg *composetypes.Config,
|
||||
cfg *composeGoTypes.Project,
|
||||
appName string,
|
||||
serverName string,
|
||||
dontWait bool,
|
||||
@ -246,7 +246,7 @@ func deployCompose(
|
||||
ctx context.Context,
|
||||
cl *dockerClient.Client,
|
||||
opts Deploy,
|
||||
config *composetypes.Config,
|
||||
config *composeGoTypes.Project,
|
||||
appName string,
|
||||
serverName string,
|
||||
dontWait bool,
|
||||
@ -325,7 +325,7 @@ func deployCompose(
|
||||
return nil
|
||||
}
|
||||
|
||||
func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
|
||||
func getServicesDeclaredNetworks(serviceConfigs map[string]composeGoTypes.ServiceConfig) map[string]struct{} {
|
||||
serviceNetworks := map[string]struct{}{}
|
||||
for _, serviceConfig := range serviceConfigs {
|
||||
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:
|
||||
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)
|
||||
}
|
||||
}
|
||||
34
vendor/github.com/compose-spec/compose-go/v2/transform/ulimits.go
generated
vendored
Normal file
34
vendor/github.com/compose-spec/compose-go/v2/transform/ulimits.go
generated
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
/*
|
||||
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 transformUlimits(data any, p tree.Path, _ bool) (any, error) {
|
||||
switch v := data.(type) {
|
||||
case map[string]any:
|
||||
return v, nil
|
||||
case int:
|
||||
return v, nil
|
||||
default:
|
||||
return data, fmt.Errorf("%s: invalid type %T for external", p, v)
|
||||
}
|
||||
}
|
||||
63
vendor/github.com/compose-spec/compose-go/v2/transform/volume.go
generated
vendored
Normal file
63
vendor/github.com/compose-spec/compose-go/v2/transform/volume.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 transform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/format"
|
||||
"github.com/compose-spec/compose-go/v2/tree"
|
||||
)
|
||||
|
||||
func transformVolumeMount(data any, p tree.Path, ignoreParseError bool) (any, error) {
|
||||
switch v := data.(type) {
|
||||
case map[string]any:
|
||||
return v, nil
|
||||
case string:
|
||||
volume, err := format.ParseVolume(v) // TODO(ndeloof) ParseVolume should not rely on types and return map[string]
|
||||
if err != nil {
|
||||
if ignoreParseError {
|
||||
return v, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
volume.Target = cleanTarget(volume.Target)
|
||||
|
||||
return encode(volume)
|
||||
default:
|
||||
return data, fmt.Errorf("%s: invalid type %T for service volume mount", p, v)
|
||||
}
|
||||
}
|
||||
|
||||
func cleanTarget(target string) string {
|
||||
if target == "" {
|
||||
return ""
|
||||
}
|
||||
return path.Clean(target)
|
||||
}
|
||||
|
||||
func defaultVolumeBind(data any, p tree.Path, _ bool) (any, error) {
|
||||
bind, ok := data.(map[string]any)
|
||||
if !ok {
|
||||
return data, fmt.Errorf("%s: invalid type %T for service volume bind", p, data)
|
||||
}
|
||||
if _, ok := bind["create_host_path"]; !ok {
|
||||
bind["create_host_path"] = true
|
||||
}
|
||||
return bind, nil
|
||||
}
|
||||
87
vendor/github.com/compose-spec/compose-go/v2/tree/path.go
generated
vendored
Normal file
87
vendor/github.com/compose-spec/compose-go/v2/tree/path.go
generated
vendored
Normal file
@ -0,0 +1,87 @@
|
||||
/*
|
||||
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 tree
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const pathSeparator = "."
|
||||
|
||||
// PathMatchAll is a token used as part of a Path to match any key at that level
|
||||
// in the nested structure
|
||||
const PathMatchAll = "*"
|
||||
|
||||
// PathMatchList is a token used as part of a Path to match items in a list
|
||||
const PathMatchList = "[]"
|
||||
|
||||
// Path is a dotted path of keys to a value in a nested mapping structure. A *
|
||||
// section in a path will match any key in the mapping structure.
|
||||
type Path string
|
||||
|
||||
// NewPath returns a new Path
|
||||
func NewPath(items ...string) Path {
|
||||
return Path(strings.Join(items, pathSeparator))
|
||||
}
|
||||
|
||||
// Next returns a new path by append part to the current path
|
||||
func (p Path) Next(part string) Path {
|
||||
if p == "" {
|
||||
return Path(part)
|
||||
}
|
||||
part = strings.ReplaceAll(part, pathSeparator, "👻")
|
||||
return Path(string(p) + pathSeparator + part)
|
||||
}
|
||||
|
||||
func (p Path) Parts() []string {
|
||||
return strings.Split(string(p), pathSeparator)
|
||||
}
|
||||
|
||||
func (p Path) Matches(pattern Path) bool {
|
||||
patternParts := pattern.Parts()
|
||||
parts := p.Parts()
|
||||
|
||||
if len(patternParts) != len(parts) {
|
||||
return false
|
||||
}
|
||||
for index, part := range parts {
|
||||
switch patternParts[index] {
|
||||
case PathMatchAll, part:
|
||||
continue
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (p Path) Last() string {
|
||||
parts := p.Parts()
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
|
||||
func (p Path) Parent() Path {
|
||||
index := strings.LastIndex(string(p), pathSeparator)
|
||||
if index > 0 {
|
||||
return p[0:index]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p Path) String() string {
|
||||
return strings.ReplaceAll(string(p), "👻", pathSeparator)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user