| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555 | 
							- /*
 
-    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 ecs
 
- import (
 
- 	"encoding/json"
 
- 	"fmt"
 
- 	"os"
 
- 	"path/filepath"
 
- 	"sort"
 
- 	"strconv"
 
- 	"strings"
 
- 	"time"
 
- 	"github.com/docker/compose-cli/ecs/secrets"
 
- 	ecsapi "github.com/aws/aws-sdk-go/service/ecs"
 
- 	"github.com/awslabs/goformation/v4/cloudformation"
 
- 	"github.com/awslabs/goformation/v4/cloudformation/ecs"
 
- 	"github.com/compose-spec/compose-go/types"
 
- 	"github.com/docker/cli/opts"
 
- 	"github.com/joho/godotenv"
 
- )
 
- const secretsInitContainerImage = "docker/ecs-secrets-sidecar"
 
- const searchDomainInitContainerImage = "docker/ecs-searchdomain-sidecar"
 
- func (b *ecsAPIService) createTaskDefinition(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefinition, error) {
 
- 	cpu, mem, err := toLimits(service)
 
- 	if err != nil {
 
- 		return nil, err
 
- 	}
 
- 	_, memReservation := toContainerReservation(service)
 
- 	credential := getRepoCredentials(service)
 
- 	logConfiguration := getLogConfiguration(service, project)
 
- 	var (
 
- 		initContainers []ecs.TaskDefinition_ContainerDefinition
 
- 		volumes        []ecs.TaskDefinition_Volume
 
- 		mounts         []ecs.TaskDefinition_MountPoint
 
- 	)
 
- 	if len(service.Secrets) > 0 {
 
- 		secretsVolume, secretsMount, secretsSideCar, err := createSecretsSideCar(project, service, logConfiguration)
 
- 		if err != nil {
 
- 			return nil, err
 
- 		}
 
- 		initContainers = append(initContainers, secretsSideCar)
 
- 		volumes = append(volumes, secretsVolume)
 
- 		mounts = append(mounts, secretsMount)
 
- 	}
 
- 	initContainers = append(initContainers, ecs.TaskDefinition_ContainerDefinition{
 
- 		Name:             fmt.Sprintf("%s_ResolvConf_InitContainer", normalizeResourceName(service.Name)),
 
- 		Image:            searchDomainInitContainerImage,
 
- 		Essential:        false,
 
- 		Command:          []string{b.Region + ".compute.internal", project.Name + ".local"},
 
- 		LogConfiguration: logConfiguration,
 
- 	})
 
- 	var dependencies []ecs.TaskDefinition_ContainerDependency
 
- 	for _, c := range initContainers {
 
- 		dependencies = append(dependencies, ecs.TaskDefinition_ContainerDependency{
 
- 			Condition:     ecsapi.ContainerConditionSuccess,
 
- 			ContainerName: c.Name,
 
- 		})
 
- 	}
 
- 	for _, v := range service.Volumes {
 
- 		source := project.Volumes[v.Source]
 
- 		volumes = append(volumes, ecs.TaskDefinition_Volume{
 
- 			EFSVolumeConfiguration: &ecs.TaskDefinition_EFSVolumeConfiguration{
 
- 				FilesystemId:  source.Name,
 
- 				RootDirectory: source.DriverOpts["root_directory"],
 
- 			},
 
- 			Name: v.Source,
 
- 		})
 
- 		mounts = append(mounts, ecs.TaskDefinition_MountPoint{
 
- 			ContainerPath: v.Target,
 
- 			ReadOnly:      v.ReadOnly,
 
- 			SourceVolume:  v.Source,
 
- 		})
 
- 	}
 
- 	pairs, err := createEnvironment(project, service)
 
- 	if err != nil {
 
- 		return nil, err
 
- 	}
 
- 	var reservations *types.Resource
 
- 	if service.Deploy != nil && service.Deploy.Resources.Reservations != nil {
 
- 		reservations = service.Deploy.Resources.Reservations
 
- 	}
 
- 	containers := append(initContainers, ecs.TaskDefinition_ContainerDefinition{
 
- 		Command:                service.Command,
 
- 		DisableNetworking:      service.NetworkMode == "none",
 
- 		DependsOnProp:          dependencies,
 
- 		DnsSearchDomains:       service.DNSSearch,
 
- 		DnsServers:             service.DNS,
 
- 		DockerSecurityOptions:  service.SecurityOpt,
 
- 		EntryPoint:             service.Entrypoint,
 
- 		Environment:            pairs,
 
- 		Essential:              true,
 
- 		ExtraHosts:             toHostEntryPtr(service.ExtraHosts),
 
- 		FirelensConfiguration:  nil,
 
- 		HealthCheck:            toHealthCheck(service.HealthCheck),
 
- 		Hostname:               service.Hostname,
 
- 		Image:                  service.Image,
 
- 		Interactive:            false,
 
- 		Links:                  nil,
 
- 		LinuxParameters:        toLinuxParameters(service),
 
- 		LogConfiguration:       logConfiguration,
 
- 		MemoryReservation:      memReservation,
 
- 		MountPoints:            mounts,
 
- 		Name:                   service.Name,
 
- 		PortMappings:           toPortMappings(service.Ports),
 
- 		Privileged:             service.Privileged,
 
- 		PseudoTerminal:         service.Tty,
 
- 		ReadonlyRootFilesystem: service.ReadOnly,
 
- 		RepositoryCredentials:  credential,
 
- 		ResourceRequirements:   toTaskResourceRequirements(reservations),
 
- 		StartTimeout:           0,
 
- 		StopTimeout:            durationToInt(service.StopGracePeriod),
 
- 		SystemControls:         toSystemControls(service.Sysctls),
 
- 		Ulimits:                toUlimits(service.Ulimits),
 
- 		User:                   service.User,
 
- 		VolumesFrom:            nil,
 
- 		WorkingDirectory:       service.WorkingDir,
 
- 	})
 
- 	launchType := ecsapi.LaunchTypeFargate
 
- 	if requireEC2(service) {
 
- 		launchType = ecsapi.LaunchTypeEc2
 
- 	}
 
- 	return &ecs.TaskDefinition{
 
- 		ContainerDefinitions: containers,
 
- 		Cpu:                  cpu,
 
- 		Family:               fmt.Sprintf("%s-%s", project.Name, service.Name),
 
- 		IpcMode:              service.Ipc,
 
- 		Memory:               mem,
 
- 		NetworkMode:          ecsapi.NetworkModeAwsvpc, // FIXME could be set by service.NetworkMode, Fargate only supports network mode ‘awsvpc’.
 
- 		PidMode:              service.Pid,
 
- 		PlacementConstraints: toPlacementConstraints(service.Deploy),
 
- 		ProxyConfiguration:   nil,
 
- 		RequiresCompatibilities: []string{
 
- 			launchType,
 
- 		},
 
- 		Volumes: volumes,
 
- 	}, nil
 
- }
 
- func toTaskResourceRequirements(reservations *types.Resource) []ecs.TaskDefinition_ResourceRequirement {
 
- 	if reservations == nil {
 
- 		return nil
 
- 	}
 
- 	var requirements []ecs.TaskDefinition_ResourceRequirement
 
- 	for _, r := range reservations.GenericResources {
 
- 		if r.DiscreteResourceSpec.Kind == "gpus" {
 
- 			requirements = append(requirements, ecs.TaskDefinition_ResourceRequirement{
 
- 				Type:  ecsapi.ResourceTypeGpu,
 
- 				Value: fmt.Sprint(r.DiscreteResourceSpec.Value),
 
- 			})
 
- 		}
 
- 	}
 
- 	return requirements
 
- }
 
- func createSecretsSideCar(project *types.Project, service types.ServiceConfig, logConfiguration *ecs.TaskDefinition_LogConfiguration) (
 
- 	ecs.TaskDefinition_Volume,
 
- 	ecs.TaskDefinition_MountPoint,
 
- 	ecs.TaskDefinition_ContainerDefinition,
 
- 	error) {
 
- 	initContainerName := fmt.Sprintf("%s_Secrets_InitContainer", normalizeResourceName(service.Name))
 
- 	secretsVolume := ecs.TaskDefinition_Volume{
 
- 		Name: "secrets",
 
- 	}
 
- 	secretsMount := ecs.TaskDefinition_MountPoint{
 
- 		ContainerPath: "/run/secrets/",
 
- 		ReadOnly:      true,
 
- 		SourceVolume:  "secrets",
 
- 	}
 
- 	var (
 
- 		args        []secrets.Secret
 
- 		taskSecrets []ecs.TaskDefinition_Secret
 
- 	)
 
- 	for _, s := range service.Secrets {
 
- 		secretConfig := project.Secrets[s.Source]
 
- 		if s.Target == "" {
 
- 			s.Target = s.Source
 
- 		}
 
- 		taskSecrets = append(taskSecrets, ecs.TaskDefinition_Secret{
 
- 			Name:      s.Target,
 
- 			ValueFrom: secretConfig.Name,
 
- 		})
 
- 		var keys []string
 
- 		if ext, ok := secretConfig.Extensions[extensionKeys]; ok {
 
- 			if key, ok := ext.(string); ok {
 
- 				keys = append(keys, key)
 
- 			} else {
 
- 				for _, k := range ext.([]interface{}) {
 
- 					keys = append(keys, k.(string))
 
- 				}
 
- 			}
 
- 		}
 
- 		args = append(args, secrets.Secret{
 
- 			Name: s.Target,
 
- 			Keys: keys,
 
- 		})
 
- 	}
 
- 	command, err := json.Marshal(args)
 
- 	if err != nil {
 
- 		return ecs.TaskDefinition_Volume{}, ecs.TaskDefinition_MountPoint{}, ecs.TaskDefinition_ContainerDefinition{}, err
 
- 	}
 
- 	secretsSideCar := ecs.TaskDefinition_ContainerDefinition{
 
- 		Name:             initContainerName,
 
- 		Image:            secretsInitContainerImage,
 
- 		Command:          []string{string(command)},
 
- 		Essential:        false, // FIXME this will be ignored, see https://github.com/awslabs/goformation/issues/61#issuecomment-625139607
 
- 		LogConfiguration: logConfiguration,
 
- 		MountPoints: []ecs.TaskDefinition_MountPoint{
 
- 			{
 
- 				ContainerPath: "/run/secrets/",
 
- 				ReadOnly:      false,
 
- 				SourceVolume:  "secrets",
 
- 			},
 
- 		},
 
- 		Secrets: taskSecrets,
 
- 	}
 
- 	return secretsVolume, secretsMount, secretsSideCar, nil
 
- }
 
- func createEnvironment(project *types.Project, service types.ServiceConfig) ([]ecs.TaskDefinition_KeyValuePair, error) {
 
- 	environment := map[string]*string{}
 
- 	for _, f := range service.EnvFile {
 
- 		if !filepath.IsAbs(f) {
 
- 			f = filepath.Join(project.WorkingDir, f)
 
- 		}
 
- 		if _, err := os.Stat(f); os.IsNotExist(err) {
 
- 			return nil, err
 
- 		}
 
- 		file, err := os.Open(f)
 
- 		if err != nil {
 
- 			return nil, err
 
- 		}
 
- 		defer file.Close() // nolint:errcheck
 
- 		env, err := godotenv.Parse(file)
 
- 		if err != nil {
 
- 			return nil, err
 
- 		}
 
- 		for k, v := range env {
 
- 			environment[k] = &v
 
- 		}
 
- 	}
 
- 	for k, v := range service.Environment {
 
- 		environment[k] = v
 
- 	}
 
- 	var pairs []ecs.TaskDefinition_KeyValuePair
 
- 	for k, v := range environment {
 
- 		name := k
 
- 		var value string
 
- 		if v != nil {
 
- 			value = *v
 
- 		}
 
- 		pairs = append(pairs, ecs.TaskDefinition_KeyValuePair{
 
- 			Name:  name,
 
- 			Value: value,
 
- 		})
 
- 	}
 
- 	return pairs, nil
 
- }
 
- func getLogConfiguration(service types.ServiceConfig, project *types.Project) *ecs.TaskDefinition_LogConfiguration {
 
- 	options := map[string]string{
 
- 		"awslogs-region":        cloudformation.Ref("AWS::Region"),
 
- 		"awslogs-group":         cloudformation.Ref("LogGroup"),
 
- 		"awslogs-stream-prefix": project.Name,
 
- 	}
 
- 	if service.Logging != nil {
 
- 		for k, v := range service.Logging.Options {
 
- 			if strings.HasPrefix(k, "awslogs-") {
 
- 				options[k] = v
 
- 			}
 
- 		}
 
- 	}
 
- 	logConfiguration := &ecs.TaskDefinition_LogConfiguration{
 
- 		LogDriver: ecsapi.LogDriverAwslogs,
 
- 		Options:   options,
 
- 	}
 
- 	return logConfiguration
 
- }
 
- func toSystemControls(sysctls types.Mapping) []ecs.TaskDefinition_SystemControl {
 
- 	sys := []ecs.TaskDefinition_SystemControl{}
 
- 	for k, v := range sysctls {
 
- 		sys = append(sys, ecs.TaskDefinition_SystemControl{
 
- 			Namespace: k,
 
- 			Value:     v,
 
- 		})
 
- 	}
 
- 	return sys
 
- }
 
- const miB = 1024 * 1024
 
- func toLimits(service types.ServiceConfig) (string, string, error) {
 
- 	mem, cpu, err := getConfiguredLimits(service)
 
- 	if err != nil {
 
- 		return "", "", err
 
- 	}
 
- 	if requireEC2(service) {
 
- 		// just return configured limits expressed in Mb and CPU units
 
- 		var cpuLimit, memLimit string
 
- 		if cpu > 0 {
 
- 			cpuLimit = fmt.Sprint(cpu)
 
- 		}
 
- 		if mem > 0 {
 
- 			memLimit = fmt.Sprint(mem / miB)
 
- 		}
 
- 		return cpuLimit, memLimit, nil
 
- 	}
 
- 	// All possible cpu/mem values for Fargate
 
- 	fargateCPUToMem := map[int64][]types.UnitBytes{
 
- 		256:  {512, 1024, 2048},
 
- 		512:  {1024, 2048, 3072, 4096},
 
- 		1024: {2048, 3072, 4096, 5120, 6144, 7168, 8192},
 
- 		2048: {4096, 5120, 6144, 7168, 8192, 9216, 10240, 11264, 12288, 13312, 14336, 15360, 16384},
 
- 		4096: {8192, 9216, 10240, 11264, 12288, 13312, 14336, 15360, 16384, 17408, 18432, 19456, 20480, 21504, 22528, 23552, 24576, 25600, 26624, 27648, 28672, 29696, 30720},
 
- 	}
 
- 	cpuLimit := "256"
 
- 	memLimit := "512"
 
- 	if mem == 0 && cpu == 0 {
 
- 		return cpuLimit, memLimit, nil
 
- 	}
 
- 	var cpus []int64
 
- 	for k := range fargateCPUToMem {
 
- 		cpus = append(cpus, k)
 
- 	}
 
- 	sort.Slice(cpus, func(i, j int) bool { return cpus[i] < cpus[j] })
 
- 	for _, fargateCPU := range cpus {
 
- 		options := fargateCPUToMem[fargateCPU]
 
- 		if cpu <= fargateCPU {
 
- 			for _, m := range options {
 
- 				if mem <= m*miB {
 
- 					cpuLimit = strconv.FormatInt(fargateCPU, 10)
 
- 					memLimit = strconv.FormatInt(int64(m), 10)
 
- 					return cpuLimit, memLimit, nil
 
- 				}
 
- 			}
 
- 		}
 
- 	}
 
- 	return "", "", fmt.Errorf("the resources requested are not supported by ECS/Fargate")
 
- }
 
- func getConfiguredLimits(service types.ServiceConfig) (types.UnitBytes, int64, error) {
 
- 	if service.Deploy == nil {
 
- 		return 0, 0, nil
 
- 	}
 
- 	limits := service.Deploy.Resources.Limits
 
- 	if limits == nil {
 
- 		return 0, 0, nil
 
- 	}
 
- 	if limits.NanoCPUs == "" {
 
- 		return limits.MemoryBytes, 0, nil
 
- 	}
 
- 	v, err := opts.ParseCPUs(limits.NanoCPUs)
 
- 	if err != nil {
 
- 		return 0, 0, err
 
- 	}
 
- 	return limits.MemoryBytes, v / 1e6, nil
 
- }
 
- func toContainerReservation(service types.ServiceConfig) (string, int) {
 
- 	cpuReservation := ".0"
 
- 	memReservation := 0
 
- 	if service.Deploy == nil {
 
- 		return cpuReservation, memReservation
 
- 	}
 
- 	reservations := service.Deploy.Resources.Reservations
 
- 	if reservations == nil {
 
- 		return cpuReservation, memReservation
 
- 	}
 
- 	return reservations.NanoCPUs, int(reservations.MemoryBytes / miB)
 
- }
 
- func toPlacementConstraints(deploy *types.DeployConfig) []ecs.TaskDefinition_TaskDefinitionPlacementConstraint {
 
- 	if deploy == nil || deploy.Placement.Constraints == nil || len(deploy.Placement.Constraints) == 0 {
 
- 		return nil
 
- 	}
 
- 	pl := []ecs.TaskDefinition_TaskDefinitionPlacementConstraint{}
 
- 	for _, c := range deploy.Placement.Constraints {
 
- 		pl = append(pl, ecs.TaskDefinition_TaskDefinitionPlacementConstraint{
 
- 			Expression: c,
 
- 			Type:       "",
 
- 		})
 
- 	}
 
- 	return pl
 
- }
 
- func toPortMappings(ports []types.ServicePortConfig) []ecs.TaskDefinition_PortMapping {
 
- 	if len(ports) == 0 {
 
- 		return nil
 
- 	}
 
- 	m := []ecs.TaskDefinition_PortMapping{}
 
- 	for _, p := range ports {
 
- 		m = append(m, ecs.TaskDefinition_PortMapping{
 
- 			ContainerPort: int(p.Target),
 
- 			HostPort:      int(p.Published),
 
- 			Protocol:      p.Protocol,
 
- 		})
 
- 	}
 
- 	return m
 
- }
 
- func toUlimits(ulimits map[string]*types.UlimitsConfig) []ecs.TaskDefinition_Ulimit {
 
- 	if len(ulimits) == 0 {
 
- 		return nil
 
- 	}
 
- 	u := []ecs.TaskDefinition_Ulimit{}
 
- 	for k, v := range ulimits {
 
- 		u = append(u, ecs.TaskDefinition_Ulimit{
 
- 			Name:      k,
 
- 			SoftLimit: v.Soft,
 
- 			HardLimit: v.Hard,
 
- 		})
 
- 	}
 
- 	return u
 
- }
 
- func toLinuxParameters(service types.ServiceConfig) *ecs.TaskDefinition_LinuxParameters {
 
- 	return &ecs.TaskDefinition_LinuxParameters{
 
- 		Capabilities:       toKernelCapabilities(service.CapAdd, service.CapDrop),
 
- 		Devices:            nil,
 
- 		InitProcessEnabled: service.Init != nil && *service.Init,
 
- 		MaxSwap:            0,
 
- 		// FIXME SharedMemorySize:   service.ShmSize,
 
- 		Swappiness: 0,
 
- 		Tmpfs:      toTmpfs(service.Tmpfs),
 
- 	}
 
- }
 
- func toTmpfs(tmpfs types.StringList) []ecs.TaskDefinition_Tmpfs {
 
- 	if tmpfs == nil || len(tmpfs) == 0 {
 
- 		return nil
 
- 	}
 
- 	o := []ecs.TaskDefinition_Tmpfs{}
 
- 	for _, path := range tmpfs {
 
- 		o = append(o, ecs.TaskDefinition_Tmpfs{
 
- 			ContainerPath: path,
 
- 			Size:          100, // size is required on ECS, unlimited by the compose spec
 
- 		})
 
- 	}
 
- 	return o
 
- }
 
- func toKernelCapabilities(add []string, drop []string) *ecs.TaskDefinition_KernelCapabilities {
 
- 	if len(add) == 0 && len(drop) == 0 {
 
- 		return nil
 
- 	}
 
- 	return &ecs.TaskDefinition_KernelCapabilities{
 
- 		Add:  add,
 
- 		Drop: drop,
 
- 	}
 
- }
 
- func toHealthCheck(check *types.HealthCheckConfig) *ecs.TaskDefinition_HealthCheck {
 
- 	if check == nil {
 
- 		return nil
 
- 	}
 
- 	retries := 0
 
- 	if check.Retries != nil {
 
- 		retries = int(*check.Retries)
 
- 	}
 
- 	return &ecs.TaskDefinition_HealthCheck{
 
- 		Command:     check.Test,
 
- 		Interval:    durationToInt(check.Interval),
 
- 		Retries:     retries,
 
- 		StartPeriod: durationToInt(check.StartPeriod),
 
- 		Timeout:     durationToInt(check.Timeout),
 
- 	}
 
- }
 
- func durationToInt(interval *types.Duration) int {
 
- 	if interval == nil {
 
- 		return 0
 
- 	}
 
- 	v := int(time.Duration(*interval).Seconds())
 
- 	return v
 
- }
 
- func toHostEntryPtr(hosts types.HostsList) []ecs.TaskDefinition_HostEntry {
 
- 	if hosts == nil || len(hosts) == 0 {
 
- 		return nil
 
- 	}
 
- 	e := []ecs.TaskDefinition_HostEntry{}
 
- 	for _, h := range hosts {
 
- 		parts := strings.SplitN(h, ":", 2) // FIXME this should be handled by compose-go
 
- 		e = append(e, ecs.TaskDefinition_HostEntry{
 
- 			Hostname:  parts[0],
 
- 			IpAddress: parts[1],
 
- 		})
 
- 	}
 
- 	return e
 
- }
 
- func getRepoCredentials(service types.ServiceConfig) *ecs.TaskDefinition_RepositoryCredentials {
 
- 	if value, ok := service.Extensions[extensionPullCredentials]; ok {
 
- 		return &ecs.TaskDefinition_RepositoryCredentials{CredentialsParameter: value.(string)}
 
- 	}
 
- 	return nil
 
- }
 
- func requireEC2(s types.ServiceConfig) bool {
 
- 	return gpuRequirements(s) > 0
 
- }
 
- func gpuRequirements(s types.ServiceConfig) int64 {
 
- 	if deploy := s.Deploy; deploy != nil {
 
- 		if reservations := deploy.Resources.Reservations; reservations != nil {
 
- 			for _, resource := range reservations.GenericResources {
 
- 				if resource.DiscreteResourceSpec.Kind == "gpus" {
 
- 					return resource.DiscreteResourceSpec.Value
 
- 				}
 
- 			}
 
- 		}
 
- 	}
 
- 	return 0
 
- }
 
 
  |