25 Commits

Author SHA1 Message Date
41c9abbe3a drop datacenter config as it is ignored by the api anyway
All checks were successful
Pipeline was successful
2023-11-10 21:27:19 +01:00
6a63daab73 error handling in agent parsing
All checks were successful
Pipeline was successful
2023-11-10 21:14:49 +01:00
24be598758 make ip version configurable
All checks were successful
Pipeline was successful
2023-11-08 22:01:20 +01:00
be83d204dc disable ipv4 again
All checks were successful
Pipeline was successful
2023-11-08 21:36:24 +01:00
2e787818d1 fix removal condition
All checks were successful
Pipeline was successful
2023-11-08 21:32:22 +01:00
f9cebaf216 dont remove agents when agent did not yet pick up job
All checks were successful
Pipeline was successful
2023-11-08 21:13:08 +01:00
9f90962d92 made version configurable as well and remove "" from agent config
All checks were successful
Pipeline was successful
2023-11-08 21:10:47 +01:00
98ec02ea0b secret as well
All checks were successful
Pipeline was successful
2023-11-08 20:45:09 +01:00
1a03808847 grpc endpoint must not be in quotes
All checks were successful
Pipeline was successful
2023-11-08 20:43:36 +01:00
bbaa89d4c2 handle empty queue
All checks were successful
Pipeline was successful
2023-11-08 20:42:20 +01:00
089df0ff7f reenable ipv4 and added userdata test
All checks were successful
Pipeline was successful
2023-11-07 21:05:28 +01:00
9349af6ad8 allow for multiple ssh keys
All checks were successful
Pipeline was successful
2023-11-06 21:52:37 +01:00
2d03c2dcc4 derp
All checks were successful
Pipeline was successful
2023-11-06 21:28:14 +01:00
bbdcedf6de #2 match owned nodes with pending/running tasks
All checks were successful
Pipeline was successful
2023-11-06 21:13:24 +01:00
98d48f006f finally fixed userdata
All checks were successful
Pipeline was successful
2023-11-06 20:55:30 +01:00
fe3a28f84b disable ipv4 in agent
All checks were successful
Pipeline was successful
2023-11-05 20:54:41 +01:00
e9cd1521fb fix datatype in compose
All checks were successful
Pipeline was successful
2023-11-05 20:39:13 +01:00
6b6aa0ad69 Add CloudConfig Header
All checks were successful
Pipeline was successful
2023-11-05 14:06:22 +00:00
f7f7e5ffde grcp because rest is old school
All checks were successful
Pipeline was successful
2023-11-04 23:23:50 +01:00
e304781ba3 refresh node information
All checks were successful
Pipeline was successful
2023-11-04 22:37:09 +01:00
047e859efa better status overview
All checks were successful
Pipeline was successful
2023-11-04 22:08:30 +01:00
3280b21f9b fix image query
All checks were successful
Pipeline was successful
2023-11-04 21:32:38 +01:00
80686523f5 fix nil derefence panic
All checks were successful
Pipeline was successful
2023-11-04 21:13:01 +01:00
72c63f3508 derp id is a string
All checks were successful
Pipeline was successful
2023-11-04 20:44:00 +01:00
01c8dc0229 WoodpeckerProtocol is unused
All checks were successful
Pipeline was successful
2023-11-04 20:41:28 +01:00
7 changed files with 160 additions and 59 deletions

View File

@ -51,7 +51,7 @@ func main() {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"Caller": "Main", "Caller": "Main",
}).Infof("Currently owning %d Agents", len(ownedNodes)) }).Infof("Currently owning %d Agents", len(ownedNodes))
if pendingTasks { if pendingTasks > len(ownedNodes) {
server, err := hetzner.CreateNewAgent(cfg) server, err := hetzner.CreateNewAgent(cfg)
if err != nil { if err != nil {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
@ -59,6 +59,12 @@ func main() {
}).Fatal(fmt.Sprintf("Error spawning new agent: %s", err.Error())) }).Fatal(fmt.Sprintf("Error spawning new agent: %s", err.Error()))
} }
for { for {
server, err = hetzner.RefreshNodeInfo(cfg, server.ID)
if err != nil {
log.WithFields(log.Fields{
"Caller": "Main",
}).Fatal(fmt.Sprintf("Failed to start Agent: %s", err.Error()))
}
if server.Status == hcloud.ServerStatusRunning { if server.Status == hcloud.ServerStatusRunning {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"Caller": "Main", "Caller": "Main",
@ -67,7 +73,8 @@ func main() {
} }
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"Caller": "Main", "Caller": "Main",
}).Infof("Waiting for agent %s to start", server.Name) }).Infof("%s is in status %s", server.Name, server.Status)
time.Sleep(30 * time.Second)
} }
} else { } else {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
@ -79,23 +86,29 @@ func main() {
"Caller": "Main", "Caller": "Main",
}).Fatal(fmt.Sprintf("Error checking woodpecker queue: %s", err.Error())) }).Fatal(fmt.Sprintf("Error checking woodpecker queue: %s", err.Error()))
} }
if runningTasks { if (runningTasks <= len(ownedNodes) && runningTasks != 0) || pendingTasks > 0 {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"Caller": "Main", "Caller": "Main",
}).Info("Still found running tasks. No agent to be removed") }).Info("Still found running tasks. No agent to be removed")
} else { } else {
log.WithFields(log.Fields{ if len(ownedNodes) == 0 {
"Caller": "Main", log.WithFields(log.Fields{
}).Info("No tasks running. Will remove agents") "Caller": "Main",
for _, server := range ownedNodes { }).Infof("Nothing running and not owning any nodes. Recheck in %d", cfg.CheckInterval)
hetzner.DecomNode(cfg, &server) } else {
agentId, err := woodpecker.GetAgentIdByName(cfg, server.Name) log.WithFields(log.Fields{
if err != nil { "Caller": "Main",
log.WithFields(log.Fields{ }).Info("No tasks running. Will remove agents")
"Caller": "Main", for _, server := range ownedNodes {
}).Warnf("Could not find agent %s in woodpecker. Assuming it was never added", server.Name) hetzner.DecomNode(cfg, &server)
} else { agentId, err := woodpecker.GetAgentIdByName(cfg, server.Name)
woodpecker.DecomAgent(cfg, agentId) if err != nil {
log.WithFields(log.Fields{
"Caller": "Main",
}).Warnf("Could not find agent %s in woodpecker. Assuming it was never added", server.Name)
} else {
woodpecker.DecomAgent(cfg, agentId)
}
} }
} }
} }

View File

@ -14,14 +14,15 @@ type Config = struct {
DryRun bool `default:"false" env:"WOODPECKER_AUTOSCALER_DRY_RUN"` DryRun bool `default:"false" env:"WOODPECKER_AUTOSCALER_DRY_RUN"`
WoodpeckerLabelSelector string `default:"uploadfilter24.eu/instance-role=Woodpecker" env:"WOODPECKER_AUTOSCALER_WOODPECKER_LABEL_SELECTOR"` WoodpeckerLabelSelector string `default:"uploadfilter24.eu/instance-role=Woodpecker" env:"WOODPECKER_AUTOSCALER_WOODPECKER_LABEL_SELECTOR"`
WoodpeckerInstance string `default:"" env:"WOODPECKER_AUTOSCALER_WOODPECKER_INSTANCE"` WoodpeckerInstance string `default:"" env:"WOODPECKER_AUTOSCALER_WOODPECKER_INSTANCE"`
WoodpeckerGrpc string `default:"" env:"WOODPECKER_AUTOSCALER_WOODPECKER_GRPC"`
WoodpeckerAgentSecret string `default:"" env:"WOODPECKER_AUTOSCALER_WOODPECKER_AGENT_SECRET"` WoodpeckerAgentSecret string `default:"" env:"WOODPECKER_AUTOSCALER_WOODPECKER_AGENT_SECRET"`
WoodpeckerApiToken string `default:"" env:"WOODPECKER_AUTOSCALER_WOODPECKER_API_TOKEN"` WoodpeckerApiToken string `default:"" env:"WOODPECKER_AUTOSCALER_WOODPECKER_API_TOKEN"`
WoodpeckerProtocol string `default:"http" env:"WOODPECKER_AUTOSCALER_WOODPECKER_PROTOCOL"` WoodpeckerAgentVersion string `default:"latest" env:"WOODPECKER_AUTOSCALER_WOODPECKER_AGENT_VERSION"`
HcloudToken string `default:"" env:"WOODPECKER_AUTOSCALER_HCLOUD_TOKEN"` HcloudToken string `default:"" env:"WOODPECKER_AUTOSCALER_HCLOUD_TOKEN"`
HcloudInstanceType string `default:"cpx21" env:"WOODPECKER_AUTOSCALER_HCLOUD_INSTANCE_TYPE"` HcloudInstanceType string `default:"cpx21" env:"WOODPECKER_AUTOSCALER_HCLOUD_INSTANCE_TYPE"`
HcloudRegion string `default:"" env:"WOODPECKER_AUTOSCALER_HCLOUD_REGION"` HcloudLocation string `default:"" env:"WOODPECKER_AUTOSCALER_HCLOUD_LOCATION"`
HcloudDatacenter string `default:"" env:"WOODPECKER_AUTOSCALER_HCLOUD_DATACENTER"` HcloudSSHKeys string `default:"" env:"WOODPECKER_AUTOSCALER_HCLOUD_SSH_KEYS"`
HcloudSSHKey string `default:"" env:"WOODPECKER_AUTOSCALER_HCLOUD_SSH_KEY"` HcloudIPv6Only bool `default:"false" env:"WOODPECKER_AUTOSCALER_HCLOUD_IPV6_ONLY"`
} }
func GenConfig() (cfg *Config, err error) { func GenConfig() (cfg *Config, err error) {

View File

@ -5,6 +5,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strings"
"text/template" "text/template"
"git.uploadfilter24.eu/covidnetes/woodpecker-autoscaler/internal/config" "git.uploadfilter24.eu/covidnetes/woodpecker-autoscaler/internal/config"
@ -15,13 +16,12 @@ import (
) )
var USER_DATA_TEMPLATE = ` var USER_DATA_TEMPLATE = `
#cloud-config
write_files: write_files:
- content: | - content: |
# docker-compose.yml # docker-compose.yml
version: '3' version: '3'
services: services:
woodpecker-agent: woodpecker-agent:
image: {{ .Image }} image: {{ .Image }}
command: agent command: agent
@ -30,7 +30,7 @@ write_files:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
environment: environment:
{{- range $key, $val := .EnvConfig }} {{- range $key, $val := .EnvConfig }}
- {{ $key }}: {{ $val }} - {{ $key }}={{ $val }}
{{- end }} {{- end }}
path: /root/docker-compose.yml path: /root/docker-compose.yml
runcmd: runcmd:
@ -39,17 +39,19 @@ runcmd:
type UserDataConfig struct { type UserDataConfig struct {
Image string Image string
EnvConfig map[string]string EnvConfig map[string]interface{}
} }
func generateConfig(cfg *config.Config, name string) (string, error) { func generateConfig(cfg *config.Config, name string) (string, error) {
envConfig := map[string]string{} envConfig := map[string]interface{}{
envConfig["WOODPECKER_SERVER"] = cfg.WoodpeckerInstance "WOODPECKER_SERVER": fmt.Sprintf("%s", cfg.WoodpeckerGrpc),
envConfig["WOODPECKER_AGENT_SECRET"] = cfg.WoodpeckerAgentSecret "WOODPECKER_GRPC_SECURE": true,
envConfig["WOODPECKER_FILTER_LABELS"] = cfg.WoodpeckerLabelSelector "WOODPECKER_AGENT_SECRET": fmt.Sprintf("%s", cfg.WoodpeckerAgentSecret),
envConfig["WOODPECKER_HOSTNAME"] = name "WOODPECKER_FILTER_LABELS": fmt.Sprintf("%s", cfg.WoodpeckerLabelSelector),
"WOODPECKER_HOSTNAME": fmt.Sprintf("%s", name),
}
config := UserDataConfig{ config := UserDataConfig{
Image: "woodpeckerci/woodpecker-agent:latest", Image: fmt.Sprintf("woodpeckerci/woodpecker-agent:%s", cfg.WoodpeckerAgentVersion),
EnvConfig: envConfig, EnvConfig: envConfig,
} }
tmpl, err := template.New("userdata").Parse(USER_DATA_TEMPLATE) tmpl, err := template.New("userdata").Parse(USER_DATA_TEMPLATE)
@ -68,31 +70,48 @@ func CreateNewAgent(cfg *config.Config) (*hcloud.Server, error) {
client := hcloud.NewClient(hcloud.WithToken(cfg.HcloudToken)) client := hcloud.NewClient(hcloud.WithToken(cfg.HcloudToken))
name := fmt.Sprintf("woodpecker-autoscaler-agent-%s", utils.RandStringBytes(5)) name := fmt.Sprintf("woodpecker-autoscaler-agent-%s", utils.RandStringBytes(5))
userdata, err := generateConfig(cfg, name) userdata, err := generateConfig(cfg, name)
img, _, err := client.Image.GetByNameAndArchitecture(context.Background(), "docker-ce", "amd64") keys := []*hcloud.SSHKey{}
loc, _, err := client.Location.GetByName(context.Background(), cfg.HcloudRegion) for _, keyName := range strings.Split(cfg.HcloudSSHKeys, ",") {
key, _, err := client.SSHKey.GetByName(context.Background(), keyName)
if err != nil {
log.WithFields(log.Fields{
"Caller": "CreateNewAgent",
}).Warnf("Failed to look up ssh key %s: %s", keyName, err.Error())
continue
}
keys = append(keys, key)
}
img, _, err := client.Image.GetByNameAndArchitecture(context.Background(), "docker-ce", "x86")
utils.CheckError(err, "GetImageByNameAndArchitecture")
loc, _, err := client.Location.GetByName(context.Background(), cfg.HcloudLocation)
utils.CheckError(err, "GetRegionByName")
pln, _, err := client.ServerType.GetByName(context.Background(), cfg.HcloudInstanceType) pln, _, err := client.ServerType.GetByName(context.Background(), cfg.HcloudInstanceType)
key, _, err := client.SSHKey.GetByName(context.Background(), cfg.HcloudSSHKey) utils.CheckError(err, "GetServerTypeByName")
dc, _, err := client.Datacenter.GetByName(context.Background(), cfg.HcloudDatacenter)
labels := map[string]string{} labels := map[string]string{}
labels["Role"] = "WoodpeckerAgent" labels["Role"] = "WoodpeckerAgent"
labels["ControledBy"] = "WoodpeckerAutoscaler" labels["ControledBy"] = "WoodpeckerAutoscaler"
if err != nil { networkConf := hcloud.ServerCreatePublicNet{
return nil, errors.New(fmt.Sprintf("Could not create new Agent: %s", err.Error())) EnableIPv4: !cfg.HcloudIPv6Only,
EnableIPv6: true,
} }
res, _, err := client.Server.Create(context.Background(), hcloud.ServerCreateOpts{ res, _, err := client.Server.Create(context.Background(), hcloud.ServerCreateOpts{
Name: name, Name: name,
ServerType: pln, ServerType: pln,
Image: img, Image: img,
SSHKeys: []*hcloud.SSHKey{key}, SSHKeys: keys,
Location: loc, Location: loc,
Datacenter: dc,
UserData: userdata, UserData: userdata,
StartAfterCreate: utils.BoolPointer(true), StartAfterCreate: utils.BoolPointer(true),
Labels: labels, Labels: labels,
PublicNet: &networkConf,
}) })
if err != nil {
return nil, errors.New(fmt.Sprintf("Could not create new Agent: %s", err.Error()))
}
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"Caller": "CreateNewAgent", "Caller": "CreateNewAgent",
}).Infof("Created new Build Agent %s", res.Server.Name) }).Infof("Created new Build Agent %s", res.Server.Name)
@ -130,3 +149,12 @@ func DecomNode(cfg *config.Config, server *hcloud.Server) error {
} }
return nil return nil
} }
func RefreshNodeInfo(cfg *config.Config, serverID int) (*hcloud.Server, error) {
client := hcloud.NewClient(hcloud.WithToken(cfg.HcloudToken))
server, _, err := client.Server.GetByID(context.Background(), serverID)
if err != nil {
return nil, errors.New(fmt.Sprintf("Could not refresh server info: %s", err.Error()))
}
return server, nil
}

View File

@ -0,0 +1,55 @@
package hetzner
import (
"testing"
"git.uploadfilter24.eu/covidnetes/woodpecker-autoscaler/internal/config"
)
func TestGenerateUserData(t *testing.T) {
cfg := config.Config{
LogLevel: "Info",
CheckInterval: 5,
DryRun: false,
WoodpeckerLabelSelector: "uploadfilter24.eu/instance-role=WoodpeckerTest",
WoodpeckerInstance: "http://woodpecker.test.tld",
WoodpeckerGrpc: "grpc-test.woodpecker.test.tld:443",
WoodpeckerAgentSecret: "Geheim1!",
WoodpeckerApiToken: "VeryGeheim1!",
WoodpeckerAgentVersion: "latest",
HcloudToken: "EvenMoreGeheim1!",
HcloudInstanceType: "cpx21",
HcloudLocation: "fsn1",
HcloudSSHKeys: "test-key",
}
wanted := `
#cloud-config
write_files:
- content: |
# docker-compose.yml
version: '3'
services:
woodpecker-agent:
image: woodpeckerci/woodpecker-agent:latest
command: agent
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WOODPECKER_AGENT_SECRET=Geheim1!
- WOODPECKER_FILTER_LABELS=uploadfilter24.eu/instance-role=WoodpeckerTest
- WOODPECKER_GRPC_SECURE=true
- WOODPECKER_HOSTNAME=test-instance
- WOODPECKER_SERVER=grpc-test.woodpecker.test.tld:443
path: /root/docker-compose.yml
runcmd:
- [ sh, -xc, "cd /root; docker run --rm --privileged multiarch/qemu-user-static --reset -p yes; docker compose up -d" ]
`
got, err := generateConfig(&cfg, "test-instance")
if err != nil {
t.Errorf("Error in generating Config: %v", err)
}
if wanted != got {
t.Errorf("got:\n%v\n, wanted:\n%v", got, wanted)
}
}

View File

@ -30,7 +30,7 @@ package models
*/ */
type JobInformation struct { type JobInformation struct {
ID int `json:"id"` ID string `json:"id"`
Data string `json:"data"` Data string `json:"data"`
Labels map[string]string `json:"labels"` Labels map[string]string `json:"labels"`
Dependencies string `json:"dependencies,omitempty"` Dependencies string `json:"dependencies,omitempty"`

View File

@ -1,6 +1,10 @@
package utils package utils
import "math/rand" import (
"math/rand"
log "github.com/sirupsen/logrus"
)
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
@ -15,3 +19,11 @@ func RandStringBytes(n int) string {
func BoolPointer(b bool) *bool { func BoolPointer(b bool) *bool {
return &b return &b
} }
func CheckError(err error, caller string) {
if err != nil {
log.WithFields(log.Fields{
"Caller": caller,
}).Warnf("Error from hetzner API: %s", err.Error())
}
}

View File

@ -35,56 +35,48 @@ func QueueInfo(cfg *config.Config, target interface{}) error {
return json.NewDecoder(resp.Body).Decode(target) return json.NewDecoder(resp.Body).Decode(target)
} }
func CheckPending(cfg *config.Config) (bool, error) { func CheckPending(cfg *config.Config) (int, error) {
expectedKV := strings.Split(cfg.WoodpeckerLabelSelector, "=") expectedKV := strings.Split(cfg.WoodpeckerLabelSelector, "=")
queueInfo := new(models.QueueInfo) queueInfo := new(models.QueueInfo)
err := QueueInfo(cfg, queueInfo) err := QueueInfo(cfg, queueInfo)
if err != nil { if err != nil {
return false, errors.New(fmt.Sprintf("Error from QueueInfo: %s", err.Error())) return 0, errors.New(fmt.Sprintf("Error from QueueInfo: %s", err.Error()))
} }
count := 0
if queueInfo.Stats.PendingCount > 0 { if queueInfo.Stats.PendingCount > 0 {
if queueInfo.Pending != nil { if queueInfo.Pending != nil {
for _, pendingJobs := range queueInfo.Pending { for _, pendingJobs := range queueInfo.Pending {
val, exists := pendingJobs.Labels[expectedKV[0]] val, exists := pendingJobs.Labels[expectedKV[0]]
if exists && val == expectedKV[1] { if exists && val == expectedKV[1] {
count++
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"Caller": "CheckPending", "Caller": "CheckPending",
}).Info("Found pending job for us") }).Debugf("Currently serving %d Jobs", count)
return true, nil
} else {
log.WithFields(log.Fields{
"Caller": "CheckPending",
}).Info("No Jobs for us in Queue")
return false, nil
} }
} }
} }
} }
return false, nil return count, nil
} }
func CheckRunning(cfg *config.Config) (bool, error) { func CheckRunning(cfg *config.Config) (int, error) {
expectedKV := strings.Split(cfg.WoodpeckerLabelSelector, "=") expectedKV := strings.Split(cfg.WoodpeckerLabelSelector, "=")
queueInfo := new(models.QueueInfo) queueInfo := new(models.QueueInfo)
err := QueueInfo(cfg, queueInfo) err := QueueInfo(cfg, queueInfo)
if err != nil { if err != nil {
return false, errors.New(fmt.Sprintf("Error from QueueInfo: %s", err.Error())) return 0, errors.New(fmt.Sprintf("Error from QueueInfo: %s", err.Error()))
} }
count := 0
if queueInfo.Stats.RunningCount > 0 { if queueInfo.Stats.RunningCount > 0 {
for _, runningJobs := range queueInfo.Running { for _, runningJobs := range queueInfo.Running {
val, exists := runningJobs.Labels[expectedKV[0]] val, exists := runningJobs.Labels[expectedKV[0]]
if exists && val == expectedKV[1] { if exists && val == expectedKV[1] {
count++
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"Caller": "CheckRunning", "Caller": "CheckRunning",
}).Info("Found running job for us") }).Debugf("Currently serving %d Jobs", count)
return true, nil
} else {
log.WithFields(log.Fields{
"Caller": "CheckRunning",
}).Info("No running job for us")
return false, nil
} }
} }
} }
return false, nil return count, nil
} }