| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456 |
- /*
- 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/containers"
- "github.com/docker/compose-cli/api/context/store"
- "github.com/docker/compose-cli/pkg/api"
- "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) api.ServiceStatus {
- var replicas = 1
- if GetStatus(container, group) != StatusRunning {
- replicas = 0
- }
- return api.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 api.UNKNOWN
- }
|