convert.go 16 KB

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