Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add cloud status aware agent #119

Merged
merged 4 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ bin
dist/
config/config.yaml
node_modules
.task
26 changes: 22 additions & 4 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,38 @@ project_name: minecraft-preempt
before:
hooks:
- go mod download
env:
- CGO_ENABLED=0
builds:
- main: ./cmd/minecraft-preempt
env:
- CGO_ENABLED=0
- &default
id: minecraft-preempt
main: ./cmd/minecraft-preempt
gcflags:
- -trimpath
ldflags:
- -s -w -X github.com/jaredallard/minecraft-preempt/internal/version.Version={{ .Version }}
goarch:
- amd64
- arm64
goos:
- linux
- windows
- darwin
- <<: *default
id: &name minecraft-preempt-agent
binary: *name
main: ./cmd/minecraft-preempt-agent

# Verifiable builds.
gomod:
proxy: true
env:
- GOPROXY=https://proxy.golang.org,direct
- GOSUMDB=sum.golang.org
mod: mod

archives:
- format: tar.gz
- format: tar.xz
# use zip for windows archives
format_overrides:
- goos: windows
Expand Down
8 changes: 8 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# CI system runs using amd64, so this allows us to not need QEMU, but
# will break builds on non-amd64 Linux systems (sorry).
FROM --platform=amd64 alpine:3.18 as cacerts
RUN apk add --no-cache ca-certificates

FROM alpine:3.18
ENTRYPOINT ["/usr/local/bin/minecraft-preempt"]
COPY --from=cacerts /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

COPY minecraft-preempt /usr/local/bin/
COPY minecraft-preempt-agent /usr/local/bin/
24 changes: 3 additions & 21 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,24 +1,6 @@
# go options
GO ?= go
PKG := go mod download
LDFLAGS := -w -s
BINDIR := $(CURDIR)/bin

# Required for globs to work correctly
SHELL=/bin/bash

.PHONY: all
all: build

.PHONY: dep
dep:
@$(PKG)
SHELL := /usr/bin/env bash

.PHONY: build
build:
CGO_ENABLED=0 $(GO) build -v -o $(BINDIR)/ -ldflags '$(LDFLAGS)' ./cmd/...

.PHONY: reload
reload:
@echo "Watching for changes to .go files..."
@go run github.com/cespare/reflex@latest --regex='\.go$$' --decoration=fancy --start-service=true bash -- -c 'make build && exec ./bin/minecraft-preempt'
@command -v task >/dev/null || (echo "task not found, please install it. https://taskfile.dev/installation/" && exit 1)
@task
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ A lightweight Minecraft server manager. Starts a server when users join, and sto

## Usage

**Hint**: There's an example configuration in [`./config`](./config).

First, define a configuration file for your server. The format is like so:

### Top level
Expand Down
21 changes: 21 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
version: "3"

tasks:
default:
cmds:
- task: build
test:
cmds:
- go test -v ./...
build:
generates:
- bin/minecraft-preempt
- bin/minecraft-preempt-agent
sources:
- "./**/*.go"
- .tool-versions # Trigger rebuild on Go version changes.
cmds:
- go build -trimpath -v -o ./bin/ -ldflags="-X github.com/jaredallard/minecraft-preempt/internal/version.Version=dev" ./cmd/...
snapshot:
cmds:
- goreleaser --snapshot --clean
178 changes: 178 additions & 0 deletions cmd/minecraft-preempt-agent/minecraft-preempt-agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Copyright (C) 2023 Jared Allard <jared@rgst.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

// Package main implements a lightweight agent that runs a minecraft
// server (via docker-compose) and handles shutting down the server when
// the proxy informs it to.
//
// Currently, this doesn't do much but tell a prebuilt docker-compose
// stack to stop and start. Shutdown signals are handled by shutting
// down the VM, which this agent in turn listens to (either via Docker
// shutting itself down, or preempting the VM).
package main

import (
"context"
"fmt"
"os"
"os/exec"
"os/signal"
"syscall"
"time"

logger "github.com/charmbracelet/log"
"github.com/egym-playground/go-prefix-writer/prefixer"
"github.com/jaredallard/minecraft-preempt/internal/cloud"
"github.com/jaredallard/minecraft-preempt/internal/cloud/docker"
"github.com/jaredallard/minecraft-preempt/internal/cloud/gcp"
"github.com/jaredallard/minecraft-preempt/internal/version"
"github.com/spf13/cobra"
)

// log is the global logger for the agent.
var log = logger.NewWithOptions(os.Stderr, logger.Options{
ReportCaller: true,
ReportTimestamp: true,
Level: logger.DebugLevel,
})

// rootCmd is the root command used by cobra
var rootCmd = &cobra.Command{
Use: "minecraft-preempt-agent",
Version: version.Version,

Short: "minecraft-preempt-agent is a companion to the minecraft-preempt proxy",
RunE: entrypoint,
}

// entrypoint is the entrypoint for the root command
func entrypoint(cCmd *cobra.Command, args []string) error {
ctx, cancel := context.WithCancel(cCmd.Context())
defer cancel()

dc := cCmd.Flag("docker-compose-file").Value.String()
cloudProvider := cCmd.Flag("cloud").Value.String()

_, err := os.Stat(dc)
if err != nil {
return fmt.Errorf("failed to find docker-compose file: %w", err)
}

log.With("version", version.Version, "cloud", cloudProvider).Info("starting agent")

cmd := exec.CommandContext(ctx, "docker", "compose", "-f", dc, "up")
cmd.Stdout = prefixer.New(os.Stdout, func() string { return "[docker-compose] " })
cmd.Stderr = prefixer.New(os.Stderr, func() string { return "[docker-compose] " })

// Start the process in a new process group so we can kill it and all
// of its children reliably. This also detaches ^C (sent to us) from
// killing the child process, instead allowing our context cancel to
// do that.
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
cmd.Cancel = func() error {
// Send SIGINT to the child process group.
return syscall.Kill(-cmd.Process.Pid, syscall.SIGINT)
}

if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start '%s': %w", cmd.String(), err)
}

// Start the watcher.
if err := watcher(ctx, cancel, cloudProvider); err != nil {
return fmt.Errorf("failed to start watcher: %w", err)
}

if err := cmd.Wait(); err != nil {
// Only report errors if the context wasn't canceled.
if ctx.Err() == nil {
return fmt.Errorf("failed to run '%s': %w", cmd.String(), err)
}
}

log.Info("exited")

return nil
}

// watcher uses cloud specific APIs to determine when this agent should
// terminate. The provided cancel function will be called when the agent
// should shutdown.
func watcher(ctx context.Context, cancel context.CancelFunc, cloudProvider string) error {
var c cloud.Provider
var err error

switch cloudProvider {
case "gcp":
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

c, err = gcp.NewClient(ctx, "", "")
case "docker":
c, err = docker.NewClient()
}
if err != nil {
return fmt.Errorf("failed to start cloud watcher for cloud %s: %w", cloudProvider, err)
}

// Start the watcher.
go func() {
t := time.NewTicker(2 * time.Second)
defer t.Stop()

for {
// If we're canceled, exit.
select {
case <-ctx.Done():
log.Debug("context canceled, exiting watcher")
return
case <-t.C:
shouldStop, err := c.ShouldTerminate(ctx)
if err != nil {
log.With("err", err).Warn("failed to determine if instance should terminate")
continue
}

if shouldStop {
log.Info("instance is being preempted, starting shutdown")
cancel()
return
}
}
}
}()

log.Info("started preemption watcher")

return nil
}

// main is the entrypoint for the proxy
func main() {
exitCode := 0
defer func() {
os.Exit(exitCode)
}()

ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()

rootCmd.PersistentFlags().String("docker-compose-file", "docker-compose.yml", "path to docker-compose.yml")
rootCmd.PersistentFlags().String("cloud", "docker", "cloud provider to use")
if err := rootCmd.ExecuteContext(ctx); err != nil {
log.With("err", err).Error("failed to run")
exitCode = 1
}
}
4 changes: 3 additions & 1 deletion cmd/minecraft-preempt/minecraft-preempt.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ import (
"github.com/spf13/cobra"

"github.com/jaredallard/minecraft-preempt/internal/config"
"github.com/jaredallard/minecraft-preempt/internal/version"
)

// rootCmd is the root command used by cobra
var rootCmd = &cobra.Command{
Use: "minecraft-preempt",
Use: "minecraft-preempt",
Version: version.Version,

Short: "minecraft-preempt is a proxy for minecraft servers that can start and stop them",
Long: `minecraft-preempt is a proxy for minecraft servers that can start and stop them based on ` +
Expand Down
File renamed without changes.
14 changes: 8 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,19 @@ module github.com/jaredallard/minecraft-preempt
go 1.21

require (
github.com/Tnze/go-mc v1.19.4
cloud.google.com/go/compute v1.23.3
cloud.google.com/go/compute/metadata v0.2.3
github.com/Tnze/go-mc v1.20.1
github.com/charmbracelet/log v0.3.0
github.com/docker/docker v24.0.7+incompatible
github.com/egym-playground/go-prefix-writer v0.0.0-20180609083313-7326ea162eca
github.com/function61/gokit v0.0.0-20231117065306-355fe206d542
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.8.0
golang.org/x/oauth2 v0.14.0
google.golang.org/api v0.151.0
gopkg.in/yaml.v3 v3.0.1
)

require (
cloud.google.com/go/compute v1.23.1 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/lipgloss v0.9.1 // indirect
Expand Down Expand Up @@ -50,12 +49,15 @@ require (
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/mod v0.13.0 // indirect
golang.org/x/net v0.18.0 // indirect
golang.org/x/oauth2 v0.14.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.14.0 // indirect
google.golang.org/api v0.151.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
Expand Down
Loading