mirror of
https://github.com/docker/compose.git
synced 2025-04-08 17:05:13 +02:00
introduce generate command as alpha command
Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
This commit is contained in:
parent
fafaa9c5b8
commit
51ebeb5441
@ -33,6 +33,7 @@ func alphaCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||
cmd.AddCommand(
|
||||
vizCommand(p, dockerCli, backend),
|
||||
publishCommand(p, dockerCli, backend),
|
||||
generateCommand(p, backend),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
82
cmd/compose/generate.go
Normal file
82
cmd/compose/generate.go
Normal file
@ -0,0 +1,82 @@
|
||||
/*
|
||||
Copyright 2023 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"
|
||||
"os"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type generateOptions struct {
|
||||
*ProjectOptions
|
||||
Format string
|
||||
}
|
||||
|
||||
func generateCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
|
||||
opts := generateOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "generate [OPTIONS] [CONTAINERS...]",
|
||||
Short: "EXPERIMENTAL - Generate a Compose file from existing containers",
|
||||
PreRunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}),
|
||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||
return runGenerate(ctx, backend, opts, args)
|
||||
}),
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&opts.ProjectName, "name", "", "Project name to set in the Compose file")
|
||||
cmd.Flags().StringVar(&opts.ProjectDir, "project-dir", "", "Directory to use for the project")
|
||||
cmd.Flags().StringVar(&opts.Format, "format", "yaml", "Format the output. Values: [yaml | json]")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runGenerate(ctx context.Context, backend api.Service, opts generateOptions, containers []string) error {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "generate command is EXPERIMENTAL")
|
||||
if len(containers) == 0 {
|
||||
return fmt.Errorf("at least one container must be specified")
|
||||
}
|
||||
project, err := backend.Generate(ctx, api.GenerateOptions{
|
||||
Containers: containers,
|
||||
ProjectName: opts.ProjectName,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var content []byte
|
||||
switch opts.Format {
|
||||
case "json":
|
||||
content, err = project.MarshalJSON()
|
||||
case "yaml":
|
||||
content, err = project.MarshalYAML()
|
||||
default:
|
||||
return fmt.Errorf("unsupported format %q", opts.Format)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(content))
|
||||
|
||||
return nil
|
||||
}
|
17
docs/reference/compose_alpha_generate.md
Normal file
17
docs/reference/compose_alpha_generate.md
Normal file
@ -0,0 +1,17 @@
|
||||
# docker compose alpha generate
|
||||
|
||||
<!---MARKER_GEN_START-->
|
||||
EXPERIMENTAL - Generate a Compose file from existing containers
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:----------------|:---------|:--------|:------------------------------------------|
|
||||
| `--dry-run` | `bool` | | Execute command in dry run mode |
|
||||
| `--format` | `string` | `yaml` | Format the output. Values: [yaml \| json] |
|
||||
| `--name` | `string` | | Project name to set in the Compose file |
|
||||
| `--project-dir` | `string` | | Directory to use for the project |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
@ -4,9 +4,11 @@ long: Experimental commands
|
||||
pname: docker compose
|
||||
plink: docker_compose.yaml
|
||||
cname:
|
||||
- docker compose alpha generate
|
||||
- docker compose alpha publish
|
||||
- docker compose alpha viz
|
||||
clink:
|
||||
- docker_compose_alpha_generate.yaml
|
||||
- docker_compose_alpha_publish.yaml
|
||||
- docker_compose_alpha_viz.yaml
|
||||
inherited_options:
|
||||
|
53
docs/reference/docker_compose_alpha_generate.yaml
Normal file
53
docs/reference/docker_compose_alpha_generate.yaml
Normal file
@ -0,0 +1,53 @@
|
||||
command: docker compose alpha generate
|
||||
short: EXPERIMENTAL - Generate a Compose file from existing containers
|
||||
long: EXPERIMENTAL - Generate a Compose file from existing containers
|
||||
usage: docker compose alpha generate [OPTIONS] [CONTAINERS...]
|
||||
pname: docker compose alpha
|
||||
plink: docker_compose_alpha.yaml
|
||||
options:
|
||||
- option: format
|
||||
value_type: string
|
||||
default_value: yaml
|
||||
description: 'Format the output. Values: [yaml | json]'
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: name
|
||||
value_type: string
|
||||
description: Project name to set in the Compose file
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
- option: project-dir
|
||||
value_type: string
|
||||
description: Directory to use for the project
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
inherited_options:
|
||||
- option: dry-run
|
||||
value_type: bool
|
||||
default_value: "false"
|
||||
description: Execute command in dry run mode
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: false
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
deprecated: false
|
||||
hidden: false
|
||||
experimental: false
|
||||
experimentalcli: true
|
||||
kubernetes: false
|
||||
swarm: false
|
||||
|
@ -92,6 +92,8 @@ type Service interface {
|
||||
Scale(ctx context.Context, project *types.Project, options ScaleOptions) error
|
||||
// Export a service container's filesystem as a tar archive
|
||||
Export(ctx context.Context, projectName string, options ExportOptions) error
|
||||
// Generate generates a Compose Project from existing containers
|
||||
Generate(ctx context.Context, options GenerateOptions) (*types.Project, error)
|
||||
}
|
||||
|
||||
type ScaleOptions struct {
|
||||
@ -563,6 +565,13 @@ type ExportOptions struct {
|
||||
Output string
|
||||
}
|
||||
|
||||
type GenerateOptions struct {
|
||||
// ProjectName to set in the Compose file
|
||||
ProjectName string
|
||||
// Containers passed in the command line to be used as reference for service definition
|
||||
Containers []string
|
||||
}
|
||||
|
||||
const (
|
||||
// STARTING indicates that stack is being deployed
|
||||
STARTING string = "Starting"
|
||||
|
@ -176,7 +176,10 @@ func (s *composeService) projectFromName(containers Containers, projectName stri
|
||||
}
|
||||
set := types.Services{}
|
||||
for _, c := range containers {
|
||||
serviceLabel := c.Labels[api.ServiceLabel]
|
||||
serviceLabel, ok := c.Labels[api.ServiceLabel]
|
||||
if !ok {
|
||||
serviceLabel = getCanonicalContainerName(c)
|
||||
}
|
||||
service, ok := set[serviceLabel]
|
||||
if !ok {
|
||||
service = types.ServiceConfig{
|
||||
|
247
pkg/compose/generate.go
Normal file
247
pkg/compose/generate.go
Normal file
@ -0,0 +1,247 @@
|
||||
/*
|
||||
Copyright 2023 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"
|
||||
"strings"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
moby "github.com/docker/docker/api/types"
|
||||
containerType "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
func (s *composeService) Generate(ctx context.Context, options api.GenerateOptions) (*types.Project, error) {
|
||||
filtersListNames := filters.NewArgs()
|
||||
filtersListIDs := filters.NewArgs()
|
||||
for _, containerName := range options.Containers {
|
||||
filtersListNames.Add("name", containerName)
|
||||
filtersListIDs.Add("id", containerName)
|
||||
}
|
||||
containers, err := s.apiClient().ContainerList(ctx, containerType.ListOptions{
|
||||
Filters: filtersListNames,
|
||||
All: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
containersByIds, err := s.apiClient().ContainerList(ctx, containerType.ListOptions{
|
||||
Filters: filtersListIDs,
|
||||
All: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, container := range containersByIds {
|
||||
if !utils.Contains(containers, container) {
|
||||
containers = append(containers, container)
|
||||
}
|
||||
}
|
||||
|
||||
if len(containers) == 0 {
|
||||
return nil, fmt.Errorf("no container(s) found with the following name(s): %s", strings.Join(options.Containers, ","))
|
||||
}
|
||||
|
||||
return s.createProjectFromContainers(containers, options.ProjectName)
|
||||
}
|
||||
|
||||
func (s *composeService) createProjectFromContainers(containers []moby.Container, projectName string) (*types.Project, error) {
|
||||
project := &types.Project{}
|
||||
services := types.Services{}
|
||||
networks := types.Networks{}
|
||||
volumes := types.Volumes{}
|
||||
secrets := types.Secrets{}
|
||||
|
||||
if projectName != "" {
|
||||
project.Name = projectName
|
||||
}
|
||||
|
||||
for _, c := range containers {
|
||||
// if the container is from a previous Compose application, use the existing service name
|
||||
serviceLabel, ok := c.Labels[api.ServiceLabel]
|
||||
if !ok {
|
||||
serviceLabel = getCanonicalContainerName(c)
|
||||
}
|
||||
service, ok := services[serviceLabel]
|
||||
if !ok {
|
||||
service = types.ServiceConfig{
|
||||
Name: serviceLabel,
|
||||
Image: c.Image,
|
||||
Labels: c.Labels,
|
||||
}
|
||||
|
||||
}
|
||||
service.Scale = increment(service.Scale)
|
||||
|
||||
inspect, err := s.apiClient().ContainerInspect(context.Background(), c.ID)
|
||||
if err != nil {
|
||||
services[serviceLabel] = service
|
||||
continue
|
||||
}
|
||||
s.extractComposeConfiguration(&service, inspect, volumes, secrets, networks)
|
||||
service.Labels = cleanDockerPreviousLabels(service.Labels)
|
||||
services[serviceLabel] = service
|
||||
}
|
||||
|
||||
project.Services = services
|
||||
project.Networks = networks
|
||||
project.Volumes = volumes
|
||||
project.Secrets = secrets
|
||||
return project, nil
|
||||
}
|
||||
|
||||
func (s *composeService) extractComposeConfiguration(service *types.ServiceConfig, inspect moby.ContainerJSON, volumes types.Volumes, secrets types.Secrets, networks types.Networks) {
|
||||
service.Environment = types.NewMappingWithEquals(inspect.Config.Env)
|
||||
if inspect.Config.Healthcheck != nil {
|
||||
healthConfig := inspect.Config.Healthcheck
|
||||
service.HealthCheck = s.toComposeHealthCheck(healthConfig)
|
||||
}
|
||||
if len(inspect.Mounts) > 0 {
|
||||
detectedVolumes, volumeConfigs, detectedSecrets, secretsConfigs := s.toComposeVolumes(inspect.Mounts)
|
||||
service.Volumes = append(service.Volumes, volumeConfigs...)
|
||||
service.Secrets = append(service.Secrets, secretsConfigs...)
|
||||
maps.Copy(volumes, detectedVolumes)
|
||||
maps.Copy(secrets, detectedSecrets)
|
||||
}
|
||||
if len(inspect.NetworkSettings.Networks) > 0 {
|
||||
detectedNetworks, networkConfigs := s.toComposeNetwork(inspect.NetworkSettings.Networks)
|
||||
service.Networks = networkConfigs
|
||||
maps.Copy(networks, detectedNetworks)
|
||||
}
|
||||
if len(inspect.HostConfig.PortBindings) > 0 {
|
||||
for key, portBindings := range inspect.HostConfig.PortBindings {
|
||||
for _, portBinding := range portBindings {
|
||||
service.Ports = append(service.Ports, types.ServicePortConfig{
|
||||
Target: uint32(key.Int()),
|
||||
Published: portBinding.HostPort,
|
||||
Protocol: key.Proto(),
|
||||
HostIP: portBinding.HostIP,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *composeService) toComposeHealthCheck(healthConfig *containerType.HealthConfig) *types.HealthCheckConfig {
|
||||
var healthCheck types.HealthCheckConfig
|
||||
healthCheck.Test = healthConfig.Test
|
||||
if healthConfig.Timeout != 0 {
|
||||
timeout := types.Duration(healthConfig.Timeout)
|
||||
healthCheck.Timeout = &timeout
|
||||
}
|
||||
if healthConfig.Interval != 0 {
|
||||
interval := types.Duration(healthConfig.Interval)
|
||||
healthCheck.Interval = &interval
|
||||
}
|
||||
if healthConfig.StartPeriod != 0 {
|
||||
startPeriod := types.Duration(healthConfig.StartPeriod)
|
||||
healthCheck.StartPeriod = &startPeriod
|
||||
}
|
||||
if healthConfig.StartInterval != 0 {
|
||||
startInterval := types.Duration(healthConfig.StartInterval)
|
||||
healthCheck.StartInterval = &startInterval
|
||||
}
|
||||
if healthConfig.Retries != 0 {
|
||||
retries := uint64(healthConfig.Retries)
|
||||
healthCheck.Retries = &retries
|
||||
}
|
||||
return &healthCheck
|
||||
}
|
||||
|
||||
func (s *composeService) toComposeVolumes(volumes []moby.MountPoint) (map[string]types.VolumeConfig,
|
||||
[]types.ServiceVolumeConfig, map[string]types.SecretConfig, []types.ServiceSecretConfig) {
|
||||
volumeConfigs := make(map[string]types.VolumeConfig)
|
||||
secretConfigs := make(map[string]types.SecretConfig)
|
||||
var serviceVolumeConfigs []types.ServiceVolumeConfig
|
||||
var serviceSecretConfigs []types.ServiceSecretConfig
|
||||
|
||||
for _, volume := range volumes {
|
||||
serviceVC := types.ServiceVolumeConfig{
|
||||
Type: string(volume.Type),
|
||||
Source: volume.Source,
|
||||
Target: volume.Destination,
|
||||
ReadOnly: !volume.RW,
|
||||
}
|
||||
switch volume.Type {
|
||||
case mount.TypeVolume:
|
||||
serviceVC.Source = volume.Name
|
||||
vol := types.VolumeConfig{}
|
||||
if volume.Driver != "local" {
|
||||
vol.Driver = volume.Driver
|
||||
vol.Name = volume.Name
|
||||
}
|
||||
volumeConfigs[volume.Name] = vol
|
||||
serviceVolumeConfigs = append(serviceVolumeConfigs, serviceVC)
|
||||
case mount.TypeBind:
|
||||
if strings.HasPrefix(volume.Destination, "/run/secrets") {
|
||||
destination := strings.Split(volume.Destination, "/")
|
||||
secret := types.SecretConfig{
|
||||
Name: destination[len(destination)-1],
|
||||
File: strings.TrimPrefix(volume.Source, "/host_mnt"),
|
||||
}
|
||||
secretConfigs[secret.Name] = secret
|
||||
serviceSecretConfigs = append(serviceSecretConfigs, types.ServiceSecretConfig{
|
||||
Source: secret.Name,
|
||||
Target: volume.Destination,
|
||||
})
|
||||
} else {
|
||||
serviceVolumeConfigs = append(serviceVolumeConfigs, serviceVC)
|
||||
}
|
||||
}
|
||||
}
|
||||
return volumeConfigs, serviceVolumeConfigs, secretConfigs, serviceSecretConfigs
|
||||
}
|
||||
|
||||
func (s *composeService) toComposeNetwork(networks map[string]*network.EndpointSettings) (map[string]types.NetworkConfig, map[string]*types.ServiceNetworkConfig) {
|
||||
networkConfigs := make(map[string]types.NetworkConfig)
|
||||
serviceNetworkConfigs := make(map[string]*types.ServiceNetworkConfig)
|
||||
|
||||
for name, net := range networks {
|
||||
inspect, err := s.apiClient().NetworkInspect(context.Background(), name, network.InspectOptions{})
|
||||
if err != nil {
|
||||
networkConfigs[name] = types.NetworkConfig{}
|
||||
} else {
|
||||
networkConfigs[name] = types.NetworkConfig{
|
||||
Internal: inspect.Internal,
|
||||
}
|
||||
|
||||
}
|
||||
serviceNetworkConfigs[name] = &types.ServiceNetworkConfig{
|
||||
Aliases: net.Aliases,
|
||||
}
|
||||
}
|
||||
return networkConfigs, serviceNetworkConfigs
|
||||
}
|
||||
|
||||
func cleanDockerPreviousLabels(labels types.Labels) types.Labels {
|
||||
cleanedLabels := types.Labels{}
|
||||
for key, value := range labels {
|
||||
if !strings.HasPrefix(key, "com.docker.compose.") && !strings.HasPrefix(key, "desktop.docker.io") {
|
||||
cleanedLabels[key] = value
|
||||
}
|
||||
}
|
||||
return cleanedLabels
|
||||
}
|
@ -169,6 +169,21 @@ func (mr *MockServiceMockRecorder) Export(ctx, projectName, options any) *gomock
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Export", reflect.TypeOf((*MockService)(nil).Export), ctx, projectName, options)
|
||||
}
|
||||
|
||||
// Generate mocks base method.
|
||||
func (m *MockService) Generate(ctx context.Context, options api.GenerateOptions) (*types.Project, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Generate", ctx, options)
|
||||
ret0, _ := ret[0].(*types.Project)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Generate indicates an expected call of Generate.
|
||||
func (mr *MockServiceMockRecorder) Generate(ctx, options any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Generate", reflect.TypeOf((*MockService)(nil).Generate), ctx, options)
|
||||
}
|
||||
|
||||
// Images mocks base method.
|
||||
func (m *MockService) Images(ctx context.Context, projectName string, options api.ImagesOptions) ([]api.ImageSummary, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
Loading…
x
Reference in New Issue
Block a user