a single place for shell-out command setup

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2025-07-02 10:18:41 +02:00 committed by Guillaume Lours
parent 8faf1eb808
commit 60ee6adcd2
4 changed files with 93 additions and 95 deletions

View File

@ -34,7 +34,6 @@ import (
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/socket"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/progress"
@ -45,8 +44,6 @@ import (
"github.com/moby/buildkit/util/progress/progressui"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"golang.org/x/sync/errgroup"
)
@ -294,26 +291,12 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
logrus.Debugf("Executing bake with args: %v", args)
cmd := exec.CommandContext(ctx, buildx.Path, args...)
// Remove DOCKER_CLI_PLUGIN... variable so buildx can detect it run standalone
cmd.Env = filter(project.Environment.Values(), manager.ReexecEnvvar)
// Use docker/cli mechanism to propagate termination signal to child process
server, err := socket.NewPluginServer(nil)
if err == nil {
defer server.Close() //nolint:errcheck
cmd.Env = replace(cmd.Env, socket.EnvKey, server.Addr().String())
err = s.prepareShellOut(ctx, project, cmd)
if err != nil {
return nil, err
}
cmd.Env = append(cmd.Env,
fmt.Sprintf("DOCKER_CONTEXT=%s", s.dockerCli.CurrentContext()),
fmt.Sprintf("DOCKER_HOST=%s", s.dockerCli.DockerEndpoint().Host),
)
// propagate opentelemetry context to child process, see https://github.com/open-telemetry/oteps/blob/main/text/0258-env-context-baggage-carriers.md
carrier := propagation.MapCarrier{}
otel.GetTextMapPropagator().Inject(ctx, &carrier)
cmd.Env = append(cmd.Env, types.Mapping(carrier).Values()...)
cmd.Stdout = s.stdout()
cmd.Stdin = bytes.NewBuffer(b)
pipe, err := cmd.StderrPipe()
@ -443,22 +426,6 @@ func toBakeSecrets(project *types.Project, secrets []types.ServiceSecretConfig)
return s
}
func filter(environ []string, variable string) []string {
prefix := variable + "="
filtered := make([]string, 0, len(environ))
for _, val := range environ {
if !strings.HasPrefix(val, prefix) {
filtered = append(filtered, val)
}
}
return filtered
}
func replace(environ []string, variable, value string) []string {
filtered := filter(environ, variable)
return append(filtered, fmt.Sprintf("%s=%s", variable, value))
}
func dockerFilePath(ctxName string, dockerfile string) string {
if dockerfile == "" {
return ""

View File

@ -21,7 +21,6 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"slices"
"strconv"
@ -32,8 +31,6 @@ import (
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/compose/v2/pkg/progress"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"golang.org/x/sync/errgroup"
)
@ -51,7 +48,11 @@ func (s *composeService) ensureModels(ctx context.Context, project *types.Projec
}
cmd := exec.CommandContext(ctx, dockerModel.Path, "ls", "--json")
s.setupChildProcess(ctx, cmd)
err = s.prepareShellOut(ctx, project, cmd)
if err != nil {
return err
}
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("error checking available models: %w", err)
@ -85,23 +86,18 @@ func (s *composeService) ensureModels(ctx context.Context, project *types.Projec
eg.Go(func() error {
w := progress.ContextWriter(gctx)
if !slices.Contains(availableModels, config.Model) {
err = s.pullModel(gctx, dockerModel, config, quietPull, w)
err = s.pullModel(gctx, dockerModel, project, config, quietPull, w)
if err != nil {
return err
}
}
err = s.configureModel(gctx, dockerModel, config, w)
if err != nil {
return err
}
w.Event(progress.CreatedEvent(config.Name))
return nil
return s.configureModel(gctx, dockerModel, project, config, w)
})
}
return eg.Wait()
}
func (s *composeService) pullModel(ctx context.Context, dockerModel *manager.Plugin, model types.ModelConfig, quietPull bool, w progress.Writer) error {
func (s *composeService) pullModel(ctx context.Context, dockerModel *manager.Plugin, project *types.Project, model types.ModelConfig, quietPull bool, w progress.Writer) error {
w.Event(progress.Event{
ID: model.Name,
Status: progress.Working,
@ -109,8 +105,10 @@ func (s *composeService) pullModel(ctx context.Context, dockerModel *manager.Plu
})
cmd := exec.CommandContext(ctx, dockerModel.Path, "pull", model.Model)
s.setupChildProcess(ctx, cmd)
err := s.prepareShellOut(ctx, project, cmd)
if err != nil {
return err
}
stream, err := cmd.StdoutPipe()
if err != nil {
return err
@ -150,7 +148,7 @@ func (s *composeService) pullModel(ctx context.Context, dockerModel *manager.Plu
return err
}
func (s *composeService) configureModel(ctx context.Context, dockerModel *manager.Plugin, config types.ModelConfig, w progress.Writer) error {
func (s *composeService) configureModel(ctx context.Context, dockerModel *manager.Plugin, project *types.Project, config types.ModelConfig, w progress.Writer) error {
w.Event(progress.Event{
ID: config.Name,
Status: progress.Working,
@ -167,13 +165,20 @@ func (s *composeService) configureModel(ctx context.Context, dockerModel *manage
args = append(args, config.RuntimeFlags...)
}
cmd := exec.CommandContext(ctx, dockerModel.Path, args...)
s.setupChildProcess(ctx, cmd)
err := s.prepareShellOut(ctx, project, cmd)
if err != nil {
return err
}
return cmd.Run()
}
func (s *composeService) setModelVariables(ctx context.Context, dockerModel *manager.Plugin, project *types.Project) error {
cmd := exec.CommandContext(ctx, dockerModel.Path, "status", "--json")
s.setupChildProcess(ctx, cmd)
err := s.prepareShellOut(ctx, project, cmd)
if err != nil {
return err
}
statusOut, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("error checking docker-model status: %w", err)
@ -211,19 +216,6 @@ func (s *composeService) setModelVariables(ctx context.Context, dockerModel *man
return nil
}
func (s *composeService) setupChildProcess(gctx context.Context, cmd *exec.Cmd) {
// exec provider command with same environment Compose is running
env := types.NewMapping(os.Environ())
// but remove DOCKER_CLI_PLUGIN... variable so plugin can detect it run standalone
delete(env, manager.ReexecEnvvar)
// propagate opentelemetry context to child process, see https://github.com/open-telemetry/oteps/blob/main/text/0258-env-context-baggage-carriers.md
carrier := propagation.MapCarrier{}
otel.GetTextMapPropagator().Inject(gctx, &carrier)
env.Merge(types.Mapping(carrier))
env["DOCKER_CONTEXT"] = s.dockerCli.CurrentContext()
cmd.Env = env.Values()
}
type Model struct {
Id string `json:"id"`
Tags []string `json:"tags"`

View File

@ -29,16 +29,12 @@ import (
"strings"
"sync"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli/config"
"github.com/docker/compose/v2/pkg/progress"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/socket"
"github.com/docker/cli/cli/config"
)
type JsonMessage struct {
@ -199,29 +195,11 @@ func (s *composeService) setupPluginCommand(ctx context.Context, project *types.
args = append(args, service.Name)
cmd := exec.CommandContext(ctx, path, args...)
// exec provider command with same environment Compose is running
env := types.NewMapping(os.Environ())
// but remove DOCKER_CLI_PLUGIN... variable so plugin can detect it run standalone
delete(env, manager.ReexecEnvvar)
// and add the explicit environment variables set for service
for key, val := range service.Environment.RemoveEmpty().ToMapping() {
env[key] = val
err := s.prepareShellOut(ctx, project, cmd)
if err != nil {
return nil, err
}
cmd.Env = env.Values()
// Use docker/cli mechanism to propagate termination signal to child process
server, err := socket.NewPluginServer(nil)
if err == nil {
defer server.Close() //nolint:errcheck
cmd.Env = replace(cmd.Env, socket.EnvKey, server.Addr().String())
}
cmd.Env = append(cmd.Env, fmt.Sprintf("DOCKER_CONTEXT=%s", s.dockerCli.CurrentContext()))
// propagate opentelemetry context to child process, see https://github.com/open-telemetry/oteps/blob/main/text/0258-env-context-baggage-carriers.md
carrier := propagation.MapCarrier{}
otel.GetTextMapPropagator().Inject(ctx, &carrier)
cmd.Env = append(cmd.Env, types.Mapping(carrier).Values()...)
return cmd, nil
}

61
pkg/compose/shellout.go Normal file
View File

@ -0,0 +1,61 @@
/*
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 (
"context"
"os/exec"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli/context/docker"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
)
// prepareShellOut prepare a shell-out command to be ran by Compose
func (s *composeService) prepareShellOut(gctx context.Context, project *types.Project, cmd *exec.Cmd) error {
// exec command with same environment Compose is running
env := types.NewMapping(project.Environment.Values())
// remove DOCKER_CLI_PLUGIN... variable so a docker-cli plugin will detect it run standalone
delete(env, manager.ReexecEnvvar)
// propagate opentelemetry context to child process, see https://github.com/open-telemetry/oteps/blob/main/text/0258-env-context-baggage-carriers.md
carrier := propagation.MapCarrier{}
otel.GetTextMapPropagator().Inject(gctx, &carrier)
env.Merge(types.Mapping(carrier))
env["DOCKER_CONTEXT"] = s.dockerCli.CurrentContext()
metadata, err := s.dockerCli.ContextStore().GetMetadata(s.dockerCli.CurrentContext())
if err != nil {
return err
}
endpoint, err := docker.EndpointFromContext(metadata)
if err != nil {
return err
}
actualHost := s.dockerCli.DockerEndpoint().Host
if endpoint.Host != actualHost {
// We are running with `--host` or `DOCKER_HOST` which overrides selected context
env["DOCKER_HOST"] = actualHost
}
cmd.Env = env.Values()
return nil
}