/* 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 convert import ( "context" "fmt" "math" "os" "strconv" "strings" "github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2018-10-01/containerinstance" "github.com/Azure/go-autorest/autorest/to" "github.com/compose-spec/compose-go/types" "github.com/pkg/errors" "github.com/docker/compose-cli/aci/login" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/containers" "github.com/docker/compose-cli/context/store" "github.com/docker/compose-cli/utils/formatter" ) const ( // StatusRunning name of the ACI running status StatusRunning = "Running" // ComposeDNSSidecarName name of the dns sidecar container ComposeDNSSidecarName = "aci--dns--sidecar" dnsSidecarImage = "busybox:1.31.1" ) // ToContainerGroup converts a compose project into a ACI container group func ToContainerGroup(ctx context.Context, aciContext store.AciContext, p types.Project, storageHelper login.StorageLogin) (containerinstance.ContainerGroup, error) { project := projectAciHelper(p) containerGroupName := strings.ToLower(project.Name) volumesCache, volumesSlice, err := project.getAciFileVolumes(ctx, storageHelper) if err != nil { return containerinstance.ContainerGroup{}, err } secretVolumes, err := project.getAciSecretVolumes() if err != nil { return containerinstance.ContainerGroup{}, err } allVolumes := append(volumesSlice, secretVolumes...) var volumes *[]containerinstance.Volume if len(allVolumes) > 0 { volumes = &allVolumes } registryCreds, err := getRegistryCredentials(p, newCliRegistryConfLoader()) if err != nil { return containerinstance.ContainerGroup{}, err } var containers []containerinstance.Container restartPolicy, err := project.getRestartPolicy() if err != nil { return containerinstance.ContainerGroup{}, err } groupDefinition := containerinstance.ContainerGroup{ Name: &containerGroupName, Location: &aciContext.Location, ContainerGroupProperties: &containerinstance.ContainerGroupProperties{ OsType: containerinstance.Linux, Containers: &containers, Volumes: volumes, ImageRegistryCredentials: ®istryCreds, RestartPolicy: restartPolicy, }, } var groupPorts []containerinstance.Port var dnsLabelName *string for _, s := range project.Services { service := serviceConfigAciHelper(s) containerDefinition, err := service.getAciContainer(volumesCache) if err != nil { return containerinstance.ContainerGroup{}, err } if service.Labels != nil && len(service.Labels) > 0 { return containerinstance.ContainerGroup{}, errors.New("ACI integration does not support labels in compose applications") } containerPorts, serviceGroupPorts, serviceDomainName, err := convertPortsToAci(service) if err != nil { return groupDefinition, err } containerDefinition.ContainerProperties.Ports = &containerPorts groupPorts = append(groupPorts, serviceGroupPorts...) if serviceDomainName != nil { if dnsLabelName != nil && *serviceDomainName != *dnsLabelName { return containerinstance.ContainerGroup{}, fmt.Errorf("ACI integration does not support specifying different domain names on services in the same compose application") } dnsLabelName = serviceDomainName } containers = append(containers, containerDefinition) } if len(groupPorts) > 0 { groupDefinition.ContainerGroupProperties.IPAddress = &containerinstance.IPAddress{ Type: containerinstance.Public, Ports: &groupPorts, DNSNameLabel: dnsLabelName, } } if len(containers) > 1 { dnsSideCar := getDNSSidecar(containers) containers = append(containers, dnsSideCar) } groupDefinition.ContainerGroupProperties.Containers = &containers return groupDefinition, nil } func getDNSSidecar(containers []containerinstance.Container) containerinstance.Container { var commands []string for _, container := range containers { commands = append(commands, fmt.Sprintf("echo 127.0.0.1 %s >> /etc/hosts", *container.Name)) } // ACI restart policy is currently at container group level, cannot let the sidecar terminate quietly once /etc/hosts has been edited // Pricing is done at the container group level so letting the sidecar container "sleep" should not impact the price for the whole group commands = append(commands, "sleep infinity") alpineCmd := []string{"sh", "-c", strings.Join(commands, ";")} dnsSideCar := containerinstance.Container{ Name: to.StringPtr(ComposeDNSSidecarName), ContainerProperties: &containerinstance.ContainerProperties{ Image: to.StringPtr(dnsSidecarImage), Command: &alpineCmd, Resources: &containerinstance.ResourceRequirements{ Requests: &containerinstance.ResourceRequests{ MemoryInGB: to.Float64Ptr(0.1), CPU: to.Float64Ptr(0.01), }, }, }, } return dnsSideCar } type projectAciHelper types.Project type serviceConfigAciHelper types.ServiceConfig func (s serviceConfigAciHelper) getAciContainer(volumesCache map[string]bool) (containerinstance.Container, error) { aciServiceVolumes, err := s.getAciFileVolumeMounts(volumesCache) if err != nil { return containerinstance.Container{}, err } serviceSecretVolumes, err := s.getAciSecretsVolumeMounts() if err != nil { return containerinstance.Container{}, err } allVolumes := append(aciServiceVolumes, serviceSecretVolumes...) var volumes *[]containerinstance.VolumeMount if len(allVolumes) > 0 { volumes = &allVolumes } resource, err := s.getResourceRequestsLimits() if err != nil { return containerinstance.Container{}, err } return containerinstance.Container{ Name: to.StringPtr(s.Name), ContainerProperties: &containerinstance.ContainerProperties{ Image: to.StringPtr(s.Image), Command: to.StringSlicePtr(s.Command), EnvironmentVariables: getEnvVariables(s.Environment), Resources: resource, VolumeMounts: volumes, }, }, nil } func (s serviceConfigAciHelper) getResourceRequestsLimits() (*containerinstance.ResourceRequirements, error) { memRequest := 1. // Default 1 Gb var cpuRequest float64 = 1 var err error hasMemoryRequest := func() bool { return s.Deploy != nil && s.Deploy.Resources.Reservations != nil && s.Deploy.Resources.Reservations.MemoryBytes != 0 } hasCPURequest := func() bool { return s.Deploy != nil && s.Deploy.Resources.Reservations != nil && s.Deploy.Resources.Reservations.NanoCPUs != "" } if hasMemoryRequest() { memRequest = BytesToGB(float64(s.Deploy.Resources.Reservations.MemoryBytes)) } if hasCPURequest() { cpuRequest, err = strconv.ParseFloat(s.Deploy.Resources.Reservations.NanoCPUs, 0) if err != nil { return nil, err } } memLimit := memRequest cpuLimit := cpuRequest if s.Deploy != nil && s.Deploy.Resources.Limits != nil { if s.Deploy.Resources.Limits.MemoryBytes != 0 { memLimit = BytesToGB(float64(s.Deploy.Resources.Limits.MemoryBytes)) if !hasMemoryRequest() { memRequest = memLimit } } if s.Deploy.Resources.Limits.NanoCPUs != "" { cpuLimit, err = strconv.ParseFloat(s.Deploy.Resources.Limits.NanoCPUs, 0) if err != nil { return nil, err } if !hasCPURequest() { cpuRequest = cpuLimit } } } resources := containerinstance.ResourceRequirements{ Requests: &containerinstance.ResourceRequests{ MemoryInGB: to.Float64Ptr(memRequest), CPU: to.Float64Ptr(cpuRequest), }, Limits: &containerinstance.ResourceLimits{ MemoryInGB: to.Float64Ptr(memLimit), CPU: to.Float64Ptr(cpuLimit), }, } return &resources, nil } func getEnvVariables(composeEnv types.MappingWithEquals) *[]containerinstance.EnvironmentVariable { result := []containerinstance.EnvironmentVariable{} for key, value := range composeEnv { var strValue string if value == nil { strValue = os.Getenv(key) } else { strValue = *value } result = append(result, containerinstance.EnvironmentVariable{ Name: to.StringPtr(key), Value: to.StringPtr(strValue), }) } return &result } // BytesToGB convert bytes To GB func BytesToGB(b float64) float64 { f := b / 1024 / 1024 / 1024 // from bytes to gigabytes return math.Round(f*100) / 100 } func gbToBytes(memInBytes float64) uint64 { return uint64(memInBytes * 1024 * 1024 * 1024) } // ContainerGroupToServiceStatus convert from an ACI container definition to service status func ContainerGroupToServiceStatus(containerID string, group containerinstance.ContainerGroup, container containerinstance.Container, region string) compose.ServiceStatus { var replicas = 1 if GetStatus(container, group) != StatusRunning { replicas = 0 } return compose.ServiceStatus{ ID: containerID, Name: *container.Name, Ports: formatter.PortsToStrings(ToPorts(group.IPAddress, *container.Ports), fqdn(group, region)), Replicas: replicas, Desired: 1, } } func fqdn(group containerinstance.ContainerGroup, region string) string { fqdn := "" if group.IPAddress != nil && group.IPAddress.DNSNameLabel != nil && *group.IPAddress.DNSNameLabel != "" { fqdn = *group.IPAddress.DNSNameLabel + "." + region + ".azurecontainer.io" } return fqdn } // ContainerGroupToContainer composes a Container from an ACI container definition func ContainerGroupToContainer(containerID string, cg containerinstance.ContainerGroup, cc containerinstance.Container, region string) containers.Container { command := "" if cc.Command != nil { command = strings.Join(*cc.Command, " ") } status := GetStatus(cc, cg) platform := string(cg.OsType) var envVars map[string]string = nil if cc.EnvironmentVariables != nil && len(*cc.EnvironmentVariables) != 0 { envVars = map[string]string{} for _, envVar := range *cc.EnvironmentVariables { envVars[*envVar.Name] = *envVar.Value } } hostConfig := ToHostConfig(cc, cg) config := &containers.RuntimeConfig{ FQDN: fqdn(cg, region), Env: envVars, } c := containers.Container{ ID: containerID, Status: status, Image: to.String(cc.Image), Command: command, CPUTime: 0, MemoryUsage: 0, PidsCurrent: 0, PidsLimit: 0, Ports: ToPorts(cg.IPAddress, *cc.Ports), Platform: platform, Config: config, HostConfig: hostConfig, } return c } // ToHostConfig convert an ACI container to host config value func ToHostConfig(cc containerinstance.Container, cg containerinstance.ContainerGroup) *containers.HostConfig { memLimits := uint64(0) memRequest := uint64(0) cpuLimit := 0. cpuReservation := 0. if cc.Resources != nil { if cc.Resources.Limits != nil { if cc.Resources.Limits.MemoryInGB != nil { memLimits = gbToBytes(*cc.Resources.Limits.MemoryInGB) } if cc.Resources.Limits.CPU != nil { cpuLimit = *cc.Resources.Limits.CPU } } if cc.Resources.Requests != nil { if cc.Resources.Requests.MemoryInGB != nil { memRequest = gbToBytes(*cc.Resources.Requests.MemoryInGB) } if cc.Resources.Requests.CPU != nil { cpuReservation = *cc.Resources.Requests.CPU } } } hostConfig := &containers.HostConfig{ CPULimit: cpuLimit, CPUReservation: cpuReservation, MemoryLimit: memLimits, MemoryReservation: memRequest, RestartPolicy: toContainerRestartPolicy(cg.RestartPolicy), } return hostConfig } // GetStatus returns status for the specified container func GetStatus(container containerinstance.Container, group containerinstance.ContainerGroup) string { status := GetGroupStatus(group) if container.InstanceView != nil && container.InstanceView.CurrentState != nil { status = *container.InstanceView.CurrentState.State } return status } // GetGroupStatus returns status for the container group func GetGroupStatus(group containerinstance.ContainerGroup) string { if group.InstanceView != nil && group.InstanceView.State != nil { return "Node " + *group.InstanceView.State } return compose.UNKNOWN }