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 }