jynx/backend/hetzner/servers.go

246 lines
6.2 KiB
Go

package hetzner
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"
"git.stinnesbeck.com/go/log"
"github.com/hetznercloud/hcloud-go/hcloud"
)
type VersionInfo struct {
Source string `json:"source"`
Version string `json:"version"`
}
func connect() *hcloud.Client {
return hcloud.NewClient(hcloud.WithToken(os.Getenv("HETZNER_CLOUD_API_TOKEN")))
}
func GetNewSuffix(servers []*hcloud.Server) (int, error) {
// cop out if there are no servers yet
if len(servers) == 0 {
return 1, nil
}
// variable to hold highest number
var highestNumber int
// loop through all servers
for i := range servers {
serverNameSlice := strings.Split(servers[i].Name, "-")
suffix, err := strconv.Atoi(serverNameSlice[len(serverNameSlice)-1])
if err != nil {
return 0, err
}
// compare our current number agains the highest yet
if suffix > highestNumber {
highestNumber = suffix
}
}
// return one number higher than highest number
return highestNumber + 1, nil
}
// ListJynxServers lists all servers managed by Jynx hosted on Hetzner Cloud
func ListJynxServers(prefix string) ([]*hcloud.Server, error) {
// connect to the hcloud API
client := connect()
// get all servers
servers, err := client.Server.All(context.Background())
if err != nil {
return nil, err
}
// create object that holds all servers managed by Jynx
var jynxServers []*hcloud.Server
// loop through servers and filter out managed servers
for _, server := range servers {
if strings.HasPrefix(server.Name, prefix) {
jynxServers = append(jynxServers, server)
}
}
// return managed servers
return jynxServers, nil
}
func CheckServerDeletions(servers []*hcloud.Server) error {
interval := os.Getenv("SCALER_POLL_INTERVAL")
if interval == "" {
interval = "5"
}
pollIntervalSeconds, err := strconv.Atoi(interval)
if err != nil {
return err
}
if pollIntervalSeconds < 60 {
pollIntervalSeconds = 60
}
if pollIntervalSeconds > 3600 {
pollIntervalSeconds %= 3600
}
for i, server := range servers {
safeInterval := time.Duration(3600-pollIntervalSeconds) * time.Second
optimalDeleteTime := server.Created.Add(safeInterval).Local()
log.PrintDebug(server.Name, "optimal delete time:", optimalDeleteTime)
if time.Now().Local().Before(optimalDeleteTime) {
continue
}
// check if HETZNER_SERVER_MIN was provided as an environment variable
if minServersString := os.Getenv("HETZNER_SERVER_MIN"); minServersString != "" {
minServers, err := strconv.Atoi(minServersString)
if err != nil {
return err
}
// if the amount of servers is less than the minimal amount required
// leave this server running
if i < minServers {
log.PrintDebug("not deleting server", server.Name, "because HETZNER_SERVER_MIN is", minServers)
continue
}
}
// optimal delete time has passed, delete the server
if err := DeleteServer(server); err != nil {
return err
}
}
return nil
}
func DeleteServer(server *hcloud.Server) error {
client := connect()
log.PrintInfo("Deleting", server.Name)
_, response, err := client.Server.DeleteWithResult(context.Background(), server)
if err != nil {
return err
}
if response.StatusCode != 200 {
return fmt.Errorf("api returned errorcode %d", response.StatusCode)
}
return nil
}
func CreateServers(servers []string) error {
// connect to hcloud API
client := connect()
// read in some environment variables
agentSecret := os.Getenv("WOODPECKER_AGENT_SECRET")
r := regexp.MustCompile(`http[s]*:\/\/(.*)`)
woodpeckerHostname := os.Getenv("WOODPECKER_HOSTNAME")
hasPort := regexp.MustCompile(`^.*(:\d+)`)
withoutPort := regexp.MustCompile(`^https?:\/\/(.*):`)
woodpeckerHost := r.FindStringSubmatch(woodpeckerHostname)[1]
if hasPort.MatchString(woodpeckerHost) {
woodpeckerHost = withoutPort.FindStringSubmatch(woodpeckerHost)[1]
}
// default port for stats
statsPort := "9000"
// check if port was set
if port := os.Getenv("WOODPECKER_STATS_PORT"); port != "" {
statsPort = port
}
woodpeckerHost += ":" + statsPort
// get woodpecker server version
// prepare request
req, err := http.NewRequest(http.MethodGet, woodpeckerHostname+"/version", nil)
if err != nil {
return err
}
c := http.DefaultClient
response, err := c.Do(req)
if err != nil {
return err
}
b, err := io.ReadAll(response.Body)
if err != nil {
return err
}
var version VersionInfo
err = json.Unmarshal(b, &version)
if err != nil {
return err
}
// loop through each server
for _, server := range servers {
// create unique AgentCMD for cloud init
AgentCMD := fmt.Sprintf("docker run -d -v /var/run/docker.sock:/var/run/docker.sock -e WOODPECKER_GRPC_SECURE=false -e WOODPECKER_SERVER=%s -e WOODPECKER_AGENT_SECRET=%s -e WOODPECKER_MAX_PROCS=%d -e WOODPECKER_HOSTNAME=%s woodpeckerci/woodpecker-agent:v"+version.Version, woodpeckerHost, agentSecret, 1, server)
userdata := fmt.Sprintf("#cloud-config\nruncmd:\n - %s", AgentCMD)
labels := make(map[string]string)
labels["created_by"] = "jynx"
labels["created"] = fmt.Sprintf("%d", time.Now().Unix())
var sshkeys []*hcloud.SSHKey
sshKeyNames := strings.Split(os.Getenv("HETZNER_SSH_KEY_NAMES"), ",")
sshkeys, err := getSSHKeys(sshKeyNames...)
if err != nil {
return nil
}
// get location and server type from environment
serverImage := os.Getenv("HETZNER_SERVER_IMAGE")
serverLocation := os.Getenv("HETZNER_SERVER_LOCATION")
serverType := os.Getenv("HETZNER_SERVER_TYPE")
// set default values for servers if none were provided
switch "" {
case serverImage:
serverImage = "docker-ce"
case serverLocation:
serverLocation = "fsn1"
case serverType:
serverType = "cax11"
}
serverOpts := hcloud.ServerCreateOpts{
Image: &hcloud.Image{Name: serverImage},
Labels: labels,
Location: &hcloud.Location{Name: serverLocation},
Name: server,
PublicNet: &hcloud.ServerCreatePublicNet{EnableIPv4: true},
SSHKeys: sshkeys,
ServerType: &hcloud.ServerType{Name: serverType},
StartAfterCreate: hcloud.Ptr(true),
UserData: userdata,
}
_, _, err = client.Server.Create(context.Background(), serverOpts)
if err != nil {
return err
}
}
return nil
}