convert.go 15 KB

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