package deployer import ( "context" "errors" "strings" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" "github.com/docker/go-connections/nat" "github.com/rs/zerolog/log" localcli "localenv/client" "localenv/services" ) const ( SwarmImageName string = "localenv-swarm:latest" SwarmContainerName string = "localenv-swarm" ) var ( ErrSwarmNotFound = errors.New("unable to find the swarm") ErrSwarmClientInit = errors.New("swarm client is not initialized") ImagesDeps = []string{ services.PostgresImageName, services.RabbitMQImageName, services.NginxImageName, services.MailhogImageName, } ) type Deployer struct { swarm Swarm cli *client.Client } func NewDeployer(ctx context.Context, cli *client.Client) (Deployer, error) { var deployer Deployer deployer.cli = cli if err := deployer.init(ctx); err != nil { return deployer, err } return deployer, nil } func (d Deployer) getSwarm(ctx context.Context) (string, error) { filterArgs := filters.NewArgs() filterArgs.Add("name", SwarmContainerName) filterArgs.Add("ancestor", SwarmImageName) options := types.ContainerListOptions{ Filters: filterArgs, All: true, } containers, err := d.cli.ContainerList(ctx, options) if err != nil { return "", err } if len(containers) == 0 { return "", ErrSwarmNotFound } return containers[0].ID, nil } func (d Deployer) createSwarm(ctx context.Context) (string, error) { containerConfig := container.Config{ Image: SwarmImageName, ExposedPorts: nat.PortSet{ "4523/tcp": struct{}{}, "4443/tcp": struct{}{}, "15672/tcp": struct{}{}, }, } hostConfig := container.HostConfig{ Runtime: "sysbox-runc", PortBindings: nat.PortMap{ // swarm dockerd "4523/tcp": []nat.PortBinding{ { HostIP: "0.0.0.0", HostPort: "4523", }, }, // nginx ssl "4443/tcp": []nat.PortBinding{ { HostIP: "0.0.0.0", HostPort: "4443", }, }, // rabbitmq admin interface "15672/tcp": []nat.PortBinding{ { HostIP: "0.0.0.0", HostPort: "15672", }, }, }, NetworkMode: "bridge", } resp, err := d.cli.ContainerCreate(ctx, &containerConfig, &hostConfig, nil, nil, SwarmContainerName) if err != nil { return "", err } log.Info().Msg("swarm created successfully") return resp.ID, nil } func (d Deployer) initSwarm(ctx context.Context) error { req := swarm.InitRequest{ ListenAddr: "0.0.0.0:2377", AdvertiseAddr: "127.0.0.1", } if _, err := d.swarm.cli.SwarmInit(ctx, req); err != nil { if strings.Contains(err.Error(), "part of a swarm") { log.Info().Msg("swarm already initialized") return nil } return err } return nil } func (d Deployer) StopSwarm(ctx context.Context) error { if d.swarm.ID == "" { log.Warn().Msg("no swarm registered, can't stop") return nil } options := container.StopOptions{} if err := d.cli.ContainerStop(ctx, d.swarm.ID, options); err != nil { return err } log.Info().Msg("swarm stopped successfully") return nil } // TODO(rmanach): get a child context instead func (d *Deployer) init(ctx context.Context) error { swarmID, err := d.getSwarm(ctx) if err != nil { if !errors.Is(err, ErrSwarmNotFound) { return err } } s := Swarm{swarmID, nil} if swarmID == "" { id, err := d.createSwarm(ctx) if err != nil { return err } s.ID = id } if err := d.cli.ContainerStart(ctx, s.ID, types.ContainerStartOptions{}); err != nil { return err } cli, errInit := localcli.GetSwarmClient() if errInit != nil { if err := d.StopSwarm(ctx); err != nil { log.Err(err).Msg("unable to stop the swarm") } return errInit } s.cli = cli d.swarm = s if err := d.initSwarm(ctx); err != nil { return err } log.Info().Msg("deployer successfully initialized") return nil } func (d *Deployer) Deploy(ctx context.Context) error { if err := d.swarm.deployServices(ctx); err != nil { return err } return nil } func (d *Deployer) GetSwarmCLI() (*client.Client, error) { if d.swarm.cli == nil { return nil, ErrSwarmClientInit } return d.swarm.cli, nil }