소스 검색

add convert subcommand to bridge command

Signed-off-by: Guillaume Lours <[email protected]>
Guillaume Lours 6 달 전
부모
커밋
024f8ebdc5
2개의 변경된 파일227개의 추가작업 그리고 3개의 파일을 삭제
  1. 16 3
      cmd/compose/bridge.go
  2. 211 0
      pkg/bridge/convert.go

+ 16 - 3
cmd/compose/bridge.go

@@ -29,7 +29,6 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/docker/compose/v2/cmd/formatter"
-	"github.com/docker/compose/v2/pkg/api"
 	"github.com/docker/compose/v2/pkg/bridge"
 )
 
@@ -47,13 +46,27 @@ func bridgeCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
 }
 
 func convertCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
-	return &cobra.Command{
+	convertOpts := bridge.ConvertOptions{}
+	cmd := &cobra.Command{
 		Use:   "convert",
 		Short: "Convert compose files to Kubernetes manifests, Helm charts, or another model",
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return api.ErrNotImplemented
+			return runConvert(ctx, dockerCli, p, convertOpts)
 		}),
 	}
+	flags := cmd.Flags()
+	flags.StringVarP(&convertOpts.Output, "output", "o", "out", "The output directory for the Kubernetes resources")
+	flags.StringArrayVarP(&convertOpts.Transformations, "transformation", "t", nil, "Transformation to apply to compose model (default: docker/compose-bridge-kubernetes)")
+	flags.StringVar(&convertOpts.Templates, "templates", "", "Directory containing transformation templates")
+	return cmd
+}
+
+func runConvert(ctx context.Context, dockerCli command.Cli, p *ProjectOptions, opts bridge.ConvertOptions) error {
+	project, _, err := p.ToProject(ctx, dockerCli, nil)
+	if err != nil {
+		return err
+	}
+	return bridge.Convert(ctx, dockerCli, project, opts)
 }
 
 func transformersCommand(dockerCli command.Cli) *cobra.Command {

+ 211 - 0
pkg/bridge/convert.go

@@ -0,0 +1,211 @@
+/*
+   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 bridge
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"strconv"
+
+	"github.com/compose-spec/compose-go/v2/types"
+	"github.com/docker/cli/cli/command"
+	cli "github.com/docker/cli/cli/command/container"
+	"github.com/docker/compose/v2/pkg/api"
+	"github.com/docker/compose/v2/pkg/utils"
+	"github.com/docker/docker/api/types/container"
+	"github.com/docker/docker/api/types/image"
+	"github.com/docker/docker/api/types/network"
+	"github.com/docker/docker/errdefs"
+	"github.com/docker/docker/pkg/jsonmessage"
+	"gopkg.in/yaml.v3"
+)
+
+type ConvertOptions struct {
+	Output          string
+	Templates       string
+	Transformations []string
+}
+
+func Convert(ctx context.Context, dockerCli command.Cli, project *types.Project, opts ConvertOptions) error {
+	if len(opts.Transformations) == 0 {
+		opts.Transformations = []string{DefaultTransformerImage}
+	}
+	// Load image references, secrets and configs, also expose ports
+	project, err := LoadAdditionalResources(ctx, dockerCli, project)
+	if err != nil {
+		return err
+	}
+	// for user to rely on compose.yaml attribute names, not go struct ones, we marshall back into YAML
+	raw, err := project.MarshalYAML(types.WithSecretContent)
+	// Marshall to YAML
+	if err != nil {
+		return fmt.Errorf("cannot render project into yaml: %w", err)
+	}
+	var model map[string]any
+	err = yaml.Unmarshal(raw, &model)
+	if err != nil {
+		return fmt.Errorf("cannot render project into yaml: %w", err)
+	}
+
+	if opts.Output != "" {
+		_ = os.RemoveAll(opts.Output)
+		err := os.MkdirAll(opts.Output, 0o744)
+		if err != nil && !os.IsExist(err) {
+			return fmt.Errorf("cannot create output folder: %w", err)
+		}
+	}
+	// Run Transformers images
+	return convert(ctx, dockerCli, model, opts)
+}
+
+func convert(ctx context.Context, dockerCli command.Cli, model map[string]any, opts ConvertOptions) error {
+	raw, err := yaml.Marshal(model)
+	if err != nil {
+		return err
+	}
+
+	dir := os.TempDir()
+	composeYaml := filepath.Join(dir, "compose.yaml")
+	err = os.WriteFile(composeYaml, raw, 0o600)
+	if err != nil {
+		return err
+	}
+
+	out, err := filepath.Abs(opts.Output)
+	if err != nil {
+		return err
+	}
+	binds := []string{
+		fmt.Sprintf("%s:%s", dir, "/in"),
+		fmt.Sprintf("%s:%s", out, "/out"),
+	}
+	if opts.Templates != "" {
+		templateDir, err := filepath.Abs(opts.Templates)
+		if err != nil {
+			return err
+		}
+		binds = append(binds, fmt.Sprintf("%s:%s", templateDir, "/templates"))
+	}
+
+	for _, transformation := range opts.Transformations {
+		_, err = inspectWithPull(ctx, dockerCli, transformation)
+		if err != nil {
+			return err
+		}
+
+		created, err := dockerCli.Client().ContainerCreate(ctx, &container.Config{
+			Image: transformation,
+			Env:   []string{"LICENSE_AGREEMENT=true"},
+		}, &container.HostConfig{
+			AutoRemove: true,
+			Binds:      binds,
+		}, &network.NetworkingConfig{}, nil, "")
+		if err != nil {
+			return err
+		}
+
+		err = cli.RunStart(ctx, dockerCli, &cli.StartOptions{
+			Attach:     true,
+			Containers: []string{created.ID},
+		})
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// LoadAdditionalResources loads additional resources from the project, such as image references, secrets, configs and exposed ports
+func LoadAdditionalResources(ctx context.Context, cli command.Cli, project *types.Project) (*types.Project, error) {
+	for name, service := range project.Services {
+		imageName := api.GetImageNameOrDefault(service, project.Name)
+
+		inspect, err := inspectWithPull(ctx, cli, imageName)
+		if err != nil {
+			return nil, err
+		}
+		service.Image = imageName
+		exposed := utils.Set[string]{}
+		exposed.AddAll(service.Expose...)
+		for port := range inspect.Config.ExposedPorts {
+			exposed.Add(port.Port())
+		}
+		for _, port := range service.Ports {
+			exposed.Add(strconv.Itoa(int(port.Target)))
+		}
+		service.Expose = exposed.Elements()
+		project.Services[name] = service
+	}
+
+	for name, secret := range project.Secrets {
+		f, err := loadFileObject(types.FileObjectConfig(secret))
+		if err != nil {
+			return nil, err
+		}
+		project.Secrets[name] = types.SecretConfig(f)
+	}
+
+	for name, config := range project.Configs {
+		f, err := loadFileObject(types.FileObjectConfig(config))
+		if err != nil {
+			return nil, err
+		}
+		project.Configs[name] = types.ConfigObjConfig(f)
+	}
+
+	return project, nil
+}
+
+func loadFileObject(conf types.FileObjectConfig) (types.FileObjectConfig, error) {
+	if !conf.External {
+		switch {
+		case conf.Environment != "":
+			conf.Content = os.Getenv(conf.Environment)
+		case conf.File != "":
+			bytes, err := os.ReadFile(conf.File)
+			if err != nil {
+				return conf, err
+			}
+			conf.Content = string(bytes)
+		}
+	}
+	return conf, nil
+}
+
+func inspectWithPull(ctx context.Context, dockerCli command.Cli, imageName string) (image.InspectResponse, error) {
+	inspect, err := dockerCli.Client().ImageInspect(ctx, imageName)
+	if errdefs.IsNotFound(err) {
+		var stream io.ReadCloser
+		stream, err = dockerCli.Client().ImagePull(ctx, imageName, image.PullOptions{})
+		if err != nil {
+			return image.InspectResponse{}, err
+		}
+		defer func() { _ = stream.Close() }()
+
+		err = jsonmessage.DisplayJSONMessagesToStream(stream, dockerCli.Out(), nil)
+		if err != nil {
+			return image.InspectResponse{}, err
+		}
+		if inspect, err = dockerCli.Client().ImageInspect(ctx, imageName); err != nil {
+			return image.InspectResponse{}, err
+		}
+	}
+	return inspect, err
+}