~egtann/terrafirma

e97186faa66f872f24a0db04c526639fe902d7af — Evan Tann a month ago b74751b
consider images in plan

Changing the image a service uses, e.g. debian-buster to
debian-bullseye, should destroy and recreate any affected VMs.
Terrafirma's plan now factors the image into its decision-making.
2 files changed, 41 insertions(+), 23 deletions(-)

M terrafirma.go
M vm_template.go
M terrafirma.go => terrafirma.go +38 -23
@@ 2,8 2,11 @@ package terrafirma

import (
	"context"
	"crypto/sha1"
	"encoding/base32"
	"errors"
	"fmt"
	"io"
	"math/rand"
	"sort"
	"strings"


@@ 43,11 46,11 @@ func vmFromBox(boxes map[BoxName]*Box, vmTemplate *VMTemplate) (*VM, error) {
		Name:      vmTemplate.VMName,
		Tags:      vmTemplate.Tags,
		IPs:       vmTemplate.IPs,
		Image:     vmTemplate.Image,
		AllowHTTP: vmTemplate.AllowHTTP,
		CPU:       box.CPU,
		Mem:       int(box.RAM),
		Disk:      int(box.Disk),
		Image:     box.Image,
		AllowHTTP: box.AllowHTTP,
	}
	return vm, nil
}


@@ 77,8 80,8 @@ func (t *Terrafirma) Plan(
	}
	forDestroy := map[string]*destroy{}
	for boxName, box := range boxes {
		p, err := t.plan(ctx, vms, boxName, box.AllowHTTP,
			services[boxName])
		box.Name = boxName
		p, err := t.plan(ctx, vms, box, services[boxName])
		if err != nil {
			return nil, fmt.Errorf("plan: %w", err)
		}


@@ 156,8 159,7 @@ func (t *Terrafirma) Plan(
func (t *Terrafirma) plan(
	ctx context.Context,
	vms []*VM,
	boxName BoxName,
	allowHTTP bool,
	box *Box,
	bins [][]string,
) (*Plan, error) {
	// Plan to create any boxes which are missing. Start by sorting


@@ 169,7 171,17 @@ func (t *Terrafirma) plan(
		if len(bin) == 0 {
			return nil, errors.New("empty bin")
		}
		bin = append(bin, string(boxName))
		bin = append(bin, string(box.Name))

		// We have to store the image name as a tag because despite
		// Google's documentation showing they return this in get/list
		// calls, they do not. However Google also limits length to 64
		// characters, so we make it smaller with a hash.
		fp, err := fingerprint(box.Image)
		if err != nil {
			return nil, fmt.Errorf("fingerprint: %w", err)
		}
		bin = append(bin, "tf-image-"+fp)
		sort.Strings(bin)
		tag := strings.Join(bin, ",")
		unmatchedBins[tag]++


@@ 183,30 195,23 @@ func (t *Terrafirma) plan(

		// Ensure the box name matches what we had before, otherwise
		// treat it as a new box entirely.
		if boxName != getBoxName(vm.Name) {
		if box.Name != getBoxName(vm.Name) {
			p.Destroy = append(p.Destroy, vm)
			continue
		}

		// Ensure that the cloud-based firewall reflects our allowHTTP
		// setting.
		if vm.AllowHTTP != allowHTTP {
		if vm.AllowHTTP != box.AllowHTTP {
			p.Destroy = append(p.Destroy, vm)
			continue
		}

		// Ensure tags are present. If only a box tag is there or
		// nothing at all then no services are running. We would never
		// create a server without at least one service, so mark it for
		// deletion.
		// Ensure tags are present. If no tags are present, then no
		// services are running. We would never create a server without
		// at least one service, so mark it for deletion.
		tag := strings.Join(vm.Tags, ",")
		if tag == "" || tag == string(boxName) {
			p.Destroy = append(p.Destroy, vm)
			continue
		}

		// Does the VM have our desired firewall settings?
		if vm.AllowHTTP != allowHTTP {
		if tag == "" {
			p.Destroy = append(p.Destroy, vm)
			continue
		}


@@ 227,10 232,11 @@ func (t *Terrafirma) plan(
		for i := 0; i < count; i++ {
			n := rand.Intn(999999-100000) + 100000
			p.Create = append(p.Create, &VMTemplate{
				VMName:    Name("vm", string(boxName), n),
				BoxName:   boxName,
				VMName:    Name("vm", string(box.Name), n),
				BoxName:   box.Name,
				Image:     box.Image,
				AllowHTTP: box.AllowHTTP,
				Tags:      strings.Split(tag, ","),
				AllowHTTP: allowHTTP,
			})
		}
	}


@@ 394,3 400,12 @@ func (t *Terrafirma) createIP(
	}
	ipCh <- ip
}

func fingerprint(s string) (string, error) {
	h := sha1.New()
	if _, err := io.WriteString(h, s); err != nil {
		return "", fmt.Errorf("write: %w", s)
	}
	enc := base32.StdEncoding.WithPadding(base32.NoPadding)
	return strings.ToLower(enc.EncodeToString(h.Sum(nil))), nil
}

M vm_template.go => vm_template.go +3 -0
@@ 9,6 9,9 @@ type VMTemplate struct {
	// the VM, which defines the image, CPU, RAM, and Disk.
	BoxName BoxName `json:"box_name"`

	// Image to be used when creating the box.
	Image string `json:"image"`

	// Tags indicate the services to be deployed on a VM.
	Tags []string `json:"tags"`