convert.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  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. "github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2018-10-01/containerinstance"
  22. "github.com/Azure/go-autorest/autorest/to"
  23. "github.com/compose-spec/compose-go/types"
  24. "github.com/pkg/errors"
  25. "github.com/docker/compose-cli/aci/login"
  26. "github.com/docker/compose-cli/api/compose"
  27. "github.com/docker/compose-cli/api/containers"
  28. "github.com/docker/compose-cli/context/store"
  29. "github.com/docker/compose-cli/utils/formatter"
  30. )
  31. const (
  32. // StatusRunning name of the ACI running status
  33. StatusRunning = "Running"
  34. // ComposeDNSSidecarName name of the dns sidecar container
  35. ComposeDNSSidecarName = "aci--dns--sidecar"
  36. dnsSidecarImage = "docker/aci-hostnames-sidecar"
  37. )
  38. // ToContainerGroup converts a compose project into a ACI container group
  39. func ToContainerGroup(ctx context.Context, aciContext store.AciContext, p types.Project, storageHelper login.StorageLogin) (containerinstance.ContainerGroup, error) {
  40. project := projectAciHelper(p)
  41. containerGroupName := strings.ToLower(project.Name)
  42. volumesCache, volumesSlice, err := project.getAciFileVolumes(ctx, storageHelper)
  43. if err != nil {
  44. return containerinstance.ContainerGroup{}, err
  45. }
  46. secretVolumes, err := project.getAciSecretVolumes()
  47. if err != nil {
  48. return containerinstance.ContainerGroup{}, err
  49. }
  50. allVolumes := append(volumesSlice, secretVolumes...)
  51. var volumes *[]containerinstance.Volume
  52. if len(allVolumes) > 0 {
  53. volumes = &allVolumes
  54. }
  55. registryCreds, err := getRegistryCredentials(p, newCliRegistryConfLoader())
  56. if err != nil {
  57. return containerinstance.ContainerGroup{}, err
  58. }
  59. var containers []containerinstance.Container
  60. restartPolicy, err := project.getRestartPolicy()
  61. if err != nil {
  62. return containerinstance.ContainerGroup{}, err
  63. }
  64. groupDefinition := containerinstance.ContainerGroup{
  65. Name: &containerGroupName,
  66. Location: &aciContext.Location,
  67. ContainerGroupProperties: &containerinstance.ContainerGroupProperties{
  68. OsType: containerinstance.Linux,
  69. Containers: &containers,
  70. Volumes: volumes,
  71. ImageRegistryCredentials: &registryCreds,
  72. RestartPolicy: restartPolicy,
  73. },
  74. }
  75. var groupPorts []containerinstance.Port
  76. var dnsLabelName *string
  77. for _, s := range project.Services {
  78. service := serviceConfigAciHelper(s)
  79. containerDefinition, err := service.getAciContainer(volumesCache)
  80. if err != nil {
  81. return containerinstance.ContainerGroup{}, err
  82. }
  83. if service.Labels != nil && len(service.Labels) > 0 {
  84. return containerinstance.ContainerGroup{}, errors.New("ACI integration does not support labels in compose applications")
  85. }
  86. containerPorts, serviceGroupPorts, serviceDomainName, err := convertPortsToAci(service)
  87. if err != nil {
  88. return groupDefinition, err
  89. }
  90. containerDefinition.ContainerProperties.Ports = &containerPorts
  91. groupPorts = append(groupPorts, serviceGroupPorts...)
  92. if serviceDomainName != nil {
  93. if dnsLabelName != nil && *serviceDomainName != *dnsLabelName {
  94. return containerinstance.ContainerGroup{}, fmt.Errorf("ACI integration does not support specifying different domain names on services in the same compose application")
  95. }
  96. dnsLabelName = serviceDomainName
  97. }
  98. containers = append(containers, containerDefinition)
  99. }
  100. if len(groupPorts) > 0 {
  101. groupDefinition.ContainerGroupProperties.IPAddress = &containerinstance.IPAddress{
  102. Type: containerinstance.Public,
  103. Ports: &groupPorts,
  104. DNSNameLabel: dnsLabelName,
  105. }
  106. }
  107. if len(containers) > 1 {
  108. dnsSideCar := getDNSSidecar(containers)
  109. containers = append(containers, dnsSideCar)
  110. }
  111. groupDefinition.ContainerGroupProperties.Containers = &containers
  112. return groupDefinition, nil
  113. }
  114. func getDNSSidecar(containers []containerinstance.Container) containerinstance.Container {
  115. names := []string{"/hosts"}
  116. for _, container := range containers {
  117. names = append(names, *container.Name)
  118. }
  119. dnsSideCar := containerinstance.Container{
  120. Name: to.StringPtr(ComposeDNSSidecarName),
  121. ContainerProperties: &containerinstance.ContainerProperties{
  122. Image: to.StringPtr(dnsSidecarImage),
  123. Command: &names,
  124. Resources: &containerinstance.ResourceRequirements{
  125. Requests: &containerinstance.ResourceRequests{
  126. MemoryInGB: to.Float64Ptr(0.1),
  127. CPU: to.Float64Ptr(0.01),
  128. },
  129. },
  130. },
  131. }
  132. return dnsSideCar
  133. }
  134. type projectAciHelper types.Project
  135. type serviceConfigAciHelper types.ServiceConfig
  136. func (s serviceConfigAciHelper) getAciContainer(volumesCache map[string]bool) (containerinstance.Container, error) {
  137. aciServiceVolumes, err := s.getAciFileVolumeMounts(volumesCache)
  138. if err != nil {
  139. return containerinstance.Container{}, err
  140. }
  141. serviceSecretVolumes, err := s.getAciSecretsVolumeMounts()
  142. if err != nil {
  143. return containerinstance.Container{}, err
  144. }
  145. allVolumes := append(aciServiceVolumes, serviceSecretVolumes...)
  146. var volumes *[]containerinstance.VolumeMount
  147. if len(allVolumes) > 0 {
  148. volumes = &allVolumes
  149. }
  150. resource, err := s.getResourceRequestsLimits()
  151. if err != nil {
  152. return containerinstance.Container{}, err
  153. }
  154. return containerinstance.Container{
  155. Name: to.StringPtr(s.Name),
  156. ContainerProperties: &containerinstance.ContainerProperties{
  157. Image: to.StringPtr(s.Image),
  158. Command: to.StringSlicePtr(s.Command),
  159. EnvironmentVariables: getEnvVariables(s.Environment),
  160. Resources: resource,
  161. VolumeMounts: volumes,
  162. },
  163. }, nil
  164. }
  165. func (s serviceConfigAciHelper) getResourceRequestsLimits() (*containerinstance.ResourceRequirements, error) {
  166. memRequest := 1. // Default 1 Gb
  167. var cpuRequest float64 = 1
  168. var err error
  169. hasMemoryRequest := func() bool {
  170. return s.Deploy != nil && s.Deploy.Resources.Reservations != nil && s.Deploy.Resources.Reservations.MemoryBytes != 0
  171. }
  172. hasCPURequest := func() bool {
  173. return s.Deploy != nil && s.Deploy.Resources.Reservations != nil && s.Deploy.Resources.Reservations.NanoCPUs != ""
  174. }
  175. if hasMemoryRequest() {
  176. memRequest = BytesToGB(float64(s.Deploy.Resources.Reservations.MemoryBytes))
  177. }
  178. if hasCPURequest() {
  179. cpuRequest, err = strconv.ParseFloat(s.Deploy.Resources.Reservations.NanoCPUs, 0)
  180. if err != nil {
  181. return nil, err
  182. }
  183. }
  184. memLimit := memRequest
  185. cpuLimit := cpuRequest
  186. if s.Deploy != nil && s.Deploy.Resources.Limits != nil {
  187. if s.Deploy.Resources.Limits.MemoryBytes != 0 {
  188. memLimit = BytesToGB(float64(s.Deploy.Resources.Limits.MemoryBytes))
  189. if !hasMemoryRequest() {
  190. memRequest = memLimit
  191. }
  192. }
  193. if s.Deploy.Resources.Limits.NanoCPUs != "" {
  194. cpuLimit, err = strconv.ParseFloat(s.Deploy.Resources.Limits.NanoCPUs, 0)
  195. if err != nil {
  196. return nil, err
  197. }
  198. if !hasCPURequest() {
  199. cpuRequest = cpuLimit
  200. }
  201. }
  202. }
  203. resources := containerinstance.ResourceRequirements{
  204. Requests: &containerinstance.ResourceRequests{
  205. MemoryInGB: to.Float64Ptr(memRequest),
  206. CPU: to.Float64Ptr(cpuRequest),
  207. },
  208. Limits: &containerinstance.ResourceLimits{
  209. MemoryInGB: to.Float64Ptr(memLimit),
  210. CPU: to.Float64Ptr(cpuLimit),
  211. },
  212. }
  213. return &resources, nil
  214. }
  215. func getEnvVariables(composeEnv types.MappingWithEquals) *[]containerinstance.EnvironmentVariable {
  216. result := []containerinstance.EnvironmentVariable{}
  217. for key, value := range composeEnv {
  218. var strValue string
  219. if value == nil {
  220. strValue = os.Getenv(key)
  221. } else {
  222. strValue = *value
  223. }
  224. result = append(result, containerinstance.EnvironmentVariable{
  225. Name: to.StringPtr(key),
  226. Value: to.StringPtr(strValue),
  227. })
  228. }
  229. return &result
  230. }
  231. // BytesToGB convert bytes To GB
  232. func BytesToGB(b float64) float64 {
  233. f := b / 1024 / 1024 / 1024 // from bytes to gigabytes
  234. return math.Round(f*100) / 100
  235. }
  236. func gbToBytes(memInBytes float64) uint64 {
  237. return uint64(memInBytes * 1024 * 1024 * 1024)
  238. }
  239. // ContainerGroupToServiceStatus convert from an ACI container definition to service status
  240. func ContainerGroupToServiceStatus(containerID string, group containerinstance.ContainerGroup, container containerinstance.Container, region string) compose.ServiceStatus {
  241. var replicas = 1
  242. if GetStatus(container, group) != StatusRunning {
  243. replicas = 0
  244. }
  245. return compose.ServiceStatus{
  246. ID: containerID,
  247. Name: *container.Name,
  248. Ports: formatter.PortsToStrings(ToPorts(group.IPAddress, *container.Ports), fqdn(group, region)),
  249. Replicas: replicas,
  250. Desired: 1,
  251. }
  252. }
  253. func fqdn(group containerinstance.ContainerGroup, region string) string {
  254. fqdn := ""
  255. if group.IPAddress != nil && group.IPAddress.DNSNameLabel != nil && *group.IPAddress.DNSNameLabel != "" {
  256. fqdn = *group.IPAddress.DNSNameLabel + "." + region + ".azurecontainer.io"
  257. }
  258. return fqdn
  259. }
  260. // ContainerGroupToContainer composes a Container from an ACI container definition
  261. func ContainerGroupToContainer(containerID string, cg containerinstance.ContainerGroup, cc containerinstance.Container, region string) containers.Container {
  262. command := ""
  263. if cc.Command != nil {
  264. command = strings.Join(*cc.Command, " ")
  265. }
  266. status := GetStatus(cc, cg)
  267. platform := string(cg.OsType)
  268. var envVars map[string]string = nil
  269. if cc.EnvironmentVariables != nil && len(*cc.EnvironmentVariables) != 0 {
  270. envVars = map[string]string{}
  271. for _, envVar := range *cc.EnvironmentVariables {
  272. envVars[*envVar.Name] = *envVar.Value
  273. }
  274. }
  275. hostConfig := ToHostConfig(cc, cg)
  276. config := &containers.RuntimeConfig{
  277. FQDN: fqdn(cg, region),
  278. Env: envVars,
  279. }
  280. c := containers.Container{
  281. ID: containerID,
  282. Status: status,
  283. Image: to.String(cc.Image),
  284. Command: command,
  285. CPUTime: 0,
  286. MemoryUsage: 0,
  287. PidsCurrent: 0,
  288. PidsLimit: 0,
  289. Ports: ToPorts(cg.IPAddress, *cc.Ports),
  290. Platform: platform,
  291. Config: config,
  292. HostConfig: hostConfig,
  293. }
  294. return c
  295. }
  296. // ToHostConfig convert an ACI container to host config value
  297. func ToHostConfig(cc containerinstance.Container, cg containerinstance.ContainerGroup) *containers.HostConfig {
  298. memLimits := uint64(0)
  299. memRequest := uint64(0)
  300. cpuLimit := 0.
  301. cpuReservation := 0.
  302. if cc.Resources != nil {
  303. if cc.Resources.Limits != nil {
  304. if cc.Resources.Limits.MemoryInGB != nil {
  305. memLimits = gbToBytes(*cc.Resources.Limits.MemoryInGB)
  306. }
  307. if cc.Resources.Limits.CPU != nil {
  308. cpuLimit = *cc.Resources.Limits.CPU
  309. }
  310. }
  311. if cc.Resources.Requests != nil {
  312. if cc.Resources.Requests.MemoryInGB != nil {
  313. memRequest = gbToBytes(*cc.Resources.Requests.MemoryInGB)
  314. }
  315. if cc.Resources.Requests.CPU != nil {
  316. cpuReservation = *cc.Resources.Requests.CPU
  317. }
  318. }
  319. }
  320. hostConfig := &containers.HostConfig{
  321. CPULimit: cpuLimit,
  322. CPUReservation: cpuReservation,
  323. MemoryLimit: memLimits,
  324. MemoryReservation: memRequest,
  325. RestartPolicy: toContainerRestartPolicy(cg.RestartPolicy),
  326. }
  327. return hostConfig
  328. }
  329. // GetStatus returns status for the specified container
  330. func GetStatus(container containerinstance.Container, group containerinstance.ContainerGroup) string {
  331. status := GetGroupStatus(group)
  332. if container.InstanceView != nil && container.InstanceView.CurrentState != nil {
  333. status = *container.InstanceView.CurrentState.State
  334. }
  335. return status
  336. }
  337. // GetGroupStatus returns status for the container group
  338. func GetGroupStatus(group containerinstance.ContainerGroup) string {
  339. if group.InstanceView != nil && group.InstanceView.State != nil {
  340. return "Node " + *group.InstanceView.State
  341. }
  342. return compose.UNKNOWN
  343. }