package stacks import ( "context" "embed" "fmt" "os" "path/filepath" "github.com/pulumi/pulumi/sdk/v3/go/auto" "github.com/pulumi/pulumi/sdk/v3/go/auto/optdestroy" "github.com/pulumi/pulumi/sdk/v3/go/auto/optpreview" "github.com/pulumi/pulumi/sdk/v3/go/auto/optup" "github.com/pulumi/pulumi/sdk/v3/go/pulumi" "forge.dananglin.me.uk/code/dananglin/helix/internal/config" "forge.dananglin.me.uk/code/dananglin/helix/internal/docker" ) // DockerStack is a stack for managing the containers // for the forge platform. type DockerStack struct { Name string Stack auto.Stack } //go:embed templates/* var templates embed.FS // newContainerStack creates the ContainerStack value. func newDockerStack(ctx context.Context, project, stack string, c config.Config) (*DockerStack, error) { deployFunc := deployDockerStack(project, c.Docker, c.Services) s, err := createOrSelectStack(ctx, project, stack, deployFunc) if err != nil { return nil, fmt.Errorf("unable to initialise the '%s' stack...\n%w", stack, err) } d := DockerStack{ Name: stack, Stack: s, } return &d, nil } // Preview the proposed changes to the docker stack. func (c *DockerStack) Preview(ctx context.Context) error { streamer := optpreview.ProgressStreams(os.Stdout) _, err := c.Stack.Preview(ctx, streamer) if err != nil { return fmt.Errorf("unable to preview the '%s' stack...\n%w", c.Name, err) } return nil } // Update the docker stack. func (c *DockerStack) Update(ctx context.Context) error { streamer := optup.ProgressStreams(os.Stdout) _, err := c.Stack.Up(ctx, streamer) if err != nil { return fmt.Errorf("unable to update '%s' stack...\n%w", c.Name, err) } return nil } // Destroy the docker stack. func (c *DockerStack) Destroy(ctx context.Context) error { streamer := optdestroy.ProgressStreams(os.Stdout) _, err := c.Stack.Destroy(ctx, streamer) if err != nil { return fmt.Errorf("unable to destroy '%s' stack...\n%w", c.Name, err) } return nil } // deployDockerStack returns a Pulumi run function that is used to deploy the docker stack. func deployDockerStack(project string, dockerConfig config.DockerConfig, services config.ServicesConfig) pulumi.RunFunc { sharedVolumeMountPath := "/helix/shared" groupId := 2239 services.Traefik.GroupId = groupId services.Gitea.GroupId = groupId services.Gitea.UserId = 2000 return func(ctx *pulumi.Context) error { // TODO: Create the provider when we start playing with remote hosts // Create the docker network networkConfig := docker.DockerNetworkConfig{ Name: pulumi.String(dockerConfig.Network.Name), Subnet: pulumi.String(dockerConfig.Network.Subnet), Driver: pulumi.String(dockerConfig.Network.Driver), } network, err := docker.CreateNetwork(ctx, networkConfig) if err != nil { return err } // Create the shared volume sharedVolumeInput := docker.DockerVolumeInput{ Name: pulumi.String(dockerConfig.SharedVolume.Name), UniqueLabel: "shared", } sharedVolume, err := docker.CreateVolume(ctx, sharedVolumeInput) if err != nil { return err } baseCache, err := os.UserCacheDir() if err != nil { return fmt.Errorf("unable to get the base cache directory...\n%w", err) } projectCacheRoot := filepath.Join(baseCache, "helix", project) // Traefik service. if err = renderTemplates(services.Traefik, "traefik", projectCacheRoot); err != nil { return err } traefikImageInput := docker.DockerImageInput{ BuildContext: pulumi.String(filepath.Join(projectCacheRoot, "traefik")), Dockerfile: pulumi.String(filepath.Join(projectCacheRoot, "traefik", "Dockerfile")), ImageName: pulumi.String("helix-traefik"), ImageTag: pulumi.String(services.Traefik.Version), UniqueLabel: "traefik-image", } traefikImage, err := docker.CreateImage(ctx, traefikImageInput) if err != nil { return err } traefikContainerInput := docker.DockerContainerInput{ Image: traefikImage.ImageName, Ipv4Address: pulumi.String(services.Traefik.ContainerIp), Name: pulumi.String("helix-traefik"), Network: network.Name, UniqueLabel: "traefik-container", DockerVolumes: []docker.DockerVolume{ { Name: sharedVolume.Name, MountPath: pulumi.String(sharedVolumeMountPath), }, }, } if err = docker.CreateContainer(ctx, traefikContainerInput); err != nil { return err } // Gitea service if err = renderTemplates(services.Gitea, "gitea", projectCacheRoot); err != nil { return err } giteaImageInput := docker.DockerImageInput{ BuildContext: pulumi.String(filepath.Join(projectCacheRoot, "gitea")), Dockerfile: pulumi.String(filepath.Join(projectCacheRoot, "gitea", "Dockerfile")), ImageName: pulumi.String("helix-gitea"), ImageTag: pulumi.String(services.Gitea.Version), UniqueLabel: "gitea-image", } giteaImage, err := docker.CreateImage(ctx, giteaImageInput) if err != nil { return err } giteaContainerInput := docker.DockerContainerInput{ Image: giteaImage.ImageName, Ipv4Address: pulumi.String(services.Gitea.ContainerIp), Name: pulumi.String("helix-gitea"), Network: network.Name, DockerVolumes: []docker.DockerVolume{ { Name: sharedVolume.Name, MountPath: pulumi.String(sharedVolumeMountPath), }, }, HostPathVolumes: []docker.HostPathVolume{ { HostPath: pulumi.String(services.Gitea.DataDirectory), MountPath: pulumi.String("/helix/gitea/data"), }, }, UniqueLabel: "gitea-container", } if err = docker.CreateContainer(ctx, giteaContainerInput); err != nil { return err } return nil } }