convert.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  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/compose"
  28. "github.com/docker/compose-cli/api/containers"
  29. "github.com/docker/compose-cli/context/store"
  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(ctnrs) > 1 {
  109. dnsSideCar := getDNSSidecar(ctnrs)
  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(containers []containerinstance.Container) containerinstance.Container {
  123. names := []string{"/hosts"}
  124. for _, container := range containers {
  125. names = append(names, *container.Name)
  126. }
  127. dnsSideCar := containerinstance.Container{
  128. Name: to.StringPtr(ComposeDNSSidecarName),
  129. ContainerProperties: &containerinstance.ContainerProperties{
  130. Image: to.StringPtr(dnsSidecarImage),
  131. Command: &names,
  132. Resources: &containerinstance.ResourceRequirements{
  133. Requests: &containerinstance.ResourceRequests{
  134. MemoryInGB: to.Float64Ptr(0.1),
  135. CPU: to.Float64Ptr(0.01),
  136. },
  137. },
  138. },
  139. }
  140. return dnsSideCar
  141. }
  142. type projectAciHelper types.Project
  143. type serviceConfigAciHelper types.ServiceConfig
  144. func (s serviceConfigAciHelper) getAciContainer() (containerinstance.Container, error) {
  145. aciServiceVolumes, err := s.getAciFileVolumeMounts()
  146. if err != nil {
  147. return containerinstance.Container{}, err
  148. }
  149. serviceSecretVolumes, err := s.getAciSecretsVolumeMounts()
  150. if err != nil {
  151. return containerinstance.Container{}, err
  152. }
  153. allVolumes := append(aciServiceVolumes, serviceSecretVolumes...)
  154. var volumes *[]containerinstance.VolumeMount
  155. if len(allVolumes) > 0 {
  156. volumes = &allVolumes
  157. }
  158. resource, err := s.getResourceRequestsLimits()
  159. if err != nil {
  160. return containerinstance.Container{}, err
  161. }
  162. return containerinstance.Container{
  163. Name: to.StringPtr(s.Name),
  164. ContainerProperties: &containerinstance.ContainerProperties{
  165. Image: to.StringPtr(s.Image),
  166. Command: to.StringSlicePtr(s.Command),
  167. EnvironmentVariables: getEnvVariables(s.Environment),
  168. Resources: resource,
  169. VolumeMounts: volumes,
  170. LivenessProbe: s.getLivenessProbe(),
  171. },
  172. }, nil
  173. }
  174. func (s serviceConfigAciHelper) getResourceRequestsLimits() (*containerinstance.ResourceRequirements, error) {
  175. memRequest := 1. // Default 1 Gb
  176. var cpuRequest float64 = 1
  177. var err error
  178. hasMemoryRequest := func() bool {
  179. return s.Deploy != nil && s.Deploy.Resources.Reservations != nil && s.Deploy.Resources.Reservations.MemoryBytes != 0
  180. }
  181. hasCPURequest := func() bool {
  182. return s.Deploy != nil && s.Deploy.Resources.Reservations != nil && s.Deploy.Resources.Reservations.NanoCPUs != ""
  183. }
  184. if hasMemoryRequest() {
  185. memRequest = BytesToGB(float64(s.Deploy.Resources.Reservations.MemoryBytes))
  186. }
  187. if hasCPURequest() {
  188. cpuRequest, err = strconv.ParseFloat(s.Deploy.Resources.Reservations.NanoCPUs, 0)
  189. if err != nil {
  190. return nil, err
  191. }
  192. }
  193. memLimit := memRequest
  194. cpuLimit := cpuRequest
  195. if s.Deploy != nil && s.Deploy.Resources.Limits != nil {
  196. if s.Deploy.Resources.Limits.MemoryBytes != 0 {
  197. memLimit = BytesToGB(float64(s.Deploy.Resources.Limits.MemoryBytes))
  198. if !hasMemoryRequest() {
  199. memRequest = memLimit
  200. }
  201. }
  202. if s.Deploy.Resources.Limits.NanoCPUs != "" {
  203. cpuLimit, err = strconv.ParseFloat(s.Deploy.Resources.Limits.NanoCPUs, 0)
  204. if err != nil {
  205. return nil, err
  206. }
  207. if !hasCPURequest() {
  208. cpuRequest = cpuLimit
  209. }
  210. }
  211. }
  212. resources := containerinstance.ResourceRequirements{
  213. Requests: &containerinstance.ResourceRequests{
  214. MemoryInGB: to.Float64Ptr(memRequest),
  215. CPU: to.Float64Ptr(cpuRequest),
  216. },
  217. Limits: &containerinstance.ResourceLimits{
  218. MemoryInGB: to.Float64Ptr(memLimit),
  219. CPU: to.Float64Ptr(cpuLimit),
  220. },
  221. }
  222. return &resources, nil
  223. }
  224. func (s serviceConfigAciHelper) getLivenessProbe() *containerinstance.ContainerProbe {
  225. if s.HealthCheck != nil && !s.HealthCheck.Disable && len(s.HealthCheck.Test) > 0 {
  226. testArray := s.HealthCheck.Test
  227. switch s.HealthCheck.Test[0] {
  228. case "NONE", "CMD", "CMD-SHELL":
  229. testArray = s.HealthCheck.Test[1:]
  230. }
  231. if len(testArray) == 0 {
  232. return nil
  233. }
  234. var retries *int32
  235. if s.HealthCheck.Retries != nil {
  236. retries = to.Int32Ptr(int32(*s.HealthCheck.Retries))
  237. }
  238. probe := containerinstance.ContainerProbe{
  239. Exec: &containerinstance.ContainerExec{
  240. Command: to.StringSlicePtr(testArray),
  241. },
  242. InitialDelaySeconds: durationToSeconds(s.HealthCheck.StartPeriod),
  243. PeriodSeconds: durationToSeconds(s.HealthCheck.Interval),
  244. TimeoutSeconds: durationToSeconds(s.HealthCheck.Timeout),
  245. }
  246. if retries != nil && *retries > 0 {
  247. probe.FailureThreshold = retries
  248. }
  249. return &probe
  250. }
  251. return nil
  252. }
  253. func getEnvVariables(composeEnv types.MappingWithEquals) *[]containerinstance.EnvironmentVariable {
  254. result := []containerinstance.EnvironmentVariable{}
  255. for key, value := range composeEnv {
  256. var strValue string
  257. if value == nil {
  258. strValue = os.Getenv(key)
  259. } else {
  260. strValue = *value
  261. }
  262. result = append(result, containerinstance.EnvironmentVariable{
  263. Name: to.StringPtr(key),
  264. Value: to.StringPtr(strValue),
  265. })
  266. }
  267. return &result
  268. }
  269. // BytesToGB convert bytes To GB
  270. func BytesToGB(b float64) float64 {
  271. f := b / 1024 / 1024 / 1024 // from bytes to gigabytes
  272. return math.Round(f*100) / 100
  273. }
  274. func gbToBytes(memInBytes float64) uint64 {
  275. return uint64(memInBytes * 1024 * 1024 * 1024)
  276. }
  277. // ContainerGroupToServiceStatus convert from an ACI container definition to service status
  278. func ContainerGroupToServiceStatus(containerID string, group containerinstance.ContainerGroup, container containerinstance.Container, region string) compose.ServiceStatus {
  279. var replicas = 1
  280. if GetStatus(container, group) != StatusRunning {
  281. replicas = 0
  282. }
  283. return compose.ServiceStatus{
  284. ID: containerID,
  285. Name: *container.Name,
  286. Ports: formatter.PortsToStrings(ToPorts(group.IPAddress, *container.Ports), FQDN(group, region)),
  287. Replicas: replicas,
  288. Desired: 1,
  289. }
  290. }
  291. // FQDN retrieve the fully qualified domain name for a ContainerGroup
  292. func FQDN(group containerinstance.ContainerGroup, region string) string {
  293. fqdn := ""
  294. if group.IPAddress != nil && group.IPAddress.DNSNameLabel != nil && *group.IPAddress.DNSNameLabel != "" {
  295. fqdn = *group.IPAddress.DNSNameLabel + "." + region + ".azurecontainer.io"
  296. }
  297. return fqdn
  298. }
  299. // ContainerGroupToContainer composes a Container from an ACI container definition
  300. func ContainerGroupToContainer(containerID string, cg containerinstance.ContainerGroup, cc containerinstance.Container, region string) containers.Container {
  301. command := ""
  302. if cc.Command != nil {
  303. command = strings.Join(*cc.Command, " ")
  304. }
  305. status := GetStatus(cc, cg)
  306. platform := string(cg.OsType)
  307. var envVars map[string]string = nil
  308. if cc.EnvironmentVariables != nil && len(*cc.EnvironmentVariables) != 0 {
  309. envVars = map[string]string{}
  310. for _, envVar := range *cc.EnvironmentVariables {
  311. envVars[*envVar.Name] = *envVar.Value
  312. }
  313. }
  314. hostConfig := ToHostConfig(cc, cg)
  315. config := &containers.RuntimeConfig{
  316. FQDN: FQDN(cg, region),
  317. Env: envVars,
  318. }
  319. var healthcheck = containers.Healthcheck{
  320. Disable: true,
  321. }
  322. if cc.LivenessProbe != nil &&
  323. cc.LivenessProbe.Exec != nil &&
  324. cc.LivenessProbe.Exec.Command != nil {
  325. if len(*cc.LivenessProbe.Exec.Command) > 0 {
  326. healthcheck.Disable = false
  327. healthcheck.Test = *cc.LivenessProbe.Exec.Command
  328. if cc.LivenessProbe.PeriodSeconds != nil {
  329. healthcheck.Interval = types.Duration(int64(*cc.LivenessProbe.PeriodSeconds) * int64(time.Second))
  330. }
  331. if cc.LivenessProbe.FailureThreshold != nil {
  332. healthcheck.Retries = int(*cc.LivenessProbe.FailureThreshold)
  333. }
  334. if cc.LivenessProbe.TimeoutSeconds != nil {
  335. healthcheck.Timeout = types.Duration(int64(*cc.LivenessProbe.TimeoutSeconds) * int64(time.Second))
  336. }
  337. if cc.LivenessProbe.InitialDelaySeconds != nil {
  338. healthcheck.StartPeriod = types.Duration(int64(*cc.LivenessProbe.InitialDelaySeconds) * int64(time.Second))
  339. }
  340. }
  341. }
  342. c := containers.Container{
  343. ID: containerID,
  344. Status: status,
  345. Image: to.String(cc.Image),
  346. Command: command,
  347. CPUTime: 0,
  348. MemoryUsage: 0,
  349. PidsCurrent: 0,
  350. PidsLimit: 0,
  351. Ports: ToPorts(cg.IPAddress, *cc.Ports),
  352. Platform: platform,
  353. Config: config,
  354. HostConfig: hostConfig,
  355. Healthcheck: healthcheck,
  356. }
  357. return c
  358. }
  359. // ToHostConfig convert an ACI container to host config value
  360. func ToHostConfig(cc containerinstance.Container, cg containerinstance.ContainerGroup) *containers.HostConfig {
  361. memLimits := uint64(0)
  362. memRequest := uint64(0)
  363. cpuLimit := 0.
  364. cpuReservation := 0.
  365. if cc.Resources != nil {
  366. if cc.Resources.Limits != nil {
  367. if cc.Resources.Limits.MemoryInGB != nil {
  368. memLimits = gbToBytes(*cc.Resources.Limits.MemoryInGB)
  369. }
  370. if cc.Resources.Limits.CPU != nil {
  371. cpuLimit = *cc.Resources.Limits.CPU
  372. }
  373. }
  374. if cc.Resources.Requests != nil {
  375. if cc.Resources.Requests.MemoryInGB != nil {
  376. memRequest = gbToBytes(*cc.Resources.Requests.MemoryInGB)
  377. }
  378. if cc.Resources.Requests.CPU != nil {
  379. cpuReservation = *cc.Resources.Requests.CPU
  380. }
  381. }
  382. }
  383. hostConfig := &containers.HostConfig{
  384. CPULimit: cpuLimit,
  385. CPUReservation: cpuReservation,
  386. MemoryLimit: memLimits,
  387. MemoryReservation: memRequest,
  388. RestartPolicy: toContainerRestartPolicy(cg.RestartPolicy),
  389. }
  390. return hostConfig
  391. }
  392. // GetStatus returns status for the specified container
  393. func GetStatus(container containerinstance.Container, group containerinstance.ContainerGroup) string {
  394. status := GetGroupStatus(group)
  395. if container.InstanceView != nil && container.InstanceView.CurrentState != nil {
  396. status = *container.InstanceView.CurrentState.State
  397. }
  398. return status
  399. }
  400. // GetGroupStatus returns status for the container group
  401. func GetGroupStatus(group containerinstance.ContainerGroup) string {
  402. if group.InstanceView != nil && group.InstanceView.State != nil {
  403. return "Node " + *group.InstanceView.State
  404. }
  405. return compose.UNKNOWN
  406. }