convert.go 14 KB

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