compose/azure/backend.go
Guillaume Tardif f403307edf Allow compose updates without having to delete the stack every time.
Update will “typically” keep the same IP, but this isn’t guaranteed by azure
(ACI has limitations on what can be updated, but this does not apply to us for the moment : https://docs.microsoft.com/en-us/azure/container-instances/container-instances-update#properties-that-require-container-delete)
For the moment I check in the test that the IP is keep the same
2020-06-18 10:03:28 +02:00

319 lines
8.1 KiB
Go

package azure
import (
"context"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"github.com/Azure/go-autorest/autorest/to"
"github.com/docker/api/context/cloud"
"github.com/docker/api/errdefs"
"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2018-10-01/containerinstance"
"github.com/compose-spec/compose-go/types"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/docker/api/azure/convert"
"github.com/docker/api/azure/login"
"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"
)
const singleContainerName = "single--container--aci"
// ErrNoSuchContainer is returned when the mentioned container does not exist
var ErrNoSuchContainer = errors.New("no such container")
func init() {
backend.Register("aci", "aci", service, getCloudService)
}
func service(ctx context.Context) (backend.Service, error) {
contextStore := store.ContextStore(ctx)
currentContext := apicontext.CurrentContext(ctx)
var aciContext store.AciContext
if err := contextStore.GetEndpoint(currentContext, &aciContext); err != nil {
return nil, err
}
return getAciAPIService(aciContext), nil
}
func getCloudService() (cloud.Service, error) {
service, err := login.NewAzureLoginService()
if err != nil {
return nil, err
}
return &aciCloudService{
loginService: service,
}, nil
}
func getAciAPIService(aciCtx store.AciContext) *aciAPIService {
return &aciAPIService{
aciContainerService: &aciContainerService{
ctx: aciCtx,
},
aciComposeService: &aciComposeService{
ctx: aciCtx,
},
}
}
type aciAPIService struct {
*aciContainerService
*aciComposeService
}
func (a *aciAPIService) ContainerService() containers.Service {
return a.aciContainerService
}
func (a *aciAPIService) ComposeService() compose.Service {
return a.aciComposeService
}
type aciContainerService struct {
ctx store.AciContext
}
func (cs *aciContainerService) List(ctx context.Context, _ bool) ([]containers.Container, error) {
groupsClient, err := getContainerGroupsClient(cs.ctx.SubscriptionID)
if err != nil {
return nil, err
}
var containerGroups []containerinstance.ContainerGroup
result, err := groupsClient.ListByResourceGroup(ctx, cs.ctx.ResourceGroup)
if err != nil {
return []containers.Container{}, err
}
for result.NotDone() {
containerGroups = append(containerGroups, result.Values()...)
if err := result.NextWithContext(ctx); err != nil {
return []containers.Container{}, err
}
}
var res []containers.Container
for _, containerGroup := range containerGroups {
group, err := groupsClient.Get(ctx, cs.ctx.ResourceGroup, *containerGroup.Name)
if err != nil {
return []containers.Container{}, err
}
for _, container := range *group.Containers {
var containerID string
if *container.Name == singleContainerName {
containerID = *containerGroup.Name
} else {
containerID = *containerGroup.Name + "_" + *container.Name
}
status := "Unknown"
if container.InstanceView != nil && container.InstanceView.CurrentState != nil {
status = *container.InstanceView.CurrentState.State
}
res = append(res, containers.Container{
ID: containerID,
Image: *container.Image,
Status: status,
Ports: convert.ToPorts(group.IPAddress, *container.Ports),
})
}
}
return res, nil
}
func (cs *aciContainerService) Run(ctx context.Context, r containers.ContainerConfig) error {
var ports []types.ServicePortConfig
for _, p := range r.Ports {
ports = append(ports, types.ServicePortConfig{
Target: p.ContainerPort,
Published: p.HostPort,
})
}
projectVolumes, serviceConfigVolumes, err := convert.GetRunVolumes(r.Volumes)
if err != nil {
return err
}
project := compose.Project{
Name: r.ID,
Config: types.Config{
Services: []types.ServiceConfig{
{
Name: singleContainerName,
Image: r.Image,
Ports: ports,
Labels: r.Labels,
Volumes: serviceConfigVolumes,
},
},
Volumes: projectVolumes,
},
}
logrus.Debugf("Running container %q with name %q\n", r.Image, r.ID)
groupDefinition, err := convert.ToContainerGroup(cs.ctx, project)
if err != nil {
return err
}
return createACIContainers(ctx, cs.ctx, groupDefinition)
}
func (cs *aciContainerService) Stop(ctx context.Context, containerName string, timeout *uint32) error {
return errdefs.ErrNotImplemented
}
func getGroupAndContainerName(containerID string) (groupName string, containerName string) {
tokens := strings.Split(containerID, "_")
groupName = tokens[0]
if len(tokens) > 1 {
containerName = tokens[len(tokens)-1]
groupName = containerID[:len(containerID)-(len(containerName)+1)]
} else {
containerName = singleContainerName
}
return groupName, containerName
}
func (cs *aciContainerService) Exec(ctx context.Context, name string, command string, reader io.Reader, writer io.Writer) error {
groupName, containerAciName := getGroupAndContainerName(name)
containerExecResponse, err := execACIContainer(ctx, cs.ctx, command, groupName, containerAciName)
if err != nil {
return err
}
return exec(
context.Background(),
*containerExecResponse.WebSocketURI,
*containerExecResponse.Password,
reader,
writer,
)
}
func (cs *aciContainerService) Logs(ctx context.Context, containerName string, req containers.LogsRequest) error {
groupName, containerAciName := getGroupAndContainerName(containerName)
logs, err := getACIContainerLogs(ctx, cs.ctx, groupName, containerAciName)
if err != nil {
return err
}
if req.Tail != "all" {
tail, err := strconv.Atoi(req.Tail)
if err != nil {
return err
}
lines := strings.Split(logs, "\n")
// If asked for less lines than exist, take only those lines
if tail <= len(lines) {
logs = strings.Join(lines[len(lines)-tail:], "\n")
}
}
_, err = fmt.Fprint(req.Writer, logs)
return err
}
func (cs *aciContainerService) Delete(ctx context.Context, containerID string, _ bool) error {
cg, err := deleteACIContainerGroup(ctx, cs.ctx, containerID)
if err != nil {
return err
}
if cg.StatusCode == http.StatusNoContent {
return ErrNoSuchContainer
}
return err
}
func (cs *aciContainerService) Inspect(ctx context.Context, containerID string) (containers.Container, error) {
groupName, containerName := getGroupAndContainerName(containerID)
cg, err := getACIContainerGroup(ctx, cs.ctx, groupName)
if err != nil {
return containers.Container{}, err
}
if cg.StatusCode == http.StatusNoContent {
return containers.Container{}, ErrNoSuchContainer
}
var cc containerinstance.Container
var found = false
for _, c := range *cg.Containers {
if to.String(c.Name) == containerName {
cc = c
found = true
break
}
}
if !found {
return containers.Container{}, ErrNoSuchContainer
}
return convert.ContainerGroupToContainer(containerID, cg, cc)
}
type aciComposeService struct {
ctx store.AciContext
}
func (cs *aciComposeService) Up(ctx context.Context, opts compose.ProjectOptions) error {
project, err := compose.ProjectFromOptions(&opts)
if err != nil {
return err
}
logrus.Debugf("Up on project with name %q\n", project.Name)
groupDefinition, err := convert.ToContainerGroup(cs.ctx, *project)
if err != nil {
return err
}
return createOrUpdateACIContainers(ctx, cs.ctx, groupDefinition)
}
func (cs *aciComposeService) Down(ctx context.Context, opts compose.ProjectOptions) error {
project, err := compose.ProjectFromOptions(&opts)
if err != nil {
return err
}
logrus.Debugf("Down on project with name %q\n", project.Name)
cg, err := deleteACIContainerGroup(ctx, cs.ctx, project.Name)
if err != nil {
return err
}
if cg.StatusCode == http.StatusNoContent {
return ErrNoSuchContainer
}
return err
}
type aciCloudService struct {
loginService login.AzureLoginService
}
func (cs *aciCloudService) Login(ctx context.Context, params map[string]string) error {
return cs.loginService.Login(ctx)
}
func (cs *aciCloudService) CreateContextData(ctx context.Context, params map[string]string) (interface{}, string, error) {
contextHelper := newContextCreateHelper()
return contextHelper.createContextData(ctx, params)
}