add convert subcommand to bridge command

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
This commit is contained in:
Guillaume Lours 2025-05-23 14:06:47 +02:00 committed by Nicolas De loof
parent 8c622da20b
commit 024f8ebdc5
2 changed files with 227 additions and 3 deletions

View File

@ -29,7 +29,6 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/compose/v2/cmd/formatter" "github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/bridge" "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 { func convertCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
return &cobra.Command{ convertOpts := bridge.ConvertOptions{}
cmd := &cobra.Command{
Use: "convert", Use: "convert",
Short: "Convert compose files to Kubernetes manifests, Helm charts, or another model", Short: "Convert compose files to Kubernetes manifests, Helm charts, or another model",
RunE: Adapt(func(ctx context.Context, args []string) error { 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 { func transformersCommand(dockerCli command.Cli) *cobra.Command {

211
pkg/bridge/convert.go Normal file
View File

@ -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
}