Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/compose/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func runConvert(ctx context.Context, dockerCli command.Cli, p *ProjectOptions, o
return err
}

project, _, err := p.ToProject(ctx, dockerCli, backend, nil)
project, _, err := p.ToProject(ctx, dockerCli, backend, nil, warnUnsupportedAttributes)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compose/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ func runBuild(ctx context.Context, dockerCli command.Cli, backendOptions *Backen
}

opts.All = true // do not drop resources as build may involve some dependencies by additional_contexts
project, _, err := opts.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
project, _, err := opts.ToProject(ctx, dockerCli, backend, nil, warnUnsupportedAttributes, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/compose/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func completeServiceNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn

// only service names are needed, so skip environment resolution: a missing
// env_file must not prevent completion
project, _, err := p.ToProject(cmd.Context(), dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
project, _, err := p.ToProject(cmd.Context(), dockerCli, backend, nil, skipUnsupportedAttributesWarning, cli.WithoutEnvironmentResolution)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
Expand Down Expand Up @@ -95,7 +95,7 @@ func completeProfileNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn

// only profile names are needed, so skip environment resolution: a missing
// env_file must not prevent completion
project, _, err := p.ToProject(cmd.Context(), dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
project, _, err := p.ToProject(cmd.Context(), dockerCli, backend, nil, skipUnsupportedAttributesWarning, cli.WithoutEnvironmentResolution)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
Expand Down
24 changes: 18 additions & 6 deletions cmd/compose/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ func (o *ProjectOptions) WithServices(dockerCli command.Cli, fn ProjectServicesF
return err
}

project, metrics, err := o.ToProject(ctx, dockerCli, backend, services, cli.WithoutEnvironmentResolution)
project, metrics, err := o.ToProject(ctx, dockerCli, backend, services, warnUnsupportedAttributes, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
Expand Down Expand Up @@ -252,7 +252,7 @@ func (o *ProjectOptions) projectOrName(ctx context.Context, dockerCli command.Cl
return nil, "", err
}

p, _, err := o.ToProject(ctx, dockerCli, backend, services, cli.WithDiscardEnvFile, cli.WithoutEnvironmentResolution)
p, _, err := o.ToProject(ctx, dockerCli, backend, services, skipUnsupportedAttributesWarning, cli.WithDiscardEnvFile, cli.WithoutEnvironmentResolution)
if err != nil {
envProjectName := os.Getenv(ComposeProjectName)
if envProjectName != "" {
Expand Down Expand Up @@ -281,7 +281,7 @@ func (o *ProjectOptions) toProjectName(ctx context.Context, dockerCli command.Cl
return "", err
}

project, _, err := o.ToProject(ctx, dockerCli, backend, nil, cli.WithDiscardEnvFile, cli.WithoutEnvironmentResolution)
project, _, err := o.ToProject(ctx, dockerCli, backend, nil, skipUnsupportedAttributesWarning, cli.WithDiscardEnvFile, cli.WithoutEnvironmentResolution)
if err != nil {
return "", err
}
Expand All @@ -306,9 +306,14 @@ func (o *ProjectOptions) ToModel(ctx context.Context, dockerCli command.Cli, ser
return options.LoadModel(ctx)
}

// ToProject loads a Compose project using the LoadProject API.
// Accepts optional cli.ProjectOptionsFn to control loader behavior.
func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, backend api.Compose, services []string, po ...cli.ProjectOptionsFn) (*types.Project, tracing.Metrics, error) {
// ToProject loads a Compose project using the LoadProject API, then — when
// warn is warnUnsupportedAttributes — warns about any unsupported-attribute
// finding compose-go's loader reports during that load. Accepts optional
// cli.ProjectOptionsFn to control loader behavior.
func (o *ProjectOptions) ToProject(
ctx context.Context, dockerCli command.Cli, backend api.Compose, services []string,
warn unsupportedAttributeWarning, po ...cli.ProjectOptionsFn,
) (*types.Project, tracing.Metrics, error) {
var metrics tracing.Metrics
remotes := o.remoteLoaders(dockerCli)

Expand Down Expand Up @@ -350,6 +355,13 @@ func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, b
LoadListeners: []api.LoadListener{metricsListener},
OCI: o.ociOptions(),
}
if warn == warnUnsupportedAttributes {
loadOpts.OnUnsupportedAttribute = func(findings []api.UnsupportedAttribute) {
for _, finding := range findings {
logrus.Warn(finding)
}
}
}

project, err := backend.LoadProject(ctx, loadOpts)
if err != nil {
Expand Down
4 changes: 3 additions & 1 deletion cmd/compose/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,11 @@ type configOptions struct {
lockImageDigests bool
}

// ToProject always warns: every config subcommand renders the resolved
// model to the user, so any unsupported-attribute finding is relevant here.
func (o *configOptions) ToProject(ctx context.Context, dockerCli command.Cli, backend api.Compose, services []string, po ...cli.ProjectOptionsFn) (*types.Project, error) {
po = append(po, o.toProjectOptionsFns()...)
project, _, err := o.ProjectOptions.ToProject(ctx, dockerCli, backend, services, po...)
project, _, err := o.ProjectOptions.ToProject(ctx, dockerCli, backend, services, warnUnsupportedAttributes, po...)
return project, err
}

Expand Down
60 changes: 23 additions & 37 deletions cmd/compose/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
package compose

import (
"io"
"os"
"strings"
"testing"

Expand All @@ -27,8 +25,6 @@ import (
"github.com/moby/moby/api/types/registry"
"github.com/moby/moby/client"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
logrustest "github.com/sirupsen/logrus/hooks/test"
"go.uber.org/mock/gomock"
"gotest.tools/v3/assert"

Expand Down Expand Up @@ -168,47 +164,37 @@ func TestImagesOnly(t *testing.T) {
}

func TestWarnHooksNotLockable(t *testing.T) {
hook := logrustest.NewGlobal()
logrus.SetOutput(io.Discard)
defer func() {
logrus.StandardLogger().ReplaceHooks(make(logrus.LevelHooks))
logrus.SetOutput(os.Stderr)
}()

warnHooksNotLockable(&types.Project{
Services: types.Services{
"with-hook-image": types.ServiceConfig{PreStart: []types.ServiceHook{{Image: "alpine:latest"}}},
"inline-hook": types.ServiceConfig{PreStart: []types.ServiceHook{{Command: types.ShellCommand{"echo"}}}},
"without-hook": types.ServiceConfig{},
},
messages := captureWarnings(t, func() {
warnHooksNotLockable(&types.Project{
Services: types.Services{
"with-hook-image": types.ServiceConfig{PreStart: []types.ServiceHook{{Image: "alpine:latest"}}},
"inline-hook": types.ServiceConfig{PreStart: []types.ServiceHook{{Command: types.ShellCommand{"echo"}}}},
"without-hook": types.ServiceConfig{},
},
})
})

assert.Equal(t, len(hook.Entries), 1)
assert.Assert(t, strings.Contains(hook.Entries[0].Message, `service "with-hook-image"`))
assert.Equal(t, len(messages), 1)
assert.Assert(t, strings.Contains(messages[0], `service "with-hook-image"`))
}

func TestWarnModelHooksNotLockable(t *testing.T) {
hook := logrustest.NewGlobal()
logrus.SetOutput(io.Discard)
defer func() {
logrus.StandardLogger().ReplaceHooks(make(logrus.LevelHooks))
logrus.SetOutput(os.Stderr)
}()

warnModelHooksNotLockable(map[string]any{
"services": map[string]any{
"with-hook-image": map[string]any{
"pre_start": []any{map[string]any{"image": "alpine:latest"}},
},
"inline-hook": map[string]any{
"pre_start": []any{map[string]any{"command": "echo"}},
messages := captureWarnings(t, func() {
warnModelHooksNotLockable(map[string]any{
"services": map[string]any{
"with-hook-image": map[string]any{
"pre_start": []any{map[string]any{"image": "alpine:latest"}},
},
"inline-hook": map[string]any{
"pre_start": []any{map[string]any{"command": "echo"}},
},
"without-hook": map[string]any{"image": "nginx"},
},
"without-hook": map[string]any{"image": "nginx"},
},
})
})

assert.Equal(t, len(hook.Entries), 1)
assert.Assert(t, strings.Contains(hook.Entries[0].Message, `service "with-hook-image"`))
assert.Equal(t, len(messages), 1)
assert.Assert(t, strings.Contains(messages[0], `service "with-hook-image"`))
}

func TestLockModel(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion cmd/compose/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func runPublish(ctx context.Context, dockerCli command.Cli, backendOptions *Back
return err
}

project, metrics, err := opts.ToProject(ctx, dockerCli, backend, nil)
project, metrics, err := opts.ToProject(ctx, dockerCli, backend, nil, warnUnsupportedAttributes)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compose/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func runPull(ctx context.Context, dockerCli command.Cli, backendOptions *Backend
return err
}

project, _, err := opts.ToProject(ctx, dockerCli, backend, services, cli.WithoutEnvironmentResolution)
project, _, err := opts.ToProject(ctx, dockerCli, backend, services, warnUnsupportedAttributes, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compose/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func runPush(ctx context.Context, dockerCli command.Cli, backendOptions *Backend
return err
}

project, _, err := opts.ToProject(ctx, dockerCli, backend, services)
project, _, err := opts.ToProject(ctx, dockerCli, backend, services, warnUnsupportedAttributes)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compose/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ func normalizeRunFlags(f *pflag.FlagSet, name string) pflag.NormalizedName {
// dependencies started by run, so hashing a different value would recreate
// their containers.
func runProject(ctx context.Context, dockerCli command.Cli, backend api.Compose, p *ProjectOptions, service string) (*types.Project, error) {
project, _, err := p.ToProject(ctx, dockerCli, backend, []string{service}, composecli.WithoutEnvironmentResolution)
project, _, err := p.ToProject(ctx, dockerCli, backend, []string{service}, warnUnsupportedAttributes, composecli.WithoutEnvironmentResolution)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/compose/scale.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func runScale(ctx context.Context, dockerCli command.Cli, backendOptions *Backen
}

services := slices.Sorted(maps.Keys(serviceReplicaTuples))
project, _, err := opts.ToProject(ctx, dockerCli, backend, services, cli.WithoutEnvironmentResolution)
project, _, err := opts.ToProject(ctx, dockerCli, backend, services, warnUnsupportedAttributes, cli.WithoutEnvironmentResolution)
if err != nil {
return err
}
Expand Down
26 changes: 26 additions & 0 deletions cmd/compose/unsupported_attributes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
Copyright 2020 Docker Compose CLI authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package compose

// unsupportedAttributeWarning is a distinct type rather than a bool so every
// ToProject call site must name one of the constants below.
type unsupportedAttributeWarning int

const (
warnUnsupportedAttributes unsupportedAttributeWarning = iota
skipUnsupportedAttributesWarning
)
123 changes: 123 additions & 0 deletions cmd/compose/unsupported_attributes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
Copyright 2020 Docker Compose CLI authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package compose

import (
"io"
"os"
"path/filepath"
"testing"

"github.com/sirupsen/logrus"
logrustest "github.com/sirupsen/logrus/hooks/test"
"gotest.tools/v3/assert"

realcompose "github.com/docker/compose/v5/pkg/compose"
)

// unsupportedAttrFixture writes a compose file with an attribute
// (deploy.mode) that the unsupported-attribute check always flags, so any
// code path that loads it can be checked for whether it warns or stays
// silent.
func unsupportedAttrFixture(t *testing.T) string {
t.Helper()
path := filepath.Join(t.TempDir(), "compose.yaml")
content := `
services:
web:
image: alpine
deploy:
mode: replicated
`
assert.NilError(t, os.WriteFile(path, []byte(content), 0o644))
return path
}

// captureWarnings runs fn with a global logrus hook installed and returns
// every message logged during the call.
func captureWarnings(t *testing.T, fn func()) []string {
t.Helper()
hook := logrustest.NewGlobal()
logrus.SetOutput(io.Discard)
defer func() {
logrus.StandardLogger().ReplaceHooks(make(logrus.LevelHooks))
logrus.SetOutput(os.Stderr)
}()
fn()
messages := make([]string, len(hook.Entries))
for i, entry := range hook.Entries {
messages[i] = entry.Message
}
return messages
}

// ToProject is the "real" load path used by commands that act on the full
// compose model (build, config, run, scale, watch, ...): it must warn.
func TestToProject_WarnsOnUnsupportedAttributes(t *testing.T) {
opts := &ProjectOptions{ConfigPaths: []string{unsupportedAttrFixture(t)}}
backend, err := realcompose.NewComposeService(nil)
assert.NilError(t, err)

messages := captureWarnings(t, func() {
_, _, err := opts.ToProject(t.Context(), nil, backend, nil, warnUnsupportedAttributes)
assert.NilError(t, err)
})

assert.Assert(t, len(messages) > 0, "ToProject should warn about deploy.mode")
}

func TestToProject_SkipsWarningWhenRequested(t *testing.T) {
opts := &ProjectOptions{ConfigPaths: []string{unsupportedAttrFixture(t)}}
backend, err := realcompose.NewComposeService(nil)
assert.NilError(t, err)

messages := captureWarnings(t, func() {
_, _, err := opts.ToProject(t.Context(), nil, backend, nil, skipUnsupportedAttributesWarning)
assert.NilError(t, err)
})

assert.Equal(t, len(messages), 0)
}

// projectOrName and toProjectName are lightweight project-name resolution
// helpers used by commands that operate on already-running
// containers/services by name (down, stop, ps, logs, ...) or by shell
// completion (completeServiceNames, completeProfileNames). Warning here
// would fire on every such invocation regardless of whether the command has
// anything to do with the flagged attribute, and even during tab-completion
// — see docker/compose#14196 review discussion.
func TestProjectOrName_DoesNotWarnOnUnsupportedAttributes(t *testing.T) {
opts := &ProjectOptions{ConfigPaths: []string{unsupportedAttrFixture(t)}}

messages := captureWarnings(t, func() {
_, _, err := opts.projectOrName(t.Context(), nil)
assert.NilError(t, err)
})

assert.Equal(t, len(messages), 0)
}

func TestToProjectName_DoesNotWarnOnUnsupportedAttributes(t *testing.T) {
opts := &ProjectOptions{ConfigPaths: []string{unsupportedAttrFixture(t)}}

messages := captureWarnings(t, func() {
_, err := opts.toProjectName(t.Context(), nil)
assert.NilError(t, err)
})

assert.Equal(t, len(messages), 0)
}
Loading
Loading