Compare commits

..

No commits in common. "main" and "v0.0.1" have entirely different histories.
main ... v0.0.1

23 changed files with 191 additions and 443 deletions

View File

@ -2,18 +2,15 @@ when:
- event: push - event: push
branch: main branch: main
labels:
uploadfilter24.eu/instance-role: Woodpecker
steps: steps:
test: test:
image: golang:1.22 image: golang:1.21
commands: commands:
- go test ./... - go test ./...
pre-release: pre-release:
image: woodpeckerci/plugin-docker-buildx image: woodpeckerci/plugin-docker-buildx
settings: settings:
platforms: linux/arm64/v8,linux/amd64 platforms: linux/arm64/v8
repo: lerentis/woodpecker-autoscaler repo: lerentis/woodpecker-autoscaler
tags: tags:
- latest - latest

View File

@ -1,18 +1,15 @@
when: when:
- event: pull_request - event: pull_request
labels:
uploadfilter24.eu/instance-role: Woodpecker
steps: steps:
test: test:
image: golang:1.22 image: golang:1.21
commands: commands:
- go test ./... - go test ./...
pr-build: pr-build:
image: woodpeckerci/plugin-docker-buildx image: woodpeckerci/plugin-docker-buildx
settings: settings:
platforms: linux/arm64/v8,linux/amd64 platforms: linux/arm64/v8
repo: lerentis/woodpecker-autoscaler repo: lerentis/woodpecker-autoscaler
tags: tags:
- latest - latest

View File

@ -1,18 +1,15 @@
when: when:
- event: tag - event: tag
labels:
uploadfilter24.eu/instance-role: Woodpecker
steps: steps:
test: test:
image: golang:1.22 image: golang:1.21
commands: commands:
- go test ./... - go test ./...
release: release:
image: woodpeckerci/plugin-docker-buildx image: woodpeckerci/plugin-docker-buildx
settings: settings:
platforms: linux/arm64/v8,linux/amd64 platforms: linux/arm64/v8
repo: lerentis/woodpecker-autoscaler repo: lerentis/woodpecker-autoscaler
tags: tags:
- latest - latest

View File

@ -1,19 +0,0 @@
CHANGELOG
v1.1.0
Updated Dependencies
Restructured Main event loop
Cost optimized mode to make use of the fully hour that is billed by hetzner
v1.0.1
Fix woodpecker agent decom
v1.0.0
First stable release
v0.0.1
First test release

View File

@ -1,4 +1,4 @@
FROM golang:1.22 as build FROM golang:1.21 as build
WORKDIR /app WORKDIR /app

View File

@ -28,12 +28,12 @@ env:
value: "define_it" value: "define_it"
- name: WOODPECKER_AUTOSCALER_HCLOUD_INSTANCE_TYPE - name: WOODPECKER_AUTOSCALER_HCLOUD_INSTANCE_TYPE
value: "cpx21" value: "cpx21"
- name: WOODPECKER_AUTOSCALER_HCLOUD_LOCATION - name: WOODPECKER_AUTOSCALER_HCLOUD_REGION
value: "define_it"
- name: WOODPECKER_AUTOSCALER_HCLOUD_DATACENTER
value: "define_it" value: "define_it"
- name: WOODPECKER_AUTOSCALER_HCLOUD_SSH_KEY - name: WOODPECKER_AUTOSCALER_HCLOUD_SSH_KEY
value: "define_it" value: "define_it"
- name: WOODPECKER_AUTOSCALER_COST_OPTIMIZED
value: "true"
``` ```
you can also create a secret manually with these information and reference the existing secret like this in the `values.yaml`: you can also create a secret manually with these information and reference the existing secret like this in the `values.yaml`:
@ -79,9 +79,9 @@ WOODPECKER_AUTOSCALER_WOODPECKER_AGENT_SECRET="define_it"
WOODPECKER_AUTOSCALER_WOODPECKER_API_TOKEN="define_it" WOODPECKER_AUTOSCALER_WOODPECKER_API_TOKEN="define_it"
WOODPECKER_AUTOSCALER_HCLOUD_TOKEN="define_it" WOODPECKER_AUTOSCALER_HCLOUD_TOKEN="define_it"
WOODPECKER_AUTOSCALER_HCLOUD_INSTANCE_TYPE=cpx21 WOODPECKER_AUTOSCALER_HCLOUD_INSTANCE_TYPE=cpx21
WOODPECKER_AUTOSCALER_HCLOUD_LOCATION="define_it" WOODPECKER_AUTOSCALER_HCLOUD_REGION="define_it"
WOODPECKER_AUTOSCALER_HCLOUD_DATACENTER="define_it"
WOODPECKER_AUTOSCALER_HCLOUD_SSH_KEY="define_it" WOODPECKER_AUTOSCALER_HCLOUD_SSH_KEY="define_it"
WOODPECKER_AUTOSCALER_COST_OPTIMIZED="true"
``` ```
Now reload the systemd daemons and start the service: Now reload the systemd daemons and start the service:

View File

@ -53,7 +53,9 @@ env:
value: "define_it" value: "define_it"
- name: WOODPECKER_AUTOSCALER_HCLOUD_INSTANCE_TYPE - name: WOODPECKER_AUTOSCALER_HCLOUD_INSTANCE_TYPE
value: "cpx21" value: "cpx21"
- name: WOODPECKER_AUTOSCALER_HCLOUD_LOCATION - name: WOODPECKER_AUTOSCALER_HCLOUD_REGION
value: "define_it"
- name: WOODPECKER_AUTOSCALER_HCLOUD_DATACENTER
value: "define_it" value: "define_it"
- name: WOODPECKER_AUTOSCALER_HCLOUD_SSH_KEY - name: WOODPECKER_AUTOSCALER_HCLOUD_SSH_KEY
value: "define_it" value: "define_it"

View File

@ -13,106 +13,6 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
func SpawnNewAgent(cfg *config.Config) {
agent, err := woodpecker.CreateWoodpeckerAgent(cfg)
if err != nil {
log.WithFields(log.Fields{
"Caller": "SpawnNewAgent",
}).Fatal(fmt.Sprintf("Error creating new agent: %s", err.Error()))
}
server, err := hetzner.CreateNewAgent(cfg, agent)
if err != nil {
log.WithFields(log.Fields{
"Caller": "SpawnNewAgent",
}).Fatal(fmt.Sprintf("Error spawning new agent: %s", err.Error()))
}
for {
server, err = hetzner.RefreshNodeInfo(cfg, server.ID)
if err != nil {
log.WithFields(log.Fields{
"Caller": "SpawnNewAgent",
}).Fatal(fmt.Sprintf("Failed to start Agent: %s", err.Error()))
}
if server.Status == hcloud.ServerStatusRunning {
log.WithFields(log.Fields{
"Caller": "SpawnNewAgent",
}).Infof("%s started!", server.Name)
break
}
log.WithFields(log.Fields{
"Caller": "SpawnNewAgent",
}).Infof("%s is in status %s", server.Name, server.Status)
time.Sleep(30 * time.Second)
}
}
func CheckJobs(cfg *config.Config, ownedNodes []hcloud.Server, pendingTasks int) {
log.WithFields(log.Fields{
"Caller": "CheckJobs",
}).Info("Checking if agents can be removed")
runningTasks, err := woodpecker.CheckRunning(cfg)
if err != nil {
log.WithFields(log.Fields{
"Caller": "CheckJobs",
}).Fatal(fmt.Sprintf("Error checking woodpecker queue: %s", err.Error()))
}
if (runningTasks <= len(ownedNodes) && runningTasks != 0) || pendingTasks > 0 {
log.WithFields(log.Fields{
"Caller": "CheckJobs",
}).Info("Still found running tasks. No agent to be removed")
} else {
if len(ownedNodes) == 0 {
log.WithFields(log.Fields{
"Caller": "CheckJobs",
}).Info("Nothing running and not owning any nodes")
} else {
log.WithFields(log.Fields{
"Caller": "CheckJobs",
}).Info("No tasks running. Will remove agents")
Decom(cfg, ownedNodes)
}
}
}
func Decom(cfg *config.Config, ownedNodes []hcloud.Server) {
for _, server := range ownedNodes {
if cfg.CostOptimizedMode {
runtime, err := hetzner.CheckRuntime(cfg, &server)
if err != nil {
log.WithFields(log.Fields{
"Caller": "Decom",
}).Warnf("Error while checking runtime of node %s: %s", server.Name, err.Error())
}
log.WithFields(log.Fields{
"Caller": "Decom",
}).Debugf("Node %s is running for %d", server.Name, runtime.Minute())
// Check if next check if sooner than the 60 Minute mark of the next hetzner check
// https://docs.hetzner.com/cloud/billing/faq/#how-do-you-bill-your-servers
if time.Duration(runtime.Add(time.Duration(cfg.CheckInterval)*time.Minute).Minute()) < (60 * time.Minute) {
log.WithFields(log.Fields{
"Caller": "Decom",
}).Infof("Skipping node termination of %s (running for %d Minutes) in Cost Optimized Mode", server.Name, runtime.Minute())
continue
}
}
agentId, err := hetzner.DecomNode(cfg, &server)
if err != nil {
log.WithFields(log.Fields{
"Caller": "Decom",
}).Warnf("Error while deleting node %s: %s", server.Name, err.Error())
}
err = woodpecker.DecomAgent(cfg, agentId)
if err != nil {
log.WithFields(log.Fields{
"Caller": "Decom",
}).Warnf("Could not delete node %s in woodpecker: %s", server.Name, err.Error())
}
log.WithFields(log.Fields{
"Caller": "Decom",
}).Infof("Deleted node %s", server.Name)
}
}
func main() { func main() {
cfg, err := config.GenConfig() cfg, err := config.GenConfig()
@ -151,14 +51,55 @@ 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 > len(ownedNodes) { if pendingTasks {
SpawnNewAgent(cfg) server, err := hetzner.CreateNewAgent(cfg)
} else { if err != nil {
CheckJobs(cfg, ownedNodes, pendingTasks) log.WithFields(log.Fields{
"Caller": "Main",
}).Fatal(fmt.Sprintf("Error spawning new agent: %s", err.Error()))
}
for {
if server.Status == hcloud.ServerStatusRunning {
log.WithFields(log.Fields{
"Caller": "Main",
}).Infof("%s started!", server.Name)
break
} }
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"Caller": "Main", "Caller": "Main",
}).Infof("Recheck in %d", cfg.CheckInterval) }).Infof("Waiting for agent %s to start", server.Name)
}
} else {
log.WithFields(log.Fields{
"Caller": "Main",
}).Info("Checking if agents can be removed")
runningTasks, err := woodpecker.CheckRunning(cfg)
if err != nil {
log.WithFields(log.Fields{
"Caller": "Main",
}).Fatal(fmt.Sprintf("Error checking woodpecker queue: %s", err.Error()))
}
if runningTasks {
log.WithFields(log.Fields{
"Caller": "Main",
}).Info("Still found running tasks. No agent to be removed")
} else {
log.WithFields(log.Fields{
"Caller": "Main",
}).Info("No tasks running. Will remove agents")
for _, server := range ownedNodes {
hetzner.DecomNode(cfg, &server)
agentId, err := woodpecker.GetAgentIdByName(cfg, server.Name)
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)
}
}
}
}
time.Sleep(time.Duration(cfg.CheckInterval) * time.Minute) time.Sleep(time.Duration(cfg.CheckInterval) * time.Minute)
} }
} }

34
go.mod
View File

@ -1,25 +1,29 @@
module git.uploadfilter24.eu/covidnetes/woodpecker-autoscaler module git.uploadfilter24.eu/covidnetes/woodpecker-autoscaler
go 1.22 go 1.21.1
require ( require (
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.0
github.com/hetznercloud/hcloud-go v1.56.0 github.com/hetznercloud/hcloud-go v1.52.0
github.com/jinzhu/configor v1.2.2 github.com/jinzhu/configor v1.2.1
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
) )
require ( require (
github.com/BurntSushi/toml v1.4.0 // indirect github.com/BurntSushi/toml v0.3.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/prometheus/client_model v0.6.1 // indirect github.com/kr/text v0.2.0 // indirect
github.com/prometheus/common v0.54.0 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/client_golang v1.16.0 // indirect
golang.org/x/net v0.26.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect
golang.org/x/sys v0.21.0 // indirect github.com/prometheus/common v0.42.0 // indirect
golang.org/x/text v0.16.0 // indirect github.com/prometheus/procfs v0.10.1 // indirect
google.golang.org/protobuf v1.34.1 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect golang.org/x/net v0.12.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
) )

77
go.sum
View File

@ -1,55 +1,70 @@
github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/hetznercloud/hcloud-go v1.56.0 h1:Swf7cFQAae5r+76Fom9JVPpWqMIs49UxIOROLjBE1vc= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/hetznercloud/hcloud-go v1.56.0/go.mod h1:oTebZCjd+osj75jlI76Z+zjN1sTxmMiQ1MWoO8aRl1c= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/jinzhu/configor v1.2.2 h1:sLgh6KMzpCmaQB4e+9Fu/29VErtBUqsS2t8C9BNIVsA= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/jinzhu/configor v1.2.2/go.mod h1:iFFSfOBKP3kC2Dku0ZGB3t3aulfQgTGJknodhFavsU8= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/hetznercloud/hcloud-go v1.52.0 h1:3r9pEulTOBB9BoArSgpQYUQVTy+Xjkg0k/QAU4c6dQ8=
github.com/hetznercloud/hcloud-go v1.52.0/go.mod h1:VzDWThl47lOnZXY0q5/LPFD+M62pfe/52TV+mOrpp9Q=
github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko=
github.com/jinzhu/configor v1.2.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8= github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ= github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -12,18 +12,16 @@ type Config = struct {
LogLevel string `default:"Info" env:"WOODPECKER_AUTOSCALER_LOGLEVEL"` LogLevel string `default:"Info" env:"WOODPECKER_AUTOSCALER_LOGLEVEL"`
CheckInterval int `default:"15" env:"WOODPECKER_AUTOSCALER_CHECK_INTERVAL"` CheckInterval int `default:"15" env:"WOODPECKER_AUTOSCALER_CHECK_INTERVAL"`
DryRun bool `default:"false" env:"WOODPECKER_AUTOSCALER_DRY_RUN"` DryRun bool `default:"false" env:"WOODPECKER_AUTOSCALER_DRY_RUN"`
CostOptimizedMode bool `default:"false" env:"WOODPECKER_AUTOSCALER_COST_OPTIMIZED"`
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"`
WoodpeckerAgentVersion string `default:"latest" env:"WOODPECKER_AUTOSCALER_WOODPECKER_AGENT_VERSION"` WoodpeckerProtocol string `default:"http" env:"WOODPECKER_AUTOSCALER_WOODPECKER_PROTOCOL"`
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"`
HcloudLocation string `default:"" env:"WOODPECKER_AUTOSCALER_HCLOUD_LOCATION"` HcloudRegion string `default:"" env:"WOODPECKER_AUTOSCALER_HCLOUD_REGION"`
HcloudSSHKeys string `default:"" env:"WOODPECKER_AUTOSCALER_HCLOUD_SSH_KEYS"` HcloudDatacenter string `default:"" env:"WOODPECKER_AUTOSCALER_HCLOUD_DATACENTER"`
HcloudIPv6Only bool `default:"false" env:"WOODPECKER_AUTOSCALER_HCLOUD_IPV6_ONLY"` HcloudSSHKey string `default:"" env:"WOODPECKER_AUTOSCALER_HCLOUD_SSH_KEY"`
} }
func GenConfig() (cfg *Config, err error) { func GenConfig() (cfg *Config, err error) {

View File

@ -5,13 +5,9 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strconv"
"strings"
"text/template" "text/template"
"time"
"git.uploadfilter24.eu/covidnetes/woodpecker-autoscaler/internal/config" "git.uploadfilter24.eu/covidnetes/woodpecker-autoscaler/internal/config"
"git.uploadfilter24.eu/covidnetes/woodpecker-autoscaler/internal/models"
"git.uploadfilter24.eu/covidnetes/woodpecker-autoscaler/internal/utils" "git.uploadfilter24.eu/covidnetes/woodpecker-autoscaler/internal/utils"
"github.com/hetznercloud/hcloud-go/hcloud" "github.com/hetznercloud/hcloud-go/hcloud"
@ -19,12 +15,13 @@ 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
@ -33,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:
@ -42,20 +39,17 @@ runcmd:
type UserDataConfig struct { type UserDataConfig struct {
Image string Image string
EnvConfig map[string]interface{} EnvConfig map[string]string
} }
func generateConfig(cfg *config.Config, name string, agentToken string) (string, error) { func generateConfig(cfg *config.Config, name string) (string, error) {
envConfig := map[string]interface{}{ envConfig := map[string]string{}
"WOODPECKER_SERVER": fmt.Sprintf("%s", cfg.WoodpeckerGrpc), envConfig["WOODPECKER_SERVER"] = cfg.WoodpeckerInstance
"WOODPECKER_GRPC_SECURE": true, envConfig["WOODPECKER_AGENT_SECRET"] = cfg.WoodpeckerAgentSecret
"WOODPECKER_AGENT_SECRET": fmt.Sprintf("%s", agentToken), envConfig["WOODPECKER_FILTER_LABELS"] = cfg.WoodpeckerLabelSelector
"WOODPECKER_FILTER_LABELS": fmt.Sprintf("%s", cfg.WoodpeckerLabelSelector), envConfig["WOODPECKER_HOSTNAME"] = name
"WOODPECKER_HOSTNAME": fmt.Sprintf("%s", name),
"WOODPECKER_MAX_WORKFLOWS": 4,
}
config := UserDataConfig{ config := UserDataConfig{
Image: fmt.Sprintf("woodpeckerci/woodpecker-agent:%s", cfg.WoodpeckerAgentVersion), Image: "woodpeckerci/woodpecker-agent:latest",
EnvConfig: envConfig, EnvConfig: envConfig,
} }
tmpl, err := template.New("userdata").Parse(USER_DATA_TEMPLATE) tmpl, err := template.New("userdata").Parse(USER_DATA_TEMPLATE)
@ -70,52 +64,35 @@ func generateConfig(cfg *config.Config, name string, agentToken string) (string,
return buf.String(), nil return buf.String(), nil
} }
func CreateNewAgent(cfg *config.Config, woodpeckerAgent *models.Agent) (*hcloud.Server, error) { func CreateNewAgent(cfg *config.Config) (*hcloud.Server, error) {
client := hcloud.NewClient(hcloud.WithToken(cfg.HcloudToken)) client := hcloud.NewClient(hcloud.WithToken(cfg.HcloudToken))
userdata, err := generateConfig(cfg, woodpeckerAgent.Name, woodpeckerAgent.Token) name := fmt.Sprintf("woodpecker-autoscaler-agent-%s", utils.RandStringBytes(5))
keys := []*hcloud.SSHKey{} userdata, err := generateConfig(cfg, name)
for _, keyName := range strings.Split(cfg.HcloudSSHKeys, ",") { img, _, err := client.Image.GetByNameAndArchitecture(context.Background(), "docker-ce", "amd64")
key, _, err := client.SSHKey.GetByName(context.Background(), keyName) loc, _, err := client.Location.GetByName(context.Background(), cfg.HcloudRegion)
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)
utils.CheckError(err, "GetServerTypeByName") key, _, err := client.SSHKey.GetByName(context.Background(), cfg.HcloudSSHKey)
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"
labels["ID"] = fmt.Sprintf("%d", woodpeckerAgent.ID)
networkConf := hcloud.ServerCreatePublicNet{
EnableIPv4: !cfg.HcloudIPv6Only,
EnableIPv6: true,
}
res, _, err := client.Server.Create(context.Background(), hcloud.ServerCreateOpts{
Name: woodpeckerAgent.Name,
ServerType: pln,
Image: img,
SSHKeys: keys,
Location: loc,
UserData: userdata,
StartAfterCreate: utils.BoolPointer(true),
Labels: labels,
PublicNet: &networkConf,
})
if err != nil { if err != nil {
return nil, errors.New(fmt.Sprintf("Could not create new Agent: %s", err.Error())) return nil, errors.New(fmt.Sprintf("Could not create new Agent: %s", err.Error()))
} }
res, _, err := client.Server.Create(context.Background(), hcloud.ServerCreateOpts{
Name: name,
ServerType: pln,
Image: img,
SSHKeys: []*hcloud.SSHKey{key},
Location: loc,
Datacenter: dc,
UserData: userdata,
StartAfterCreate: utils.BoolPointer(true),
Labels: labels,
})
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)
@ -142,44 +119,14 @@ func ListAgents(cfg *config.Config) ([]hcloud.Server, error) {
return myServers, nil return myServers, nil
} }
func DecomNode(cfg *config.Config, server *hcloud.Server) (int64, error) { func DecomNode(cfg *config.Config, server *hcloud.Server) error {
client := hcloud.NewClient(hcloud.WithToken(cfg.HcloudToken)) client := hcloud.NewClient(hcloud.WithToken(cfg.HcloudToken))
var woodpeckerAgentID int64
val, exists := server.Labels["ID"]
if exists {
log.WithFields(log.Fields{
"Caller": "DecomNode",
}).Debugf("Found woodpecker agent id: %s", val)
woodpeckerAgentID, _ = strconv.ParseInt(val, 10, 64)
} else {
log.WithFields(log.Fields{
"Caller": "DecomNode",
}).Warnf("Did not find woodpecker agent id for node %s", server.Name)
}
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"Caller": "DecomNode", "Caller": "DecomNode",
}).Debugf("Deleting %s node", server.Name) }).Debugf("Deleting %s node", server.Name)
_, _, err := client.Server.DeleteWithResult(context.Background(), server) _, _, err := client.Server.DeleteWithResult(context.Background(), server)
if err != nil { if err != nil {
return woodpeckerAgentID, errors.New(fmt.Sprintf("Could not delete Agent: %s", err.Error())) return errors.New(fmt.Sprintf("Could not delete Agent: %s", err.Error()))
} }
return woodpeckerAgentID, 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
}
func CheckRuntime(cfg *config.Config, server *hcloud.Server) (time.Time, error) {
server, err := RefreshNodeInfo(cfg, server.ID)
now := time.Now()
if err != nil {
return time.Time{}, errors.New(fmt.Sprintf("Could not check Runtime: %s", err.Error()))
}
return server.Created.Add(time.Duration(now.Minute())), nil
} }

View File

@ -1,56 +0,0 @@
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_MAX_WORKFLOWS=4
- 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", "Geheim1!")
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 string `json:"id"` ID int `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"`
@ -90,8 +90,3 @@ type Agent struct {
type AgentList struct { type AgentList struct {
Agents []Agent Agents []Agent
} }
type AgentRequest struct {
Name string `json:"name"`
NoSchedule bool `json:"no_schedule"`
}

View File

@ -1,10 +1,6 @@
package utils package utils
import ( import "math/rand"
"math/rand"
log "github.com/sirupsen/logrus"
)
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
@ -19,11 +15,3 @@ 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

@ -1,7 +1,6 @@
package woodpecker package woodpecker
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -9,12 +8,11 @@ import (
"git.uploadfilter24.eu/covidnetes/woodpecker-autoscaler/internal/config" "git.uploadfilter24.eu/covidnetes/woodpecker-autoscaler/internal/config"
"git.uploadfilter24.eu/covidnetes/woodpecker-autoscaler/internal/models" "git.uploadfilter24.eu/covidnetes/woodpecker-autoscaler/internal/models"
"git.uploadfilter24.eu/covidnetes/woodpecker-autoscaler/internal/utils"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
func DecomAgent(cfg *config.Config, agentId int64) error { func DecomAgent(cfg *config.Config, agentId int) error {
apiRoute := fmt.Sprintf("%s/api/agents/%d", cfg.WoodpeckerInstance, agentId) apiRoute := fmt.Sprintf("%s/api/agents/%d", cfg.WoodpeckerInstance, agentId)
req, err := http.NewRequest("DELETE", apiRoute, nil) req, err := http.NewRequest("DELETE", apiRoute, nil)
if err != nil { if err != nil {
@ -25,7 +23,7 @@ func DecomAgent(cfg *config.Config, agentId int64) error {
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"Caller": "DecomAgent", "Caller": "DecomAgent",
}).Debugf("Deleting agent with id %d from woodpecker", agentId) }).Debugf("Deleting %d agent from woodpecker", agentId)
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
@ -37,7 +35,7 @@ func DecomAgent(cfg *config.Config, agentId int64) error {
func GetAgentIdByName(cfg *config.Config, name string) (int, error) { func GetAgentIdByName(cfg *config.Config, name string) (int, error) {
apiRoute := fmt.Sprintf("%s/api/agents?page=1&perPage=100", cfg.WoodpeckerInstance) apiRoute := fmt.Sprintf("%s/api/agents?page=1&perPage=100", cfg.WoodpeckerInstance)
req, err := http.NewRequest(http.MethodGet, apiRoute, nil) req, err := http.NewRequest("GET", apiRoute, nil)
if err != nil { if err != nil {
return 0, errors.New(fmt.Sprintf("Could not create agent query request: %s", err.Error())) return 0, errors.New(fmt.Sprintf("Could not create agent query request: %s", err.Error()))
} }
@ -69,67 +67,3 @@ func GetAgentIdByName(cfg *config.Config, name string) (int, error) {
} }
return 0, errors.New(fmt.Sprintf("Agent with name %s is not in server", name)) return 0, errors.New(fmt.Sprintf("Agent with name %s is not in server", name))
} }
func ListAgents(cfg *config.Config) (*models.AgentList, error) {
agentList := new(models.AgentList)
apiRoute := fmt.Sprintf("%s/api/agents?page=1&perPage=100", cfg.WoodpeckerInstance)
req, err := http.NewRequest(http.MethodGet, apiRoute, nil)
if err != nil {
return agentList, errors.New(fmt.Sprintf("Could not create agent query request: %s", err.Error()))
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.WoodpeckerApiToken))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return agentList, errors.New(fmt.Sprintf("Could not query agent list: %s", err.Error()))
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return agentList, errors.New(fmt.Sprintf("Invalid status code from API: %d", resp.StatusCode))
}
err = json.NewDecoder(resp.Body).Decode(agentList)
if err != nil {
return agentList, errors.New(fmt.Sprintf("Could not unmarshal api response: %s", err.Error()))
}
return agentList, nil
}
func CreateWoodpeckerAgent(cfg *config.Config) (*models.Agent, error) {
name := fmt.Sprintf("woodpecker-autoscaler-agent-%s", utils.RandStringBytes(5))
agentRequest := models.AgentRequest{
Name: name,
NoSchedule: false,
}
jsonBody, _ := json.Marshal(agentRequest)
bodyReader := bytes.NewReader(jsonBody)
apiRoute := fmt.Sprintf("%s/api/agents", cfg.WoodpeckerInstance)
log.WithFields(log.Fields{
"Caller": "CreateWoodpeckerAgent",
}).Debugf("Sending the following data to %s: %s", apiRoute, jsonBody)
req, err := http.NewRequest(http.MethodPost, apiRoute, bodyReader)
if err != nil {
return nil, errors.New(fmt.Sprintf("Could not create agent request: %s", err.Error()))
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.WoodpeckerApiToken))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, errors.New(fmt.Sprintf("Could not create new Agent: %s", err.Error()))
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New(fmt.Sprintf("Invalid status code from API: %d", resp.StatusCode))
}
newAgent := new(models.Agent)
err = json.NewDecoder(resp.Body).Decode(newAgent)
if err != nil {
return nil, errors.New(fmt.Sprintf("Could not unmarshal api response: %s", err.Error()))
}
return newAgent, nil
}

View File

@ -15,7 +15,7 @@ import (
func QueueInfo(cfg *config.Config, target interface{}) error { func QueueInfo(cfg *config.Config, target interface{}) error {
apiRoute := fmt.Sprintf("%s/api/queue/info", cfg.WoodpeckerInstance) apiRoute := fmt.Sprintf("%s/api/queue/info", cfg.WoodpeckerInstance)
req, err := http.NewRequest(http.MethodGet, apiRoute, nil) req, err := http.NewRequest("GET", apiRoute, nil)
if err != nil { if err != nil {
return errors.New(fmt.Sprintf("Could not create queue request: %s", err.Error())) return errors.New(fmt.Sprintf("Could not create queue request: %s", err.Error()))
} }
@ -29,54 +29,62 @@ func QueueInfo(cfg *config.Config, target interface{}) error {
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
return errors.New(fmt.Sprintf("Error from queue info api: %s", resp.Status)) return errors.New(fmt.Sprintf("Error from queue info api: %s", err.Error()))
} }
return json.NewDecoder(resp.Body).Decode(target) return json.NewDecoder(resp.Body).Decode(target)
} }
func CheckPending(cfg *config.Config) (int, error) { func CheckPending(cfg *config.Config) (bool, 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 0, errors.New(fmt.Sprintf("Error from QueueInfo: %s", err.Error())) return false, 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",
}).Debugf("Currently serving %d Jobs", count) }).Info("Found pending job for us")
return true, nil
} else {
log.WithFields(log.Fields{
"Caller": "CheckPending",
}).Info("No Jobs for us in Queue")
return false, nil
} }
} }
} }
} }
return count, nil return false, nil
} }
func CheckRunning(cfg *config.Config) (int, error) { func CheckRunning(cfg *config.Config) (bool, 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 0, errors.New(fmt.Sprintf("Error from QueueInfo: %s", err.Error())) return false, 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",
}).Debugf("Currently serving %d Jobs", count) }).Info("Found running job for us")
return true, nil
} else {
log.WithFields(log.Fields{
"Caller": "CheckRunning",
}).Info("No running job for us")
return false, nil
} }
} }
} }
return count, nil return false, nil
} }