diff --git a/cmd/compose/bridge.go b/cmd/compose/bridge.go new file mode 100644 index 000000000..ee8fde396 --- /dev/null +++ b/cmd/compose/bridge.go @@ -0,0 +1,136 @@ +/* + 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" + "fmt" + "io" + + "github.com/distribution/reference" + "github.com/docker/cli/cli/command" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/go-units" + "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" +) + +func bridgeCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "bridge CMD [OPTIONS]", + Short: "Convert compose files into another model", + TraverseChildren: true, + } + cmd.AddCommand( + convertCommand(p, dockerCli), + transformersCommand(dockerCli), + ) + return cmd +} + +func convertCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command { + return &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 + }), + } +} + +func transformersCommand(dockerCli command.Cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "transformations CMD [OPTIONS]", + Short: "Manage transformation images", + } + cmd.AddCommand( + listTransformersCommand(dockerCli), + createTransformerCommand(dockerCli), + ) + return cmd +} + +func listTransformersCommand(dockerCli command.Cli) *cobra.Command { + options := lsOptions{} + cmd := &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List available transformations", + RunE: Adapt(func(ctx context.Context, args []string) error { + transformers, err := bridge.ListTransformers(ctx, dockerCli) + if err != nil { + return err + } + return displayTransformer(dockerCli, transformers, options) + }), + } + cmd.Flags().StringVar(&options.Format, "format", "table", "Format the output. Values: [table | json]") + cmd.Flags().BoolVarP(&options.Quiet, "quiet", "q", false, "Only display transformer names") + return cmd +} + +func displayTransformer(dockerCli command.Cli, transformers []image.Summary, options lsOptions) error { + if options.Quiet { + for _, t := range transformers { + if len(t.RepoTags) > 0 { + _, _ = fmt.Fprintln(dockerCli.Out(), t.RepoTags[0]) + } else { + _, _ = fmt.Fprintln(dockerCli.Out(), t.ID) + } + } + return nil + } + return formatter.Print(transformers, options.Format, dockerCli.Out(), + func(w io.Writer) { + for _, img := range transformers { + id := stringid.TruncateID(img.ID) + size := units.HumanSizeWithPrecision(float64(img.Size), 3) + repo, tag := "", "" + if len(img.RepoTags) > 0 { + ref, err := reference.ParseDockerRef(img.RepoTags[0]) + if err == nil { + // ParseDockerRef will reject a local image ID + repo = reference.FamiliarName(ref) + if tagged, ok := ref.(reference.Tagged); ok { + tag = tagged.Tag() + } + } + } + + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", id, repo, tag, size) + } + }, + "IMAGE ID", "REPO", "TAGS", "SIZE") +} + +func createTransformerCommand(dockerCli command.Cli) *cobra.Command { + var opts bridge.CreateTransformerOptions + cmd := &cobra.Command{ + Use: "create [OPTION] PATH", + Short: "Create a new transformation", + RunE: Adapt(func(ctx context.Context, args []string) error { + opts.Dest = args[0] + return bridge.CreateTransformer(ctx, dockerCli, opts) + }), + } + cmd.Flags().StringVarP(&opts.From, "from", "f", "", "Existing transformation to copy (default: docker/compose-bridge-kubernetes)") + return cmd +} diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index 1e1377194..4b8396974 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -645,6 +645,7 @@ func RootCommand(dockerCli command.Cli, backend Backend) *cobra.Command { //noli watchCommand(&opts, dockerCli, backend), publishCommand(&opts, dockerCli, backend), alphaCommand(&opts, dockerCli, backend), + bridgeCommand(&opts, dockerCli), ) c.Flags().SetInterspersed(false) diff --git a/pkg/bridge/transformers.go b/pkg/bridge/transformers.go new file mode 100644 index 000000000..cd3bdd29e --- /dev/null +++ b/pkg/bridge/transformers.go @@ -0,0 +1,118 @@ +/* + 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" + "os" + "path/filepath" + + "github.com/docker/cli/cli/command" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/api/types/network" + "github.com/moby/go-archive" +) + +const ( + TransformerLabel = "com.docker.compose.bridge" + DefaultTransformerImage = "docker/compose-bridge-kubernetes" +) + +type CreateTransformerOptions struct { + Dest string + From string +} + +func CreateTransformer(ctx context.Context, dockerCli command.Cli, options CreateTransformerOptions) error { + if options.From == "" { + options.From = DefaultTransformerImage + } + out, err := filepath.Abs(options.Dest) + if err != nil { + return err + } + + if _, err := os.Stat(out); err == nil { + return fmt.Errorf("output folder %s already exists", out) + } + + tmpl := filepath.Join(out, "templates") + err = os.MkdirAll(tmpl, 0o744) + if err != nil && !os.IsExist(err) { + return fmt.Errorf("cannot create output folder: %w", err) + } + + if err := command.ValidateOutputPath(out); err != nil { + return err + } + + created, err := dockerCli.Client().ContainerCreate(ctx, &container.Config{ + Image: options.From, + }, &container.HostConfig{}, &network.NetworkingConfig{}, nil, "") + defer func() { + _ = dockerCli.Client().ContainerRemove(context.Background(), created.ID, container.RemoveOptions{Force: true}) + }() + + if err != nil { + return err + } + content, stat, err := dockerCli.Client().CopyFromContainer(ctx, created.ID, "/templates") + if err != nil { + return err + } + defer func() { + _ = content.Close() + }() + + srcInfo := archive.CopyInfo{ + Path: "/templates", + Exists: true, + IsDir: stat.Mode.IsDir(), + } + + preArchive := content + if srcInfo.RebaseName != "" { + _, srcBase := archive.SplitPathDirEntry(srcInfo.Path) + preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName) + } + + if err := archive.CopyTo(preArchive, srcInfo, out); err != nil { + return err + } + + dockerfile := `FROM docker/compose-bridge-transformer +LABEL com.docker.compose.bridge=transformation +COPY templates /templates +` + if err := os.WriteFile(filepath.Join(out, "Dockerfile"), []byte(dockerfile), 0o700); err != nil { + return err + } + _, err = fmt.Fprintf(dockerCli.Out(), "Transformer created in %q\n", out) + return err +} + +func ListTransformers(ctx context.Context, dockerCli command.Cli) ([]image.Summary, error) { + api := dockerCli.Client() + return api.ImageList(ctx, image.ListOptions{ + Filters: filters.NewArgs( + filters.Arg("label", fmt.Sprintf("%s=%s", TransformerLabel, "transformation")), + ), + }) +}