convert.go 12 KB

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