248 lines
7.9 KiB
Go
248 lines
7.9 KiB
Go
/*
|
|
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
|
|
}
|