convert.go 12 KB

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