Compare commits

...

42 Commits
v0.0.1 ... main

Author SHA1 Message Date
a467baf847
no need to ship patch version
All checks were successful
ci/woodpecker/push/main Pipeline was successful
also update ci ref
2024-06-04 22:35:43 +02:00
ae783cf9d6
update dependencies and go version
Some checks failed
ci/woodpecker/push/main Pipeline failed
2024-06-04 22:28:10 +02:00
9858f89140
remove label binding for ci
Some checks failed
ci/woodpecker/push/main Pipeline failed
2024-06-04 22:02:17 +02:00
69d6147582
fix nil dereference
Some checks are pending
ci/woodpecker/push/main Pipeline is pending
2024-06-04 22:01:24 +02:00
c392bfe7eb Merge pull request '(chore) update dependencies and ship multiarch builds' (#10) from chore/tt/update-dependencies-03-2024 into main
All checks were successful
ci/woodpecker/push/main Pipeline was successful
Reviewed-on: #10
2024-03-04 22:15:48 +00:00
31da09ef6f
update dependencies and ship multiarch builds
All checks were successful
ci/woodpecker/pr/pr Pipeline was successful
2024-03-04 23:00:51 +01:00
45ebc96c13 Merge pull request 'hopefully fix time comparison' (#9) from bugfix/tt/costoptimizedmode into main
All checks were successful
ci/woodpecker/push/main Pipeline was successful
Reviewed-on: #9
2024-02-03 23:14:55 +00:00
cb1a931b4c
hopefully fix time comparison
All checks were successful
ci/woodpecker/pr/pr Pipeline was successful
2024-02-04 00:08:43 +01:00
d4357767f5 Merge pull request '(feat) Cost optimized mode' (#8) from feature/tt/implement-cost-optimization into main
All checks were successful
ci/woodpecker/push/main Pipeline was successful
Reviewed-on: #8
2024-01-03 15:23:55 +00:00
30a219c91f
(feat) Cost optimized mode
All checks were successful
ci/woodpecker/pr/pr Pipeline was successful
2023-12-31 23:02:32 +01:00
a190c1e0fe
update dependencies
All checks were successful
ci/woodpecker/push/main Pipeline was successful
2023-12-19 20:55:05 +01:00
174c40c72c
better log message
All checks were successful
Pipeline was successful
2023-11-13 21:21:58 +01:00
48b4b273ee
smaller json and fix header
All checks were successful
Pipeline was successful
2023-11-13 21:01:32 +01:00
09c85fc3bd
debug statements
All checks were successful
Pipeline was successful
2023-11-13 20:50:33 +01:00
c431131d5f Merge pull request 'change agent creation logic to use agentToken instead of systemToken' (#5) from feature/tt/fix-agent-decom into main
All checks were successful
Pipeline was successful
Reviewed-on: #5
2023-11-13 19:29:05 +00:00
fe52d864a4
change agent creation logic to use agentToken instead of systemToken
All checks were successful
Pipeline was successful
2023-11-12 22:24:44 +01:00
b536c88db8
rename chart and fix docs
All checks were successful
Pipeline was successful
2023-11-10 21:49:50 +01:00
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
23 changed files with 443 additions and 191 deletions

View File

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

View File

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

View File

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

19
CHANGELOG Normal file
View File

@ -0,0 +1,19 @@
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.21 as build
FROM golang:1.22 as build
WORKDIR /app

View File

@ -28,12 +28,12 @@ env:
value: "define_it"
- name: WOODPECKER_AUTOSCALER_HCLOUD_INSTANCE_TYPE
value: "cpx21"
- name: WOODPECKER_AUTOSCALER_HCLOUD_REGION
value: "define_it"
- name: WOODPECKER_AUTOSCALER_HCLOUD_DATACENTER
- name: WOODPECKER_AUTOSCALER_HCLOUD_LOCATION
value: "define_it"
- name: WOODPECKER_AUTOSCALER_HCLOUD_SSH_KEY
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`:
@ -79,9 +79,9 @@ WOODPECKER_AUTOSCALER_WOODPECKER_AGENT_SECRET="define_it"
WOODPECKER_AUTOSCALER_WOODPECKER_API_TOKEN="define_it"
WOODPECKER_AUTOSCALER_HCLOUD_TOKEN="define_it"
WOODPECKER_AUTOSCALER_HCLOUD_INSTANCE_TYPE=cpx21
WOODPECKER_AUTOSCALER_HCLOUD_REGION="define_it"
WOODPECKER_AUTOSCALER_HCLOUD_DATACENTER="define_it"
WOODPECKER_AUTOSCALER_HCLOUD_LOCATION="define_it"
WOODPECKER_AUTOSCALER_HCLOUD_SSH_KEY="define_it"
WOODPECKER_AUTOSCALER_COST_OPTIMIZED="true"
```
Now reload the systemd daemons and start the service:

View File

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

View File

@ -13,6 +13,106 @@ import (
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() {
cfg, err := config.GenConfig()
@ -51,55 +151,14 @@ func main() {
log.WithFields(log.Fields{
"Caller": "Main",
}).Infof("Currently owning %d Agents", len(ownedNodes))
if pendingTasks {
server, err := hetzner.CreateNewAgent(cfg)
if err != nil {
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{
"Caller": "Main",
}).Infof("Waiting for agent %s to start", server.Name)
}
if pendingTasks > len(ownedNodes) {
SpawnNewAgent(cfg)
} 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)
}
}
}
CheckJobs(cfg, ownedNodes, pendingTasks)
}
log.WithFields(log.Fields{
"Caller": "Main",
}).Infof("Recheck in %d", cfg.CheckInterval)
time.Sleep(time.Duration(cfg.CheckInterval) * time.Minute)
}
}

34
go.mod
View File

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

77
go.sum
View File

@ -1,70 +1,55 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
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/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/hetznercloud/hcloud-go v1.56.0 h1:Swf7cFQAae5r+76Fom9JVPpWqMIs49UxIOROLjBE1vc=
github.com/hetznercloud/hcloud-go v1.56.0/go.mod h1:oTebZCjd+osj75jlI76Z+zjN1sTxmMiQ1MWoO8aRl1c=
github.com/jinzhu/configor v1.2.2 h1:sLgh6KMzpCmaQB4e+9Fu/29VErtBUqsS2t8C9BNIVsA=
github.com/jinzhu/configor v1.2.2/go.mod h1:iFFSfOBKP3kC2Dku0ZGB3t3aulfQgTGJknodhFavsU8=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
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/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8=
github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
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/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/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
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/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
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/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

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

View File

@ -5,9 +5,13 @@ import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"text/template"
"time"
"git.uploadfilter24.eu/covidnetes/woodpecker-autoscaler/internal/config"
"git.uploadfilter24.eu/covidnetes/woodpecker-autoscaler/internal/models"
"git.uploadfilter24.eu/covidnetes/woodpecker-autoscaler/internal/utils"
"github.com/hetznercloud/hcloud-go/hcloud"
@ -15,13 +19,12 @@ import (
)
var USER_DATA_TEMPLATE = `
#cloud-config
write_files:
- content: |
# docker-compose.yml
version: '3'
services:
woodpecker-agent:
image: {{ .Image }}
command: agent
@ -30,7 +33,7 @@ write_files:
- /var/run/docker.sock:/var/run/docker.sock
environment:
{{- range $key, $val := .EnvConfig }}
- {{ $key }}: {{ $val }}
- {{ $key }}={{ $val }}
{{- end }}
path: /root/docker-compose.yml
runcmd:
@ -39,17 +42,20 @@ runcmd:
type UserDataConfig struct {
Image string
EnvConfig map[string]string
EnvConfig map[string]interface{}
}
func generateConfig(cfg *config.Config, name string) (string, error) {
envConfig := map[string]string{}
envConfig["WOODPECKER_SERVER"] = cfg.WoodpeckerInstance
envConfig["WOODPECKER_AGENT_SECRET"] = cfg.WoodpeckerAgentSecret
envConfig["WOODPECKER_FILTER_LABELS"] = cfg.WoodpeckerLabelSelector
envConfig["WOODPECKER_HOSTNAME"] = name
func generateConfig(cfg *config.Config, name string, agentToken string) (string, error) {
envConfig := map[string]interface{}{
"WOODPECKER_SERVER": fmt.Sprintf("%s", cfg.WoodpeckerGrpc),
"WOODPECKER_GRPC_SECURE": true,
"WOODPECKER_AGENT_SECRET": fmt.Sprintf("%s", agentToken),
"WOODPECKER_FILTER_LABELS": fmt.Sprintf("%s", cfg.WoodpeckerLabelSelector),
"WOODPECKER_HOSTNAME": fmt.Sprintf("%s", name),
"WOODPECKER_MAX_WORKFLOWS": 4,
}
config := UserDataConfig{
Image: "woodpeckerci/woodpecker-agent:latest",
Image: fmt.Sprintf("woodpeckerci/woodpecker-agent:%s", cfg.WoodpeckerAgentVersion),
EnvConfig: envConfig,
}
tmpl, err := template.New("userdata").Parse(USER_DATA_TEMPLATE)
@ -64,35 +70,52 @@ func generateConfig(cfg *config.Config, name string) (string, error) {
return buf.String(), nil
}
func CreateNewAgent(cfg *config.Config) (*hcloud.Server, error) {
func CreateNewAgent(cfg *config.Config, woodpeckerAgent *models.Agent) (*hcloud.Server, error) {
client := hcloud.NewClient(hcloud.WithToken(cfg.HcloudToken))
name := fmt.Sprintf("woodpecker-autoscaler-agent-%s", utils.RandStringBytes(5))
userdata, err := generateConfig(cfg, name)
img, _, err := client.Image.GetByNameAndArchitecture(context.Background(), "docker-ce", "amd64")
loc, _, err := client.Location.GetByName(context.Background(), cfg.HcloudRegion)
userdata, err := generateConfig(cfg, woodpeckerAgent.Name, woodpeckerAgent.Token)
keys := []*hcloud.SSHKey{}
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)
key, _, err := client.SSHKey.GetByName(context.Background(), cfg.HcloudSSHKey)
dc, _, err := client.Datacenter.GetByName(context.Background(), cfg.HcloudDatacenter)
utils.CheckError(err, "GetServerTypeByName")
labels := map[string]string{}
labels["Role"] = "WoodpeckerAgent"
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 {
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{
"Caller": "CreateNewAgent",
}).Infof("Created new Build Agent %s", res.Server.Name)
@ -119,14 +142,44 @@ func ListAgents(cfg *config.Config) ([]hcloud.Server, error) {
return myServers, nil
}
func DecomNode(cfg *config.Config, server *hcloud.Server) error {
func DecomNode(cfg *config.Config, server *hcloud.Server) (int64, error) {
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{
"Caller": "DecomNode",
}).Debugf("Deleting %s node", server.Name)
_, _, err := client.Server.DeleteWithResult(context.Background(), server)
if err != nil {
return errors.New(fmt.Sprintf("Could not delete Agent: %s", err.Error()))
return woodpeckerAgentID, errors.New(fmt.Sprintf("Could not delete Agent: %s", err.Error()))
}
return nil
return woodpeckerAgentID, 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

@ -0,0 +1,56 @@
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 {
ID int `json:"id"`
ID string `json:"id"`
Data string `json:"data"`
Labels map[string]string `json:"labels"`
Dependencies string `json:"dependencies,omitempty"`
@ -90,3 +90,8 @@ type Agent struct {
type AgentList struct {
Agents []Agent
}
type AgentRequest struct {
Name string `json:"name"`
NoSchedule bool `json:"no_schedule"`
}

View File

@ -1,6 +1,10 @@
package utils
import "math/rand"
import (
"math/rand"
log "github.com/sirupsen/logrus"
)
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
@ -15,3 +19,11 @@ func RandStringBytes(n int) string {
func BoolPointer(b bool) *bool {
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,6 +1,7 @@
package woodpecker
import (
"bytes"
"encoding/json"
"errors"
"fmt"
@ -8,11 +9,12 @@ import (
"git.uploadfilter24.eu/covidnetes/woodpecker-autoscaler/internal/config"
"git.uploadfilter24.eu/covidnetes/woodpecker-autoscaler/internal/models"
"git.uploadfilter24.eu/covidnetes/woodpecker-autoscaler/internal/utils"
log "github.com/sirupsen/logrus"
)
func DecomAgent(cfg *config.Config, agentId int) error {
func DecomAgent(cfg *config.Config, agentId int64) error {
apiRoute := fmt.Sprintf("%s/api/agents/%d", cfg.WoodpeckerInstance, agentId)
req, err := http.NewRequest("DELETE", apiRoute, nil)
if err != nil {
@ -23,7 +25,7 @@ func DecomAgent(cfg *config.Config, agentId int) error {
log.WithFields(log.Fields{
"Caller": "DecomAgent",
}).Debugf("Deleting %d agent from woodpecker", agentId)
}).Debugf("Deleting agent with id %d from woodpecker", agentId)
resp, err := http.DefaultClient.Do(req)
if err != nil {
@ -35,7 +37,7 @@ func DecomAgent(cfg *config.Config, agentId int) error {
func GetAgentIdByName(cfg *config.Config, name string) (int, error) {
apiRoute := fmt.Sprintf("%s/api/agents?page=1&perPage=100", cfg.WoodpeckerInstance)
req, err := http.NewRequest("GET", apiRoute, nil)
req, err := http.NewRequest(http.MethodGet, apiRoute, nil)
if err != nil {
return 0, errors.New(fmt.Sprintf("Could not create agent query request: %s", err.Error()))
}
@ -67,3 +69,67 @@ func GetAgentIdByName(cfg *config.Config, name string) (int, error) {
}
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 {
apiRoute := fmt.Sprintf("%s/api/queue/info", cfg.WoodpeckerInstance)
req, err := http.NewRequest("GET", apiRoute, nil)
req, err := http.NewRequest(http.MethodGet, apiRoute, nil)
if err != nil {
return errors.New(fmt.Sprintf("Could not create queue request: %s", err.Error()))
}
@ -29,62 +29,54 @@ func QueueInfo(cfg *config.Config, target interface{}) error {
defer resp.Body.Close()
if resp.StatusCode != 200 {
return errors.New(fmt.Sprintf("Error from queue info api: %s", err.Error()))
return errors.New(fmt.Sprintf("Error from queue info api: %s", resp.Status))
}
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, "=")
queueInfo := new(models.QueueInfo)
err := QueueInfo(cfg, queueInfo)
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.Pending != nil {
for _, pendingJobs := range queueInfo.Pending {
val, exists := pendingJobs.Labels[expectedKV[0]]
if exists && val == expectedKV[1] {
count++
log.WithFields(log.Fields{
"Caller": "CheckPending",
}).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
}).Debugf("Currently serving %d Jobs", count)
}
}
}
}
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, "=")
queueInfo := new(models.QueueInfo)
err := QueueInfo(cfg, queueInfo)
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 {
for _, runningJobs := range queueInfo.Running {
val, exists := runningJobs.Labels[expectedKV[0]]
if exists && val == expectedKV[1] {
count++
log.WithFields(log.Fields{
"Caller": "CheckRunning",
}).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
}).Debugf("Currently serving %d Jobs", count)
}
}
}
return false, nil
return count, nil
}