convert.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  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/2018-10-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. func fqdn(group containerinstance.ContainerGroup, region string) string {
  292. fqdn := ""
  293. if group.IPAddress != nil && group.IPAddress.DNSNameLabel != nil && *group.IPAddress.DNSNameLabel != "" {
  294. fqdn = *group.IPAddress.DNSNameLabel + "." + region + ".azurecontainer.io"
  295. }
  296. return fqdn
  297. }
  298. // ContainerGroupToContainer composes a Container from an ACI container definition
  299. func ContainerGroupToContainer(containerID string, cg containerinstance.ContainerGroup, cc containerinstance.Container, region string) containers.Container {
  300. command := ""
  301. if cc.Command != nil {
  302. command = strings.Join(*cc.Command, " ")
  303. }
  304. status := GetStatus(cc, cg)
  305. platform := string(cg.OsType)
  306. var envVars map[string]string = nil
  307. if cc.EnvironmentVariables != nil && len(*cc.EnvironmentVariables) != 0 {
  308. envVars = map[string]string{}
  309. for _, envVar := range *cc.EnvironmentVariables {
  310. envVars[*envVar.Name] = *envVar.Value
  311. }
  312. }
  313. hostConfig := ToHostConfig(cc, cg)
  314. config := &containers.RuntimeConfig{
  315. FQDN: fqdn(cg, region),
  316. Env: envVars,
  317. }
  318. var healthcheck = containers.Healthcheck{
  319. Disable: true,
  320. }
  321. if cc.LivenessProbe != nil &&
  322. cc.LivenessProbe.Exec != nil &&
  323. cc.LivenessProbe.Exec.Command != nil {
  324. if len(*cc.LivenessProbe.Exec.Command) > 0 {
  325. healthcheck.Disable = false
  326. healthcheck.Test = *cc.LivenessProbe.Exec.Command
  327. if cc.LivenessProbe.PeriodSeconds != nil {
  328. healthcheck.Interval = types.Duration(int64(*cc.LivenessProbe.PeriodSeconds) * int64(time.Second))
  329. }
  330. if cc.LivenessProbe.FailureThreshold != nil {
  331. healthcheck.Retries = int(*cc.LivenessProbe.FailureThreshold)
  332. }
  333. if cc.LivenessProbe.TimeoutSeconds != nil {
  334. healthcheck.Timeout = types.Duration(int64(*cc.LivenessProbe.TimeoutSeconds) * int64(time.Second))
  335. }
  336. if cc.LivenessProbe.InitialDelaySeconds != nil {
  337. healthcheck.StartPeriod = types.Duration(int64(*cc.LivenessProbe.InitialDelaySeconds) * int64(time.Second))
  338. }
  339. }
  340. }
  341. c := containers.Container{
  342. ID: containerID,
  343. Status: status,
  344. Image: to.String(cc.Image),
  345. Command: command,
  346. CPUTime: 0,
  347. MemoryUsage: 0,
  348. PidsCurrent: 0,
  349. PidsLimit: 0,
  350. Ports: ToPorts(cg.IPAddress, *cc.Ports),
  351. Platform: platform,
  352. Config: config,
  353. HostConfig: hostConfig,
  354. Healthcheck: healthcheck,
  355. }
  356. return c
  357. }
  358. // ToHostConfig convert an ACI container to host config value
  359. func ToHostConfig(cc containerinstance.Container, cg containerinstance.ContainerGroup) *containers.HostConfig {
  360. memLimits := uint64(0)
  361. memRequest := uint64(0)
  362. cpuLimit := 0.
  363. cpuReservation := 0.
  364. if cc.Resources != nil {
  365. if cc.Resources.Limits != nil {
  366. if cc.Resources.Limits.MemoryInGB != nil {
  367. memLimits = gbToBytes(*cc.Resources.Limits.MemoryInGB)
  368. }
  369. if cc.Resources.Limits.CPU != nil {
  370. cpuLimit = *cc.Resources.Limits.CPU
  371. }
  372. }
  373. if cc.Resources.Requests != nil {
  374. if cc.Resources.Requests.MemoryInGB != nil {
  375. memRequest = gbToBytes(*cc.Resources.Requests.MemoryInGB)
  376. }
  377. if cc.Resources.Requests.CPU != nil {
  378. cpuReservation = *cc.Resources.Requests.CPU
  379. }
  380. }
  381. }
  382. hostConfig := &containers.HostConfig{
  383. CPULimit: cpuLimit,
  384. CPUReservation: cpuReservation,
  385. MemoryLimit: memLimits,
  386. MemoryReservation: memRequest,
  387. RestartPolicy: toContainerRestartPolicy(cg.RestartPolicy),
  388. }
  389. return hostConfig
  390. }
  391. // GetStatus returns status for the specified container
  392. func GetStatus(container containerinstance.Container, group containerinstance.ContainerGroup) string {
  393. status := GetGroupStatus(group)
  394. if container.InstanceView != nil && container.InstanceView.CurrentState != nil {
  395. status = *container.InstanceView.CurrentState.State
  396. }
  397. return status
  398. }
  399. // GetGroupStatus returns status for the container group
  400. func GetGroupStatus(group containerinstance.ContainerGroup) string {
  401. if group.InstanceView != nil && group.InstanceView.State != nil {
  402. return "Node " + *group.InstanceView.State
  403. }
  404. return compose.UNKNOWN
  405. }