/* 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" "maps" "slices" "strings" "github.com/compose-spec/compose-go/v2/types" "github.com/docker/compose/v2/pkg/api" "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" ) 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, container.ListOptions{ Filters: filtersListNames, All: true, }) if err != nil { return nil, err } containersByIds, err := s.apiClient().ContainerList(ctx, container.ListOptions{ Filters: filtersListIDs, All: true, }) if err != nil { return nil, err } for _, ctr := range containersByIds { if !slices.ContainsFunc(containers, func(summary container.Summary) bool { return summary.ID == ctr.ID }) { containers = append(containers, ctr) } } 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 []container.Summary, 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 container.InspectResponse, 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 *container.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 []container.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 }