convert.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. /*
  2. Copyright 2020 Docker, Inc.
  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. "encoding/base64"
  17. "fmt"
  18. "io/ioutil"
  19. "math"
  20. "os"
  21. "strconv"
  22. "strings"
  23. "github.com/docker/compose-cli/compose"
  24. "github.com/docker/compose-cli/utils/formatter"
  25. "github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2018-10-01/containerinstance"
  26. "github.com/Azure/go-autorest/autorest/to"
  27. "github.com/compose-spec/compose-go/types"
  28. "github.com/pkg/errors"
  29. "github.com/docker/compose-cli/aci/login"
  30. "github.com/docker/compose-cli/containers"
  31. "github.com/docker/compose-cli/context/store"
  32. )
  33. const (
  34. // ComposeDNSSidecarName name of the dns sidecar container
  35. ComposeDNSSidecarName = "aci--dns--sidecar"
  36. dnsSidecarImage = "busybox:1.31.1"
  37. azureFileDriverName = "azure_file"
  38. volumeDriveroptsShareNameKey = "share_name"
  39. volumeDriveroptsAccountNameKey = "storage_account_name"
  40. secretInlineMark = "inline:"
  41. )
  42. // ToContainerGroup converts a compose project into a ACI container group
  43. func ToContainerGroup(ctx context.Context, aciContext store.AciContext, p types.Project) (containerinstance.ContainerGroup, error) {
  44. project := projectAciHelper(p)
  45. containerGroupName := strings.ToLower(project.Name)
  46. loginService, err := login.NewAzureLoginService()
  47. if err != nil {
  48. return containerinstance.ContainerGroup{}, err
  49. }
  50. storageHelper := login.StorageAccountHelper{
  51. LoginService: *loginService,
  52. AciContext: aciContext,
  53. }
  54. volumesCache, volumesSlice, err := project.getAciFileVolumes(ctx, storageHelper)
  55. if err != nil {
  56. return containerinstance.ContainerGroup{}, err
  57. }
  58. secretVolumes, err := project.getAciSecretVolumes()
  59. if err != nil {
  60. return containerinstance.ContainerGroup{}, err
  61. }
  62. allVolumes := append(volumesSlice, secretVolumes...)
  63. var volumes *[]containerinstance.Volume
  64. if len(allVolumes) == 0 {
  65. volumes = nil
  66. } else {
  67. volumes = &allVolumes
  68. }
  69. registryCreds, err := getRegistryCredentials(p, newCliRegistryConfLoader())
  70. if err != nil {
  71. return containerinstance.ContainerGroup{}, err
  72. }
  73. var containers []containerinstance.Container
  74. restartPolicy, err := project.getRestartPolicy()
  75. if err != nil {
  76. return containerinstance.ContainerGroup{}, err
  77. }
  78. groupDefinition := containerinstance.ContainerGroup{
  79. Name: &containerGroupName,
  80. Location: &aciContext.Location,
  81. ContainerGroupProperties: &containerinstance.ContainerGroupProperties{
  82. OsType: containerinstance.Linux,
  83. Containers: &containers,
  84. Volumes: volumes,
  85. ImageRegistryCredentials: &registryCreds,
  86. RestartPolicy: restartPolicy,
  87. },
  88. }
  89. var groupPorts []containerinstance.Port
  90. for _, s := range project.Services {
  91. service := serviceConfigAciHelper(s)
  92. containerDefinition, err := service.getAciContainer(volumesCache)
  93. if err != nil {
  94. return containerinstance.ContainerGroup{}, err
  95. }
  96. if service.Labels != nil && len(service.Labels) > 0 {
  97. return containerinstance.ContainerGroup{}, errors.New("ACI integration does not support labels in compose applications")
  98. }
  99. if service.Ports != nil {
  100. var containerPorts []containerinstance.ContainerPort
  101. for _, portConfig := range service.Ports {
  102. if portConfig.Published != 0 && portConfig.Published != portConfig.Target {
  103. msg := fmt.Sprintf("Port mapping is not supported with ACI, cannot map port %d to %d for container %s",
  104. portConfig.Published, portConfig.Target, service.Name)
  105. return groupDefinition, errors.New(msg)
  106. }
  107. portNumber := int32(portConfig.Target)
  108. containerPorts = append(containerPorts, containerinstance.ContainerPort{
  109. Port: to.Int32Ptr(portNumber),
  110. })
  111. groupPorts = append(groupPorts, containerinstance.Port{
  112. Port: to.Int32Ptr(portNumber),
  113. Protocol: containerinstance.TCP,
  114. })
  115. }
  116. containerDefinition.ContainerProperties.Ports = &containerPorts
  117. groupDefinition.ContainerGroupProperties.IPAddress = &containerinstance.IPAddress{
  118. Type: containerinstance.Public,
  119. Ports: &groupPorts,
  120. }
  121. }
  122. containers = append(containers, containerDefinition)
  123. }
  124. if len(containers) > 1 {
  125. dnsSideCar := getDNSSidecar(containers)
  126. containers = append(containers, dnsSideCar)
  127. }
  128. groupDefinition.ContainerGroupProperties.Containers = &containers
  129. return groupDefinition, nil
  130. }
  131. func getDNSSidecar(containers []containerinstance.Container) containerinstance.Container {
  132. var commands []string
  133. for _, container := range containers {
  134. commands = append(commands, fmt.Sprintf("echo 127.0.0.1 %s >> /etc/hosts", *container.Name))
  135. }
  136. // ACI restart policy is currently at container group level, cannot let the sidecar terminate quietly once /etc/hosts has been edited
  137. // Pricing is done at the container group level so letting the sidecar container "sleep" should not impact the price for the whole group
  138. commands = append(commands, "sleep infinity")
  139. alpineCmd := []string{"sh", "-c", strings.Join(commands, ";")}
  140. dnsSideCar := containerinstance.Container{
  141. Name: to.StringPtr(ComposeDNSSidecarName),
  142. ContainerProperties: &containerinstance.ContainerProperties{
  143. Image: to.StringPtr(dnsSidecarImage),
  144. Command: &alpineCmd,
  145. Resources: &containerinstance.ResourceRequirements{
  146. Limits: &containerinstance.ResourceLimits{
  147. MemoryInGB: to.Float64Ptr(0.1), // "The memory requirement should be in incrememts of 0.1 GB."
  148. CPU: to.Float64Ptr(0.01), // "The CPU requirement should be in incrememts of 0.01."
  149. },
  150. Requests: &containerinstance.ResourceRequests{
  151. MemoryInGB: to.Float64Ptr(0.1),
  152. CPU: to.Float64Ptr(0.01),
  153. },
  154. },
  155. },
  156. }
  157. return dnsSideCar
  158. }
  159. type projectAciHelper types.Project
  160. func (p projectAciHelper) getAciSecretVolumes() ([]containerinstance.Volume, error) {
  161. var secretVolumes []containerinstance.Volume
  162. for secretName, filepathToRead := range p.Secrets {
  163. var data []byte
  164. if strings.HasPrefix(filepathToRead.File, secretInlineMark) {
  165. data = []byte(filepathToRead.File[len(secretInlineMark):])
  166. } else {
  167. var err error
  168. data, err = ioutil.ReadFile(filepathToRead.File)
  169. if err != nil {
  170. return secretVolumes, err
  171. }
  172. }
  173. if len(data) == 0 {
  174. continue
  175. }
  176. dataStr := base64.StdEncoding.EncodeToString(data)
  177. secretVolumes = append(secretVolumes, containerinstance.Volume{
  178. Name: to.StringPtr(secretName),
  179. Secret: map[string]*string{
  180. secretName: &dataStr,
  181. },
  182. })
  183. }
  184. return secretVolumes, nil
  185. }
  186. func (p projectAciHelper) getAciFileVolumes(ctx context.Context, helper login.StorageAccountHelper) (map[string]bool, []containerinstance.Volume, error) {
  187. azureFileVolumesMap := make(map[string]bool, len(p.Volumes))
  188. var azureFileVolumesSlice []containerinstance.Volume
  189. for name, v := range p.Volumes {
  190. if v.Driver == azureFileDriverName {
  191. shareName, ok := v.DriverOpts[volumeDriveroptsShareNameKey]
  192. if !ok {
  193. return nil, nil, fmt.Errorf("cannot retrieve share name for Azurefile")
  194. }
  195. accountName, ok := v.DriverOpts[volumeDriveroptsAccountNameKey]
  196. if !ok {
  197. return nil, nil, fmt.Errorf("cannot retrieve account name for Azurefile")
  198. }
  199. accountKey, err := helper.GetAzureStorageAccountKey(ctx, accountName)
  200. if err != nil {
  201. return nil, nil, err
  202. }
  203. aciVolume := containerinstance.Volume{
  204. Name: to.StringPtr(name),
  205. AzureFile: &containerinstance.AzureFileVolume{
  206. ShareName: to.StringPtr(shareName),
  207. StorageAccountName: to.StringPtr(accountName),
  208. StorageAccountKey: to.StringPtr(accountKey),
  209. },
  210. }
  211. azureFileVolumesMap[name] = true
  212. azureFileVolumesSlice = append(azureFileVolumesSlice, aciVolume)
  213. }
  214. }
  215. return azureFileVolumesMap, azureFileVolumesSlice, nil
  216. }
  217. func (p projectAciHelper) getRestartPolicy() (containerinstance.ContainerGroupRestartPolicy, error) {
  218. var restartPolicyCondition containerinstance.ContainerGroupRestartPolicy
  219. if len(p.Services) >= 1 {
  220. alreadySpecified := false
  221. restartPolicyCondition = containerinstance.Always
  222. for _, service := range p.Services {
  223. if service.Deploy != nil &&
  224. service.Deploy.RestartPolicy != nil {
  225. if !alreadySpecified {
  226. alreadySpecified = true
  227. restartPolicyCondition = toAciRestartPolicy(service.Deploy.RestartPolicy.Condition)
  228. }
  229. if alreadySpecified && restartPolicyCondition != toAciRestartPolicy(service.Deploy.RestartPolicy.Condition) {
  230. return "", errors.New("ACI integration does not support specifying different restart policies on containers in the same compose application")
  231. }
  232. }
  233. }
  234. }
  235. return restartPolicyCondition, nil
  236. }
  237. func toAciRestartPolicy(restartPolicy string) containerinstance.ContainerGroupRestartPolicy {
  238. switch restartPolicy {
  239. case containers.RestartPolicyNone:
  240. return containerinstance.Never
  241. case containers.RestartPolicyAny:
  242. return containerinstance.Always
  243. case containers.RestartPolicyOnFailure:
  244. return containerinstance.OnFailure
  245. default:
  246. return containerinstance.Always
  247. }
  248. }
  249. func toContainerRestartPolicy(aciRestartPolicy containerinstance.ContainerGroupRestartPolicy) string {
  250. switch aciRestartPolicy {
  251. case containerinstance.Never:
  252. return containers.RestartPolicyNone
  253. case containerinstance.Always:
  254. return containers.RestartPolicyAny
  255. case containerinstance.OnFailure:
  256. return containers.RestartPolicyOnFailure
  257. default:
  258. return containers.RestartPolicyAny
  259. }
  260. }
  261. type serviceConfigAciHelper types.ServiceConfig
  262. func (s serviceConfigAciHelper) getAciFileVolumeMounts(volumesCache map[string]bool) ([]containerinstance.VolumeMount, error) {
  263. var aciServiceVolumes []containerinstance.VolumeMount
  264. for _, sv := range s.Volumes {
  265. if !volumesCache[sv.Source] {
  266. return []containerinstance.VolumeMount{}, fmt.Errorf("could not find volume source %q", sv.Source)
  267. }
  268. aciServiceVolumes = append(aciServiceVolumes, containerinstance.VolumeMount{
  269. Name: to.StringPtr(sv.Source),
  270. MountPath: to.StringPtr(sv.Target),
  271. })
  272. }
  273. return aciServiceVolumes, nil
  274. }
  275. func (s serviceConfigAciHelper) getAciSecretsVolumeMounts() []containerinstance.VolumeMount {
  276. var secretVolumeMounts []containerinstance.VolumeMount
  277. for _, secret := range s.Secrets {
  278. secretsMountPath := "/run/secrets"
  279. if secret.Target == "" {
  280. secret.Target = secret.Source
  281. }
  282. // Specifically use "/" here and not filepath.Join() to avoid windows path being sent and used inside containers
  283. secretsMountPath = secretsMountPath + "/" + secret.Target
  284. vmName := strings.Split(secret.Source, "=")[0]
  285. vm := containerinstance.VolumeMount{
  286. Name: to.StringPtr(vmName),
  287. MountPath: to.StringPtr(secretsMountPath),
  288. ReadOnly: to.BoolPtr(true), // TODO Confirm if the secrets are read only
  289. }
  290. secretVolumeMounts = append(secretVolumeMounts, vm)
  291. }
  292. return secretVolumeMounts
  293. }
  294. func (s serviceConfigAciHelper) getAciContainer(volumesCache map[string]bool) (containerinstance.Container, error) {
  295. secretVolumeMounts := s.getAciSecretsVolumeMounts()
  296. aciServiceVolumes, err := s.getAciFileVolumeMounts(volumesCache)
  297. if err != nil {
  298. return containerinstance.Container{}, err
  299. }
  300. allVolumes := append(aciServiceVolumes, secretVolumeMounts...)
  301. var volumes *[]containerinstance.VolumeMount
  302. if len(allVolumes) == 0 {
  303. volumes = nil
  304. } else {
  305. volumes = &allVolumes
  306. }
  307. memLimit := 1. // Default 1 Gb
  308. var cpuLimit float64 = 1
  309. if s.Deploy != nil && s.Deploy.Resources.Limits != nil {
  310. if s.Deploy.Resources.Limits.MemoryBytes != 0 {
  311. memLimit = bytesToGb(s.Deploy.Resources.Limits.MemoryBytes)
  312. }
  313. if s.Deploy.Resources.Limits.NanoCPUs != "" {
  314. cpuLimit, err = strconv.ParseFloat(s.Deploy.Resources.Limits.NanoCPUs, 0)
  315. if err != nil {
  316. return containerinstance.Container{}, err
  317. }
  318. }
  319. }
  320. return containerinstance.Container{
  321. Name: to.StringPtr(s.Name),
  322. ContainerProperties: &containerinstance.ContainerProperties{
  323. Image: to.StringPtr(s.Image),
  324. EnvironmentVariables: getEnvVariables(s.Environment),
  325. Resources: &containerinstance.ResourceRequirements{
  326. Limits: &containerinstance.ResourceLimits{
  327. MemoryInGB: to.Float64Ptr(memLimit),
  328. CPU: to.Float64Ptr(cpuLimit),
  329. },
  330. Requests: &containerinstance.ResourceRequests{
  331. MemoryInGB: to.Float64Ptr(memLimit), // TODO: use the memory requests here and not limits
  332. CPU: to.Float64Ptr(cpuLimit), // TODO: use the cpu requests here and not limits
  333. },
  334. },
  335. VolumeMounts: volumes,
  336. },
  337. }, nil
  338. }
  339. func getEnvVariables(composeEnv types.MappingWithEquals) *[]containerinstance.EnvironmentVariable {
  340. result := []containerinstance.EnvironmentVariable{}
  341. for key, value := range composeEnv {
  342. var strValue string
  343. if value == nil {
  344. strValue = os.Getenv(key)
  345. } else {
  346. strValue = *value
  347. }
  348. result = append(result, containerinstance.EnvironmentVariable{
  349. Name: to.StringPtr(key),
  350. Value: to.StringPtr(strValue),
  351. })
  352. }
  353. return &result
  354. }
  355. func bytesToGb(b types.UnitBytes) float64 {
  356. f := float64(b) / 1024 / 1024 / 1024 // from bytes to gigabytes
  357. return math.Round(f*100) / 100
  358. }
  359. // ContainerGroupToServiceStatus convert from an ACI container definition to service status
  360. func ContainerGroupToServiceStatus(containerID string, group containerinstance.ContainerGroup, container containerinstance.Container) compose.ServiceStatus {
  361. var replicas = 1
  362. if GetStatus(container, group) != "Running" {
  363. replicas = 0
  364. }
  365. return compose.ServiceStatus{
  366. ID: containerID,
  367. Name: *container.Name,
  368. Ports: formatter.PortsToStrings(ToPorts(group.IPAddress, *container.Ports)),
  369. Replicas: replicas,
  370. Desired: 1,
  371. }
  372. }
  373. // ContainerGroupToContainer composes a Container from an ACI container definition
  374. func ContainerGroupToContainer(containerID string, cg containerinstance.ContainerGroup, cc containerinstance.Container) containers.Container {
  375. memLimits := 0.
  376. if cc.Resources != nil &&
  377. cc.Resources.Limits != nil &&
  378. cc.Resources.Limits.MemoryInGB != nil {
  379. memLimits = *cc.Resources.Limits.MemoryInGB * 1024 * 1024 * 1024
  380. }
  381. cpuLimit := 0.
  382. if cc.Resources != nil &&
  383. cc.Resources.Limits != nil &&
  384. cc.Resources.Limits.CPU != nil {
  385. cpuLimit = *cc.Resources.Limits.CPU
  386. }
  387. command := ""
  388. if cc.Command != nil {
  389. command = strings.Join(*cc.Command, " ")
  390. }
  391. status := GetStatus(cc, cg)
  392. platform := string(cg.OsType)
  393. c := containers.Container{
  394. ID: containerID,
  395. Status: status,
  396. Image: to.String(cc.Image),
  397. Command: command,
  398. CPUTime: 0,
  399. CPULimit: cpuLimit,
  400. MemoryUsage: 0,
  401. MemoryLimit: uint64(memLimits),
  402. PidsCurrent: 0,
  403. PidsLimit: 0,
  404. Labels: nil,
  405. Ports: ToPorts(cg.IPAddress, *cc.Ports),
  406. Platform: platform,
  407. RestartPolicyCondition: toContainerRestartPolicy(cg.RestartPolicy),
  408. }
  409. return c
  410. }
  411. // GetStatus returns status for the specified container
  412. func GetStatus(container containerinstance.Container, group containerinstance.ContainerGroup) string {
  413. status := "Unknown"
  414. if group.InstanceView != nil && group.InstanceView.State != nil {
  415. status = "Node " + *group.InstanceView.State
  416. }
  417. if container.InstanceView != nil && container.InstanceView.CurrentState != nil {
  418. status = *container.InstanceView.CurrentState.State
  419. }
  420. return status
  421. }