diff --git a/azure/aci.go b/azure/aci.go index f8bf58da1..67374f7ee 100644 --- a/azure/aci.go +++ b/azure/aci.go @@ -3,21 +3,15 @@ package azure import ( "bufio" "context" - "encoding/base64" - "errors" "fmt" "io" - "io/ioutil" "net/http" "os" "os/signal" "runtime" "strings" - "github.com/compose-spec/compose-go/types" - "github.com/docker/api/compose" "github.com/docker/api/context/store" - "github.com/sirupsen/logrus" "github.com/gobwas/ws" "github.com/gobwas/ws/wsutil" @@ -30,25 +24,17 @@ import ( tm "github.com/buger/goterm" ) -const ( - AzureFileDriverName = "azure_file" - VolumeDriveroptsShareNameKey = "share_name" - VolumeDriveroptsAccountNameKey = "storage_account_name" - VolumeDriveroptsAccountKeyKey = "storage_account_key" -) -const singleContainerName = "single--container--aci" +func init() { + // required to get auth.NewAuthorizerFromCLI() work, otherwise getting "The access token has been obtained for wrong audience or resource 'https://vault.azure.net'." + _ = os.Setenv("AZURE_KEYVAULT_RESOURCE", "https://management.azure.com") +} -func CreateACIContainers(ctx context.Context, project compose.Project, aciContext store.AciContext) (c containerinstance.ContainerGroup, err error) { +func CreateACIContainers(ctx context.Context, aciContext store.AciContext, groupDefinition containerinstance.ContainerGroup) (c containerinstance.ContainerGroup, err error) { containerGroupsClient, err := getContainerGroupsClient(aciContext.SubscriptionID) if err != nil { return c, fmt.Errorf("cannot get container group client: %v", err) } - groupDefinition, err := convert(project, aciContext) - if err != nil { - return c, err - } - // Check if the container group already exists _, err = containerGroupsClient.Get(ctx, aciContext.ResourceGroup, *groupDefinition.Name) if err != nil { @@ -83,14 +69,16 @@ func CreateACIContainers(ctx context.Context, project compose.Project, aciContex return c, err } - if len(project.Services) > 1 { + if len(*containerGroup.Containers) > 1 { var commands []string - for _, service := range project.Services { - commands = append(commands, fmt.Sprintf("echo 127.0.0.1 %s >> /etc/hosts", service.Name)) + for _, container := range *containerGroup.Containers { + commands = append(commands, fmt.Sprintf("echo 127.0.0.1 %s >> /etc/hosts", *container.Name)) } commands = append(commands, "exit") - response, err := ExecACIContainer(ctx, "/bin/sh", project.Name, project.Services[0].Name, aciContext) + containers := *containerGroup.Containers + container := containers[0] + response, err := ExecACIContainer(ctx, "/bin/sh", *containerGroup.Name, *container.Name, aciContext) if err != nil { return c, err } @@ -109,135 +97,6 @@ func CreateACIContainers(ctx context.Context, project compose.Project, aciContex return containerGroup, err } -type ProjectAciHelper compose.Project - -func (p ProjectAciHelper) getAciSecretVolumes() ([]containerinstance.Volume, error) { - var secretVolumes []containerinstance.Volume - for secretName, filepathToRead := range p.Secrets { - var data []byte - if strings.HasPrefix(filepathToRead.File, compose.SecretInlineMark) { - data = []byte(filepathToRead.File[len(compose.SecretInlineMark):]) - } else { - var err error - data, err = ioutil.ReadFile(filepathToRead.File) - if err != nil { - return secretVolumes, err - } - } - if len(data) == 0 { - continue - } - dataStr := base64.StdEncoding.EncodeToString(data) - secretVolumes = append(secretVolumes, containerinstance.Volume{ - Name: to.StringPtr(secretName), - Secret: map[string]*string{ - secretName: &dataStr, - }, - }) - } - return secretVolumes, nil -} - -func (p ProjectAciHelper) getAciFileVolumes() (map[string]bool, []containerinstance.Volume, error) { - azureFileVolumesMap := make(map[string]bool, len(p.Volumes)) - var azureFileVolumesSlice []containerinstance.Volume - for name, v := range p.Volumes { - if v.Driver == AzureFileDriverName { - shareName, ok := v.DriverOpts[VolumeDriveroptsShareNameKey] - if !ok { - return nil, nil, fmt.Errorf("cannot retrieve share name for Azurefile") - } - accountName, ok := v.DriverOpts[VolumeDriveroptsAccountNameKey] - if !ok { - return nil, nil, fmt.Errorf("cannot retrieve account name for Azurefile") - } - accountKey, ok := v.DriverOpts[VolumeDriveroptsAccountKeyKey] - if !ok { - return nil, nil, fmt.Errorf("cannot retrieve account key for Azurefile") - } - aciVolume := containerinstance.Volume{ - Name: to.StringPtr(name), - AzureFile: &containerinstance.AzureFileVolume{ - ShareName: to.StringPtr(shareName), - StorageAccountName: to.StringPtr(accountName), - StorageAccountKey: to.StringPtr(accountKey), - }, - } - azureFileVolumesMap[name] = true - azureFileVolumesSlice = append(azureFileVolumesSlice, aciVolume) - } - } - return azureFileVolumesMap, azureFileVolumesSlice, nil -} - -type ServiceConfigAciHelper types.ServiceConfig - -func (s ServiceConfigAciHelper) getAciFileVolumeMounts(volumesCache map[string]bool) ([]containerinstance.VolumeMount, error) { - var aciServiceVolumes []containerinstance.VolumeMount - for _, sv := range s.Volumes { - if !volumesCache[sv.Source] { - return []containerinstance.VolumeMount{}, fmt.Errorf("could not find volume source %q", sv.Source) - } - aciServiceVolumes = append(aciServiceVolumes, containerinstance.VolumeMount{ - Name: to.StringPtr(sv.Source), - MountPath: to.StringPtr(sv.Target), - }) - } - return aciServiceVolumes, nil -} - -func (s ServiceConfigAciHelper) getAciSecretsVolumeMounts() []containerinstance.VolumeMount { - var secretVolumeMounts []containerinstance.VolumeMount - for _, secret := range s.Secrets { - secretsMountPath := "/run/secrets" - if secret.Target == "" { - secret.Target = secret.Source - } - // Specifically use "/" here and not filepath.Join() to avoid windows path being sent and used inside containers - secretsMountPath = secretsMountPath + "/" + secret.Target - vmName := strings.Split(secret.Source, "=")[0] - vm := containerinstance.VolumeMount{ - Name: to.StringPtr(vmName), - MountPath: to.StringPtr(secretsMountPath), - ReadOnly: to.BoolPtr(true), // TODO Confirm if the secrets are read only - } - secretVolumeMounts = append(secretVolumeMounts, vm) - } - return secretVolumeMounts -} - -func (s ServiceConfigAciHelper) getAciContainer(volumesCache map[string]bool) (containerinstance.Container, error) { - secretVolumeMounts := s.getAciSecretsVolumeMounts() - aciServiceVolumes, err := s.getAciFileVolumeMounts(volumesCache) - if err != nil { - return containerinstance.Container{}, err - } - allVolumes := append(aciServiceVolumes, secretVolumeMounts...) - var volumes *[]containerinstance.VolumeMount - if len(allVolumes) == 0 { - volumes = nil - } else { - volumes = &allVolumes - } - return containerinstance.Container{ - Name: to.StringPtr(s.Name), - ContainerProperties: &containerinstance.ContainerProperties{ - Image: to.StringPtr(s.Image), - Resources: &containerinstance.ResourceRequirements{ - Limits: &containerinstance.ResourceLimits{ - MemoryInGB: to.Float64Ptr(1), - CPU: to.Float64Ptr(1), - }, - Requests: &containerinstance.ResourceRequests{ - MemoryInGB: to.Float64Ptr(1), - CPU: to.Float64Ptr(1), - }, - }, - VolumeMounts: volumes, - }, - }, nil -} - // ListACIContainers List available containers func ListACIContainers(aciContext store.AciContext) (c []containerinstance.ContainerGroup, err error) { ctx := context.TODO() @@ -361,75 +220,6 @@ func ExecWebSocketLoopWithCmd(ctx context.Context, wsURL, passwd string, command } } -func convert(p compose.Project, aciContext store.AciContext) (containerinstance.ContainerGroup, error) { - project := ProjectAciHelper(p) - containerGroupName := strings.ToLower(project.Name) - volumesCache, volumesSlice, err := project.getAciFileVolumes() - if err != nil { - return containerinstance.ContainerGroup{}, err - } - secretVolumes, err := project.getAciSecretVolumes() - if err != nil { - return containerinstance.ContainerGroup{}, err - } - allVolumes := append(volumesSlice, secretVolumes...) - var volumes *[]containerinstance.Volume - if len(allVolumes) == 0 { - volumes = nil - } else { - volumes = &allVolumes - } - var containers []containerinstance.Container - groupDefinition := containerinstance.ContainerGroup{ - Name: &containerGroupName, - Location: &aciContext.Location, - ContainerGroupProperties: &containerinstance.ContainerGroupProperties{ - OsType: containerinstance.Linux, - Containers: &containers, - Volumes: volumes, - }, - } - - for _, s := range project.Services { - service := ServiceConfigAciHelper(s) - if s.Name != singleContainerName { - logrus.Debugf("Adding %q\n", service.Name) - } - containerDefinition, err := service.getAciContainer(volumesCache) - if err != nil { - return containerinstance.ContainerGroup{}, err - } - if service.Ports != nil { - var containerPorts []containerinstance.ContainerPort - var groupPorts []containerinstance.Port - for _, portConfig := range service.Ports { - if portConfig.Published != 0 && portConfig.Published != portConfig.Target { - msg := fmt.Sprintf("Port mapping is not supported with ACI, cannot map port %d to %d for container %s", - portConfig.Published, portConfig.Target, service.Name) - return groupDefinition, errors.New(msg) - } - portNumber := int32(portConfig.Target) - containerPorts = append(containerPorts, containerinstance.ContainerPort{ - Port: to.Int32Ptr(portNumber), - }) - groupPorts = append(groupPorts, containerinstance.Port{ - Port: to.Int32Ptr(portNumber), - Protocol: containerinstance.TCP, - }) - } - containerDefinition.ContainerProperties.Ports = &containerPorts - groupDefinition.ContainerGroupProperties.IPAddress = &containerinstance.IPAddress{ - Type: containerinstance.Public, - Ports: &groupPorts, - } - } - - containers = append(containers, containerDefinition) - } - groupDefinition.ContainerGroupProperties.Containers = &containers - return groupDefinition, nil -} - func cleanLastCommand(lastCommandLen int) { tm.MoveCursorUp(1) tm.MoveCursorForward(lastCommandLen) diff --git a/azure/backend.go b/azure/backend.go index 5a2cf905a..c7a8beae4 100644 --- a/azure/backend.go +++ b/azure/backend.go @@ -9,12 +9,12 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" - "github.com/docker/api/context/store" - + "github.com/docker/api/azure/convert" "github.com/docker/api/backend" "github.com/docker/api/compose" "github.com/docker/api/containers" apicontext "github.com/docker/api/context" + "github.com/docker/api/context/store" ) type containerService struct { @@ -102,6 +102,11 @@ func (cs *containerService) Run(ctx context.Context, r containers.ContainerConfi } logrus.Debugf("Running container %q with name %q\n", r.Image, r.ID) - _, err := CreateACIContainers(ctx, project, cs.ctx) + groupDefinition, err := convert.ToContainerGroup(cs.ctx, project) + if err != nil { + return err + } + + _, err = CreateACIContainers(ctx, cs.ctx, groupDefinition) return err } diff --git a/azure/convert/convert.go b/azure/convert/convert.go new file mode 100644 index 000000000..0206613f0 --- /dev/null +++ b/azure/convert/convert.go @@ -0,0 +1,223 @@ +package convert + +import ( + "encoding/base64" + "errors" + "fmt" + "io/ioutil" + "strings" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/containerinstance/mgmt/containerinstance" + "github.com/Azure/go-autorest/autorest/to" + "github.com/compose-spec/compose-go/types" + "github.com/docker/api/compose" + "github.com/docker/api/context/store" + "github.com/sirupsen/logrus" +) + +const ( + azureFileDriverName = "azure_file" + volumeDriveroptsShareNameKey = "share_name" + volumeDriveroptsAccountNameKey = "storage_account_name" + volumeDriveroptsAccountKeyKey = "storage_account_key" + singleContainerName = "single--container--aci" +) + +func ToContainerGroup(aciContext store.AciContext, p compose.Project) (containerinstance.ContainerGroup, error) { + project := projectAciHelper(p) + containerGroupName := strings.ToLower(project.Name) + volumesCache, volumesSlice, err := project.getAciFileVolumes() + if err != nil { + return containerinstance.ContainerGroup{}, err + } + secretVolumes, err := project.getAciSecretVolumes() + if err != nil { + return containerinstance.ContainerGroup{}, err + } + allVolumes := append(volumesSlice, secretVolumes...) + var volumes *[]containerinstance.Volume + if len(allVolumes) == 0 { + volumes = nil + } else { + volumes = &allVolumes + } + var containers []containerinstance.Container + groupDefinition := containerinstance.ContainerGroup{ + Name: &containerGroupName, + Location: &aciContext.Location, + ContainerGroupProperties: &containerinstance.ContainerGroupProperties{ + OsType: containerinstance.Linux, + Containers: &containers, + Volumes: volumes, + }, + } + + for _, s := range project.Services { + service := serviceConfigAciHelper(s) + if s.Name != singleContainerName { + logrus.Debugf("Adding %q\n", service.Name) + } + containerDefinition, err := service.getAciContainer(volumesCache) + if err != nil { + return containerinstance.ContainerGroup{}, err + } + if service.Ports != nil { + var containerPorts []containerinstance.ContainerPort + var groupPorts []containerinstance.Port + for _, portConfig := range service.Ports { + if portConfig.Published != 0 && portConfig.Published != portConfig.Target { + msg := fmt.Sprintf("Port mapping is not supported with ACI, cannot map port %d to %d for container %s", + portConfig.Published, portConfig.Target, service.Name) + return groupDefinition, errors.New(msg) + } + portNumber := int32(portConfig.Target) + containerPorts = append(containerPorts, containerinstance.ContainerPort{ + Port: to.Int32Ptr(portNumber), + }) + groupPorts = append(groupPorts, containerinstance.Port{ + Port: to.Int32Ptr(portNumber), + Protocol: containerinstance.TCP, + }) + } + containerDefinition.ContainerProperties.Ports = &containerPorts + groupDefinition.ContainerGroupProperties.IPAddress = &containerinstance.IPAddress{ + Type: containerinstance.Public, + Ports: &groupPorts, + } + } + + containers = append(containers, containerDefinition) + } + groupDefinition.ContainerGroupProperties.Containers = &containers + return groupDefinition, nil +} + +type projectAciHelper compose.Project + +func (p projectAciHelper) getAciSecretVolumes() ([]containerinstance.Volume, error) { + var secretVolumes []containerinstance.Volume + for secretName, filepathToRead := range p.Secrets { + var data []byte + if strings.HasPrefix(filepathToRead.File, compose.SecretInlineMark) { + data = []byte(filepathToRead.File[len(compose.SecretInlineMark):]) + } else { + var err error + data, err = ioutil.ReadFile(filepathToRead.File) + if err != nil { + return secretVolumes, err + } + } + if len(data) == 0 { + continue + } + dataStr := base64.StdEncoding.EncodeToString(data) + secretVolumes = append(secretVolumes, containerinstance.Volume{ + Name: to.StringPtr(secretName), + Secret: map[string]*string{ + secretName: &dataStr, + }, + }) + } + return secretVolumes, nil +} + +func (p projectAciHelper) getAciFileVolumes() (map[string]bool, []containerinstance.Volume, error) { + azureFileVolumesMap := make(map[string]bool, len(p.Volumes)) + var azureFileVolumesSlice []containerinstance.Volume + for name, v := range p.Volumes { + if v.Driver == azureFileDriverName { + shareName, ok := v.DriverOpts[volumeDriveroptsShareNameKey] + if !ok { + return nil, nil, fmt.Errorf("cannot retrieve share name for Azurefile") + } + accountName, ok := v.DriverOpts[volumeDriveroptsAccountNameKey] + if !ok { + return nil, nil, fmt.Errorf("cannot retrieve account name for Azurefile") + } + accountKey, ok := v.DriverOpts[volumeDriveroptsAccountKeyKey] + if !ok { + return nil, nil, fmt.Errorf("cannot retrieve account key for Azurefile") + } + aciVolume := containerinstance.Volume{ + Name: to.StringPtr(name), + AzureFile: &containerinstance.AzureFileVolume{ + ShareName: to.StringPtr(shareName), + StorageAccountName: to.StringPtr(accountName), + StorageAccountKey: to.StringPtr(accountKey), + }, + } + azureFileVolumesMap[name] = true + azureFileVolumesSlice = append(azureFileVolumesSlice, aciVolume) + } + } + return azureFileVolumesMap, azureFileVolumesSlice, nil +} + +type serviceConfigAciHelper types.ServiceConfig + +func (s serviceConfigAciHelper) getAciFileVolumeMounts(volumesCache map[string]bool) ([]containerinstance.VolumeMount, error) { + var aciServiceVolumes []containerinstance.VolumeMount + for _, sv := range s.Volumes { + if !volumesCache[sv.Source] { + return []containerinstance.VolumeMount{}, fmt.Errorf("could not find volume source %q", sv.Source) + } + aciServiceVolumes = append(aciServiceVolumes, containerinstance.VolumeMount{ + Name: to.StringPtr(sv.Source), + MountPath: to.StringPtr(sv.Target), + }) + } + return aciServiceVolumes, nil +} + +func (s serviceConfigAciHelper) getAciSecretsVolumeMounts() []containerinstance.VolumeMount { + var secretVolumeMounts []containerinstance.VolumeMount + for _, secret := range s.Secrets { + secretsMountPath := "/run/secrets" + if secret.Target == "" { + secret.Target = secret.Source + } + // Specifically use "/" here and not filepath.Join() to avoid windows path being sent and used inside containers + secretsMountPath = secretsMountPath + "/" + secret.Target + vmName := strings.Split(secret.Source, "=")[0] + vm := containerinstance.VolumeMount{ + Name: to.StringPtr(vmName), + MountPath: to.StringPtr(secretsMountPath), + ReadOnly: to.BoolPtr(true), // TODO Confirm if the secrets are read only + } + secretVolumeMounts = append(secretVolumeMounts, vm) + } + return secretVolumeMounts +} + +func (s serviceConfigAciHelper) getAciContainer(volumesCache map[string]bool) (containerinstance.Container, error) { + secretVolumeMounts := s.getAciSecretsVolumeMounts() + aciServiceVolumes, err := s.getAciFileVolumeMounts(volumesCache) + if err != nil { + return containerinstance.Container{}, err + } + allVolumes := append(aciServiceVolumes, secretVolumeMounts...) + var volumes *[]containerinstance.VolumeMount + if len(allVolumes) == 0 { + volumes = nil + } else { + volumes = &allVolumes + } + return containerinstance.Container{ + Name: to.StringPtr(s.Name), + ContainerProperties: &containerinstance.ContainerProperties{ + Image: to.StringPtr(s.Image), + Resources: &containerinstance.ResourceRequirements{ + Limits: &containerinstance.ResourceLimits{ + MemoryInGB: to.Float64Ptr(1), + CPU: to.Float64Ptr(1), + }, + Requests: &containerinstance.ResourceRequests{ + MemoryInGB: to.Float64Ptr(1), + CPU: to.Float64Ptr(1), + }, + }, + VolumeMounts: volumes, + }, + }, nil + +}