readysite / cmd / publish / main.go
5.5 KB
main.go
package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"os"
	"os/exec"
	"strings"
)

func main() {
	var (
		tag           = flag.String("tag", "latest", "Image tag")
		dockerhub     = flag.String("dockerhub", "ccutch/readysite", "Docker Hub repository")
		skipRegistry  = flag.Bool("skip-registry", false, "Skip pushing to private registry")
		skipDockerhub = flag.Bool("skip-dockerhub", false, "Skip pushing to Docker Hub")
		buildServer   = flag.String("build-server", "", "Server to build on (uses launch if set)")
		pushServer    = flag.String("push-server", "readysite-org-1", "Server to push from (must be logged into Docker Hub)")
		skipTests     = flag.Bool("skip-tests", false, "Skip running tests before publish")
	)
	flag.Parse()

	// Run tests before publishing
	if !*skipTests {
		if err := runTests(); err != nil {
			log.Fatalf("Tests failed, aborting publish: %v", err)
		}
	}

	imageName := "website"

	// Get push server IP and registry from infra.json
	pushServerIP := getServerIP(*pushServer)
	if pushServerIP == "" {
		log.Fatalf("Could not find IP for push server %s in infra.json", *pushServer)
	}
	registry := pushServerIP + ":5001"

	// Check if Docker is available locally
	localDocker := exec.Command("docker", "info").Run() == nil

	if localDocker {
		// Build locally
		log.Printf("Building %s locally...", imageName)
		if err := run("docker", "build", "-t", imageName+":"+*tag, "./website"); err != nil {
			log.Fatalf("Build failed: %v", err)
		}

		// Push to private registry
		if !*skipRegistry {
			registryImage := fmt.Sprintf("%s/%s:%s", registry, imageName, *tag)
			log.Printf("Pushing to %s...", registryImage)
			if err := run("docker", "tag", imageName+":"+*tag, registryImage); err != nil {
				log.Fatalf("Tag failed: %v", err)
			}
			if err := run("docker", "push", registryImage); err != nil {
				log.Fatalf("Push to registry failed: %v", err)
			}
			log.Printf("Pushed to %s ✓", registryImage)
		}

		// Push to Docker Hub
		if !*skipDockerhub {
			dockerhubImage := fmt.Sprintf("%s:%s", *dockerhub, *tag)
			log.Printf("Pushing to %s...", dockerhubImage)
			if err := run("docker", "tag", imageName+":"+*tag, dockerhubImage); err != nil {
				log.Fatalf("Tag failed: %v", err)
			}
			if err := run("docker", "push", dockerhubImage); err != nil {
				log.Fatalf("Push to Docker Hub failed: %v", err)
			}
			log.Printf("Pushed to %s ✓", dockerhubImage)
		}
	} else {
		// Build remotely using launch, then push from server
		log.Println("Local Docker unavailable, using remote build...")

		if *buildServer != "" {
			// Build on specified server using launch
			log.Printf("Building on %s using launch...", *buildServer)
			if err := run("go", "run", "./cmd/launch", "--server", *buildServer); err != nil {
				log.Fatalf("Remote build failed: %v", err)
			}

			// Push from build server to registry
			if !*skipRegistry {
				log.Printf("Pushing to registry from %s...", *buildServer)
				serverIP := getServerIP(*buildServer)
				if serverIP == "" {
					log.Fatalf("Could not find IP for server %s", *buildServer)
				}
				registryImage := fmt.Sprintf("%s/%s:%s", registry, imageName, *tag)
				if err := sshRun(serverIP, fmt.Sprintf("docker tag %s:latest %s && docker push %s", imageName, registryImage, registryImage)); err != nil {
					log.Fatalf("Push to registry failed: %v", err)
				}
				log.Printf("Pushed to %s ✓", registryImage)
			}
		} else {
			log.Println("Skipping build (no --build-server specified, assuming image exists in registry)")
		}

		// Push to Docker Hub from push server
		if !*skipDockerhub {
			log.Printf("Pushing to Docker Hub from %s...", *pushServer)
			dockerhubImage := fmt.Sprintf("%s:%s", *dockerhub, *tag)
			registryImage := fmt.Sprintf("localhost:5001/%s:%s", imageName, *tag)

			cmds := fmt.Sprintf("docker pull %s && docker tag %s %s && docker push %s",
				registryImage, registryImage, dockerhubImage, dockerhubImage)

			if err := sshRun(pushServerIP, cmds); err != nil {
				log.Fatalf("Push to Docker Hub failed: %v", err)
			}
			log.Printf("Pushed to %s ✓", dockerhubImage)
		}
	}

	log.Println("Publish complete!")
}

func run(name string, args ...string) error {
	log.Printf("   %s %s", name, strings.Join(args, " "))
	cmd := exec.Command(name, args...)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return cmd.Run()
}

// sshRun executes a shell command on a remote host via SSH.
// The command is passed as a single string to be interpreted by the remote shell.
// Only use with trusted, hardcoded command strings — never with user-supplied input.
func sshRun(host, command string) error {
	log.Printf("   ssh root@%s %q", host, command)
	cmd := exec.Command("ssh", "root@"+host, "--", command)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return cmd.Run()
}

func runTests() error {
	log.Println("Running tests...")
	cmd := exec.Command("go", "test", "-timeout", "5m", "./...")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return cmd.Run()
}

func getServerIP(name string) string {
	data, err := os.ReadFile("infra.json")
	if err != nil {
		log.Printf("Warning: could not read infra.json: %v", err)
		return ""
	}

	var config struct {
		Instances map[string][]struct {
			Name string `json:"name"`
			IP   string `json:"ip"`
		} `json:"instances"`
	}

	if err := json.Unmarshal(data, &config); err != nil {
		log.Printf("Warning: could not parse infra.json: %v", err)
		return ""
	}

	for _, instances := range config.Instances {
		for _, inst := range instances {
			if inst.Name == name {
				return inst.IP
			}
		}
	}

	return ""
}
← Back