mirror of https://github.com/docker/compose.git
457 lines
14 KiB
Go
457 lines
14 KiB
Go
/*
|
|
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"
|
|
"time"
|
|
|
|
"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2019-12-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/api/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 = "docker/aci-hostnames-sidecar:1.0"
|
|
)
|
|
|
|
// 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)
|
|
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 ctnrs []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: &ctnrs,
|
|
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()
|
|
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
|
|
}
|
|
|
|
ctnrs = append(ctnrs, containerDefinition)
|
|
}
|
|
if len(groupPorts) > 0 {
|
|
groupDefinition.ContainerGroupProperties.IPAddress = &containerinstance.IPAddress{
|
|
Type: containerinstance.Public,
|
|
Ports: &groupPorts,
|
|
DNSNameLabel: dnsLabelName,
|
|
}
|
|
}
|
|
if len(project.Services) > 1 {
|
|
dnsSideCar := getDNSSidecar(project.Services)
|
|
ctnrs = append(ctnrs, dnsSideCar)
|
|
}
|
|
groupDefinition.ContainerGroupProperties.Containers = &ctnrs
|
|
|
|
return groupDefinition, nil
|
|
}
|
|
|
|
func durationToSeconds(d *types.Duration) *int32 {
|
|
if d == nil || *d == 0 {
|
|
return nil
|
|
}
|
|
v := int32(time.Duration(*d).Seconds())
|
|
return &v
|
|
}
|
|
|
|
func getDNSSidecar(services types.Services) containerinstance.Container {
|
|
names := []string{"/hosts"}
|
|
for _, service := range services {
|
|
names = append(names, service.Name)
|
|
if service.ContainerName != "" {
|
|
names = append(names, service.ContainerName)
|
|
}
|
|
}
|
|
dnsSideCar := containerinstance.Container{
|
|
Name: to.StringPtr(ComposeDNSSidecarName),
|
|
ContainerProperties: &containerinstance.ContainerProperties{
|
|
Image: to.StringPtr(dnsSidecarImage),
|
|
Command: &names,
|
|
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() (containerinstance.Container, error) {
|
|
aciServiceVolumes, err := s.getAciFileVolumeMounts()
|
|
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
|
|
}
|
|
|
|
containerName := s.Name
|
|
if s.ContainerName != "" {
|
|
containerName = s.ContainerName
|
|
}
|
|
|
|
return containerinstance.Container{
|
|
Name: to.StringPtr(containerName),
|
|
ContainerProperties: &containerinstance.ContainerProperties{
|
|
Image: to.StringPtr(s.Image),
|
|
Command: to.StringSlicePtr(s.Command),
|
|
EnvironmentVariables: getEnvVariables(s.Environment),
|
|
Resources: resource,
|
|
VolumeMounts: volumes,
|
|
LivenessProbe: s.getLivenessProbe(),
|
|
},
|
|
}, 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 (s serviceConfigAciHelper) getLivenessProbe() *containerinstance.ContainerProbe {
|
|
if s.HealthCheck != nil && !s.HealthCheck.Disable && len(s.HealthCheck.Test) > 0 {
|
|
testArray := s.HealthCheck.Test
|
|
switch s.HealthCheck.Test[0] {
|
|
case "NONE", "CMD", "CMD-SHELL":
|
|
testArray = s.HealthCheck.Test[1:]
|
|
}
|
|
if len(testArray) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var retries *int32
|
|
if s.HealthCheck.Retries != nil {
|
|
retries = to.Int32Ptr(int32(*s.HealthCheck.Retries))
|
|
}
|
|
probe := containerinstance.ContainerProbe{
|
|
Exec: &containerinstance.ContainerExec{
|
|
Command: to.StringSlicePtr(testArray),
|
|
},
|
|
InitialDelaySeconds: durationToSeconds(s.HealthCheck.StartPeriod),
|
|
PeriodSeconds: durationToSeconds(s.HealthCheck.Interval),
|
|
TimeoutSeconds: durationToSeconds(s.HealthCheck.Timeout),
|
|
}
|
|
if retries != nil && *retries > 0 {
|
|
probe.FailureThreshold = retries
|
|
}
|
|
return &probe
|
|
}
|
|
return 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,
|
|
}
|
|
}
|
|
|
|
// FQDN retrieve the fully qualified domain name for a ContainerGroup
|
|
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
|
|
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,
|
|
}
|
|
|
|
var healthcheck = containers.Healthcheck{
|
|
Disable: true,
|
|
}
|
|
if cc.LivenessProbe != nil &&
|
|
cc.LivenessProbe.Exec != nil &&
|
|
cc.LivenessProbe.Exec.Command != nil {
|
|
if len(*cc.LivenessProbe.Exec.Command) > 0 {
|
|
healthcheck.Disable = false
|
|
healthcheck.Test = *cc.LivenessProbe.Exec.Command
|
|
if cc.LivenessProbe.PeriodSeconds != nil {
|
|
healthcheck.Interval = types.Duration(int64(*cc.LivenessProbe.PeriodSeconds) * int64(time.Second))
|
|
}
|
|
if cc.LivenessProbe.FailureThreshold != nil {
|
|
healthcheck.Retries = int(*cc.LivenessProbe.FailureThreshold)
|
|
}
|
|
if cc.LivenessProbe.TimeoutSeconds != nil {
|
|
healthcheck.Timeout = types.Duration(int64(*cc.LivenessProbe.TimeoutSeconds) * int64(time.Second))
|
|
}
|
|
if cc.LivenessProbe.InitialDelaySeconds != nil {
|
|
healthcheck.StartPeriod = types.Duration(int64(*cc.LivenessProbe.InitialDelaySeconds) * int64(time.Second))
|
|
}
|
|
}
|
|
}
|
|
|
|
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,
|
|
Healthcheck: healthcheck,
|
|
}
|
|
|
|
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
|
|
}
|