convert.go 16 KB

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