convert.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  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. "encoding/base64"
  16. "errors"
  17. "fmt"
  18. "io/ioutil"
  19. "strings"
  20. "github.com/Azure/azure-sdk-for-go/profiles/latest/containerinstance/mgmt/containerinstance"
  21. "github.com/Azure/go-autorest/autorest/to"
  22. "github.com/compose-spec/compose-go/types"
  23. "github.com/docker/api/compose"
  24. "github.com/docker/api/containers"
  25. "github.com/docker/api/context/store"
  26. )
  27. const (
  28. // ComposeDNSSidecarName name of the dns sidecar container
  29. ComposeDNSSidecarName = "aci--dns--sidecar"
  30. dnsSidecarImage = "busybox:1.31.1"
  31. azureFileDriverName = "azure_file"
  32. volumeDriveroptsShareNameKey = "share_name"
  33. volumeDriveroptsAccountNameKey = "storage_account_name"
  34. volumeDriveroptsAccountKeyKey = "storage_account_key"
  35. secretInlineMark = "inline:"
  36. )
  37. // ToContainerGroup converts a compose project into a ACI container group
  38. func ToContainerGroup(aciContext store.AciContext, p compose.Project) (containerinstance.ContainerGroup, error) {
  39. project := projectAciHelper(p)
  40. containerGroupName := strings.ToLower(project.Name)
  41. volumesCache, volumesSlice, err := project.getAciFileVolumes()
  42. if err != nil {
  43. return containerinstance.ContainerGroup{}, err
  44. }
  45. secretVolumes, err := project.getAciSecretVolumes()
  46. if err != nil {
  47. return containerinstance.ContainerGroup{}, err
  48. }
  49. allVolumes := append(volumesSlice, secretVolumes...)
  50. var volumes *[]containerinstance.Volume
  51. if len(allVolumes) == 0 {
  52. volumes = nil
  53. } else {
  54. volumes = &allVolumes
  55. }
  56. registryCreds, err := getRegistryCredentials(p, newCliRegistryConfLoader())
  57. if err != nil {
  58. return containerinstance.ContainerGroup{}, err
  59. }
  60. var containers []containerinstance.Container
  61. groupDefinition := containerinstance.ContainerGroup{
  62. Name: &containerGroupName,
  63. Location: &aciContext.Location,
  64. ContainerGroupProperties: &containerinstance.ContainerGroupProperties{
  65. OsType: containerinstance.Linux,
  66. Containers: &containers,
  67. Volumes: volumes,
  68. ImageRegistryCredentials: &registryCreds,
  69. },
  70. }
  71. var groupPorts []containerinstance.Port
  72. for _, s := range project.Services {
  73. service := serviceConfigAciHelper(s)
  74. containerDefinition, err := service.getAciContainer(volumesCache)
  75. if err != nil {
  76. return containerinstance.ContainerGroup{}, err
  77. }
  78. if service.Ports != nil {
  79. var containerPorts []containerinstance.ContainerPort
  80. for _, portConfig := range service.Ports {
  81. if portConfig.Published != 0 && portConfig.Published != portConfig.Target {
  82. msg := fmt.Sprintf("Port mapping is not supported with ACI, cannot map port %d to %d for container %s",
  83. portConfig.Published, portConfig.Target, service.Name)
  84. return groupDefinition, errors.New(msg)
  85. }
  86. portNumber := int32(portConfig.Target)
  87. containerPorts = append(containerPorts, containerinstance.ContainerPort{
  88. Port: to.Int32Ptr(portNumber),
  89. })
  90. groupPorts = append(groupPorts, containerinstance.Port{
  91. Port: to.Int32Ptr(portNumber),
  92. Protocol: containerinstance.TCP,
  93. })
  94. }
  95. containerDefinition.ContainerProperties.Ports = &containerPorts
  96. groupDefinition.ContainerGroupProperties.IPAddress = &containerinstance.IPAddress{
  97. Type: containerinstance.Public,
  98. Ports: &groupPorts,
  99. }
  100. }
  101. containers = append(containers, containerDefinition)
  102. }
  103. if len(containers) > 1 {
  104. dnsSideCar := getDNSSidecar(containers)
  105. containers = append(containers, dnsSideCar)
  106. }
  107. groupDefinition.ContainerGroupProperties.Containers = &containers
  108. return groupDefinition, nil
  109. }
  110. func getDNSSidecar(containers []containerinstance.Container) containerinstance.Container {
  111. var commands []string
  112. for _, container := range containers {
  113. commands = append(commands, fmt.Sprintf("echo 127.0.0.1 %s >> /etc/hosts", *container.Name))
  114. }
  115. // ACI restart policy is currently at container group level, cannot let the sidecar terminate quietly once /etc/hosts has been edited
  116. // Pricing is done at the container group level so letting the sidecar container "sleep" should not impact the price for the whole group
  117. commands = append(commands, "sleep infinity")
  118. alpineCmd := []string{"sh", "-c", strings.Join(commands, ";")}
  119. dnsSideCar := containerinstance.Container{
  120. Name: to.StringPtr(ComposeDNSSidecarName),
  121. ContainerProperties: &containerinstance.ContainerProperties{
  122. Image: to.StringPtr(dnsSidecarImage),
  123. Command: &alpineCmd,
  124. Resources: &containerinstance.ResourceRequirements{
  125. Limits: &containerinstance.ResourceLimits{
  126. MemoryInGB: to.Float64Ptr(0.1), // "The memory requirement should be in incrememts of 0.1 GB."
  127. CPU: to.Float64Ptr(0.01), // "The CPU requirement should be in incrememts of 0.01."
  128. },
  129. Requests: &containerinstance.ResourceRequests{
  130. MemoryInGB: to.Float64Ptr(0.1),
  131. CPU: to.Float64Ptr(0.01),
  132. },
  133. },
  134. },
  135. }
  136. return dnsSideCar
  137. }
  138. type projectAciHelper compose.Project
  139. func (p projectAciHelper) getAciSecretVolumes() ([]containerinstance.Volume, error) {
  140. var secretVolumes []containerinstance.Volume
  141. for secretName, filepathToRead := range p.Secrets {
  142. var data []byte
  143. if strings.HasPrefix(filepathToRead.File, secretInlineMark) {
  144. data = []byte(filepathToRead.File[len(secretInlineMark):])
  145. } else {
  146. var err error
  147. data, err = ioutil.ReadFile(filepathToRead.File)
  148. if err != nil {
  149. return secretVolumes, err
  150. }
  151. }
  152. if len(data) == 0 {
  153. continue
  154. }
  155. dataStr := base64.StdEncoding.EncodeToString(data)
  156. secretVolumes = append(secretVolumes, containerinstance.Volume{
  157. Name: to.StringPtr(secretName),
  158. Secret: map[string]*string{
  159. secretName: &dataStr,
  160. },
  161. })
  162. }
  163. return secretVolumes, nil
  164. }
  165. func (p projectAciHelper) getAciFileVolumes() (map[string]bool, []containerinstance.Volume, error) {
  166. azureFileVolumesMap := make(map[string]bool, len(p.Volumes))
  167. var azureFileVolumesSlice []containerinstance.Volume
  168. for name, v := range p.Volumes {
  169. if v.Driver == azureFileDriverName {
  170. shareName, ok := v.DriverOpts[volumeDriveroptsShareNameKey]
  171. if !ok {
  172. return nil, nil, fmt.Errorf("cannot retrieve share name for Azurefile")
  173. }
  174. accountName, ok := v.DriverOpts[volumeDriveroptsAccountNameKey]
  175. if !ok {
  176. return nil, nil, fmt.Errorf("cannot retrieve account name for Azurefile")
  177. }
  178. accountKey, ok := v.DriverOpts[volumeDriveroptsAccountKeyKey]
  179. if !ok {
  180. return nil, nil, fmt.Errorf("cannot retrieve account key for Azurefile")
  181. }
  182. aciVolume := containerinstance.Volume{
  183. Name: to.StringPtr(name),
  184. AzureFile: &containerinstance.AzureFileVolume{
  185. ShareName: to.StringPtr(shareName),
  186. StorageAccountName: to.StringPtr(accountName),
  187. StorageAccountKey: to.StringPtr(accountKey),
  188. },
  189. }
  190. azureFileVolumesMap[name] = true
  191. azureFileVolumesSlice = append(azureFileVolumesSlice, aciVolume)
  192. }
  193. }
  194. return azureFileVolumesMap, azureFileVolumesSlice, nil
  195. }
  196. type serviceConfigAciHelper types.ServiceConfig
  197. func (s serviceConfigAciHelper) getAciFileVolumeMounts(volumesCache map[string]bool) ([]containerinstance.VolumeMount, error) {
  198. var aciServiceVolumes []containerinstance.VolumeMount
  199. for _, sv := range s.Volumes {
  200. if !volumesCache[sv.Source] {
  201. return []containerinstance.VolumeMount{}, fmt.Errorf("could not find volume source %q", sv.Source)
  202. }
  203. aciServiceVolumes = append(aciServiceVolumes, containerinstance.VolumeMount{
  204. Name: to.StringPtr(sv.Source),
  205. MountPath: to.StringPtr(sv.Target),
  206. })
  207. }
  208. return aciServiceVolumes, nil
  209. }
  210. func (s serviceConfigAciHelper) getAciSecretsVolumeMounts() []containerinstance.VolumeMount {
  211. var secretVolumeMounts []containerinstance.VolumeMount
  212. for _, secret := range s.Secrets {
  213. secretsMountPath := "/run/secrets"
  214. if secret.Target == "" {
  215. secret.Target = secret.Source
  216. }
  217. // Specifically use "/" here and not filepath.Join() to avoid windows path being sent and used inside containers
  218. secretsMountPath = secretsMountPath + "/" + secret.Target
  219. vmName := strings.Split(secret.Source, "=")[0]
  220. vm := containerinstance.VolumeMount{
  221. Name: to.StringPtr(vmName),
  222. MountPath: to.StringPtr(secretsMountPath),
  223. ReadOnly: to.BoolPtr(true), // TODO Confirm if the secrets are read only
  224. }
  225. secretVolumeMounts = append(secretVolumeMounts, vm)
  226. }
  227. return secretVolumeMounts
  228. }
  229. func (s serviceConfigAciHelper) getAciContainer(volumesCache map[string]bool) (containerinstance.Container, error) {
  230. secretVolumeMounts := s.getAciSecretsVolumeMounts()
  231. aciServiceVolumes, err := s.getAciFileVolumeMounts(volumesCache)
  232. if err != nil {
  233. return containerinstance.Container{}, err
  234. }
  235. allVolumes := append(aciServiceVolumes, secretVolumeMounts...)
  236. var volumes *[]containerinstance.VolumeMount
  237. if len(allVolumes) == 0 {
  238. volumes = nil
  239. } else {
  240. volumes = &allVolumes
  241. }
  242. return containerinstance.Container{
  243. Name: to.StringPtr(s.Name),
  244. ContainerProperties: &containerinstance.ContainerProperties{
  245. Image: to.StringPtr(s.Image),
  246. Resources: &containerinstance.ResourceRequirements{
  247. Limits: &containerinstance.ResourceLimits{
  248. MemoryInGB: to.Float64Ptr(1),
  249. CPU: to.Float64Ptr(1),
  250. },
  251. Requests: &containerinstance.ResourceRequests{
  252. MemoryInGB: to.Float64Ptr(1),
  253. CPU: to.Float64Ptr(1),
  254. },
  255. },
  256. VolumeMounts: volumes,
  257. },
  258. }, nil
  259. }
  260. // ContainerGroupToContainer composes a Container from an ACI container definition
  261. func ContainerGroupToContainer(containerID string, cg containerinstance.ContainerGroup, cc containerinstance.Container) (containers.Container, error) {
  262. memLimits := -1.
  263. if cc.Resources != nil &&
  264. cc.Resources.Limits != nil &&
  265. cc.Resources.Limits.MemoryInGB != nil {
  266. memLimits = *cc.Resources.Limits.MemoryInGB
  267. }
  268. command := ""
  269. if cc.Command != nil {
  270. command = strings.Join(*cc.Command, " ")
  271. }
  272. status := "Unknown"
  273. if cc.InstanceView != nil && cc.InstanceView.CurrentState != nil {
  274. status = *cc.InstanceView.CurrentState.State
  275. }
  276. c := containers.Container{
  277. ID: containerID,
  278. Status: status,
  279. Image: to.String(cc.Image),
  280. Command: command,
  281. CPUTime: 0,
  282. MemoryUsage: 0,
  283. MemoryLimit: uint64(memLimits),
  284. PidsCurrent: 0,
  285. PidsLimit: 0,
  286. Labels: nil,
  287. Ports: ToPorts(cg.IPAddress, *cc.Ports),
  288. }
  289. return c, nil
  290. }