convert.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. /*
  2. Copyright 2020 Docker Compose CLI authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package convert
  14. import (
  15. "context"
  16. "fmt"
  17. "math"
  18. "os"
  19. "strconv"
  20. "strings"
  21. "time"
  22. "github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2019-12-01/containerinstance"
  23. "github.com/Azure/go-autorest/autorest/to"
  24. "github.com/compose-spec/compose-go/types"
  25. "github.com/pkg/errors"
  26. "github.com/docker/compose-cli/aci/login"
  27. "github.com/docker/compose-cli/api/containers"
  28. "github.com/docker/compose-cli/api/context/store"
  29. "github.com/docker/compose-cli/pkg/api"
  30. "github.com/docker/compose-cli/utils/formatter"
  31. )
  32. const (
  33. // StatusRunning name of the ACI running status
  34. StatusRunning = "Running"
  35. // ComposeDNSSidecarName name of the dns sidecar container
  36. ComposeDNSSidecarName = "aci--dns--sidecar"
  37. dnsSidecarImage = "docker/aci-hostnames-sidecar:1.0"
  38. )
  39. // ToContainerGroup converts a compose project into a ACI container group
  40. func ToContainerGroup(ctx context.Context, aciContext store.AciContext, p types.Project, storageHelper login.StorageLogin) (containerinstance.ContainerGroup, error) {
  41. project := projectAciHelper(p)
  42. containerGroupName := strings.ToLower(project.Name)
  43. volumesSlice, err := project.getAciFileVolumes(ctx, storageHelper)
  44. if err != nil {
  45. return containerinstance.ContainerGroup{}, err
  46. }
  47. secretVolumes, err := project.getAciSecretVolumes()
  48. if err != nil {
  49. return containerinstance.ContainerGroup{}, err
  50. }
  51. allVolumes := append(volumesSlice, secretVolumes...)
  52. var volumes *[]containerinstance.Volume
  53. if len(allVolumes) > 0 {
  54. volumes = &allVolumes
  55. }
  56. registryCreds, err := getRegistryCredentials(p, newCliRegistryConfLoader())
  57. if err != nil {
  58. return containerinstance.ContainerGroup{}, err
  59. }
  60. var ctnrs []containerinstance.Container
  61. restartPolicy, err := project.getRestartPolicy()
  62. if err != nil {
  63. return containerinstance.ContainerGroup{}, err
  64. }
  65. groupDefinition := containerinstance.ContainerGroup{
  66. Name: &containerGroupName,
  67. Location: &aciContext.Location,
  68. ContainerGroupProperties: &containerinstance.ContainerGroupProperties{
  69. OsType: containerinstance.Linux,
  70. Containers: &ctnrs,
  71. Volumes: volumes,
  72. ImageRegistryCredentials: &registryCreds,
  73. RestartPolicy: restartPolicy,
  74. },
  75. }
  76. var groupPorts []containerinstance.Port
  77. var dnsLabelName *string
  78. for _, s := range project.Services {
  79. service := serviceConfigAciHelper(s)
  80. containerDefinition, err := service.getAciContainer()
  81. if err != nil {
  82. return containerinstance.ContainerGroup{}, err
  83. }
  84. if service.Labels != nil && len(service.Labels) > 0 {
  85. return containerinstance.ContainerGroup{}, errors.New("ACI integration does not support labels in compose applications")
  86. }
  87. containerPorts, serviceGroupPorts, serviceDomainName, err := convertPortsToAci(service)
  88. if err != nil {
  89. return groupDefinition, err
  90. }
  91. containerDefinition.ContainerProperties.Ports = &containerPorts
  92. groupPorts = append(groupPorts, serviceGroupPorts...)
  93. if serviceDomainName != nil {
  94. if dnsLabelName != nil && *serviceDomainName != *dnsLabelName {
  95. return containerinstance.ContainerGroup{}, fmt.Errorf("ACI integration does not support specifying different domain names on services in the same compose application")
  96. }
  97. dnsLabelName = serviceDomainName
  98. }
  99. ctnrs = append(ctnrs, containerDefinition)
  100. }
  101. if len(groupPorts) > 0 {
  102. groupDefinition.ContainerGroupProperties.IPAddress = &containerinstance.IPAddress{
  103. Type: containerinstance.Public,
  104. Ports: &groupPorts,
  105. DNSNameLabel: dnsLabelName,
  106. }
  107. }
  108. if len(project.Services) > 1 {
  109. dnsSideCar := getDNSSidecar(project.Services)
  110. ctnrs = append(ctnrs, dnsSideCar)
  111. }
  112. groupDefinition.ContainerGroupProperties.Containers = &ctnrs
  113. return groupDefinition, nil
  114. }
  115. func durationToSeconds(d *types.Duration) *int32 {
  116. if d == nil || *d == 0 {
  117. return nil
  118. }
  119. v := int32(time.Duration(*d).Seconds())
  120. return &v
  121. }
  122. func getDNSSidecar(services types.Services) containerinstance.Container {
  123. names := []string{"/hosts"}
  124. for _, service := range services {
  125. names = append(names, service.Name)
  126. if service.ContainerName != "" {
  127. names = append(names, service.ContainerName)
  128. }
  129. }
  130. dnsSideCar := containerinstance.Container{
  131. Name: to.StringPtr(ComposeDNSSidecarName),
  132. ContainerProperties: &containerinstance.ContainerProperties{
  133. Image: to.StringPtr(dnsSidecarImage),
  134. Command: &names,
  135. Resources: &containerinstance.ResourceRequirements{
  136. Requests: &containerinstance.ResourceRequests{
  137. MemoryInGB: to.Float64Ptr(0.1),
  138. CPU: to.Float64Ptr(0.01),
  139. },
  140. },
  141. },
  142. }
  143. return dnsSideCar
  144. }
  145. type projectAciHelper types.Project
  146. type serviceConfigAciHelper types.ServiceConfig
  147. func (s serviceConfigAciHelper) getAciContainer() (containerinstance.Container, error) {
  148. aciServiceVolumes, err := s.getAciFileVolumeMounts()
  149. if err != nil {
  150. return containerinstance.Container{}, err
  151. }
  152. serviceSecretVolumes, err := s.getAciSecretsVolumeMounts()
  153. if err != nil {
  154. return containerinstance.Container{}, err
  155. }
  156. allVolumes := append(aciServiceVolumes, serviceSecretVolumes...)
  157. var volumes *[]containerinstance.VolumeMount
  158. if len(allVolumes) > 0 {
  159. volumes = &allVolumes
  160. }
  161. resource, err := s.getResourceRequestsLimits()
  162. if err != nil {
  163. return containerinstance.Container{}, err
  164. }
  165. containerName := s.Name
  166. if s.ContainerName != "" {
  167. containerName = s.ContainerName
  168. }
  169. return containerinstance.Container{
  170. Name: to.StringPtr(containerName),
  171. ContainerProperties: &containerinstance.ContainerProperties{
  172. Image: to.StringPtr(s.Image),
  173. Command: to.StringSlicePtr(s.Command),
  174. EnvironmentVariables: getEnvVariables(s.Environment),
  175. Resources: resource,
  176. VolumeMounts: volumes,
  177. LivenessProbe: s.getLivenessProbe(),
  178. },
  179. }, nil
  180. }
  181. func (s serviceConfigAciHelper) getResourceRequestsLimits() (*containerinstance.ResourceRequirements, error) {
  182. memRequest := 1. // Default 1 Gb
  183. var cpuRequest float64 = 1
  184. var err error
  185. hasMemoryRequest := func() bool {
  186. return s.Deploy != nil && s.Deploy.Resources.Reservations != nil && s.Deploy.Resources.Reservations.MemoryBytes != 0
  187. }
  188. hasCPURequest := func() bool {
  189. return s.Deploy != nil && s.Deploy.Resources.Reservations != nil && s.Deploy.Resources.Reservations.NanoCPUs != ""
  190. }
  191. if hasMemoryRequest() {
  192. memRequest = BytesToGB(float64(s.Deploy.Resources.Reservations.MemoryBytes))
  193. }
  194. if hasCPURequest() {
  195. cpuRequest, err = strconv.ParseFloat(s.Deploy.Resources.Reservations.NanoCPUs, 0)
  196. if err != nil {
  197. return nil, err
  198. }
  199. }
  200. memLimit := memRequest
  201. cpuLimit := cpuRequest
  202. if s.Deploy != nil && s.Deploy.Resources.Limits != nil {
  203. if s.Deploy.Resources.Limits.MemoryBytes != 0 {
  204. memLimit = BytesToGB(float64(s.Deploy.Resources.Limits.MemoryBytes))
  205. if !hasMemoryRequest() {
  206. memRequest = memLimit
  207. }
  208. }
  209. if s.Deploy.Resources.Limits.NanoCPUs != "" {
  210. cpuLimit, err = strconv.ParseFloat(s.Deploy.Resources.Limits.NanoCPUs, 0)
  211. if err != nil {
  212. return nil, err
  213. }
  214. if !hasCPURequest() {
  215. cpuRequest = cpuLimit
  216. }
  217. }
  218. }
  219. resources := containerinstance.ResourceRequirements{
  220. Requests: &containerinstance.ResourceRequests{
  221. MemoryInGB: to.Float64Ptr(memRequest),
  222. CPU: to.Float64Ptr(cpuRequest),
  223. },
  224. Limits: &containerinstance.ResourceLimits{
  225. MemoryInGB: to.Float64Ptr(memLimit),
  226. CPU: to.Float64Ptr(cpuLimit),
  227. },
  228. }
  229. return &resources, nil
  230. }
  231. func (s serviceConfigAciHelper) getLivenessProbe() *containerinstance.ContainerProbe {
  232. if s.HealthCheck != nil && !s.HealthCheck.Disable && len(s.HealthCheck.Test) > 0 {
  233. testArray := s.HealthCheck.Test
  234. switch s.HealthCheck.Test[0] {
  235. case "NONE", "CMD", "CMD-SHELL":
  236. testArray = s.HealthCheck.Test[1:]
  237. }
  238. if len(testArray) == 0 {
  239. return nil
  240. }
  241. var retries *int32
  242. if s.HealthCheck.Retries != nil {
  243. retries = to.Int32Ptr(int32(*s.HealthCheck.Retries))
  244. }
  245. probe := containerinstance.ContainerProbe{
  246. Exec: &containerinstance.ContainerExec{
  247. Command: to.StringSlicePtr(testArray),
  248. },
  249. InitialDelaySeconds: durationToSeconds(s.HealthCheck.StartPeriod),
  250. PeriodSeconds: durationToSeconds(s.HealthCheck.Interval),
  251. TimeoutSeconds: durationToSeconds(s.HealthCheck.Timeout),
  252. }
  253. if retries != nil && *retries > 0 {
  254. probe.FailureThreshold = retries
  255. }
  256. return &probe
  257. }
  258. return nil
  259. }
  260. func getEnvVariables(composeEnv types.MappingWithEquals) *[]containerinstance.EnvironmentVariable {
  261. result := []containerinstance.EnvironmentVariable{}
  262. for key, value := range composeEnv {
  263. var strValue string
  264. if value == nil {
  265. strValue = os.Getenv(key)
  266. } else {
  267. strValue = *value
  268. }
  269. result = append(result, containerinstance.EnvironmentVariable{
  270. Name: to.StringPtr(key),
  271. Value: to.StringPtr(strValue),
  272. })
  273. }
  274. return &result
  275. }
  276. // BytesToGB convert bytes To GB
  277. func BytesToGB(b float64) float64 {
  278. f := b / 1024 / 1024 / 1024 // from bytes to gigabytes
  279. return math.Round(f*100) / 100
  280. }
  281. func gbToBytes(memInBytes float64) uint64 {
  282. return uint64(memInBytes * 1024 * 1024 * 1024)
  283. }
  284. // ContainerGroupToServiceStatus convert from an ACI container definition to service status
  285. func ContainerGroupToServiceStatus(containerID string, group containerinstance.ContainerGroup, container containerinstance.Container, region string) api.ServiceStatus {
  286. var replicas = 1
  287. if GetStatus(container, group) != StatusRunning {
  288. replicas = 0
  289. }
  290. return api.ServiceStatus{
  291. ID: containerID,
  292. Name: *container.Name,
  293. Ports: formatter.PortsToStrings(ToPorts(group.IPAddress, *container.Ports), FQDN(group, region)),
  294. Replicas: replicas,
  295. Desired: 1,
  296. }
  297. }
  298. // FQDN retrieve the fully qualified domain name for a ContainerGroup
  299. func FQDN(group containerinstance.ContainerGroup, region string) string {
  300. fqdn := ""
  301. if group.IPAddress != nil && group.IPAddress.DNSNameLabel != nil && *group.IPAddress.DNSNameLabel != "" {
  302. fqdn = *group.IPAddress.DNSNameLabel + "." + region + ".azurecontainer.io"
  303. }
  304. return fqdn
  305. }
  306. // ContainerGroupToContainer composes a Container from an ACI container definition
  307. func ContainerGroupToContainer(containerID string, cg containerinstance.ContainerGroup, cc containerinstance.Container, region string) containers.Container {
  308. command := ""
  309. if cc.Command != nil {
  310. command = strings.Join(*cc.Command, " ")
  311. }
  312. status := GetStatus(cc, cg)
  313. platform := string(cg.OsType)
  314. var envVars map[string]string
  315. if cc.EnvironmentVariables != nil && len(*cc.EnvironmentVariables) != 0 {
  316. envVars = map[string]string{}
  317. for _, envVar := range *cc.EnvironmentVariables {
  318. envVars[*envVar.Name] = *envVar.Value
  319. }
  320. }
  321. hostConfig := ToHostConfig(cc, cg)
  322. config := &containers.RuntimeConfig{
  323. FQDN: FQDN(cg, region),
  324. Env: envVars,
  325. }
  326. var healthcheck = containers.Healthcheck{
  327. Disable: true,
  328. }
  329. if cc.LivenessProbe != nil &&
  330. cc.LivenessProbe.Exec != nil &&
  331. cc.LivenessProbe.Exec.Command != nil {
  332. if len(*cc.LivenessProbe.Exec.Command) > 0 {
  333. healthcheck.Disable = false
  334. healthcheck.Test = *cc.LivenessProbe.Exec.Command
  335. if cc.LivenessProbe.PeriodSeconds != nil {
  336. healthcheck.Interval = types.Duration(int64(*cc.LivenessProbe.PeriodSeconds) * int64(time.Second))
  337. }
  338. if cc.LivenessProbe.FailureThreshold != nil {
  339. healthcheck.Retries = int(*cc.LivenessProbe.FailureThreshold)
  340. }
  341. if cc.LivenessProbe.TimeoutSeconds != nil {
  342. healthcheck.Timeout = types.Duration(int64(*cc.LivenessProbe.TimeoutSeconds) * int64(time.Second))
  343. }
  344. if cc.LivenessProbe.InitialDelaySeconds != nil {
  345. healthcheck.StartPeriod = types.Duration(int64(*cc.LivenessProbe.InitialDelaySeconds) * int64(time.Second))
  346. }
  347. }
  348. }
  349. c := containers.Container{
  350. ID: containerID,
  351. Status: status,
  352. Image: to.String(cc.Image),
  353. Command: command,
  354. CPUTime: 0,
  355. MemoryUsage: 0,
  356. PidsCurrent: 0,
  357. PidsLimit: 0,
  358. Ports: ToPorts(cg.IPAddress, *cc.Ports),
  359. Platform: platform,
  360. Config: config,
  361. HostConfig: hostConfig,
  362. Healthcheck: healthcheck,
  363. }
  364. return c
  365. }
  366. // ToHostConfig convert an ACI container to host config value
  367. func ToHostConfig(cc containerinstance.Container, cg containerinstance.ContainerGroup) *containers.HostConfig {
  368. memLimits := uint64(0)
  369. memRequest := uint64(0)
  370. cpuLimit := 0.
  371. cpuReservation := 0.
  372. if cc.Resources != nil {
  373. if cc.Resources.Limits != nil {
  374. if cc.Resources.Limits.MemoryInGB != nil {
  375. memLimits = gbToBytes(*cc.Resources.Limits.MemoryInGB)
  376. }
  377. if cc.Resources.Limits.CPU != nil {
  378. cpuLimit = *cc.Resources.Limits.CPU
  379. }
  380. }
  381. if cc.Resources.Requests != nil {
  382. if cc.Resources.Requests.MemoryInGB != nil {
  383. memRequest = gbToBytes(*cc.Resources.Requests.MemoryInGB)
  384. }
  385. if cc.Resources.Requests.CPU != nil {
  386. cpuReservation = *cc.Resources.Requests.CPU
  387. }
  388. }
  389. }
  390. hostConfig := &containers.HostConfig{
  391. CPULimit: cpuLimit,
  392. CPUReservation: cpuReservation,
  393. MemoryLimit: memLimits,
  394. MemoryReservation: memRequest,
  395. RestartPolicy: toContainerRestartPolicy(cg.RestartPolicy),
  396. }
  397. return hostConfig
  398. }
  399. // GetStatus returns status for the specified container
  400. func GetStatus(container containerinstance.Container, group containerinstance.ContainerGroup) string {
  401. status := GetGroupStatus(group)
  402. if container.InstanceView != nil && container.InstanceView.CurrentState != nil {
  403. status = *container.InstanceView.CurrentState.State
  404. }
  405. return status
  406. }
  407. // GetGroupStatus returns status for the container group
  408. func GetGroupStatus(group containerinstance.ContainerGroup) string {
  409. if group.InstanceView != nil && group.InstanceView.State != nil {
  410. return "Node " + *group.InstanceView.State
  411. }
  412. return api.UNKNOWN
  413. }