diff --git a/go.mod b/go.mod index 923ab5a545..3bb2195e1e 100644 --- a/go.mod +++ b/go.mod @@ -7,13 +7,13 @@ require ( github.com/akutz/memconn v0.1.0 github.com/alessio/shellescape v1.4.1 github.com/blang/semver v3.5.1+incompatible - github.com/compose-spec/compose-go v1.2.4 + github.com/compose-spec/compose-go v1.6.0 github.com/davecgh/go-spew v1.1.1 github.com/docker/cli v20.10.17+incompatible github.com/docker/distribution v2.8.1+incompatible github.com/docker/docker v20.10.17+incompatible github.com/docker/go-connections v0.4.0 - github.com/docker/go-units v0.4.0 + github.com/docker/go-units v0.5.0 github.com/fatih/color v1.13.0 github.com/gdamore/tcell v1.1.3 github.com/ghodss/yaml v1.0.0 @@ -121,7 +121,7 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/d4l3k/messagediff v1.2.1 // indirect github.com/denisbrodbeck/machineid v1.0.0 // indirect - github.com/distribution/distribution/v3 v3.0.0-20220526142353-ffbd94cbe269 // indirect + github.com/distribution/distribution/v3 v3.0.0-20220725133111-4bf3547399eb // indirect github.com/docker/docker-credential-helpers v0.6.4 // indirect github.com/docker/go v1.5.1-1 // indirect github.com/docker/go-metrics v0.0.1 // indirect @@ -169,7 +169,7 @@ require ( github.com/miekg/pkcs11 v1.1.1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect - github.com/mitchellh/mapstructure v1.4.3 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/spdystream v0.2.0 // indirect github.com/moby/sys/mount v0.2.0 // indirect @@ -191,7 +191,7 @@ require ( github.com/russross/blackfriday v1.5.2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/segmentio/encoding v0.2.7 // indirect - github.com/sirupsen/logrus v1.8.1 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect github.com/smacker/go-tree-sitter v0.0.0-20220209044044-0d3022e933c3 // indirect github.com/theupdateframework/notary v0.6.1 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect diff --git a/go.sum b/go.sum index ee8a8ff2f7..b93d00d9f8 100644 --- a/go.sum +++ b/go.sum @@ -306,8 +306,8 @@ github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20160425231609-f8ad88b59a58/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/compose-spec/compose-go v1.2.4 h1:nzTFqM8+2J7Veao5Pq5U451thinv3U1wChIvcjX59/A= -github.com/compose-spec/compose-go v1.2.4/go.mod h1:pAy7Mikpeft4pxkFU565/DRHEbDfR84G6AQuiL+Hdg8= +github.com/compose-spec/compose-go v1.6.0 h1:7Ol/UULMUtbPmB0EYrETASRoum821JpOh/XaEf+hN+Q= +github.com/compose-spec/compose-go v1.6.0/go.mod h1:os+Ulh2jlZxY1XT1hbciERadjSUU/BtZ6+gcN7vD7J0= 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= @@ -458,8 +458,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/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= -github.com/distribution/distribution/v3 v3.0.0-20220526142353-ffbd94cbe269 h1:hbCT8ZPPMqefiAWD2ZKjn7ypokIGViTvBBg/ExLSdCk= -github.com/distribution/distribution/v3 v3.0.0-20220526142353-ffbd94cbe269/go.mod h1:28YO/VJk9/64+sTGNuYaBjWxrXTPrj0C0XmgTIOjxX4= +github.com/distribution/distribution/v3 v3.0.0-20220725133111-4bf3547399eb h1:oCCuuU3kMO3sjZH/p7LamvQNW9SWoT4yQuMGcdSxGAE= +github.com/distribution/distribution/v3 v3.0.0-20220725133111-4bf3547399eb/go.mod h1:28YO/VJk9/64+sTGNuYaBjWxrXTPrj0C0XmgTIOjxX4= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/docker/cli v0.0.0-20190925022749-754388324470/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= @@ -494,8 +494,9 @@ github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libnetwork v0.8.0-dev.2.0.20200917202933-d0951081b35f h1:jC/ZXgYdzCUuKFkKGNiekhnIkGfUrdelEqvg4Miv440= github.com/docker/libnetwork v0.8.0-dev.2.0.20200917202933-d0951081b35f/go.mod h1:93m0aTqz6z+g32wla4l4WxTrdtvBRmVzYRkYvasA5Z8= github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= @@ -1061,8 +1062,8 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= -github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= @@ -1331,8 +1332,9 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smacker/go-tree-sitter v0.0.0-20220209044044-0d3022e933c3 h1:WrsSqod9T70HFyq8hjL6wambOKb4ISUXzFUuNTJHDwo= github.com/smacker/go-tree-sitter v0.0.0-20220209044044-0d3022e933c3/go.mod h1:EiUuVMUfLQj8Sul+S8aKWJwQy7FRYnJCO2EWzf8F5hk= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -1901,6 +1903,7 @@ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220731174439-a90be440212d h1:Sv5ogFZatcgIMMtBSTTAgMYsicp25MXBubjXNDKwm80= golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2266,7 +2269,7 @@ gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= -gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk= +gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= helm.sh/helm/v3 v3.10.0 h1:y/MYONZ/bsld9kHwqgBX2uPggnUr5hahpjwt9/jrHlI= helm.sh/helm/v3 v3.10.0/go.mod h1:paPw0hO5KVfrCMbi1M8+P8xdfBri3IiJiVKATZsFR94= diff --git a/internal/cli/down.go b/internal/cli/down.go index 9cc6966b45..886b034a3c 100644 --- a/internal/cli/down.go +++ b/internal/cli/down.go @@ -89,15 +89,19 @@ func (c *downCmd) down(ctx context.Context, downDeps DownDeps, args []string) er return err } - var dcProject v1alpha1.DockerComposeProject + dcProjects := make(map[string]v1alpha1.DockerComposeProject) for _, m := range sortedManifests { - if m.IsDC() { - dcProject = m.DockerComposeTarget().Spec.Project - break + if !m.IsDC() { + continue + } + proj := m.DockerComposeTarget().Spec.Project + + if _, exists := dcProjects[proj.Name]; !exists { + dcProjects[proj.Name] = proj } } - if !model.IsEmptyDockerComposeProject(dcProject) { + for _, dcProject := range dcProjects { dcc := downDeps.dcClient err = dcc.Down(ctx, dcProject, logger.Get(ctx).Writer(logger.InfoLvl), logger.Get(ctx).Writer(logger.InfoLvl)) if err != nil { diff --git a/internal/dockercompose/fake_client.go b/internal/dockercompose/fake_client.go index 3dae32ffb5..9ed439de54 100644 --- a/internal/dockercompose/fake_client.go +++ b/internal/dockercompose/fake_client.go @@ -18,7 +18,6 @@ import ( "github.com/tilt-dev/tilt/internal/container" "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" - "github.com/tilt-dev/tilt/pkg/model" ) type FakeDCClient struct { @@ -188,7 +187,7 @@ func (c *FakeDCClient) Project(_ context.Context, m v1alpha1.DockerComposeProjec workDir := opts.WorkingDir projectName := opts.Name if projectName == "" { - projectName = model.NormalizeName(workDir) + projectName = loader.NormalizeProjectName(workDir) } if projectName == "" { projectName = "fakedc" diff --git a/internal/tiltfile/api/__init__.py b/internal/tiltfile/api/__init__.py index 8f62cc8a39..fde380d0d5 100644 --- a/internal/tiltfile/api/__init__.py +++ b/internal/tiltfile/api/__init__.py @@ -278,7 +278,9 @@ def docker_compose(configPaths: Union[str, Blob, List[Union[str, Blob]]], env_fi Args: configPaths: Path(s) and/or Blob(s) to Docker Compose yaml files or content. env_file: Path to env file to use; defaults to ``.env`` in current directory. - project_name: The Docker Compose project name. If unspecified, the main Tiltfile's directory name is used. + project_name: The Docker Compose project name. If unspecified, uses either the + name of the directory containing the first compose file, or, in the case of + inline YAML, the current Tiltfile's directory name. """ @@ -428,7 +430,9 @@ def dc_resource(name: str, resource_deps: List[str] = [], links: Union[str, Link, List[Union[str, Link]]] = [], labels: Union[str, List[str]] = [], - auto_init: bool = True) -> None: + auto_init: bool = True, + project_name: str = "", + new_name: str = "") -> None: """Configures the Docker Compose resource of the given name. Note: Tilt does an amount of resource configuration for you(for more info, see `Tiltfile Concepts: Resources `_); you only need to invoke this function if you want to configure your resource beyond what Tilt does automatically. @@ -444,6 +448,9 @@ def dc_resource(name: str, labels: used to group resources in the Web UI, (e.g. you want all frontend services displayed together, while test and backend services are displayed seperately). A label must start and end with an alphanumeric character, can include ``_``, ``-``, and ``.``, and must be 63 characters or less. For an example, see `Resource Grouping `_. auto_init: whether this resource runs on ``tilt up``. Defaults to ``True``. For more info, see the `Manual Update Control docs `_. + project_name: The Docker Compose project name to match the corresponding project loaded by + ``docker_compose``, if necessary for disambiguation. + new_name: If non-empty, will be used as the new name for this resource. """ pass diff --git a/internal/tiltfile/docker_compose.go b/internal/tiltfile/docker_compose.go index 01b44f233a..c8e9c01126 100644 --- a/internal/tiltfile/docker_compose.go +++ b/internal/tiltfile/docker_compose.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" + "github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/types" // DANGER: some compose-go types are not friendly to being marshaled with gopkg.in/yaml.v3 @@ -24,6 +25,7 @@ import ( "github.com/tilt-dev/tilt/internal/container" "github.com/tilt-dev/tilt/internal/controllers/apis/liveupdate" "github.com/tilt-dev/tilt/internal/dockercompose" + "github.com/tilt-dev/tilt/internal/sliceutils" "github.com/tilt-dev/tilt/internal/tiltfile/io" "github.com/tilt-dev/tilt/internal/tiltfile/links" "github.com/tilt-dev/tilt/internal/tiltfile/starkit" @@ -37,12 +39,26 @@ type dcResourceSet struct { Project v1alpha1.DockerComposeProject configPaths []string - services []*dcService tiltfilePath string + services map[string]*dcService + serviceNames []string + resOptions map[string]*dcResourceOptions } +type dcResourceMap map[string]*dcResourceSet + func (dc dcResourceSet) Empty() bool { return reflect.DeepEqual(dc, dcResourceSet{}) } +func (dc dcResourceSet) ServiceCount() int { return len(dc.services) } + +func (dcm dcResourceMap) ServiceCount() int { + svcCount := 0 + for _, dc := range dcm { + svcCount += dc.ServiceCount() + } + return svcCount +} + func (s *tiltfileState) dockerCompose(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { var configPaths starlark.Value var projectName string @@ -63,24 +79,9 @@ func (s *tiltfileState) dockerCompose(thread *starlark.Thread, fn *starlark.Buil return nil, fmt.Errorf("Nothing to compose") } - dc := s.dc - - currentTiltfilePath := starkit.CurrentExecPath(thread) - if dc.tiltfilePath != "" && dc.tiltfilePath != currentTiltfilePath { - return starlark.None, fmt.Errorf("Cannot load docker-compose files from two different Tiltfiles.\n"+ - "docker-compose must have a single working directory:\n"+ - "(%s, %s)", dc.tiltfilePath, currentTiltfilePath) - } - - if projectName == "" { - projectName = model.NormalizeName(filepath.Base(filepath.Dir(currentTiltfilePath))) - } - project := v1alpha1.DockerComposeProject{ - ConfigPaths: dc.configPaths, - ProjectPath: dc.Project.ProjectPath, - Name: projectName, - EnvFile: envFile.Value, + Name: projectName, + EnvFile: envFile.Value, } if project.EnvFile != "" { @@ -121,10 +122,13 @@ func (s *tiltfileState) dockerCompose(thread *starlark.Thread, fn *starlark.Buil return starlark.None, fmt.Errorf("expected blob | path (string). Actual type: %T", val) } - // Set project path to dir of first compose file, like DC CLI does + // Set project path/name to dir of first compose file, like DC CLI does if project.ProjectPath == "" { project.ProjectPath = filepath.Dir(path) } + if project.Name == "" { + project.Name = loader.NormalizeProjectName(filepath.Base(filepath.Dir(path))) + } project.ConfigPaths = append(project.ConfigPaths, path) err = io.RecordReadPath(thread, io.WatchFileOnly, path) @@ -134,26 +138,51 @@ func (s *tiltfileState) dockerCompose(thread *starlark.Thread, fn *starlark.Buil } } + currentTiltfilePath := starkit.CurrentExecPath(thread) + + if project.Name == "" { + project.Name = loader.NormalizeProjectName(filepath.Base(filepath.Dir(currentTiltfilePath))) + } + // Set to tiltfile directory for YAML blob tempfiles if project.ProjectPath == "" { project.ProjectPath = filepath.Dir(currentTiltfilePath) } + dc := s.dc[project.Name] + if dc == nil { + dc = &dcResourceSet{ + Project: project, + services: make(map[string]*dcService), + resOptions: make(map[string]*dcResourceOptions), + configPaths: project.ConfigPaths, + tiltfilePath: currentTiltfilePath, + } + s.dc[project.Name] = dc + } else { + dc.configPaths = sliceutils.AppendWithoutDupes(dc.configPaths, project.ConfigPaths...) + dc.Project.ConfigPaths = dc.configPaths + if project.EnvFile != "" { + dc.Project.EnvFile = project.EnvFile + } + project = dc.Project + } + services, err := parseDCConfig(s.ctx, s.dcCli, project) if err != nil { return nil, err } + dc.services = make(map[string]*dcService) + dc.serviceNames = []string{} for _, svc := range services { - previousSvc := s.dcByName[svc.Name] - if previousSvc != nil { - delete(s.dcByName, svc.Name) - } err := s.checkResourceConflict(svc.Name) if err != nil { return nil, err } - svc.Options = s.dcResOptions[svc.Name] + + dc.serviceNames = append(dc.serviceNames, svc.Name) + svc.Options = dc.resOptions[svc.Name] for _, f := range svc.ServiceConfig.EnvFile { if !filepath.IsAbs(f) { f = filepath.Join(project.ProjectPath, f) @@ -163,14 +192,7 @@ func (s *tiltfileState) dockerCompose(thread *starlark.Thread, fn *starlark.Buil return nil, err } } - s.dcByName[svc.Name] = svc - } - - s.dc = dcResourceSet{ - Project: project, - configPaths: project.ConfigPaths, - services: services, - tiltfilePath: currentTiltfilePath, + dc.services[svc.Name] = svc } return starlark.None, nil @@ -180,6 +202,8 @@ func (s *tiltfileState) dockerCompose(thread *starlark.Thread, fn *starlark.Buil // to be defined in a `docker_compose.yml` func (s *tiltfileState) dcResource(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { var name string + var projectName string + var newName string var imageVal starlark.Value var triggerMode triggerMode var resourceDepsVal starlark.Sequence @@ -197,6 +221,8 @@ func (s *tiltfileState) dcResource(thread *starlark.Thread, fn *starlark.Builtin "links?", &links, "labels?", &labels, "auto_init?", &autoInit, + "project_name?", &projectName, + "new_name?", &newName, ); err != nil { return nil, err } @@ -215,12 +241,19 @@ func (s *tiltfileState) dcResource(thread *starlark.Thread, fn *starlark.Builtin return nil, fmt.Errorf("image arg must be a string; got %T", imageVal) } - svc, err := s.getDCService(name) + projectName, svc, err := s.getDCService(name, projectName) if err != nil { return nil, err } - options := s.dcResOptions[name] + if newName != "" { + name, err = s.renameDCService(projectName, name, newName, svc) + if err != nil { + return nil, err + } + } + + options := s.dc[projectName].resOptions[name] if options == nil { options = newDcResourceOptions() } @@ -253,27 +286,73 @@ func (s *tiltfileState) dcResource(thread *starlark.Thread, fn *starlark.Builtin options.AutoInit = autoInit } - s.dcResOptions[name] = options + s.dc[projectName].resOptions[name] = options svc.Options = options return starlark.None, nil } -func (s *tiltfileState) getDCService(name string) (*dcService, error) { - allNames := make([]string, len(s.dc.services)) - for i, svc := range s.dc.services { - if svc.Name == name { - return svc, nil +func (s *tiltfileState) getDCService(svcName, projName string) (string, *dcService, error) { + allNames := []string{} + foundProjName := "" + var foundSvc *dcService + + for _, dc := range s.dc { + if projName != "" && dc.Project.Name != projName { + continue + } + + for key, svc := range dc.services { + if key == svcName { + if foundSvc != nil { + return "", nil, fmt.Errorf("found multiple resources named %q, "+ + "please specify which one with project_name= argument", svcName) + } + foundProjName = dc.Project.Name + foundSvc = svc + } + allNames = append(allNames, key) + } + } + + if foundSvc == nil { + return "", nil, fmt.Errorf("no Docker Compose service found with name %q. "+ + "Found these instead:\n\t%s", svcName, strings.Join(allNames, "; ")) + } + + return foundProjName, foundSvc, nil +} + +func (s *tiltfileState) renameDCService(projectName, name, newName string, svc *dcService) (string, error) { + err := s.checkResourceConflict(newName) + if err != nil { + return "", err + } + + s.dc[projectName].services[newName] = svc + delete(s.dc[projectName].services, name) + if opts, exists := s.dc[projectName].resOptions[name]; exists { + s.dc[projectName].resOptions[newName] = opts + delete(s.dc[projectName].resOptions, name) + } + index := -1 + for i, n := range s.dc[projectName].serviceNames { + if n == name { + index = i + break } - allNames[i] = svc.Name } - return nil, fmt.Errorf("no Docker Compose service found with name '%s'. "+ - "Found these instead:\n\t%s", name, strings.Join(allNames, "; ")) + s.dc[projectName].serviceNames[index] = newName + svc.Name = newName + return newName, nil } // A docker-compose service, according to Tilt. type dcService struct { Name string + // Contains the name of the service as referenced in the compose file where it was loaded. + ServiceName string + // these are the host machine paths that DC will sync from the local volume into the container // https://docs.docker.com/compose/compose-file/#volumes MountedLocalDirs []string @@ -354,6 +433,7 @@ func dockerComposeConfigToService(projectName string, svcConfig types.ServiceCon svc := dcService{ Name: svcConfig.Name, + ServiceName: svcConfig.Name, ServiceConfig: svcConfig, MountedLocalDirs: mountedLocalDirs, ServiceYAML: rawConfig, @@ -386,7 +466,7 @@ func parseDCConfig(ctx context.Context, dcc dockercompose.DockerComposeClient, s return services, nil } -func (s *tiltfileState) dcServiceToManifest(service *dcService, dcSet dcResourceSet, iTargets []model.ImageTarget) (model.Manifest, error) { +func (s *tiltfileState) dcServiceToManifest(service *dcService, dcSet *dcResourceSet, iTargets []model.ImageTarget) (model.Manifest, error) { options := service.Options if options == nil { options = newDcResourceOptions() @@ -395,7 +475,7 @@ func (s *tiltfileState) dcServiceToManifest(service *dcService, dcSet dcResource dcInfo := model.DockerComposeTarget{ Name: model.TargetName(service.Name), Spec: v1alpha1.DockerComposeServiceSpec{ - Service: service.Name, + Service: service.ServiceName, Project: dcSet.Project, }, ServiceYAML: string(service.ServiceYAML), diff --git a/internal/tiltfile/k8s.go b/internal/tiltfile/k8s.go index 356f941296..e0ac5a49c5 100644 --- a/internal/tiltfile/k8s.go +++ b/internal/tiltfile/k8s.go @@ -639,8 +639,12 @@ func (s *tiltfileState) checkResourceConflict(name string) error { if s.localByName[name] != nil { return fmt.Errorf("local_resource named %q already exists", name) } - if s.dcByName[name] != nil { - return fmt.Errorf("dc_resource named %q already exists", name) + for _, dc := range s.dc { + for n := range dc.services { + if name == n { + return fmt.Errorf("dc_resource named %q already exists", name) + } + } } return nil } diff --git a/internal/tiltfile/tiltfile_docker_compose_test.go b/internal/tiltfile/tiltfile_docker_compose_test.go index 1f1d53319c..391b34789f 100644 --- a/internal/tiltfile/tiltfile_docker_compose_test.go +++ b/internal/tiltfile/tiltfile_docker_compose_test.go @@ -3,6 +3,7 @@ package tiltfile import ( "fmt" "path/filepath" + "strconv" "testing" "github.com/stretchr/testify/assert" @@ -372,7 +373,7 @@ networks: f.assertConfigFiles(expectedConfFiles...) } -func TestMultipleDockerComposeDifferentDirsNotSupported(t *testing.T) { +func TestMultipleDockerComposeDifferentDirs(t *testing.T) { f := newFixture(t) f.dockerfile(filepath.Join("foo", "Dockerfile")) @@ -384,10 +385,31 @@ func TestMultipleDockerComposeDifferentDirsNotSupported(t *testing.T) { tf := ` include('./subdir/Tiltfile') +dc_resource('foo', project_name='subdir', new_name='foo2') docker_compose('docker-compose1.yml')` f.file("Tiltfile", tf) - f.loadErrString("Cannot load docker-compose files from two different Tiltfiles") + f.load() + + assert.Equal(t, 2, len(f.loadResult.Manifests)) +} + +func TestMultipleDockerComposeNameConflict(t *testing.T) { + f := newFixture(t) + + f.dockerfile(filepath.Join("foo", "Dockerfile")) + f.file("docker-compose1.yml", simpleConfig) + + f.dockerfile(filepath.Join("subdir", "foo", "Dockerfile")) + f.file(filepath.Join("subdir", "Tiltfile"), `docker_compose('docker-compose2.yml')`) + f.file(filepath.Join("subdir", "docker-compose2.yml"), simpleConfig) + + tf := ` +include('./subdir/Tiltfile') +docker_compose('docker-compose1.yml')` + f.file("Tiltfile", tf) + + f.loadErrString(`dc_resource named "foo" already exists`) } func TestMultipleDockerComposeSameDir(t *testing.T) { @@ -421,6 +443,29 @@ k8s_yaml('bar.yaml')` assert.Equal(t, 2, len(f.loadResult.Manifests)) } +func TestResourceConflictCombinations(t *testing.T) { + tt := [][2]string{ + {`docker_compose('docker-compose.yml') +k8s_yaml('foo.yaml')`, `dc_resource named "foo" already exists`}, + {`k8s_yaml('foo.yaml') +docker_compose('docker-compose.yml')`, `dc_resource named "foo" already exists`}, + {`docker_compose('docker-compose.yml') +local_resource('foo', 'echo hello')`, `dc_resource named "foo" already exists`}, + {`local_resource('foo', 'echo hello') +docker_compose('docker-compose.yml')`, `local_resource named "foo" already exists`}, + } + + for i, tc := range tt { + t.Run(strconv.Itoa(i), func(t *testing.T) { + f := newFixture(t) + f.setupFooAndBar() + f.file("docker-compose.yml", simpleConfig) + f.file("Tiltfile", tc[0]) + f.loadErrString(tc[1]) + }) + } +} + func TestDockerComposeResourceCreationFromAbsPath(t *testing.T) { f := newFixture(t) diff --git a/internal/tiltfile/tiltfile_state.go b/internal/tiltfile/tiltfile_state.go index fbf97b08af..e8ce2b45df 100644 --- a/internal/tiltfile/tiltfile_state.go +++ b/internal/tiltfile/tiltfile_state.go @@ -70,7 +70,7 @@ var unmatchedImageAllUnresourcedWarning = "No Kubernetes configs with images fou var pkgInitTime = time.Now() type resourceSet struct { - dc dcResourceSet // currently only support one d-c.yml + dc []*dcResourceSet k8s []*k8sResource } @@ -101,9 +101,7 @@ type tiltfileState struct { k8sByName map[string]*k8sResource k8sUnresourced []k8s.K8sEntity - dc dcResourceSet // currently only support one d-c.yml - dcByName map[string]*dcService - dcResOptions map[string]*dcResourceOptions + dc dcResourceMap k8sResourceOptions []k8sResourceOptions localResources []*localResource @@ -177,8 +175,7 @@ func newTiltfileState( buildIndex: newBuildIndex(), k8sObjectIndex: tiltfile_k8s.NewState(), k8sByName: make(map[string]*k8sResource), - dcByName: make(map[string]*dcService), - dcResOptions: make(map[string]*dcResourceOptions), + dc: make(map[string]*dcResourceSet), localByName: make(map[string]*localResource), usedImages: make(map[string]bool), logger: logger.Get(ctx), @@ -279,16 +276,18 @@ to your Tiltfile. Otherwise, switch k8s contexts and restart Tilt.`, kubeContext } } - if !resources.dc.Empty() { + if len(resources.dc) > 0 { if err := s.validateDockerComposeVersion(); err != nil { return nil, result, err } - ms, err := s.translateDC(resources.dc) - if err != nil { - return nil, result, err + for _, dc := range resources.dc { + ms, err := s.translateDC(dc) + if err != nil { + return nil, result, err + } + manifests = append(manifests, ms...) } - manifests = append(manifests, ms...) } err = s.validateLiveUpdatesForManifests(manifests) @@ -607,8 +606,13 @@ func (s *tiltfileState) assemble() (resourceSet, []k8s.K8sEntity, error) { return resourceSet{}, nil, err } + dcRes := []*dcResourceSet{} + for _, resSet := range s.dc { + dcRes = append(dcRes, resSet) + } + return resourceSet{ - dc: s.dc, + dc: dcRes, k8s: s.k8s, }, s.k8sUnresourced, nil } @@ -635,7 +639,9 @@ func (s *tiltfileState) assertAllImagesMatched(us model.UpdateSettings) error { return nil } - if len(s.dc.services) == 0 && len(s.k8s) == 0 && len(s.k8sUnresourced) == 0 { + dcSvcCount := s.dc.ServiceCount() + + if dcSvcCount == 0 && len(s.k8s) == 0 && len(s.k8sUnresourced) == 0 { return fmt.Errorf(unmatchedImageNoConfigsWarning) } @@ -644,7 +650,7 @@ func (s *tiltfileState) assertAllImagesMatched(us model.UpdateSettings) error { } configType := "Kubernetes" - if len(s.dc.services) > 0 { + if dcSvcCount > 0 { configType = "Docker Compose" } return s.buildIndex.unmatchedImageWarning(unmatchedImages[0], configType) @@ -685,25 +691,25 @@ func (s *tiltfileState) assembleImages() error { } func (s *tiltfileState) assembleDC() error { - if len(s.dc.services) > 0 && !container.IsEmptyRegistry(s.defaultReg) { + if s.dc.ServiceCount() > 0 && !container.IsEmptyRegistry(s.defaultReg) { return errors.New("default_registry is not supported with docker compose") } - for _, svc := range s.dc.services { - builder := s.buildIndex.findBuilderForConsumedImage(svc.ImageRef()) - if builder != nil { - // there's a Tilt-managed builder (e.g. docker_build or custom_build) for this image reference, so use that - svc.ImageMapDeps = append(svc.ImageMapDeps, builder.ImageMapName()) - } else { - // create a DockerComposeBuild image target and consume it if this service has a build section in YAML - err := s.maybeAddDockerComposeImageBuilder(svc) - if err != nil { - return err + for _, resSet := range s.dc { + for _, svcName := range resSet.serviceNames { + svc := resSet.services[svcName] + builder := s.buildIndex.findBuilderForConsumedImage(svc.ImageRef()) + if builder != nil { + // there's a Tilt-managed builder (e.g. docker_build or custom_build) for this image reference, so use that + svc.ImageMapDeps = append(svc.ImageMapDeps, builder.ImageMapName()) + } else { + // create a DockerComposeBuild image target and consume it if this service has a build section in YAML + err := s.maybeAddDockerComposeImageBuilder(svc) + if err != nil { + return err + } } } - // TODO(maia): throw warning if - // a. there is an img ref from config, and img ref from user doesn't match - // b. there is no img ref from config, and img ref from user is not of form .*_ } return nil } @@ -719,7 +725,7 @@ func (s *tiltfileState) maybeAddDockerComposeImageBuilder(svc *dcService) error buildContext := build.Context if !filepath.IsAbs(buildContext) { // the Compose loader should always ensure that context paths are absolute upfront - return fmt.Errorf("Docker Compose service %q has a relative build path: %q", svc.Name, buildContext) + return fmt.Errorf("Docker Compose service %q has a relative build path: %q", svc.ServiceName, buildContext) } dfPath := build.Dockerfile @@ -737,7 +743,7 @@ func (s *tiltfileState) maybeAddDockerComposeImageBuilder(svc *dcService) error &dockerImage{ buildType: DockerComposeBuild, configurationRef: container.NewRefSelector(imageRef), - dockerComposeService: svc.Name, + dockerComposeService: svc.ServiceName, dockerComposeLocalVolumePaths: svc.MountedLocalDirs, dbBuildPath: buildContext, dbDockerfilePath: dfPath, @@ -1494,11 +1500,11 @@ func (s *tiltfileState) imgTargetsForDepsHelper(mn model.ManifestName, imageMapD return iTargets, nil } -func (s *tiltfileState) translateDC(dc dcResourceSet) ([]model.Manifest, error) { +func (s *tiltfileState) translateDC(dc *dcResourceSet) ([]model.Manifest, error) { var result []model.Manifest - for _, svc := range dc.services { - + for _, name := range dc.serviceNames { + svc := dc.services[name] iTargets, err := s.imgTargetsForDeps(model.ManifestName(svc.Name), svc.ImageMapDeps) if err != nil { return nil, errors.Wrapf(err, "getting image build info for %s", svc.Name) diff --git a/pkg/model/dockercompose.go b/pkg/model/dockercompose.go deleted file mode 100644 index 17283a9052..0000000000 --- a/pkg/model/dockercompose.go +++ /dev/null @@ -1,20 +0,0 @@ -package model - -import ( - "regexp" - "strings" - - "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" -) - -func IsEmptyDockerComposeProject(p v1alpha1.DockerComposeProject) bool { - return len(p.ConfigPaths) == 0 && p.YAML == "" -} - -// normalization logic from https://github.com/compose-spec/compose-go/blob/c39f6e771fe5034fe1bec40ba5f0285ec60f5efe/cli/options.go#L366-L371 -func NormalizeName(s string) string { - r := regexp.MustCompile("[a-z0-9_-]") - s = strings.ToLower(s) - s = strings.Join(r.FindAllString(s, -1), "") - return strings.TrimLeft(s, "_-") -} diff --git a/vendor/github.com/compose-spec/compose-go/cli/options.go b/vendor/github.com/compose-spec/compose-go/cli/options.go index 65ec53977f..0c798f19e7 100644 --- a/vendor/github.com/compose-spec/compose-go/cli/options.go +++ b/vendor/github.com/compose-spec/compose-go/cli/options.go @@ -18,7 +18,7 @@ package cli import ( "fmt" - "io/ioutil" + "io" "os" "path/filepath" "strings" @@ -63,6 +63,9 @@ func NewProjectOptions(configs []string, opts ...ProjectOptionsFn) (*ProjectOpti // WithName defines ProjectOptions' name func WithName(name string) ProjectOptionsFn { return func(o *ProjectOptions) error { + if name != loader.NormalizeProjectName(name) { + return fmt.Errorf("%q is not a valid project name", name) + } o.Name = name return nil } @@ -142,7 +145,7 @@ func WithDefaultConfigPath(o *ProjectOptions) error { // 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 getAsEqualsMap(env) { + for k, v := range utils.GetAsEqualsMap(env) { o.Environment[k] = v } return nil @@ -166,7 +169,10 @@ func WithLoadOptions(loadOptions ...func(*loader.Options)) ProjectOptionsFn { // WithOsEnv imports environment variables from OS func WithOsEnv(o *ProjectOptions) error { - for k, v := range getAsEqualsMap(os.Environ()) { + for k, v := range utils.GetAsEqualsMap(os.Environ()) { + if _, set := o.Environment[k]; set { + continue + } o.Environment[k] = v } return nil @@ -182,63 +188,75 @@ func WithEnvFile(file string) ProjectOptionsFn { // WithDotEnv imports environment variables from .env file func WithDotEnv(o *ProjectOptions) error { - dotEnvFile := o.EnvFile - if dotEnvFile == "" { - wd, err := o.GetWorkingDir() - if err != nil { - return err + wd, err := o.GetWorkingDir() + if err != nil { + return err + } + envMap, err := GetEnvFromFile(o.Environment, wd, o.EnvFile) + if err != nil { + return err + } + for k, v := range envMap { + o.Environment[k] = v + if osVal, ok := os.LookupEnv(k); ok { + o.Environment[k] = osVal } - dotEnvFile = filepath.Join(wd, ".env") + } + return nil +} + +func GetEnvFromFile(currentEnv map[string]string, workingDir string, filename string) (map[string]string, error) { + envMap := make(map[string]string) + + dotEnvFile := filename + if dotEnvFile == "" { + dotEnvFile = filepath.Join(workingDir, ".env") } abs, err := filepath.Abs(dotEnvFile) if err != nil { - return err + return envMap, err } dotEnvFile = abs s, err := os.Stat(dotEnvFile) if os.IsNotExist(err) { - if o.EnvFile != "" { - return errors.Errorf("Couldn't find env file: %s", o.EnvFile) + if filename != "" { + return nil, errors.Errorf("Couldn't find env file: %s", filename) } - return nil + return envMap, nil } if err != nil { - return err + return envMap, err } if s.IsDir() { - if o.EnvFile == "" { - return nil + if filename == "" { + return envMap, nil } - return errors.Errorf("%s is a directory", dotEnvFile) + return envMap, errors.Errorf("%s is a directory", dotEnvFile) } file, err := os.Open(dotEnvFile) if err != nil { - return err + return envMap, err } defer file.Close() - notInEnvSet := make(map[string]interface{}) env, err := dotenv.ParseWithLookup(file, func(k string) (string, bool) { - v, ok := os.LookupEnv(k) + v, ok := currentEnv[k] if !ok { - notInEnvSet[k] = nil - return "", true + return "", false } return v, true }) if err != nil { - return err + return envMap, err } for k, v := range env { - if _, ok := notInEnvSet[k]; ok { - continue - } - o.Environment[k] = v + envMap[k] = v } - return nil + + return envMap, nil } // WithInterpolation set ProjectOptions to enable/skip interpolation @@ -304,7 +322,7 @@ func ProjectFromOptions(options *ProjectOptions) (*types.Project, error) { for _, f := range configPaths { var b []byte if f == "-" { - b, err = ioutil.ReadAll(os.Stdin) + b, err = io.ReadAll(os.Stdin) if err != nil { return nil, err } @@ -313,7 +331,7 @@ func ProjectFromOptions(options *ProjectOptions) (*types.Project, error) { if err != nil { return nil, err } - b, err = ioutil.ReadFile(f) + b, err = os.ReadFile(f) if err != nil { return nil, err } @@ -365,6 +383,7 @@ func withNamePrecedenceLoad(absWorkingDir string, options *ProjectOptions) func( func withConvertWindowsPaths(options *ProjectOptions) func(*loader.Options) { return func(o *loader.Options) { o.ConvertWindowsPaths = utils.StringToBool(options.Environment["COMPOSE_CONVERT_WINDOWS_PATHS"]) + o.ResolvePaths = true } } @@ -406,22 +425,3 @@ func absolutePaths(p []string) ([]string, error) { } return paths, nil } - -// getAsEqualsMap split key=value formatted strings into a key : value map -func getAsEqualsMap(em []string) map[string]string { - m := make(map[string]string) - for _, v := range em { - kv := strings.SplitN(v, "=", 2) - m[kv[0]] = kv[1] - } - return m -} - -// getAsEqualsMap format a key : value map into key=value strings -func getAsStringList(em map[string]string) []string { - m := make([]string, 0, len(em)) - for k, v := range em { - m = append(m, fmt.Sprintf("%s=%s", k, v)) - } - return m -} diff --git a/vendor/github.com/compose-spec/compose-go/dotenv/godotenv.go b/vendor/github.com/compose-spec/compose-go/dotenv/godotenv.go index 479831aac8..c1c12eafe0 100644 --- a/vendor/github.com/compose-spec/compose-go/dotenv/godotenv.go +++ b/vendor/github.com/compose-spec/compose-go/dotenv/godotenv.go @@ -4,29 +4,32 @@ // // The TL;DR is that you make a .env file that looks something like // -// SOME_ENV_VAR=somevalue +// SOME_ENV_VAR=somevalue // // and then in your go code you can call // -// godotenv.Load() +// godotenv.Load() // // and all the env vars declared in .env will be available through os.Getenv("SOME_ENV_VAR") package dotenv import ( - "errors" + "bytes" "fmt" "io" - "io/ioutil" "os" "os/exec" "regexp" "sort" "strconv" "strings" + + "github.com/compose-spec/compose-go/template" ) -const doubleQuoteSpecialChars = "\\\n\r\"!$`" +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) @@ -42,86 +45,91 @@ func Parse(r io.Reader) (map[string]string, error) { // 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) { - data, err := ioutil.ReadAll(r) + data, err := io.ReadAll(r) if err != nil { return nil, 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 UnmarshalBytesWithLookup(data, 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) +// 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 +// 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 +// You can otherwise tell it which files to load (there can be more than one) like: // -// godotenv.Load("fileone", "filetwo") +// 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) (err error) { +func Load(filenames ...string) error { return load(false, filenames...) } // Overload 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) +// Call this function as close as possible to the start of your program (ideally in main). // -// If you call Overload without any args it will default to loading .env in the current path +// If you call Overload 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 +// You can otherwise tell it which files to load (there can be more than one) like: // -// godotenv.Overload("fileone", "filetwo") +// godotenv.Overload("fileone", "filetwo") // // It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefilly set all vars. -func Overload(filenames ...string) (err error) { +func Overload(filenames ...string) error { return load(true, filenames...) } -func load(overload bool, filenames ...string) (err error) { +func load(overload bool, filenames ...string) error { filenames = filenamesOrDefault(filenames) - for _, filename := range filenames { - err = loadFile(filename, overload) + err := loadFile(filename, overload) if err != nil { - return // return early on a spazout + return err } } - return + 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) (envMap map[string]string, err error) { +func ReadWithLookup(lookupFn LookupFn, filenames ...string) (map[string]string, error) { filenames = filenamesOrDefault(filenames) - envMap = make(map[string]string) + envMap := make(map[string]string) for _, filename := range filenames { individualEnvMap, individualErr := readFile(filename, lookupFn) if individualErr != nil { - err = individualErr - return // return early on a spazout + return envMap, individualErr } for key, value := range individualEnvMap { + if startsWithDigitRegex.MatchString(key) { + continue + } envMap[key] = value } } - return + 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) (envMap map[string]string, err error) { +func Read(filenames ...string) (map[string]string, error) { return ReadWithLookup(nil, filenames...) } // Unmarshal reads an env file from a string, returning a map of keys and values. -func Unmarshal(str string) (envMap map[string]string, err error) { +func Unmarshal(str string) (map[string]string, error) { return UnmarshalBytes([]byte(str)) } @@ -144,6 +152,8 @@ func UnmarshalBytesWithLookup(src []byte, lookupFn LookupFn) (map[string]string, // // If you want more fine grained control over your command it's recommended // that you use `Load()` or `Read()` and the `os/exec` package yourself. +// +// Deprecated: Use the `os/exec` package directly. func Exec(filenames []string, cmd string, cmdArgs []string) error { if err := Load(filenames...); err != nil { return err @@ -157,7 +167,10 @@ func Exec(filenames []string, cmd string, cmdArgs []string) error { } // Write serializes the given environment and writes it to a file +// +// Deprecated: The serialization functions are untested and unmaintained. func Write(envMap map[string]string, filename string) error { + //goland:noinspection GoDeprecation content, err := Marshal(envMap) if err != nil { return err @@ -176,13 +189,15 @@ func Write(envMap map[string]string, filename string) error { // Marshal outputs the given environment as a dotenv-formatted environment file. // Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped. +// +// Deprecated: The serialization functions are untested and unmaintained. func Marshal(envMap map[string]string) (string, error) { lines := make([]string, 0, len(envMap)) for k, v := range envMap { if d, err := strconv.Atoi(v); err == nil { lines = append(lines, fmt.Sprintf(`%s=%d`, k, d)) } else { - lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v))) + lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v))) // nolint // Cannot use %q here } } sort.Strings(lines) @@ -218,149 +233,32 @@ func loadFile(filename string, overload bool) error { return nil } -func readFile(filename string, lookupFn LookupFn) (envMap map[string]string, err error) { +func readFile(filename string, lookupFn LookupFn) (map[string]string, error) { file, err := os.Open(filename) if err != nil { - return + return nil, err } defer file.Close() return ParseWithLookup(file, lookupFn) } -var exportRegex = regexp.MustCompile(`^\s*(?:export\s+)?(.*?)\s*$`) - -func parseLine(line string, envMap map[string]string) (key string, value string, err error) { - return parseLineWithLookup(line, envMap, nil) -} -func parseLineWithLookup(line string, envMap map[string]string, lookupFn LookupFn) (key string, value string, err error) { - if len(line) == 0 { - err = errors.New("zero length string") - return - } - - // ditch the comments (but keep quoted hashes) - if strings.Contains(line, "#") { - segmentsBetweenHashes := strings.Split(line, "#") - quotesAreOpen := false - var segmentsToKeep []string - for _, segment := range segmentsBetweenHashes { - if strings.Count(segment, "\"") == 1 || strings.Count(segment, "'") == 1 { - if quotesAreOpen { - quotesAreOpen = false - segmentsToKeep = append(segmentsToKeep, segment) - } else { - quotesAreOpen = true - } - } - - if len(segmentsToKeep) == 0 || quotesAreOpen { - segmentsToKeep = append(segmentsToKeep, segment) - } - } - - line = strings.Join(segmentsToKeep, "#") - } - - firstEquals := strings.Index(line, "=") - firstColon := strings.Index(line, ":") - splitString := strings.SplitN(line, "=", 2) - if firstColon != -1 && (firstColon < firstEquals || firstEquals == -1) { - // This is a yaml-style line - splitString = strings.SplitN(line, ":", 2) - } - - if len(splitString) != 2 { - err = errors.New("can't separate key from value") - return - } - key = exportRegex.ReplaceAllString(splitString[0], "$1") - - // Parse the value - value = parseValue(splitString[1], envMap, lookupFn) - return -} - -var ( - singleQuotesRegex = regexp.MustCompile(`\A'(.*)'\z`) - doubleQuotesRegex = regexp.MustCompile(`\A"(.*)"\z`) - escapeRegex = regexp.MustCompile(`\\.`) - unescapeCharsRegex = regexp.MustCompile(`\\([^$])`) -) - -func parseValue(value string, envMap map[string]string, lookupFn LookupFn) string { - - // trim - value = strings.Trim(value, " ") - - // check if we've got quoted values or possible escapes - if len(value) > 1 { - singleQuotes := singleQuotesRegex.FindStringSubmatch(value) - - doubleQuotes := doubleQuotesRegex.FindStringSubmatch(value) - - if singleQuotes != nil || doubleQuotes != nil { - // pull the quotes off the edges - value = value[1 : len(value)-1] - } - - if doubleQuotes != nil { - // expand newlines - value = escapeRegex.ReplaceAllStringFunc(value, func(match string) string { - c := strings.TrimPrefix(match, `\`) - switch c { - case "n": - return "\n" - case "r": - return "\r" - default: - return match - } - }) - // unescape characters - value = unescapeCharsRegex.ReplaceAllString(value, "$1") +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 := envMap[k]; ok { + return v, ok } - - if singleQuotes == nil { - value = expandVariables(value, envMap, lookupFn) - } - } - - return value -} - -var expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`) - -func expandVariables(v string, envMap map[string]string, lookupFn LookupFn) string { - return expandVarRegex.ReplaceAllStringFunc(v, func(s string) string { - submatch := expandVarRegex.FindStringSubmatch(s) - - if submatch == nil { - return s - } - if submatch[1] == "\\" || submatch[2] == "(" { - return submatch[0][1:] - } else if submatch[4] != "" { - // first check if we have defined this already earlier - if envMap[submatch[4]] != "" { - return envMap[submatch[4]] - } - if lookupFn == nil { - return "" - } - // if we have not defined it, check the lookup function provided - // by the user - s2, ok := lookupFn(submatch[4]) - if ok { - return s2 - } - return "" - } - return s + return lookupFn(k) }) + if err != nil { + return value, err + } + return retVal, nil } +// Deprecated: only used by unsupported/untested code for Marshal/Write. func doubleQuoteEscape(line string) string { + const doubleQuoteSpecialChars = "\\\n\r\"!$`" for _, c := range doubleQuoteSpecialChars { toReplace := "\\" + string(c) if c == '\n' { @@ -369,7 +267,7 @@ func doubleQuoteEscape(line string) string { if c == '\r' { toReplace = `\r` } - line = strings.Replace(line, string(c), toReplace, -1) + line = strings.ReplaceAll(line, string(c), toReplace) } return line } diff --git a/vendor/github.com/compose-spec/compose-go/dotenv/parser.go b/vendor/github.com/compose-spec/compose-go/dotenv/parser.go index 85ed2c0088..9a8d0f609e 100644 --- a/vendor/github.com/compose-spec/compose-go/dotenv/parser.go +++ b/vendor/github.com/compose-spec/compose-go/dotenv/parser.go @@ -4,6 +4,8 @@ import ( "bytes" "errors" "fmt" + "regexp" + "strconv" "strings" "unicode" ) @@ -12,12 +14,18 @@ const ( charComment = '#' prefixSingleQuote = '\'' prefixDoubleQuote = '"' +) - exportPrefix = "export" +var ( + escapeSeqRegex = regexp.MustCompile(`(\\(?:[abcfnrtv$"\\]|0\d{0,3}))`) + exportRegex = regexp.MustCompile(`^export\s+`) ) func parseBytes(src []byte, out map[string]string, lookupFn LookupFn) error { cutset := src + if lookupFn == nil { + lookupFn = noLookupFn + } for { cutset = getStatementStart(cutset) if cutset == nil { @@ -34,10 +42,6 @@ func parseBytes(src []byte, out map[string]string, lookupFn LookupFn) error { } if inherited { - if lookupFn == nil { - lookupFn = noLookupFn - } - value, ok := lookupFn(key) if ok { out[key] = value @@ -82,9 +86,11 @@ func getStatementStart(src []byte) []byte { } // locateKeyName locates and parses key name and returns rest of slice -func locateKeyName(src []byte) (key string, cutset []byte, inherited bool, err error) { +func locateKeyName(src []byte) (string, []byte, bool, error) { + var key string + var inherited bool // trim "export" and space at beginning - src = bytes.TrimLeftFunc(bytes.TrimPrefix(src, []byte(exportPrefix)), isSpace) + src = bytes.TrimLeftFunc(exportRegex.ReplaceAll(src, nil), isSpace) // locate key name end and validate it in single loop offset := 0 @@ -102,9 +108,9 @@ loop: offset = i + 1 inherited = char == '\n' break loop - case '_': + case '_', '.': default: - // variable name should match [A-Za-z0-9_] + // variable name should match [A-Za-z0-9_.] if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) { continue } @@ -121,27 +127,22 @@ loop: // trim whitespace key = strings.TrimRightFunc(key, unicode.IsSpace) - cutset = bytes.TrimLeftFunc(src[offset:], isSpace) + cutset := bytes.TrimLeftFunc(src[offset:], isSpace) return key, cutset, inherited, nil } // extractVarValue extracts variable value and returns rest of slice -func extractVarValue(src []byte, envMap map[string]string, lookupFn LookupFn) (value string, rest []byte, err error) { +func extractVarValue(src []byte, envMap map[string]string, lookupFn LookupFn) (string, []byte, error) { quote, isQuoted := hasQuotePrefix(src) if !isQuoted { // unquoted value - read until new line - end := bytes.IndexFunc(src, isNewLine) - var rest []byte - if end < 0 { - value := strings.Split(string(src), "#")[0] // Remove inline comments on unquoted lines - value = strings.TrimRightFunc(value, unicode.IsSpace) - return expandVariables(value, envMap, lookupFn), nil, nil - } + value, rest, _ := bytes.Cut(src, []byte("\n")) - value := strings.Split(string(src[0:end]), "#")[0] - value = strings.TrimRightFunc(value, unicode.IsSpace) - rest = src[end:] - return expandVariables(value, envMap, lookupFn), rest, nil + // Remove inline comments on unquoted lines + value, _, _ = bytes.Cut(value, []byte(" #")) + value = bytes.TrimRightFunc(value, unicode.IsSpace) + retVal, err := expandVariables(string(value), envMap, lookupFn) + return retVal, rest, err } // lookup quoted string terminator @@ -156,12 +157,15 @@ func extractVarValue(src []byte, envMap map[string]string, lookupFn LookupFn) (v } // trim quotes - trimFunc := isCharFunc(rune(quote)) - value = string(bytes.TrimLeftFunc(bytes.TrimRightFunc(src[0:i], trimFunc), trimFunc)) + value := string(src[1:i]) if quote == prefixDoubleQuote { - // unescape newlines for double quote (this is compat feature) - // and expand environment variables - value = expandVariables(expandEscapes(value), envMap, lookupFn) + // expand standard shell escape sequences & then interpolate + // variables on the result + retVal, err := expandVariables(expandEscapes(value), envMap, lookupFn) + if err != nil { + return "", nil, err + } + value = retVal } return value, src[i+1:], nil @@ -177,18 +181,35 @@ func extractVarValue(src []byte, envMap map[string]string, lookupFn LookupFn) (v } func expandEscapes(str string) string { - out := escapeRegex.ReplaceAllStringFunc(str, func(match string) string { - c := strings.TrimPrefix(match, `\`) - switch c { - case "n": - return "\n" - case "r": - return "\r" - default: + 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 unescapeCharsRegex.ReplaceAllString(out, "$1") + return out } func indexOfNonSpaceChar(src []byte) int { @@ -198,14 +219,14 @@ func indexOfNonSpaceChar(src []byte) int { } // hasQuotePrefix reports whether charset starts with single or double quote and returns quote character -func hasQuotePrefix(src []byte) (quote byte, isQuoted bool) { +func hasQuotePrefix(src []byte) (byte, bool) { if len(src) == 0 { return 0, false } - switch prefix := src[0]; prefix { + switch quote := src[0]; quote { case prefixDoubleQuote, prefixSingleQuote: - return prefix, true + return quote, true // isQuoted default: return 0, false } @@ -227,8 +248,3 @@ func isSpace(r rune) bool { } return false } - -// isNewLine reports whether the rune is a new line character -func isNewLine(r rune) bool { - return r == '\n' -} diff --git a/vendor/github.com/compose-spec/compose-go/interpolation/interpolation.go b/vendor/github.com/compose-spec/compose-go/interpolation/interpolation.go index 9c36e6d8b1..befc6f049f 100644 --- a/vendor/github.com/compose-spec/compose-go/interpolation/interpolation.go +++ b/vendor/github.com/compose-spec/compose-go/interpolation/interpolation.go @@ -115,7 +115,7 @@ func newPathError(path Path, err error) error { return nil case *template.InvalidTemplateError: return errors.Errorf( - "invalid interpolation format for %s: %#v. You may need to escape any $ with another $", + "invalid interpolation format for %s.\nYou may need to escape any $ with another $.\n%s", path, err.Template) default: return errors.Wrapf(err, "error while interpolating %s", path) diff --git a/vendor/github.com/compose-spec/compose-go/loader/example1.env b/vendor/github.com/compose-spec/compose-go/loader/example1.env index f19ec0df4e..61716e93b5 100644 --- a/vendor/github.com/compose-spec/compose-go/loader/example1.env +++ b/vendor/github.com/compose-spec/compose-go/loader/example1.env @@ -1,5 +1,7 @@ # passed through FOO=foo_from_env_file +ENV.WITH.DOT=ok +ENV_WITH_UNDERSCORE=ok # overridden in example2.env BAR=bar_from_env_file diff --git a/vendor/github.com/compose-spec/compose-go/loader/full-example.yml b/vendor/github.com/compose-spec/compose-go/loader/full-example.yml index 0a77f3aa3e..cb300c9f92 100644 --- a/vendor/github.com/compose-spec/compose-go/loader/full-example.yml +++ b/vendor/github.com/compose-spec/compose-go/loader/full-example.yml @@ -22,6 +22,13 @@ services: 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: @@ -402,6 +409,7 @@ configs: external: true config4: name: foo + file: ~/config_data x-bar: baz x-foo: bar @@ -417,6 +425,7 @@ secrets: external: true secret4: name: bar + environment: BAR x-bar: baz x-foo: bar x-bar: baz diff --git a/vendor/github.com/compose-spec/compose-go/loader/loader.go b/vendor/github.com/compose-spec/compose-go/loader/loader.go index 895bdb2609..acf11a9541 100644 --- a/vendor/github.com/compose-spec/compose-go/loader/loader.go +++ b/vendor/github.com/compose-spec/compose-go/loader/loader.go @@ -17,14 +17,14 @@ package loader import ( + "bytes" "fmt" - "io/ioutil" + "io" "os" - "path" + paths "path" "path/filepath" "reflect" "regexp" - "sort" "strconv" "strings" "time" @@ -70,7 +70,7 @@ type Options struct { } func (o *Options) SetProjectName(name string, imperativelySet bool) { - o.projectName = normalizeProjectName(name) + o.projectName = NormalizeProjectName(name) o.projectNameImperativelySet = imperativelySet } @@ -161,6 +161,8 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types. op(opts) } + projectName := projectName(configDetails, opts) + var configs []*types.Config for i, file := range configDetails.ConfigFiles { configDict := file.Config @@ -208,15 +210,6 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types. s.EnvFile = newEnvFiles } - projectName, projectNameImperativelySet := opts.GetProjectName() - model.Name = normalizeProjectName(model.Name) - if !projectNameImperativelySet && model.Name != "" { - projectName = model.Name - } - - if projectName != "" { - configDetails.Environment[consts.ComposeProjectName] = projectName - } project := &types.Project{ Name: projectName, WorkingDir: configDetails.WorkingDir, @@ -246,7 +239,31 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types. return project, nil } -func normalizeProjectName(s string) string { +func projectName(details types.ConfigDetails, opts *Options) string { + projectName, projectNameImperativelySet := opts.GetProjectName() + var pjNameFromConfigFile string + + for _, configFile := range details.ConfigFiles { + yml, err := ParseYAML(configFile.Content) + if err != nil { + return "" + } + if val, ok := yml["name"]; ok && val != "" { + pjNameFromConfigFile = yml["name"].(string) + } + } + pjNameFromConfigFile = NormalizeProjectName(pjNameFromConfigFile) + if !projectNameImperativelySet && pjNameFromConfigFile != "" { + projectName = pjNameFromConfigFile + } + + if _, ok := details.Environment[consts.ComposeProjectName]; !ok && projectName != "" { + details.Environment[consts.ComposeProjectName] = projectName + } + return projectName +} + +func NormalizeProjectName(s string) string { r := regexp.MustCompile("[a-z0-9_-]") s = strings.ToLower(s) s = strings.Join(r.FindAllString(s, -1), "") @@ -385,7 +402,7 @@ func createTransformHook(additionalTransformers ...Transformer) mapstructure.Dec reflect.TypeOf(types.MappingWithEquals{}): transformMappingOrListFunc("=", true), reflect.TypeOf(types.Labels{}): transformMappingOrListFunc("=", false), reflect.TypeOf(types.MappingWithColon{}): transformMappingOrListFunc(":", false), - reflect.TypeOf(types.HostsList{}): transformListOrMappingFunc(":", false), + reflect.TypeOf(types.HostsList{}): transformMappingOrListFunc(":", false), reflect.TypeOf(types.ServiceVolumeConfig{}): transformServiceVolumeConfig, reflect.TypeOf(types.BuildConfig{}): transformBuildConfig, reflect.TypeOf(types.Duration(0)): transformStringToDuration, @@ -508,12 +525,12 @@ func loadServiceWithExtends(filename, name string, servicesDict map[string]inter // Resolve the path to the imported file, and load it. baseFilePath := absPath(workingDir, *file) - bytes, err := ioutil.ReadFile(baseFilePath) + b, err := os.ReadFile(baseFilePath) if err != nil { return nil, err } - baseFile, err := parseConfig(bytes, opts) + baseFile, err := parseConfig(b, opts) if err != nil { return nil, err } @@ -571,18 +588,18 @@ func LoadService(name string, serviceDict map[string]interface{}, workingDir str if volume.Type != types.VolumeTypeBind { continue } - if volume.Source == "" { return nil, errors.New(`invalid mount config for type "bind": field Source must not be empty`) } - if resolvePaths { - serviceConfig.Volumes[i] = resolveVolumePath(volume, workingDir, lookupEnv) + if resolvePaths || convertPaths { + volume = resolveVolumePath(volume, workingDir, lookupEnv) } if convertPaths { - serviceConfig.Volumes[i] = convertVolumePath(volume) + volume = convertVolumePath(volume) } + serviceConfig.Volumes[i] = volume } return serviceConfig, nil @@ -607,14 +624,25 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, l environment := types.MappingWithEquals{} if len(serviceConfig.EnvFile) > 0 { + if serviceConfig.Environment == nil { + serviceConfig.Environment = types.MappingWithEquals{} + } for _, envFile := range serviceConfig.EnvFile { filePath := absPath(workingDir, envFile) file, err := os.Open(filePath) if err != nil { return err } - defer file.Close() - fileVars, err := dotenv.ParseWithLookup(file, dotenv.LookupFn(lookupEnv)) + + b, err := io.ReadAll(file) + if err != nil { + return err + } + + // Do not defer to avoid it inside a loop + file.Close() //nolint:errcheck + + fileVars, err := dotenv.ParseWithLookup(bytes.NewBuffer(b), dotenv.LookupFn(lookupEnv)) if err != nil { return err } @@ -640,7 +668,7 @@ func resolveVolumePath(volume types.ServiceVolumeConfig, workingDir string, look // 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(filePath) && !isAbs(filePath) { + if !paths.IsAbs(filePath) && !isAbs(filePath) { filePath = absPath(workingDir, filePath) } volume.Source = filePath @@ -794,10 +822,8 @@ func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfi logrus.Warnf("%[1]s %[2]s: %[1]s.external.name is deprecated in favor of %[1]s.name", objType, name) obj.Name = obj.External.Name obj.External.Name = "" - } else { - if obj.Name == "" { - obj.Name = name - } + } else if obj.Name == "" { + obj.Name = name } // if not "external: true" case obj.Driver != "": @@ -805,7 +831,7 @@ func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfi return obj, errors.Errorf("%[1]s %[2]s: %[1]s.driver and %[1]s.file conflict; only use %[1]s.driver", objType, name) } default: - if resolvePaths { + if obj.File != "" && resolvePaths { obj.File = absPath(details.WorkingDir, obj.File) } } @@ -929,7 +955,7 @@ func cleanTarget(target string) string { if target == "" { return "" } - return path.Clean(target) + return paths.Clean(target) } var transformBuildConfig TransformerFunc = func(data interface{}) (interface{}, error) { @@ -1060,22 +1086,6 @@ func transformMappingOrListFunc(sep string, allowNil bool) TransformerFunc { } } -func transformListOrMappingFunc(sep string, allowNil bool) TransformerFunc { - return func(data interface{}) (interface{}, error) { - return transformListOrMapping(data, sep, allowNil) - } -} - -func transformListOrMapping(listOrMapping interface{}, sep string, allowNil bool) (interface{}, error) { - switch value := listOrMapping.(type) { - case map[string]interface{}: - return toStringList(value, sep, allowNil), nil - case []interface{}: - return listOrMapping, nil - } - return nil, errors.Errorf("expected a map or a list, got %T: %#v", listOrMapping, listOrMapping) -} - func transformMappingOrList(mappingOrList interface{}, sep string, allowNil bool) (interface{}, error) { switch value := mappingOrList.(type) { case map[string]interface{}: @@ -1168,15 +1178,3 @@ func toString(value interface{}, allowNil bool) interface{} { return "" } } - -func toStringList(value map[string]interface{}, separator string, allowNil bool) []string { - var output []string - for key, value := range value { - if value == nil && !allowNil { - continue - } - output = append(output, fmt.Sprintf("%s%s%s", key, separator, value)) - } - sort.Strings(output) - return output -} diff --git a/vendor/github.com/compose-spec/compose-go/loader/merge.go b/vendor/github.com/compose-spec/compose-go/loader/merge.go index f6138ca292..bf10cf7794 100644 --- a/vendor/github.com/compose-spec/compose-go/loader/merge.go +++ b/vendor/github.com/compose-spec/compose-go/loader/merge.go @@ -299,7 +299,7 @@ func mergeLoggingConfig(dst, src reflect.Value) error { return nil } -// nolint: unparam +//nolint: unparam func mergeUlimitsConfig(dst, src reflect.Value) error { if src.Interface() != reflect.Zero(reflect.TypeOf(src.Interface())).Interface() { dst.Elem().Set(src.Elem()) @@ -307,7 +307,7 @@ func mergeUlimitsConfig(dst, src reflect.Value) error { return nil } -// nolint: unparam +//nolint: unparam func mergeServiceNetworkConfig(dst, src reflect.Value) error { if src.Interface() != reflect.Zero(reflect.TypeOf(src.Interface())).Interface() { dst.Elem().FieldByName("Aliases").Set(src.Elem().FieldByName("Aliases")) diff --git a/vendor/github.com/compose-spec/compose-go/loader/validate.go b/vendor/github.com/compose-spec/compose-go/loader/validate.go index 727e0b2613..4d635889d2 100644 --- a/vendor/github.com/compose-spec/compose-go/loader/validate.go +++ b/vendor/github.com/compose-spec/compose-go/loader/validate.go @@ -38,6 +38,12 @@ func checkConsistency(project *types.Project) error { } } + for dependedService := range s.DependsOn { + if _, err := project.GetService(dependedService); err != nil { + return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %q depends on undefined service %s", s.Name, dependedService)) + } + } + if strings.HasPrefix(s.NetworkMode, types.ServicePrefix) { serviceName := s.NetworkMode[len(types.ServicePrefix):] if _, err := project.GetServices(serviceName); err != nil { @@ -46,12 +52,9 @@ func checkConsistency(project *types.Project) error { } for _, volume := range s.Volumes { - switch volume.Type { - case types.VolumeTypeVolume: - if volume.Source != "" { // non anonymous volumes - if _, ok := project.Volumes[volume.Source]; !ok { - return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %q refers to undefined volume %s", s.Name, volume.Source)) - } + if volume.Type == types.VolumeTypeVolume && volume.Source != "" { // non anonymous volumes + if _, ok := project.Volumes[volume.Source]; !ok { + return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("service %q refers to undefined volume %s", s.Name, volume.Source)) } } } @@ -68,5 +71,15 @@ func checkConsistency(project *types.Project) error { } } } + + for name, secret := range project.Secrets { + if secret.External.External { + continue + } + if secret.File == "" && secret.Environment == "" { + return errors.Wrap(errdefs.ErrInvalid, fmt.Sprintf("secret %q must declare either `file` or `environment`", name)) + } + } + return nil } diff --git a/vendor/github.com/compose-spec/compose-go/loader/windows_path.go b/vendor/github.com/compose-spec/compose-go/loader/windows_path.go index 5094f5b576..e88261084a 100644 --- a/vendor/github.com/compose-spec/compose-go/loader/windows_path.go +++ b/vendor/github.com/compose-spec/compose-go/loader/windows_path.go @@ -44,7 +44,7 @@ func isAbs(path string) (b bool) { // volumeNameLen returns length of the leading volume name on Windows. // It returns 0 elsewhere. -// nolint: gocyclo +//nolint: gocyclo func volumeNameLen(path string) int { if len(path) < 2 { return 0 diff --git a/vendor/github.com/compose-spec/compose-go/schema/compose-spec.json b/vendor/github.com/compose-spec/compose-go/schema/compose-spec.json index b3485bee41..d444e71d82 100644 --- a/vendor/github.com/compose-spec/compose-go/schema/compose-spec.json +++ b/vendor/github.com/compose-spec/compose-go/schema/compose-spec.json @@ -102,7 +102,9 @@ "shm_size": {"type": ["integer", "string"]}, "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, "isolation": {"type": "string"}, - "secrets": {"$ref": "#/definitions/service_config_or_secret"} + "secrets": {"$ref": "#/definitions/service_config_or_secret"}, + "tags": {"type": "array", "items": {"type": "string"}}, + "platforms": {"type": "array", "items": {"type": "string"}} }, "additionalProperties": false, "patternProperties": {"^x-": {}} @@ -684,6 +686,7 @@ "type": "object", "properties": { "name": {"type": "string"}, + "environment": {"type": "string"}, "file": {"type": "string"}, "external": { "type": ["boolean", "object"], diff --git a/vendor/github.com/compose-spec/compose-go/template/template.go b/vendor/github.com/compose-spec/compose-go/template/template.go index 22e4e95ada..27f7067a73 100644 --- a/vendor/github.com/compose-spec/compose-go/template/template.go +++ b/vendor/github.com/compose-spec/compose-go/template/template.go @@ -19,6 +19,7 @@ package template import ( "fmt" "regexp" + "sort" "strings" "github.com/sirupsen/logrus" @@ -27,10 +28,10 @@ import ( var delimiter = "\\$" var substitutionNamed = "[_a-z][_a-z0-9]*" -var substitutionBraced = "[_a-z][_a-z0-9]*(?::?[-?](.*}|[^}]*))?" +var substitutionBraced = "[_a-z][_a-z0-9]*(?::?[-+?](.*}|[^}]*))?" var patternString = fmt.Sprintf( - "%s(?i:(?P%s)|(?P%s)|{(?P%s)}|(?P))", + "%s(?i:(?P%s)|(?P%s)|{(?:(?P%s)}|(?P)))", delimiter, delimiter, substitutionNamed, substitutionBraced, ) @@ -60,11 +61,15 @@ type SubstituteFunc func(string, Mapping) (string, bool, error) // 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) { - if len(subsFuncs) == 0 { - subsFuncs = getDefaultSortedSubstitutionFunctions(template) - } - var err error + var outerErr error + var returnErr error + result := pattern.ReplaceAllStringFunc(template, func(substring string) string { + _, subsFunc := getSubstitutionFunctionForTemplate(substring) + if len(subsFuncs) > 0 { + subsFunc = subsFuncs[0] + } + closingBraceIndex := getFirstBraceClosingIndex(substring) rest := "" if closingBraceIndex > -1 { @@ -86,24 +91,27 @@ func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, su } if substitution == "" { - err = &InvalidTemplateError{Template: template} + outerErr = &InvalidTemplateError{Template: template} + if returnErr == nil { + returnErr = outerErr + } return "" } if braced { - for _, f := range subsFuncs { - var ( - value string - applied bool - ) - value, applied, err = f(substitution, mapping) - if err != nil { - return "" - } - if !applied { - continue + var ( + value string + applied bool + ) + value, applied, outerErr = subsFunc(substitution, mapping) + if outerErr != nil { + if returnErr == nil { + returnErr = outerErr } - interpolatedNested, err := SubstituteWith(rest, mapping, pattern, subsFuncs...) + return "" + } + if applied { + interpolatedNested, err := SubstituteWith(rest, mapping, pattern) if err != nil { return "" } @@ -118,26 +126,34 @@ func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, su return value }) - return result, err + return result, returnErr } -func getDefaultSortedSubstitutionFunctions(template string, fns ...SubstituteFunc) []SubstituteFunc { - hyphenIndex := strings.IndexByte(template, '-') - questionIndex := strings.IndexByte(template, '?') - if hyphenIndex < 0 || hyphenIndex > questionIndex { - return []SubstituteFunc{ - requiredNonEmpty, - required, - softDefault, - hardDefault, - } - } - return []SubstituteFunc{ - softDefault, - hardDefault, - requiredNonEmpty, - required, +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 { @@ -203,9 +219,10 @@ func recurseExtract(value interface{}, pattern *regexp.Regexp) map[string]Variab } type Variable struct { - Name string - DefaultValue string - Required bool + Name string + DefaultValue string + PresenceValue string + Required bool } func extractVariable(value interface{}, pattern *regexp.Regexp) ([]Variable, bool) { @@ -229,6 +246,7 @@ func extractVariable(value interface{}, pattern *regexp.Regexp) ([]Variable, boo } name := val var defaultValue string + var presenceValue string var required bool switch { case strings.Contains(val, ":?"): @@ -241,19 +259,52 @@ func extractVariable(value interface{}, pattern *regexp.Regexp) ([]Variable, boo name, defaultValue = partition(val, ":-") case strings.Contains(val, "-"): name, defaultValue = partition(val, "-") + case strings.Contains(val, ":+"): + name, presenceValue = partition(val, ":+") + case strings.Contains(val, "+"): + name, presenceValue = partition(val, "+") } values = append(values, Variable{ - Name: name, - DefaultValue: defaultValue, - Required: required, + Name: name, + DefaultValue: defaultValue, + PresenceValue: presenceValue, + Required: required, }) } return values, len(values) > 0 } // Soft default (fall back if unset or empty) -func softDefault(substitution string, mapping Mapping) (string, bool, error) { - sep := ":-" +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 } @@ -263,15 +314,17 @@ func softDefault(substitution string, mapping Mapping) (string, bool, error) { return "", false, err } value, ok := mapping(name) - if !ok || value == "" { + if ok && (!notEmpty || (notEmpty && value != "")) { return defaultValue, true, nil } return value, true, nil } -// Hard default (fall back if-and-only-if empty) -func hardDefault(substitution string, mapping Mapping) (string, bool, error) { +func withDefaultWhenAbsence(substitution string, mapping Mapping, emptyOrUnset bool) (string, bool, error) { sep := "-" + if emptyOrUnset { + sep = ":-" + } if !strings.Contains(substitution, sep) { return "", false, nil } @@ -281,20 +334,12 @@ func hardDefault(substitution string, mapping Mapping) (string, bool, error) { return "", false, err } value, ok := mapping(name) - if !ok { + if !ok || (emptyOrUnset && value == "") { return defaultValue, true, nil } return value, true, nil } -func requiredNonEmpty(substitution string, mapping Mapping) (string, bool, error) { - return withRequired(substitution, mapping, ":?", func(v string) bool { return v != "" }) -} - -func required(substitution string, mapping Mapping) (string, bool, error) { - return withRequired(substitution, mapping, "?", func(_ string) bool { return true }) -} - func withRequired(substitution string, mapping Mapping, sep string, valid func(string) bool) (string, bool, error) { if !strings.Contains(substitution, sep) { return "", false, nil diff --git a/vendor/github.com/compose-spec/compose-go/types/config.go b/vendor/github.com/compose-spec/compose-go/types/config.go index b395363bd3..020e67dd24 100644 --- a/vendor/github.com/compose-spec/compose-go/types/config.go +++ b/vendor/github.com/compose-spec/compose-go/types/config.go @@ -18,10 +18,17 @@ package types import ( "encoding/json" + "runtime" + "strings" "github.com/mitchellh/mapstructure" ) +var ( + // isCaseInsensitiveEnvVars is true on platforms where environment variable names are treated case-insensitively. + isCaseInsensitiveEnvVars = (runtime.GOOS == "windows") +) + // ConfigDetails are the details about a group of ConfigFiles type ConfigDetails struct { Version string @@ -33,7 +40,21 @@ type ConfigDetails struct { // LookupEnv provides a lookup function for environment variables func (cd ConfigDetails) LookupEnv(key string) (string, bool) { v, ok := cd.Environment[key] - return v, ok + if !isCaseInsensitiveEnvVars || ok { + return v, ok + } + // variable names must be treated case-insensitively on some platforms (that is, Windows). + // Resolves in this way: + // * Return the value if its name matches with the passed name case-sensitively. + // * Otherwise, return the value if its lower-cased name matches lower-cased passed name. + // * The value is indefinite if multiple variables match. + lowerKey := strings.ToLower(key) + for k, v := range cd.Environment { + if strings.ToLower(k) == lowerKey { + return v, true + } + } + return "", false } // ConfigFile is a filename and the contents of the file as a Dict diff --git a/vendor/github.com/compose-spec/compose-go/types/project.go b/vendor/github.com/compose-spec/compose-go/types/project.go index 1c7a7e4019..6b28e863b6 100644 --- a/vendor/github.com/compose-spec/compose-go/types/project.go +++ b/vendor/github.com/compose-spec/compose-go/types/project.go @@ -23,7 +23,7 @@ import ( "sort" "github.com/distribution/distribution/v3/reference" - "github.com/opencontainers/go-digest" + godigest "github.com/opencontainers/go-digest" "golang.org/x/sync/errgroup" ) @@ -142,18 +142,19 @@ func (p Project) WithServices(names []string, fn ServiceFunc) error { return p.withServices(names, fn, map[string]bool{}) } -func (p Project) withServices(names []string, fn ServiceFunc, done map[string]bool) error { +func (p Project) withServices(names []string, fn ServiceFunc, seen map[string]bool) error { services, err := p.GetServices(names...) if err != nil { return err } for _, service := range services { - if done[service.Name] { + if seen[service.Name] { continue } + seen[service.Name] = true dependencies := service.GetDependencies() if len(dependencies) > 0 { - err := p.withServices(dependencies, fn, done) + err := p.withServices(dependencies, fn, seen) if err != nil { return err } @@ -161,7 +162,6 @@ func (p Project) withServices(names []string, fn ServiceFunc, done map[string]bo if err := fn(service); err != nil { return err } - done[service.Name] = true } return nil } @@ -311,7 +311,7 @@ func (p *Project) ForServices(names []string) error { } // ResolveImages updates services images to include digest computed by a resolver function -func (p *Project) ResolveImages(resolver func(named reference.Named) (digest.Digest, error)) error { +func (p *Project) ResolveImages(resolver func(named reference.Named) (godigest.Digest, error)) error { eg := errgroup.Group{} for i, s := range p.Services { idx := i diff --git a/vendor/github.com/compose-spec/compose-go/types/types.go b/vendor/github.com/compose-spec/compose-go/types/types.go index 58369d4c49..cb42c6e941 100644 --- a/vendor/github.com/compose-spec/compose-go/types/types.go +++ b/vendor/github.com/compose-spec/compose-go/types/types.go @@ -88,90 +88,102 @@ type ServiceConfig struct { Name string `yaml:"-" json:"-"` Profiles []string `mapstructure:"profiles" yaml:"profiles,omitempty" json:"profiles,omitempty"` - Build *BuildConfig `yaml:",omitempty" json:"build,omitempty"` - BlkioConfig *BlkioConfig `mapstructure:"blkio_config" yaml:",omitempty" json:"blkio_config,omitempty"` - CapAdd []string `mapstructure:"cap_add" yaml:"cap_add,omitempty" json:"cap_add,omitempty"` - CapDrop []string `mapstructure:"cap_drop" yaml:"cap_drop,omitempty" json:"cap_drop,omitempty"` - CgroupParent string `mapstructure:"cgroup_parent" yaml:"cgroup_parent,omitempty" json:"cgroup_parent,omitempty"` - CPUCount int64 `mapstructure:"cpu_count" yaml:"cpu_count,omitempty" json:"cpu_count,omitempty"` - CPUPercent float32 `mapstructure:"cpu_percent" yaml:"cpu_percent,omitempty" json:"cpu_percent,omitempty"` - CPUPeriod int64 `mapstructure:"cpu_period" yaml:"cpu_period,omitempty" json:"cpu_period,omitempty"` - CPUQuota int64 `mapstructure:"cpu_quota" yaml:"cpu_quota,omitempty" json:"cpu_quota,omitempty"` - CPURTPeriod int64 `mapstructure:"cpu_rt_period" yaml:"cpu_rt_period,omitempty" json:"cpu_rt_period,omitempty"` - CPURTRuntime int64 `mapstructure:"cpu_rt_runtime" yaml:"cpu_rt_runtime,omitempty" json:"cpu_rt_runtime,omitempty"` - CPUS float32 `mapstructure:"cpus" yaml:"cpus,omitempty" json:"cpus,omitempty"` - CPUSet string `mapstructure:"cpuset" yaml:"cpuset,omitempty" json:"cpuset,omitempty"` - CPUShares int64 `mapstructure:"cpu_shares" yaml:"cpu_shares,omitempty" json:"cpu_shares,omitempty"` - Command ShellCommand `yaml:",omitempty" json:"command,omitempty"` - Configs []ServiceConfigObjConfig `yaml:",omitempty" json:"configs,omitempty"` - ContainerName string `mapstructure:"container_name" yaml:"container_name,omitempty" json:"container_name,omitempty"` - CredentialSpec *CredentialSpecConfig `mapstructure:"credential_spec" yaml:"credential_spec,omitempty" json:"credential_spec,omitempty"` - DependsOn DependsOnConfig `mapstructure:"depends_on" yaml:"depends_on,omitempty" json:"depends_on,omitempty"` - Deploy *DeployConfig `yaml:",omitempty" json:"deploy,omitempty"` - DeviceCgroupRules []string `mapstructure:"device_cgroup_rules" yaml:"device_cgroup_rules,omitempty" json:"device_cgroup_rules,omitempty"` - Devices []string `yaml:",omitempty" json:"devices,omitempty"` - DNS StringList `yaml:",omitempty" json:"dns,omitempty"` - DNSOpts []string `mapstructure:"dns_opt" yaml:"dns_opt,omitempty" json:"dns_opt,omitempty"` - DNSSearch StringList `mapstructure:"dns_search" yaml:"dns_search,omitempty" json:"dns_search,omitempty"` - Dockerfile string `yaml:"dockerfile,omitempty" json:"dockerfile,omitempty"` - DomainName string `mapstructure:"domainname" yaml:"domainname,omitempty" json:"domainname,omitempty"` - Entrypoint ShellCommand `yaml:",omitempty" json:"entrypoint,omitempty"` - Environment MappingWithEquals `yaml:",omitempty" json:"environment,omitempty"` - EnvFile StringList `mapstructure:"env_file" yaml:"env_file,omitempty" json:"env_file,omitempty"` - Expose StringOrNumberList `yaml:",omitempty" json:"expose,omitempty"` - Extends ExtendsConfig `yaml:"extends,omitempty" json:"extends,omitempty"` - ExternalLinks []string `mapstructure:"external_links" yaml:"external_links,omitempty" json:"external_links,omitempty"` - ExtraHosts HostsList `mapstructure:"extra_hosts" yaml:"extra_hosts,omitempty" json:"extra_hosts,omitempty"` - GroupAdd []string `mapstructure:"group_add" yaml:"group_add,omitempty" json:"group_add,omitempty"` - Hostname string `yaml:",omitempty" json:"hostname,omitempty"` - HealthCheck *HealthCheckConfig `yaml:",omitempty" json:"healthcheck,omitempty"` - Image string `yaml:",omitempty" json:"image,omitempty"` - Init *bool `yaml:",omitempty" json:"init,omitempty"` - Ipc string `yaml:",omitempty" json:"ipc,omitempty"` - Isolation string `mapstructure:"isolation" yaml:"isolation,omitempty" json:"isolation,omitempty"` - Labels Labels `yaml:",omitempty" json:"labels,omitempty"` - CustomLabels Labels `yaml:"-" json:"-"` - Links []string `yaml:",omitempty" json:"links,omitempty"` - Logging *LoggingConfig `yaml:",omitempty" json:"logging,omitempty"` - LogDriver string `mapstructure:"log_driver" yaml:"log_driver,omitempty" json:"log_driver,omitempty"` - LogOpt map[string]string `mapstructure:"log_opt" yaml:"log_opt,omitempty" json:"log_opt,omitempty"` - MemLimit UnitBytes `mapstructure:"mem_limit" yaml:"mem_limit,omitempty" json:"mem_limit,omitempty"` - MemReservation UnitBytes `mapstructure:"mem_reservation" yaml:"mem_reservation,omitempty" json:"mem_reservation,omitempty"` - MemSwapLimit UnitBytes `mapstructure:"memswap_limit" yaml:"memswap_limit,omitempty" json:"memswap_limit,omitempty"` - MemSwappiness UnitBytes `mapstructure:"mem_swappiness" yaml:"mem_swappiness,omitempty" json:"mem_swappiness,omitempty"` - MacAddress string `mapstructure:"mac_address" yaml:"mac_address,omitempty" json:"mac_address,omitempty"` - Net string `yaml:"net,omitempty" json:"net,omitempty"` - NetworkMode string `mapstructure:"network_mode" yaml:"network_mode,omitempty" json:"network_mode,omitempty"` - Networks map[string]*ServiceNetworkConfig `yaml:",omitempty" json:"networks,omitempty"` - OomKillDisable bool `mapstructure:"oom_kill_disable" yaml:"oom_kill_disable,omitempty" json:"oom_kill_disable,omitempty"` - OomScoreAdj int64 `mapstructure:"oom_score_adj" yaml:"oom_score_adj,omitempty" json:"oom_score_adj,omitempty"` - Pid string `yaml:",omitempty" json:"pid,omitempty"` - PidsLimit int64 `mapstructure:"pids_limit" yaml:"pids_limit,omitempty" json:"pids_limit,omitempty"` - Platform string `yaml:",omitempty" json:"platform,omitempty"` - Ports []ServicePortConfig `yaml:",omitempty" json:"ports,omitempty"` - Privileged bool `yaml:",omitempty" json:"privileged,omitempty"` - PullPolicy string `mapstructure:"pull_policy" yaml:"pull_policy,omitempty" json:"pull_policy,omitempty"` - ReadOnly bool `mapstructure:"read_only" yaml:"read_only,omitempty" json:"read_only,omitempty"` - Restart string `yaml:",omitempty" json:"restart,omitempty"` - Runtime string `yaml:",omitempty" json:"runtime,omitempty"` - Scale int `yaml:"-" json:"-"` - Secrets []ServiceSecretConfig `yaml:",omitempty" json:"secrets,omitempty"` - SecurityOpt []string `mapstructure:"security_opt" yaml:"security_opt,omitempty" json:"security_opt,omitempty"` - ShmSize UnitBytes `mapstructure:"shm_size" yaml:"shm_size,omitempty" json:"shm_size,omitempty"` - StdinOpen bool `mapstructure:"stdin_open" yaml:"stdin_open,omitempty" json:"stdin_open,omitempty"` - StopGracePeriod *Duration `mapstructure:"stop_grace_period" yaml:"stop_grace_period,omitempty" json:"stop_grace_period,omitempty"` - StopSignal string `mapstructure:"stop_signal" yaml:"stop_signal,omitempty" json:"stop_signal,omitempty"` - Sysctls Mapping `yaml:",omitempty" json:"sysctls,omitempty"` - Tmpfs StringList `yaml:",omitempty" json:"tmpfs,omitempty"` - Tty bool `mapstructure:"tty" yaml:"tty,omitempty" json:"tty,omitempty"` - Ulimits map[string]*UlimitsConfig `yaml:",omitempty" json:"ulimits,omitempty"` - User string `yaml:",omitempty" json:"user,omitempty"` - UserNSMode string `mapstructure:"userns_mode" yaml:"userns_mode,omitempty" json:"userns_mode,omitempty"` - Uts string `yaml:"uts,omitempty" json:"uts,omitempty"` - VolumeDriver string `mapstructure:"volume_driver" yaml:"volume_driver,omitempty" json:"volume_driver,omitempty"` - Volumes []ServiceVolumeConfig `yaml:",omitempty" json:"volumes,omitempty"` - VolumesFrom []string `mapstructure:"volumes_from" yaml:"volumes_from,omitempty" json:"volumes_from,omitempty"` - WorkingDir string `mapstructure:"working_dir" yaml:"working_dir,omitempty" json:"working_dir,omitempty"` + Build *BuildConfig `yaml:",omitempty" json:"build,omitempty"` + BlkioConfig *BlkioConfig `mapstructure:"blkio_config" yaml:",omitempty" json:"blkio_config,omitempty"` + CapAdd []string `mapstructure:"cap_add" yaml:"cap_add,omitempty" json:"cap_add,omitempty"` + CapDrop []string `mapstructure:"cap_drop" yaml:"cap_drop,omitempty" json:"cap_drop,omitempty"` + CgroupParent string `mapstructure:"cgroup_parent" yaml:"cgroup_parent,omitempty" json:"cgroup_parent,omitempty"` + CPUCount int64 `mapstructure:"cpu_count" yaml:"cpu_count,omitempty" json:"cpu_count,omitempty"` + CPUPercent float32 `mapstructure:"cpu_percent" yaml:"cpu_percent,omitempty" json:"cpu_percent,omitempty"` + CPUPeriod int64 `mapstructure:"cpu_period" yaml:"cpu_period,omitempty" json:"cpu_period,omitempty"` + CPUQuota int64 `mapstructure:"cpu_quota" yaml:"cpu_quota,omitempty" json:"cpu_quota,omitempty"` + CPURTPeriod int64 `mapstructure:"cpu_rt_period" yaml:"cpu_rt_period,omitempty" json:"cpu_rt_period,omitempty"` + CPURTRuntime int64 `mapstructure:"cpu_rt_runtime" yaml:"cpu_rt_runtime,omitempty" json:"cpu_rt_runtime,omitempty"` + CPUS float32 `mapstructure:"cpus" yaml:"cpus,omitempty" json:"cpus,omitempty"` + CPUSet string `mapstructure:"cpuset" yaml:"cpuset,omitempty" json:"cpuset,omitempty"` + CPUShares int64 `mapstructure:"cpu_shares" yaml:"cpu_shares,omitempty" json:"cpu_shares,omitempty"` + + // Command for the service containers. + // If set, overrides COMMAND from the image. + // + // Set to `[]` or `''` to clear the command from the image. + Command ShellCommand `yaml:",omitempty" json:"command"` // NOTE: we can NOT omitempty for JSON! see ShellCommand type for details. + + Configs []ServiceConfigObjConfig `yaml:",omitempty" json:"configs,omitempty"` + ContainerName string `mapstructure:"container_name" yaml:"container_name,omitempty" json:"container_name,omitempty"` + CredentialSpec *CredentialSpecConfig `mapstructure:"credential_spec" yaml:"credential_spec,omitempty" json:"credential_spec,omitempty"` + DependsOn DependsOnConfig `mapstructure:"depends_on" yaml:"depends_on,omitempty" json:"depends_on,omitempty"` + Deploy *DeployConfig `yaml:",omitempty" json:"deploy,omitempty"` + DeviceCgroupRules []string `mapstructure:"device_cgroup_rules" yaml:"device_cgroup_rules,omitempty" json:"device_cgroup_rules,omitempty"` + Devices []string `yaml:",omitempty" json:"devices,omitempty"` + DNS StringList `yaml:",omitempty" json:"dns,omitempty"` + DNSOpts []string `mapstructure:"dns_opt" yaml:"dns_opt,omitempty" json:"dns_opt,omitempty"` + DNSSearch StringList `mapstructure:"dns_search" yaml:"dns_search,omitempty" json:"dns_search,omitempty"` + Dockerfile string `yaml:"dockerfile,omitempty" json:"dockerfile,omitempty"` + DomainName string `mapstructure:"domainname" yaml:"domainname,omitempty" json:"domainname,omitempty"` + + // Entrypoint for the service containers. + // If set, overrides ENTRYPOINT from the image. + // + // Set to `[]` or `''` to clear the entrypoint from the image. + Entrypoint ShellCommand `yaml:"entrypoint,omitempty" json:"entrypoint"` // NOTE: we can NOT omitempty for JSON! see ShellCommand type for details. + + Environment MappingWithEquals `yaml:",omitempty" json:"environment,omitempty"` + EnvFile StringList `mapstructure:"env_file" yaml:"env_file,omitempty" json:"env_file,omitempty"` + Expose StringOrNumberList `yaml:",omitempty" json:"expose,omitempty"` + Extends ExtendsConfig `yaml:"extends,omitempty" json:"extends,omitempty"` + ExternalLinks []string `mapstructure:"external_links" yaml:"external_links,omitempty" json:"external_links,omitempty"` + ExtraHosts HostsList `mapstructure:"extra_hosts" yaml:"extra_hosts,omitempty" json:"extra_hosts,omitempty"` + GroupAdd []string `mapstructure:"group_add" yaml:"group_add,omitempty" json:"group_add,omitempty"` + Hostname string `yaml:",omitempty" json:"hostname,omitempty"` + HealthCheck *HealthCheckConfig `yaml:",omitempty" json:"healthcheck,omitempty"` + Image string `yaml:",omitempty" json:"image,omitempty"` + Init *bool `yaml:",omitempty" json:"init,omitempty"` + Ipc string `yaml:",omitempty" json:"ipc,omitempty"` + Isolation string `mapstructure:"isolation" yaml:"isolation,omitempty" json:"isolation,omitempty"` + Labels Labels `yaml:",omitempty" json:"labels,omitempty"` + CustomLabels Labels `yaml:"-" json:"-"` + Links []string `yaml:",omitempty" json:"links,omitempty"` + Logging *LoggingConfig `yaml:",omitempty" json:"logging,omitempty"` + LogDriver string `mapstructure:"log_driver" yaml:"log_driver,omitempty" json:"log_driver,omitempty"` + LogOpt map[string]string `mapstructure:"log_opt" yaml:"log_opt,omitempty" json:"log_opt,omitempty"` + MemLimit UnitBytes `mapstructure:"mem_limit" yaml:"mem_limit,omitempty" json:"mem_limit,omitempty"` + MemReservation UnitBytes `mapstructure:"mem_reservation" yaml:"mem_reservation,omitempty" json:"mem_reservation,omitempty"` + MemSwapLimit UnitBytes `mapstructure:"memswap_limit" yaml:"memswap_limit,omitempty" json:"memswap_limit,omitempty"` + MemSwappiness UnitBytes `mapstructure:"mem_swappiness" yaml:"mem_swappiness,omitempty" json:"mem_swappiness,omitempty"` + MacAddress string `mapstructure:"mac_address" yaml:"mac_address,omitempty" json:"mac_address,omitempty"` + Net string `yaml:"net,omitempty" json:"net,omitempty"` + NetworkMode string `mapstructure:"network_mode" yaml:"network_mode,omitempty" json:"network_mode,omitempty"` + Networks map[string]*ServiceNetworkConfig `yaml:",omitempty" json:"networks,omitempty"` + OomKillDisable bool `mapstructure:"oom_kill_disable" yaml:"oom_kill_disable,omitempty" json:"oom_kill_disable,omitempty"` + OomScoreAdj int64 `mapstructure:"oom_score_adj" yaml:"oom_score_adj,omitempty" json:"oom_score_adj,omitempty"` + Pid string `yaml:",omitempty" json:"pid,omitempty"` + PidsLimit int64 `mapstructure:"pids_limit" yaml:"pids_limit,omitempty" json:"pids_limit,omitempty"` + Platform string `yaml:",omitempty" json:"platform,omitempty"` + Ports []ServicePortConfig `yaml:",omitempty" json:"ports,omitempty"` + Privileged bool `yaml:",omitempty" json:"privileged,omitempty"` + PullPolicy string `mapstructure:"pull_policy" yaml:"pull_policy,omitempty" json:"pull_policy,omitempty"` + ReadOnly bool `mapstructure:"read_only" yaml:"read_only,omitempty" json:"read_only,omitempty"` + Restart string `yaml:",omitempty" json:"restart,omitempty"` + Runtime string `yaml:",omitempty" json:"runtime,omitempty"` + Scale int `yaml:"-" json:"-"` + Secrets []ServiceSecretConfig `yaml:",omitempty" json:"secrets,omitempty"` + SecurityOpt []string `mapstructure:"security_opt" yaml:"security_opt,omitempty" json:"security_opt,omitempty"` + ShmSize UnitBytes `mapstructure:"shm_size" yaml:"shm_size,omitempty" json:"shm_size,omitempty"` + StdinOpen bool `mapstructure:"stdin_open" yaml:"stdin_open,omitempty" json:"stdin_open,omitempty"` + StopGracePeriod *Duration `mapstructure:"stop_grace_period" yaml:"stop_grace_period,omitempty" json:"stop_grace_period,omitempty"` + StopSignal string `mapstructure:"stop_signal" yaml:"stop_signal,omitempty" json:"stop_signal,omitempty"` + Sysctls Mapping `yaml:",omitempty" json:"sysctls,omitempty"` + Tmpfs StringList `yaml:",omitempty" json:"tmpfs,omitempty"` + Tty bool `mapstructure:"tty" yaml:"tty,omitempty" json:"tty,omitempty"` + Ulimits map[string]*UlimitsConfig `yaml:",omitempty" json:"ulimits,omitempty"` + User string `yaml:",omitempty" json:"user,omitempty"` + UserNSMode string `mapstructure:"userns_mode" yaml:"userns_mode,omitempty" json:"userns_mode,omitempty"` + Uts string `yaml:"uts,omitempty" json:"uts,omitempty"` + VolumeDriver string `mapstructure:"volume_driver" yaml:"volume_driver,omitempty" json:"volume_driver,omitempty"` + Volumes []ServiceVolumeConfig `yaml:",omitempty" json:"volumes,omitempty"` + VolumesFrom []string `mapstructure:"volumes_from" yaml:"volumes_from,omitempty" json:"volumes_from,omitempty"` + WorkingDir string `mapstructure:"working_dir" yaml:"working_dir,omitempty" json:"working_dir,omitempty"` Extensions map[string]interface{} `yaml:",inline" json:"-"` } @@ -204,26 +216,26 @@ func (s *ServiceConfig) NetworksByPriority() []string { } const ( - //PullPolicyAlways always pull images + // PullPolicyAlways always pull images PullPolicyAlways = "always" - //PullPolicyNever never pull images + // PullPolicyNever never pull images PullPolicyNever = "never" - //PullPolicyIfNotPresent pull missing images + // PullPolicyIfNotPresent pull missing images PullPolicyIfNotPresent = "if_not_present" - //PullPolicyMissing pull missing images + // PullPolicyMissing pull missing images PullPolicyMissing = "missing" - //PullPolicyBuild force building images + // PullPolicyBuild force building images PullPolicyBuild = "build" ) const ( - //RestartPolicyAlways always restart the container if it stops + // RestartPolicyAlways always restart the container if it stops RestartPolicyAlways = "always" - //RestartPolicyOnFailure restart the container if it exits due to an error + // RestartPolicyOnFailure restart the container if it exits due to an error RestartPolicyOnFailure = "on-failure" - //RestartPolicyNo do not automatically restart the container + // RestartPolicyNo do not automatically restart the container RestartPolicyNo = "no" - //RestartPolicyUnlessStopped always restart the container unless the container is stopped (manually or otherwise) + // RestartPolicyUnlessStopped always restart the container unless the container is stopped (manually or otherwise) RestartPolicyUnlessStopped = "unless-stopped" ) @@ -275,8 +287,8 @@ func (s ServiceConfig) GetDependencies() []string { type set map[string]struct{} -func (s set) append(strings ...string) { - for _, str := range strings { +func (s set) append(strs ...string) { + for _, str := range strs { s[str] = struct{}{} } } @@ -305,6 +317,8 @@ type BuildConfig struct { Network string `yaml:",omitempty" json:"network,omitempty"` Target string `yaml:",omitempty" json:"target,omitempty"` Secrets []ServiceSecretConfig `yaml:",omitempty" json:"secrets,omitempty"` + Tags StringList `mapstructure:"tags" yaml:"tags,omitempty" json:"tags,omitempty"` + Platforms StringList `mapstructure:"platforms" yaml:"platforms,omitempty" json:"platforms,omitempty"` Extensions map[string]interface{} `yaml:",inline" json:"-"` } @@ -337,9 +351,55 @@ type ThrottleDevice struct { Extensions map[string]interface{} `yaml:",inline" json:"-"` } -// ShellCommand is a string or list of string args +// ShellCommand is a string or list of string args. +// +// When marshaled to YAML, nil command fields will be omitted if `omitempty` +// is specified as a struct tag. Explicitly empty commands (i.e. `[]` or `''`) +// will serialize to an empty array (`[]`). +// +// When marshaled to JSON, the `omitempty` struct must NOT be specified. +// If the command field is nil, it will be serialized as `null`. +// Explicitly empty commands (i.e. `[]` or `''`) will serialize to an empty +// array (`[]`). +// +// The distinction between nil and explicitly empty is important to distinguish +// between an unset value and a provided, but empty, value, which should be +// preserved so that it can override any base value (e.g. container entrypoint). +// +// The different semantics between YAML and JSON are due to limitations with +// JSON marshaling + `omitempty` in the Go stdlib, while gopkg.in/yaml.v2 gives +// us more flexibility via the yaml.IsZeroer interface. +// +// In the future, it might make sense to make fields of this type be +// `*ShellCommand` to avoid this situation, but that would constitute a +// breaking change. type ShellCommand []string +// IsZero returns true if the slice is nil. +// +// Empty (but non-nil) slices are NOT considered zero values. +func (s ShellCommand) IsZero() bool { + // we do NOT want len(s) == 0, ONLY explicitly nil + return s == nil +} + +// MarshalYAML returns nil (which will be serialized as `null`) for nil slices +// and delegates to the standard marshaller behavior otherwise. +// +// NOTE: Typically the nil case here is not hit because IsZero has already +// short-circuited marshalling, but this ensures that the type serializes +// accurately if the `omitempty` struct tag is omitted/forgotten. +// +// A similar MarshalJSON() implementation is not needed because the Go stdlib +// already serializes nil slices to `null`, whereas gopkg.in/yaml.v2 by default +// serializes nil slices to `[]`. +func (s ShellCommand) MarshalYAML() (interface{}, error) { + if s == nil { + return nil, nil + } + return []string(s), nil +} + // StringList is a type for fields that can be a string or list of strings type StringList []string @@ -458,9 +518,9 @@ func (s SSHKey) MarshalYAML() (interface{}, error) { // MarshalJSON makes SSHKey implement json.Marshaller func (s SSHKey) MarshalJSON() ([]byte, error) { if s.Path == "" { - return []byte(fmt.Sprintf(`"%s"`, s.ID)), nil + return []byte(fmt.Sprintf(`%q`, s.ID)), nil } - return []byte(fmt.Sprintf(`"%s": %s`, s.ID, s.Path)), nil + return []byte(fmt.Sprintf(`%q: %s`, s.ID, s.Path)), nil } // MappingWithColon is a mapping type that can be converted from a list of @@ -468,7 +528,16 @@ func (s SSHKey) MarshalJSON() ([]byte, error) { type MappingWithColon map[string]string // HostsList is a list of colon-separated host-ip mappings -type HostsList []string +type HostsList map[string]string + +// AsList return host-ip mappings as a list of colon-separated strings +func (h HostsList) AsList() []string { + l := make([]string, 0, len(h)) + for k, v := range h { + l = append(l, fmt.Sprintf("%s:%s", k, v)) + } + return l +} // LoggingConfig the logging configuration for a service type LoggingConfig struct { @@ -607,10 +676,11 @@ type PlacementPreferences struct { // ServiceNetworkConfig is the network configuration for a service type ServiceNetworkConfig struct { - Priority int `yaml:",omitempty" json:"priotirt,omitempty"` - Aliases []string `yaml:",omitempty" json:"aliases,omitempty"` - Ipv4Address string `mapstructure:"ipv4_address" yaml:"ipv4_address,omitempty" json:"ipv4_address,omitempty"` - Ipv6Address string `mapstructure:"ipv6_address" yaml:"ipv6_address,omitempty" json:"ipv6_address,omitempty"` + Priority int `yaml:",omitempty" json:"priority,omitempty"` + Aliases []string `yaml:",omitempty" json:"aliases,omitempty"` + Ipv4Address string `mapstructure:"ipv4_address" yaml:"ipv4_address,omitempty" json:"ipv4_address,omitempty"` + Ipv6Address string `mapstructure:"ipv6_address" yaml:"ipv6_address,omitempty" json:"ipv6_address,omitempty"` + LinkLocalIPs []string `mapstructure:"link_local_ips" yaml:"link_local_ips,omitempty" json:"link_local_ips,omitempty"` Extensions map[string]interface{} `yaml:",inline" json:"-"` } @@ -881,6 +951,7 @@ type CredentialSpecConfig struct { type FileObjectConfig struct { Name string `yaml:",omitempty" json:"name,omitempty"` File string `yaml:",omitempty" json:"file,omitempty"` + Environment string `yaml:",omitempty" json:"environment,omitempty"` External External `yaml:",omitempty" json:"external,omitempty"` Labels Labels `yaml:",omitempty" json:"labels,omitempty"` Driver string `yaml:",omitempty" json:"driver,omitempty"` diff --git a/vendor/github.com/compose-spec/compose-go/utils/stringutils.go b/vendor/github.com/compose-spec/compose-go/utils/stringutils.go index bd95e7bbfd..182ddf8302 100644 --- a/vendor/github.com/compose-spec/compose-go/utils/stringutils.go +++ b/vendor/github.com/compose-spec/compose-go/utils/stringutils.go @@ -17,6 +17,7 @@ package utils import ( + "fmt" "strconv" "strings" ) @@ -36,3 +37,22 @@ func StringToBool(s string) bool { b, _ := strconv.ParseBool(strings.ToLower(strings.TrimSpace(s))) return b } + +// GetAsEqualsMap split key=value formatted strings into a key : value map +func GetAsEqualsMap(em []string) map[string]string { + m := make(map[string]string) + for _, v := range em { + kv := strings.SplitN(v, "=", 2) + m[kv[0]] = kv[1] + } + return m +} + +// GetAsEqualsMap format a key : value map into key=value strings +func GetAsStringList(em map[string]string) []string { + m := make([]string, 0, len(em)) + for k, v := range em { + m = append(m, fmt.Sprintf("%s=%s", k, v)) + } + return m +} diff --git a/vendor/github.com/distribution/distribution/v3/reference/reference.go b/vendor/github.com/distribution/distribution/v3/reference/reference.go index 8c0c23b2fe..23490afa4e 100644 --- a/vendor/github.com/distribution/distribution/v3/reference/reference.go +++ b/vendor/github.com/distribution/distribution/v3/reference/reference.go @@ -5,7 +5,9 @@ // // reference := name [ ":" tag ] [ "@" digest ] // name := [domain '/'] path-component ['/' path-component]* -// domain := domain-component ['.' domain-component]* [':' port-number] +// domain := host [':' port-number] +// host := domain-name | IPv4address | \[ IPv6address \] ; rfc3986 appendix-A +// domain-name := domain-component ['.' domain-component]* // domain-component := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/ // port-number := /[0-9]+/ // path-component := alpha-numeric [separator alpha-numeric]* diff --git a/vendor/github.com/distribution/distribution/v3/reference/regexp.go b/vendor/github.com/distribution/distribution/v3/reference/regexp.go index 7677ca15da..7ecf4f76c6 100644 --- a/vendor/github.com/distribution/distribution/v3/reference/regexp.go +++ b/vendor/github.com/distribution/distribution/v3/reference/regexp.go @@ -23,15 +23,40 @@ var ( alphaNumeric, optional(repeated(separator, alphaNumeric))) - // domainComponent restricts the registry domain component of a - // repository name to start with a component as defined by DomainRegexp - // and followed by an optional port. - domainComponent = `(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])` - + // domainNameComponent restricts the registry domain component of a + // repository name to start with a component as defined by DomainRegexp. + domainNameComponent = `(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])` + + // ipv6address are enclosed between square brackets and may be represented + // in many ways, see rfc5952. Only IPv6 in compressed or uncompressed format + // are allowed, IPv6 zone identifiers (rfc6874) or Special addresses such as + // IPv4-Mapped are deliberately excluded. + ipv6address = expression( + literal(`[`), `(?:[a-fA-F0-9:]+)`, literal(`]`), + ) + + // domainName defines the structure of potential domain components + // that may be part of image names. This is purposely a subset of what is + // allowed by DNS to ensure backwards compatibility with Docker image + // names. This includes IPv4 addresses on decimal format. + domainName = expression( + domainNameComponent, + optional(repeated(literal(`.`), domainNameComponent)), + ) + + // host defines the structure of potential domains based on the URI + // Host subcomponent on rfc3986. It may be a subset of DNS domain name, + // or an IPv4 address in decimal format, or an IPv6 address between square + // brackets (excluding zone identifiers as defined by rfc6874 or special + // addresses such as IPv4-Mapped). + host = `(?:` + domainName + `|` + ipv6address + `)` + + // allowed by the URI Host subcomponent on rfc3986 to ensure backwards + // compatibility with Docker image names. domain = expression( - domainComponent, - optional(repeated(literal(`.`), domainComponent)), + host, optional(literal(`:`), `[0-9]+`)) + // DomainRegexp defines the structure of potential domain components // that may be part of image names. This is purposely a subset of what is // allowed by DNS to ensure backwards compatibility with Docker image diff --git a/vendor/github.com/docker/go-units/size.go b/vendor/github.com/docker/go-units/size.go index 85f6ab0715..c245a89513 100644 --- a/vendor/github.com/docker/go-units/size.go +++ b/vendor/github.com/docker/go-units/size.go @@ -2,7 +2,6 @@ package units import ( "fmt" - "regexp" "strconv" "strings" ) @@ -26,16 +25,17 @@ const ( PiB = 1024 * TiB ) -type unitMap map[string]int64 +type unitMap map[byte]int64 var ( - decimalMap = unitMap{"k": KB, "m": MB, "g": GB, "t": TB, "p": PB} - binaryMap = unitMap{"k": KiB, "m": MiB, "g": GiB, "t": TiB, "p": PiB} - sizeRegex = regexp.MustCompile(`^(\d+(\.\d+)*) ?([kKmMgGtTpP])?[iI]?[bB]?$`) + decimalMap = unitMap{'k': KB, 'm': MB, 'g': GB, 't': TB, 'p': PB} + binaryMap = unitMap{'k': KiB, 'm': MiB, 'g': GiB, 't': TiB, 'p': PiB} ) -var decimapAbbrs = []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"} -var binaryAbbrs = []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"} +var ( + decimapAbbrs = []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"} + binaryAbbrs = []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"} +) func getSizeAndUnit(size float64, base float64, _map []string) (float64, string) { i := 0 @@ -89,20 +89,66 @@ func RAMInBytes(size string) (int64, error) { // Parses the human-readable size string into the amount it represents. func parseSize(sizeStr string, uMap unitMap) (int64, error) { - matches := sizeRegex.FindStringSubmatch(sizeStr) - if len(matches) != 4 { + // TODO: rewrite to use strings.Cut if there's a space + // once Go < 1.18 is deprecated. + sep := strings.LastIndexAny(sizeStr, "01234567890. ") + if sep == -1 { + // There should be at least a digit. return -1, fmt.Errorf("invalid size: '%s'", sizeStr) } + var num, sfx string + if sizeStr[sep] != ' ' { + num = sizeStr[:sep+1] + sfx = sizeStr[sep+1:] + } else { + // Omit the space separator. + num = sizeStr[:sep] + sfx = sizeStr[sep+1:] + } - size, err := strconv.ParseFloat(matches[1], 64) + size, err := strconv.ParseFloat(num, 64) if err != nil { return -1, err } + // Backward compatibility: reject negative sizes. + if size < 0 { + return -1, fmt.Errorf("invalid size: '%s'", sizeStr) + } + + if len(sfx) == 0 { + return int64(size), nil + } - unitPrefix := strings.ToLower(matches[3]) - if mul, ok := uMap[unitPrefix]; ok { + // Process the suffix. + + if len(sfx) > 3 { // Too long. + goto badSuffix + } + sfx = strings.ToLower(sfx) + // Trivial case: b suffix. + if sfx[0] == 'b' { + if len(sfx) > 1 { // no extra characters allowed after b. + goto badSuffix + } + return int64(size), nil + } + // A suffix from the map. + if mul, ok := uMap[sfx[0]]; ok { size *= float64(mul) + } else { + goto badSuffix + } + + // The suffix may have extra "b" or "ib" (e.g. KiB or MB). + switch { + case len(sfx) == 2 && sfx[1] != 'b': + goto badSuffix + case len(sfx) == 3 && sfx[1:] != "ib": + goto badSuffix } return int64(size), nil + +badSuffix: + return -1, fmt.Errorf("invalid suffix: '%s'", sfx) } diff --git a/vendor/github.com/mitchellh/mapstructure/CHANGELOG.md b/vendor/github.com/mitchellh/mapstructure/CHANGELOG.md index 38a099162c..c758234904 100644 --- a/vendor/github.com/mitchellh/mapstructure/CHANGELOG.md +++ b/vendor/github.com/mitchellh/mapstructure/CHANGELOG.md @@ -1,3 +1,16 @@ +## 1.5.0 + +* New option `IgnoreUntaggedFields` to ignore decoding to any fields + without `mapstructure` (or the configured tag name) set [GH-277] +* New option `ErrorUnset` which makes it an error if any fields + in a target struct are not set by the decoding process. [GH-225] +* New function `OrComposeDecodeHookFunc` to help compose decode hooks. [GH-240] +* Decoding to slice from array no longer crashes [GH-265] +* Decode nested struct pointers to map [GH-271] +* Fix issue where `,squash` was ignored if `Squash` option was set. [GH-280] +* Fix issue where fields with `,omitempty` would sometimes decode + into a map with an empty string key [GH-281] + ## 1.4.3 * Fix cases where `json.Number` didn't decode properly [GH-261] diff --git a/vendor/github.com/mitchellh/mapstructure/decode_hooks.go b/vendor/github.com/mitchellh/mapstructure/decode_hooks.go index 4d4bbc733b..3a754ca724 100644 --- a/vendor/github.com/mitchellh/mapstructure/decode_hooks.go +++ b/vendor/github.com/mitchellh/mapstructure/decode_hooks.go @@ -77,6 +77,28 @@ func ComposeDecodeHookFunc(fs ...DecodeHookFunc) DecodeHookFunc { } } +// OrComposeDecodeHookFunc executes all input hook functions until one of them returns no error. In that case its value is returned. +// If all hooks return an error, OrComposeDecodeHookFunc returns an error concatenating all error messages. +func OrComposeDecodeHookFunc(ff ...DecodeHookFunc) DecodeHookFunc { + return func(a, b reflect.Value) (interface{}, error) { + var allErrs string + var out interface{} + var err error + + for _, f := range ff { + out, err = DecodeHookExec(f, a, b) + if err != nil { + allErrs += err.Error() + "\n" + continue + } + + return out, nil + } + + return nil, errors.New(allErrs) + } +} + // StringToSliceHookFunc returns a DecodeHookFunc that converts // string to []string by splitting on the given sep. func StringToSliceHookFunc(sep string) DecodeHookFunc { diff --git a/vendor/github.com/mitchellh/mapstructure/mapstructure.go b/vendor/github.com/mitchellh/mapstructure/mapstructure.go index 6b81b00679..1efb22ac36 100644 --- a/vendor/github.com/mitchellh/mapstructure/mapstructure.go +++ b/vendor/github.com/mitchellh/mapstructure/mapstructure.go @@ -122,7 +122,7 @@ // field value is zero and a numeric type, the field is empty, and it won't // be encoded into the destination type. // -// type Source { +// type Source struct { // Age int `mapstructure:",omitempty"` // } // @@ -215,6 +215,12 @@ type DecoderConfig struct { // (extra keys). ErrorUnused bool + // If ErrorUnset is true, then it is an error for there to exist + // fields in the result that were not set in the decoding process + // (extra fields). This only applies to decoding to a struct. This + // will affect all nested structs as well. + ErrorUnset bool + // ZeroFields, if set to true, will zero fields before writing them. // For example, a map will be emptied before decoded values are put in // it. If this is false, a map will be merged. @@ -259,6 +265,10 @@ type DecoderConfig struct { // defaults to "mapstructure" TagName string + // IgnoreUntaggedFields ignores all struct fields without explicit + // TagName, comparable to `mapstructure:"-"` as default behaviour. + IgnoreUntaggedFields bool + // MatchName is the function used to match the map key to the struct // field name or tag. Defaults to `strings.EqualFold`. This can be used // to implement case-sensitive tag values, support snake casing, etc. @@ -284,6 +294,11 @@ type Metadata struct { // Unused is a slice of keys that were found in the raw value but // weren't decoded since there was no matching field in the result interface Unused []string + + // Unset is a slice of field names that were found in the result interface + // but weren't set in the decoding process since there was no matching value + // in the input + Unset []string } // Decode takes an input structure and uses reflection to translate it to @@ -375,6 +390,10 @@ func NewDecoder(config *DecoderConfig) (*Decoder, error) { if config.Metadata.Unused == nil { config.Metadata.Unused = make([]string, 0) } + + if config.Metadata.Unset == nil { + config.Metadata.Unset = make([]string, 0) + } } if config.TagName == "" { @@ -906,9 +925,15 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re tagValue := f.Tag.Get(d.config.TagName) keyName := f.Name + if tagValue == "" && d.config.IgnoreUntaggedFields { + continue + } + // If Squash is set in the config, we squash the field down. squash := d.config.Squash && v.Kind() == reflect.Struct && f.Anonymous + v = dereferencePtrToStructIfNeeded(v, d.config.TagName) + // Determine the name of the key in the map if index := strings.Index(tagValue, ","); index != -1 { if tagValue[:index] == "-" { @@ -920,7 +945,7 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re } // If "squash" is specified in the tag, we squash the field down. - squash = !squash && strings.Index(tagValue[index+1:], "squash") != -1 + squash = squash || strings.Index(tagValue[index+1:], "squash") != -1 if squash { // When squashing, the embedded type can be a pointer to a struct. if v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct { @@ -932,7 +957,9 @@ func (d *Decoder) decodeMapFromStruct(name string, dataVal reflect.Value, val re return fmt.Errorf("cannot squash non-struct type '%s'", v.Type()) } } - keyName = tagValue[:index] + if keyNameTagValue := tagValue[:index]; keyNameTagValue != "" { + keyName = keyNameTagValue + } } else if len(tagValue) > 0 { if tagValue == "-" { continue @@ -1088,7 +1115,7 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value) } // If the input value is nil, then don't allocate since empty != nil - if dataVal.IsNil() { + if dataValKind != reflect.Array && dataVal.IsNil() { return nil } @@ -1250,6 +1277,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e dataValKeysUnused[dataValKey.Interface()] = struct{}{} } + targetValKeysUnused := make(map[interface{}]struct{}) errors := make([]string, 0) // This slice will keep track of all the structs we'll be decoding. @@ -1354,7 +1382,8 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e if !rawMapVal.IsValid() { // There was no matching key in the map for the value in - // the struct. Just ignore. + // the struct. Remember it for potential errors and metadata. + targetValKeysUnused[fieldName] = struct{}{} continue } } @@ -1414,6 +1443,17 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e errors = appendErrors(errors, err) } + if d.config.ErrorUnset && len(targetValKeysUnused) > 0 { + keys := make([]string, 0, len(targetValKeysUnused)) + for rawKey := range targetValKeysUnused { + keys = append(keys, rawKey.(string)) + } + sort.Strings(keys) + + err := fmt.Errorf("'%s' has unset fields: %s", name, strings.Join(keys, ", ")) + errors = appendErrors(errors, err) + } + if len(errors) > 0 { return &Error{errors} } @@ -1428,6 +1468,14 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e d.config.Metadata.Unused = append(d.config.Metadata.Unused, key) } + for rawKey := range targetValKeysUnused { + key := rawKey.(string) + if name != "" { + key = name + "." + key + } + + d.config.Metadata.Unset = append(d.config.Metadata.Unset, key) + } } return nil @@ -1465,3 +1513,28 @@ func getKind(val reflect.Value) reflect.Kind { return kind } } + +func isStructTypeConvertibleToMap(typ reflect.Type, checkMapstructureTags bool, tagName string) bool { + for i := 0; i < typ.NumField(); i++ { + f := typ.Field(i) + if f.PkgPath == "" && !checkMapstructureTags { // check for unexported fields + return true + } + if checkMapstructureTags && f.Tag.Get(tagName) != "" { // check for mapstructure tags inside + return true + } + } + return false +} + +func dereferencePtrToStructIfNeeded(v reflect.Value, tagName string) reflect.Value { + if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { + return v + } + deref := v.Elem() + derefT := deref.Type() + if isStructTypeConvertibleToMap(derefT, true, tagName) { + return deref + } + return v +} diff --git a/vendor/github.com/sirupsen/logrus/README.md b/vendor/github.com/sirupsen/logrus/README.md index 5152b6aa40..b042c896f2 100644 --- a/vendor/github.com/sirupsen/logrus/README.md +++ b/vendor/github.com/sirupsen/logrus/README.md @@ -1,4 +1,4 @@ -# Logrus :walrus: [![Build Status](https://travis-ci.org/sirupsen/logrus.svg?branch=master)](https://travis-ci.org/sirupsen/logrus) [![GoDoc](https://godoc.org/github.com/sirupsen/logrus?status.svg)](https://godoc.org/github.com/sirupsen/logrus) +# Logrus :walrus: [![Build Status](https://github.com/sirupsen/logrus/workflows/CI/badge.svg)](https://github.com/sirupsen/logrus/actions?query=workflow%3ACI) [![Build Status](https://travis-ci.org/sirupsen/logrus.svg?branch=master)](https://travis-ci.org/sirupsen/logrus) [![Go Reference](https://pkg.go.dev/badge/github.com/sirupsen/logrus.svg)](https://pkg.go.dev/github.com/sirupsen/logrus) Logrus is a structured logger for Go (golang), completely API compatible with the standard library logger. @@ -341,7 +341,7 @@ import ( log "github.com/sirupsen/logrus" ) -init() { +func init() { // do something here to set environment depending on an environment variable // or command-line flag if Environment == "production" { diff --git a/vendor/github.com/sirupsen/logrus/buffer_pool.go b/vendor/github.com/sirupsen/logrus/buffer_pool.go index 4545dec07d..c7787f77cb 100644 --- a/vendor/github.com/sirupsen/logrus/buffer_pool.go +++ b/vendor/github.com/sirupsen/logrus/buffer_pool.go @@ -26,15 +26,6 @@ func (p *defaultPool) Get() *bytes.Buffer { return p.pool.Get().(*bytes.Buffer) } -func getBuffer() *bytes.Buffer { - return bufferPool.Get() -} - -func putBuffer(buf *bytes.Buffer) { - buf.Reset() - bufferPool.Put(buf) -} - // SetBufferPool allows to replace the default logrus buffer pool // to better meets the specific needs of an application. func SetBufferPool(bp BufferPool) { diff --git a/vendor/github.com/sirupsen/logrus/entry.go b/vendor/github.com/sirupsen/logrus/entry.go index 07a1e5fa72..71cdbbc35d 100644 --- a/vendor/github.com/sirupsen/logrus/entry.go +++ b/vendor/github.com/sirupsen/logrus/entry.go @@ -232,6 +232,7 @@ func (entry *Entry) log(level Level, msg string) { newEntry.Logger.mu.Lock() reportCaller := newEntry.Logger.ReportCaller + bufPool := newEntry.getBufferPool() newEntry.Logger.mu.Unlock() if reportCaller { @@ -239,11 +240,11 @@ func (entry *Entry) log(level Level, msg string) { } newEntry.fireHooks() - - buffer = getBuffer() + buffer = bufPool.Get() defer func() { newEntry.Buffer = nil - putBuffer(buffer) + buffer.Reset() + bufPool.Put(buffer) }() buffer.Reset() newEntry.Buffer = buffer @@ -260,6 +261,13 @@ func (entry *Entry) log(level Level, msg string) { } } +func (entry *Entry) getBufferPool() (pool BufferPool) { + if entry.Logger.BufferPool != nil { + return entry.Logger.BufferPool + } + return bufferPool +} + func (entry *Entry) fireHooks() { var tmpHooks LevelHooks entry.Logger.mu.Lock() @@ -276,18 +284,21 @@ func (entry *Entry) fireHooks() { } func (entry *Entry) write() { + entry.Logger.mu.Lock() + defer entry.Logger.mu.Unlock() serialized, err := entry.Logger.Formatter.Format(entry) if err != nil { fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err) return } - entry.Logger.mu.Lock() - defer entry.Logger.mu.Unlock() if _, err := entry.Logger.Out.Write(serialized); err != nil { fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err) } } +// Log will log a message at the level given as parameter. +// Warning: using Log at Panic or Fatal level will not respectively Panic nor Exit. +// For this behaviour Entry.Panic or Entry.Fatal should be used instead. func (entry *Entry) Log(level Level, args ...interface{}) { if entry.Logger.IsLevelEnabled(level) { entry.log(level, fmt.Sprint(args...)) diff --git a/vendor/github.com/sirupsen/logrus/logger.go b/vendor/github.com/sirupsen/logrus/logger.go index 337704457a..5ff0aef6d3 100644 --- a/vendor/github.com/sirupsen/logrus/logger.go +++ b/vendor/github.com/sirupsen/logrus/logger.go @@ -44,6 +44,9 @@ type Logger struct { entryPool sync.Pool // Function to exit the application, defaults to `os.Exit()` ExitFunc exitFunc + // The buffer pool used to format the log. If it is nil, the default global + // buffer pool will be used. + BufferPool BufferPool } type exitFunc func(int) @@ -192,6 +195,9 @@ func (logger *Logger) Panicf(format string, args ...interface{}) { logger.Logf(PanicLevel, format, args...) } +// Log will log a message at the level given as parameter. +// Warning: using Log at Panic or Fatal level will not respectively Panic nor Exit. +// For this behaviour Logger.Panic or Logger.Fatal should be used instead. func (logger *Logger) Log(level Level, args ...interface{}) { if logger.IsLevelEnabled(level) { entry := logger.newEntry() @@ -402,3 +408,10 @@ func (logger *Logger) ReplaceHooks(hooks LevelHooks) LevelHooks { logger.mu.Unlock() return oldHooks } + +// SetBufferPool sets the logger buffer pool. +func (logger *Logger) SetBufferPool(pool BufferPool) { + logger.mu.Lock() + defer logger.mu.Unlock() + logger.BufferPool = pool +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 4aa3d36822..fd20053f84 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -77,8 +77,8 @@ github.com/chai2010/gettext-go/plural github.com/chai2010/gettext-go/po # github.com/cloudflare/cfssl v1.4.1 ## explicit; go 1.13 -# github.com/compose-spec/compose-go v1.2.4 -## explicit; go 1.17 +# github.com/compose-spec/compose-go v1.6.0 +## explicit; go 1.18 github.com/compose-spec/compose-go/cli github.com/compose-spec/compose-go/consts github.com/compose-spec/compose-go/dotenv @@ -144,7 +144,7 @@ github.com/davecgh/go-spew/spew # github.com/denisbrodbeck/machineid v1.0.0 ## explicit github.com/denisbrodbeck/machineid -# github.com/distribution/distribution/v3 v3.0.0-20220526142353-ffbd94cbe269 +# github.com/distribution/distribution/v3 v3.0.0-20220725133111-4bf3547399eb ## explicit; go 1.18 github.com/distribution/distribution/v3/digestset github.com/distribution/distribution/v3/reference @@ -235,7 +235,7 @@ github.com/docker/go-connections/tlsconfig # github.com/docker/go-metrics v0.0.1 ## explicit; go 1.11 github.com/docker/go-metrics -# github.com/docker/go-units v0.4.0 +# github.com/docker/go-units v0.5.0 ## explicit github.com/docker/go-units # github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 @@ -463,7 +463,7 @@ github.com/mitchellh/go-homedir # github.com/mitchellh/go-wordwrap v1.0.0 ## explicit github.com/mitchellh/go-wordwrap -# github.com/mitchellh/mapstructure v1.4.3 +# github.com/mitchellh/mapstructure v1.5.0 ## explicit; go 1.14 github.com/mitchellh/mapstructure # github.com/moby/buildkit v0.8.3 => github.com/tilt-dev/buildkit v0.8.3-tilt-20220505 @@ -613,7 +613,7 @@ github.com/schollz/closestmatch ## explicit; go 1.14 github.com/segmentio/encoding/ascii github.com/segmentio/encoding/json -# github.com/sirupsen/logrus v1.8.1 +# github.com/sirupsen/logrus v1.9.0 ## explicit; go 1.13 github.com/sirupsen/logrus # github.com/smacker/go-tree-sitter v0.0.0-20220209044044-0d3022e933c3