feat: add Timeweb Cloud provider for Woodpecker CI autoscaler
- Implement timewebcloud provider with DeployAgent, RemoveAgent, ListDeployedAgentNames - Add minimal HTTP API client for Timeweb Cloud (create/list/delete servers) - Register provider in main.go with CLI flags - Add timeweb-list and timeweb-tester utilities - Include Dockerfile and docker-compose.yml for deployment - Update DEPLOY.md with verified OS/preset IDs
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
woodpecker-autoscaler
|
||||||
|
timeweb-list
|
||||||
|
timeweb-tester
|
||||||
|
timeweb-debug
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
.DS_Store
|
||||||
743
CHANGELOG.md
Normal file
743
CHANGELOG.md
Normal file
@@ -0,0 +1,743 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [1.4.0](https://github.com/woodpecker-ci/autoscaler/releases/tag/1.4.0) - 2026-04-29
|
||||||
|
|
||||||
|
### ❤️ Thanks to all contributors! ❤️
|
||||||
|
|
||||||
|
@6543, @mendarb
|
||||||
|
|
||||||
|
### 📈 Enhancement
|
||||||
|
|
||||||
|
- Make agent extra labels an explicit option [[#584](https://github.com/woodpecker-ci/autoscaler/pull/584)]
|
||||||
|
- Move code in subpackages [[#585](https://github.com/woodpecker-ci/autoscaler/pull/585)]
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Propagate tags to EBS volumes on AWS instances [[#568](https://github.com/woodpecker-ci/autoscaler/pull/568)]
|
||||||
|
|
||||||
|
### 📦️ Dependency
|
||||||
|
|
||||||
|
- fix(deps): update module github.com/hetznercloud/hcloud-go/v2 to v2.39.0 [[#599](https://github.com/woodpecker-ci/autoscaler/pull/599)]
|
||||||
|
- fix(deps): update golang deps non-major [[#591](https://github.com/woodpecker-ci/autoscaler/pull/591)]
|
||||||
|
- fix(deps): update module github.com/vultr/govultr/v3 to v3.31.1 [[#589](https://github.com/woodpecker-ci/autoscaler/pull/589)]
|
||||||
|
|
||||||
|
## [1.3.0](https://github.com/woodpecker-ci/autoscaler/releases/tag/1.3.0) - 2026-04-21
|
||||||
|
|
||||||
|
### ❤️ Thanks to all contributors! ❤️
|
||||||
|
|
||||||
|
@6543, @BnMcG
|
||||||
|
|
||||||
|
### 📈 Enhancement
|
||||||
|
|
||||||
|
- Surface regex compile error [[#586](https://github.com/woodpecker-ci/autoscaler/pull/586)]
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- fix(scaleway): tidy up scaleway volumes [[#559](https://github.com/woodpecker-ci/autoscaler/pull/559)]
|
||||||
|
|
||||||
|
### 📦️ Dependency
|
||||||
|
|
||||||
|
- fix(deps): update golang deps non-major [[#588](https://github.com/woodpecker-ci/autoscaler/pull/588)]
|
||||||
|
- chore(deps): update docker.io/woodpeckerci/plugin-ready-release-go docker tag to v4.1.1 [[#587](https://github.com/woodpecker-ci/autoscaler/pull/587)]
|
||||||
|
- fix(deps): update golang deps non-major [[#578](https://github.com/woodpecker-ci/autoscaler/pull/578)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 746e56f [[#579](https://github.com/woodpecker-ci/autoscaler/pull/579)]
|
||||||
|
- chore(deps): update dependency golangci/golangci-lint to v2.11.4 [[#554](https://github.com/woodpecker-ci/autoscaler/pull/554)]
|
||||||
|
- fix(deps): update module github.com/vultr/govultr/v3 to v3.30.0 [[#576](https://github.com/woodpecker-ci/autoscaler/pull/576)]
|
||||||
|
- chore(deps): update docker.io/woodpeckerci/plugin-ready-release-go docker tag to v4 [[#556](https://github.com/woodpecker-ci/autoscaler/pull/556)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 7ab1446 [[#560](https://github.com/woodpecker-ci/autoscaler/pull/560)]
|
||||||
|
- fix(deps): update golang deps non-major [[#575](https://github.com/woodpecker-ci/autoscaler/pull/575)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/config to v1.32.14 [[#574](https://github.com/woodpecker-ci/autoscaler/pull/574)]
|
||||||
|
- fix(deps): update golang deps non-major [[#573](https://github.com/woodpecker-ci/autoscaler/pull/573)]
|
||||||
|
- fix(deps): update golang deps non-major [[#572](https://github.com/woodpecker-ci/autoscaler/pull/572)]
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v2.11.4 [[#569](https://github.com/woodpecker-ci/autoscaler/pull/569)]
|
||||||
|
- fix(deps): update golang deps non-major [[#571](https://github.com/woodpecker-ci/autoscaler/pull/571)]
|
||||||
|
- fix(deps): update module github.com/urfave/cli/v3 to v3.8.0 [[#570](https://github.com/woodpecker-ci/autoscaler/pull/570)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.296.0 [[#562](https://github.com/woodpecker-ci/autoscaler/pull/562)]
|
||||||
|
- fix(deps): update golang deps non-major [[#561](https://github.com/woodpecker-ci/autoscaler/pull/561)]
|
||||||
|
- fix(deps): update golang deps non-major [[#557](https://github.com/woodpecker-ci/autoscaler/pull/557)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 3dfff04 [[#548](https://github.com/woodpecker-ci/autoscaler/pull/548)]
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v2.11.3 [[#555](https://github.com/woodpecker-ci/autoscaler/pull/555)]
|
||||||
|
- chore(deps): update dependency go to v1.26.1 [[#553](https://github.com/woodpecker-ci/autoscaler/pull/553)]
|
||||||
|
- chore(deps): update dependency golangci/golangci-lint to v2.11.0 [[#543](https://github.com/woodpecker-ci/autoscaler/pull/543)]
|
||||||
|
- fix(deps): update golang deps non-major [[#552](https://github.com/woodpecker-ci/autoscaler/pull/552)]
|
||||||
|
- fix(deps): update golang deps non-major [[#551](https://github.com/woodpecker-ci/autoscaler/pull/551)]
|
||||||
|
- fix(deps): update golang deps non-major [[#541](https://github.com/woodpecker-ci/autoscaler/pull/541)]
|
||||||
|
- chore(deps): update docker.io/golang docker tag to v1.26 [[#544](https://github.com/woodpecker-ci/autoscaler/pull/544)]
|
||||||
|
- chore(deps): update pre-commit non-major [[#549](https://github.com/woodpecker-ci/autoscaler/pull/549)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 81e46e3 [[#542](https://github.com/woodpecker-ci/autoscaler/pull/542)]
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v2.9.0 [[#546](https://github.com/woodpecker-ci/autoscaler/pull/546)]
|
||||||
|
- chore(deps): update golang docker tag to v1.26 [[#545](https://github.com/woodpecker-ci/autoscaler/pull/545)]
|
||||||
|
- fix(deps): update module golang.org/x/oauth2 to v0.35.0 [[#540](https://github.com/woodpecker-ci/autoscaler/pull/540)]
|
||||||
|
- fix(deps): update module github.com/vultr/govultr/v3 to v3.27.0 [[#536](https://github.com/woodpecker-ci/autoscaler/pull/536)]
|
||||||
|
- chore(deps): update dependency go to v1.25.7 [[#535](https://github.com/woodpecker-ci/autoscaler/pull/535)]
|
||||||
|
- fix(deps): update module github.com/linode/linodego to v1.65.0 [[#533](https://github.com/woodpecker-ci/autoscaler/pull/533)]
|
||||||
|
|
||||||
|
## [1.2.0](https://github.com/woodpecker-ci/autoscaler/releases/tag/1.2.0) - 2026-01-30
|
||||||
|
|
||||||
|
### ❤️ Thanks to all contributors! ❤️
|
||||||
|
|
||||||
|
@jooola, @xoxys
|
||||||
|
|
||||||
|
### 📈 Enhancement
|
||||||
|
|
||||||
|
- feat: add version flag [[#523](https://github.com/woodpecker-ci/autoscaler/pull/523)]
|
||||||
|
- feat: add Woodpecker to Hetzner Cloud client user agent [[#519](https://github.com/woodpecker-ci/autoscaler/pull/519)]
|
||||||
|
|
||||||
|
### 📦️ Dependency
|
||||||
|
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.285.0 [[#527](https://github.com/woodpecker-ci/autoscaler/pull/527)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.284.0 [[#526](https://github.com/woodpecker-ci/autoscaler/pull/526)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.283.0 [[#525](https://github.com/woodpecker-ci/autoscaler/pull/525)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.282.0 [[#524](https://github.com/woodpecker-ci/autoscaler/pull/524)]
|
||||||
|
- fix(deps): update golang deps non-major [[#521](https://github.com/woodpecker-ci/autoscaler/pull/521)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.280.0 [[#520](https://github.com/woodpecker-ci/autoscaler/pull/520)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 716be56 [[#515](https://github.com/woodpecker-ci/autoscaler/pull/515)]
|
||||||
|
- fix(deps): update module github.com/urfave/cli/v3 to v3.6.2 [[#518](https://github.com/woodpecker-ci/autoscaler/pull/518)]
|
||||||
|
- chore(deps): update pre-commit hook adrienverge/yamllint to v1.38.0 [[#516](https://github.com/woodpecker-ci/autoscaler/pull/516)]
|
||||||
|
- fix(deps): update module github.com/hetznercloud/hcloud-go/v2 to v2.35.0 [[#517](https://github.com/woodpecker-ci/autoscaler/pull/517)]
|
||||||
|
- fix(deps): update golang deps non-major [[#514](https://github.com/woodpecker-ci/autoscaler/pull/514)]
|
||||||
|
- fix(deps): update module go.woodpecker-ci.org/woodpecker/v3 to v3.13.0 [[#512](https://github.com/woodpecker-ci/autoscaler/pull/512)]
|
||||||
|
|
||||||
|
## [1.1.5](https://github.com/woodpecker-ci/autoscaler/releases/tag/1.1.5) - 2026-01-13
|
||||||
|
|
||||||
|
### ❤️ Thanks to all contributors! ❤️
|
||||||
|
|
||||||
|
@qwerty287
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Fix log format messages [[#502](https://github.com/woodpecker-ci/autoscaler/pull/502)]
|
||||||
|
|
||||||
|
### 📦️ Dependency
|
||||||
|
|
||||||
|
- chore(deps): update pre-commit non-major [[#511](https://github.com/woodpecker-ci/autoscaler/pull/511)]
|
||||||
|
- chore(deps): update docker.io/woodpeckerci/plugin-ready-release-go docker tag to v3.4.1 [[#509](https://github.com/woodpecker-ci/autoscaler/pull/509)]
|
||||||
|
- fix(deps): update golang deps non-major [[#507](https://github.com/woodpecker-ci/autoscaler/pull/507)]
|
||||||
|
- chore(deps): update docker.io/woodpeckerci/plugin-editorconfig-checker docker tag to v0.3.3 [[#506](https://github.com/woodpecker-ci/autoscaler/pull/506)]
|
||||||
|
- chore(deps): update docker.io/woodpeckerci/plugin-docker-buildx docker tag to v6.0.4 [[#508](https://github.com/woodpecker-ci/autoscaler/pull/508)]
|
||||||
|
- chore(deps): update dependency golangci/golangci-lint to v2.8.0 [[#510](https://github.com/woodpecker-ci/autoscaler/pull/510)]
|
||||||
|
- fix(deps): update module github.com/hetznercloud/hcloud-go/v2 to v2.34.0 [[#505](https://github.com/woodpecker-ci/autoscaler/pull/505)]
|
||||||
|
- fix(deps): update module github.com/linode/linodego to v1.64.0 [[#504](https://github.com/woodpecker-ci/autoscaler/pull/504)]
|
||||||
|
|
||||||
|
## [1.1.4](https://github.com/woodpecker-ci/autoscaler/releases/tag/1.1.4) - 2025-12-23
|
||||||
|
|
||||||
|
### ❤️ Thanks to all contributors! ❤️
|
||||||
|
|
||||||
|
@mossylion
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Allow setting Scaleway storage type and default to l_ssd [[#501](https://github.com/woodpecker-ci/autoscaler/pull/501)]
|
||||||
|
|
||||||
|
### 📦️ Dependency
|
||||||
|
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 944ab1f [[#499](https://github.com/woodpecker-ci/autoscaler/pull/499)]
|
||||||
|
- fix(deps): update golang deps non-major [[#500](https://github.com/woodpecker-ci/autoscaler/pull/500)]
|
||||||
|
- fix(deps): update golang deps non-major [[#498](https://github.com/woodpecker-ci/autoscaler/pull/498)]
|
||||||
|
- fix(deps): update golang deps non-major [[#496](https://github.com/woodpecker-ci/autoscaler/pull/496)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.277.0 [[#495](https://github.com/woodpecker-ci/autoscaler/pull/495)]
|
||||||
|
- chore(deps): update pre-commit hook igorshubovych/markdownlint-cli to v0.47.0 [[#494](https://github.com/woodpecker-ci/autoscaler/pull/494)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 8475f28 [[#493](https://github.com/woodpecker-ci/autoscaler/pull/493)]
|
||||||
|
- fix(deps): update module github.com/vultr/govultr/v3 to v3.26.0 [[#492](https://github.com/woodpecker-ci/autoscaler/pull/492)]
|
||||||
|
- fix(deps): update golang deps non-major [[#491](https://github.com/woodpecker-ci/autoscaler/pull/491)]
|
||||||
|
- chore(deps): update dependency golangci/golangci-lint to v2.7.2 [[#489](https://github.com/woodpecker-ci/autoscaler/pull/489)]
|
||||||
|
- fix(deps): update golang deps non-major [[#490](https://github.com/woodpecker-ci/autoscaler/pull/490)]
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v2.7.2 [[#487](https://github.com/woodpecker-ci/autoscaler/pull/487)]
|
||||||
|
|
||||||
|
## [1.1.3](https://github.com/woodpecker-ci/autoscaler/releases/tag/1.1.3) - 2025-12-06
|
||||||
|
|
||||||
|
### ❤️ Thanks to all contributors! ❤️
|
||||||
|
|
||||||
|
@6543, @xoxys
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Ensure latest qemu packages are installed [[#484](https://github.com/woodpecker-ci/autoscaler/pull/484)]
|
||||||
|
|
||||||
|
### 📦️ Dependency
|
||||||
|
|
||||||
|
- chore(deps): update dependency golangci/golangci-lint to v2.7.1 [[#485](https://github.com/woodpecker-ci/autoscaler/pull/485)]
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v2.7.1 [[#486](https://github.com/woodpecker-ci/autoscaler/pull/486)]
|
||||||
|
- fix(deps): update golang deps non-major [[#483](https://github.com/woodpecker-ci/autoscaler/pull/483)]
|
||||||
|
- chore(deps): update dependency golangci/golangci-lint to v2.6.2 [[#471](https://github.com/woodpecker-ci/autoscaler/pull/471)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 87e1e73 [[#482](https://github.com/woodpecker-ci/autoscaler/pull/482)]
|
||||||
|
- fix(deps): update golang deps non-major [[#481](https://github.com/woodpecker-ci/autoscaler/pull/481)]
|
||||||
|
- chore(deps): update pre-commit hook igorshubovych/markdownlint-cli to v0.46.0 [[#479](https://github.com/woodpecker-ci/autoscaler/pull/479)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/config to v1.32.1 [[#480](https://github.com/woodpecker-ci/autoscaler/pull/480)]
|
||||||
|
- fix(deps): update golang deps non-major [[#478](https://github.com/woodpecker-ci/autoscaler/pull/478)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.273.0 [[#477](https://github.com/woodpecker-ci/autoscaler/pull/477)]
|
||||||
|
- fix(deps): update golang deps non-major [[#476](https://github.com/woodpecker-ci/autoscaler/pull/476)]
|
||||||
|
- fix(deps): update golang deps non-major [[#475](https://github.com/woodpecker-ci/autoscaler/pull/475)]
|
||||||
|
- fix(deps): update module github.com/urfave/cli/v3 to v3.6.1 [[#474](https://github.com/woodpecker-ci/autoscaler/pull/474)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.269.0 [[#473](https://github.com/woodpecker-ci/autoscaler/pull/473)]
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v2.6.2 [[#472](https://github.com/woodpecker-ci/autoscaler/pull/472)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to e25ba8c [[#470](https://github.com/woodpecker-ci/autoscaler/pull/470)]
|
||||||
|
- fix(deps): update golang deps non-major [[#469](https://github.com/woodpecker-ci/autoscaler/pull/469)]
|
||||||
|
- fix(deps): update golang deps non-major [[#468](https://github.com/woodpecker-ci/autoscaler/pull/468)]
|
||||||
|
- fix(deps): update module github.com/urfave/cli/v3 to v3.6.0 [[#467](https://github.com/woodpecker-ci/autoscaler/pull/467)]
|
||||||
|
- chore(deps): update dependency golangci/golangci-lint to v2.6.1 [[#464](https://github.com/woodpecker-ci/autoscaler/pull/464)]
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v2.6.1 [[#465](https://github.com/woodpecker-ci/autoscaler/pull/465)]
|
||||||
|
- fix(deps): update golang deps non-major [[#466](https://github.com/woodpecker-ci/autoscaler/pull/466)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.263.0 [[#463](https://github.com/woodpecker-ci/autoscaler/pull/463)]
|
||||||
|
- fix(deps): update golang deps non-major [[#462](https://github.com/woodpecker-ci/autoscaler/pull/462)]
|
||||||
|
- fix(deps): update golang deps non-major [[#461](https://github.com/woodpecker-ci/autoscaler/pull/461)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.261.0 [[#460](https://github.com/woodpecker-ci/autoscaler/pull/460)]
|
||||||
|
- chore(deps): update node.js to v24 [[#458](https://github.com/woodpecker-ci/autoscaler/pull/458)]
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v2.6.0 [[#457](https://github.com/woodpecker-ci/autoscaler/pull/457)]
|
||||||
|
- chore(deps): update dependency golangci/golangci-lint to v2.6.0 [[#456](https://github.com/woodpecker-ci/autoscaler/pull/456)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.260.0 [[#459](https://github.com/woodpecker-ci/autoscaler/pull/459)]
|
||||||
|
- fix(deps): update golang deps non-major [[#455](https://github.com/woodpecker-ci/autoscaler/pull/455)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.259.0 [[#454](https://github.com/woodpecker-ci/autoscaler/pull/454)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to a4bb9ff [[#451](https://github.com/woodpecker-ci/autoscaler/pull/451)]
|
||||||
|
- chore(deps): update dependency mvdan/gofumpt to v0.9.2 [[#452](https://github.com/woodpecker-ci/autoscaler/pull/452)]
|
||||||
|
- fix(deps): update golang deps non-major [[#453](https://github.com/woodpecker-ci/autoscaler/pull/453)]
|
||||||
|
- fix(deps): update golang deps non-major [[#450](https://github.com/woodpecker-ci/autoscaler/pull/450)]
|
||||||
|
- Use our own editorconfig checker plugin [[#447](https://github.com/woodpecker-ci/autoscaler/pull/447)]
|
||||||
|
- fix(deps): update module go.woodpecker-ci.org/woodpecker/v3 to v3.11.0 [[#448](https://github.com/woodpecker-ci/autoscaler/pull/448)]
|
||||||
|
|
||||||
|
## [1.1.2](https://github.com/woodpecker-ci/autoscaler/releases/tag/1.1.2) - 2025-10-19
|
||||||
|
|
||||||
|
### ❤️ Thanks to all contributors! ❤️
|
||||||
|
|
||||||
|
@xoxys
|
||||||
|
|
||||||
|
### 📦️ Dependency
|
||||||
|
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 90e834f [[#444](https://github.com/woodpecker-ci/autoscaler/pull/444)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.257.2 [[#445](https://github.com/woodpecker-ci/autoscaler/pull/445)]
|
||||||
|
- fix(deps): update golang deps non-major [[#443](https://github.com/woodpecker-ci/autoscaler/pull/443)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.257.0 [[#442](https://github.com/woodpecker-ci/autoscaler/pull/442)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.256.0 [[#441](https://github.com/woodpecker-ci/autoscaler/pull/441)]
|
||||||
|
- fix(deps): update golang deps non-major [[#440](https://github.com/woodpecker-ci/autoscaler/pull/440)]
|
||||||
|
- chore(deps): update docker.io/mstruebing/editorconfig-checker docker tag to v3.4.1 [[#439](https://github.com/woodpecker-ci/autoscaler/pull/439)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to d2f985d [[#438](https://github.com/woodpecker-ci/autoscaler/pull/438)]
|
||||||
|
- fix(deps): update golang deps non-major [[#437](https://github.com/woodpecker-ci/autoscaler/pull/437)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 27f1f14 [[#434](https://github.com/woodpecker-ci/autoscaler/pull/434)]
|
||||||
|
- fix(deps): update module github.com/hetznercloud/hcloud-go/v2 to v2.27.0 [[#436](https://github.com/woodpecker-ci/autoscaler/pull/436)]
|
||||||
|
- chore(deps): update docker.io/woodpeckerci/plugin-docker-buildx docker tag to v6.0.3 [[#435](https://github.com/woodpecker-ci/autoscaler/pull/435)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/config to v1.31.12 [[#433](https://github.com/woodpecker-ci/autoscaler/pull/433)]
|
||||||
|
- fix(deps): update module go.woodpecker-ci.org/woodpecker/v3 to v3.10.0 [[#432](https://github.com/woodpecker-ci/autoscaler/pull/432)]
|
||||||
|
- chore(deps): update pre-commit hook hadolint/hadolint to v2.14.0 [[#431](https://github.com/woodpecker-ci/autoscaler/pull/431)]
|
||||||
|
- fix(deps): update golang deps non-major [[#430](https://github.com/woodpecker-ci/autoscaler/pull/430)]
|
||||||
|
- fix(deps): update golang deps non-major [[#429](https://github.com/woodpecker-ci/autoscaler/pull/429)]
|
||||||
|
- fix(deps): update golang deps non-major [[#427](https://github.com/woodpecker-ci/autoscaler/pull/427)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/config to v1.31.9 [[#426](https://github.com/woodpecker-ci/autoscaler/pull/426)]
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v2.5.0 [[#425](https://github.com/woodpecker-ci/autoscaler/pull/425)]
|
||||||
|
- fix(deps): update module github.com/hetznercloud/hcloud-go/v2 to v2.24.0 [[#424](https://github.com/woodpecker-ci/autoscaler/pull/424)]
|
||||||
|
- fix(deps): update module github.com/vultr/govultr/v3 to v3.24.0 [[#423](https://github.com/woodpecker-ci/autoscaler/pull/423)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.253.0 [[#422](https://github.com/woodpecker-ci/autoscaler/pull/422)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.252.0 [[#421](https://github.com/woodpecker-ci/autoscaler/pull/421)]
|
||||||
|
- fix(deps): update golang deps non-major [[#420](https://github.com/woodpecker-ci/autoscaler/pull/420)]
|
||||||
|
- chore(deps): update docker.io/mstruebing/editorconfig-checker docker tag to v3.4.0 [[#419](https://github.com/woodpecker-ci/autoscaler/pull/419)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to df92998 [[#418](https://github.com/woodpecker-ci/autoscaler/pull/418)]
|
||||||
|
- fix(deps): update golang deps non-major [[#417](https://github.com/woodpecker-ci/autoscaler/pull/417)]
|
||||||
|
- fix(deps): update golang deps non-major [[#416](https://github.com/woodpecker-ci/autoscaler/pull/416)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.245.0 [[#414](https://github.com/woodpecker-ci/autoscaler/pull/414)]
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
|
||||||
|
- Regenerate woodpecker-client mocks [[#446](https://github.com/woodpecker-ci/autoscaler/pull/446)]
|
||||||
|
- Migrate mockery to v3 [[#428](https://github.com/woodpecker-ci/autoscaler/pull/428)]
|
||||||
|
|
||||||
|
## [1.1.1](https://github.com/woodpecker-ci/autoscaler/releases/tag/1.1.1) - 2025-08-18
|
||||||
|
|
||||||
|
### ❤️ Thanks to all contributors! ❤️
|
||||||
|
|
||||||
|
@xoxys
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Improve logging for fallback server types in hcloud provider [[#413](https://github.com/woodpecker-ci/autoscaler/pull/413)]
|
||||||
|
|
||||||
|
### 📦️ Dependency
|
||||||
|
|
||||||
|
- chore(deps): update golang docker tag to v1.25 [[#409](https://github.com/woodpecker-ci/autoscaler/pull/409)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 42675ad [[#407](https://github.com/woodpecker-ci/autoscaler/pull/407)]
|
||||||
|
- chore(deps): update docker.io/golang docker tag to v1.25 [[#408](https://github.com/woodpecker-ci/autoscaler/pull/408)]
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v2.4.0 [[#411](https://github.com/woodpecker-ci/autoscaler/pull/411)]
|
||||||
|
- fix(deps): update golang deps non-major [[#406](https://github.com/woodpecker-ci/autoscaler/pull/406)]
|
||||||
|
- fix(deps): update golang deps non-major [[#405](https://github.com/woodpecker-ci/autoscaler/pull/405)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-ready-release-go docker tag to v3.4.0 [[#402](https://github.com/woodpecker-ci/autoscaler/pull/402)]
|
||||||
|
- chore(deps): update pre-commit hook pre-commit/pre-commit-hooks to v6 [[#403](https://github.com/woodpecker-ci/autoscaler/pull/403)]
|
||||||
|
- fix(deps): update golang deps non-major [[#404](https://github.com/woodpecker-ci/autoscaler/pull/404)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 51f8813 [[#401](https://github.com/woodpecker-ci/autoscaler/pull/401)]
|
||||||
|
- fix(deps): update module golang.org/x/net to v0.43.0 [[#400](https://github.com/woodpecker-ci/autoscaler/pull/400)]
|
||||||
|
- fix(deps): update golang deps non-major [[#399](https://github.com/woodpecker-ci/autoscaler/pull/399)]
|
||||||
|
- fix(deps): update golang deps non-major [[#398](https://github.com/woodpecker-ci/autoscaler/pull/398)]
|
||||||
|
- fix(deps): update golang deps non-major [[#395](https://github.com/woodpecker-ci/autoscaler/pull/395)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.236.0 [[#394](https://github.com/woodpecker-ci/autoscaler/pull/394)]
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v2.3.0 [[#393](https://github.com/woodpecker-ci/autoscaler/pull/393)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 645b1fa [[#389](https://github.com/woodpecker-ci/autoscaler/pull/389)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.235.0 [[#390](https://github.com/woodpecker-ci/autoscaler/pull/390)]
|
||||||
|
- fix(deps): update golang deps non-major [[#388](https://github.com/woodpecker-ci/autoscaler/pull/388)]
|
||||||
|
- fix(deps): update module github.com/vultr/govultr/v3 to v3.21.1 [[#387](https://github.com/woodpecker-ci/autoscaler/pull/387)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.232.0 [[#385](https://github.com/woodpecker-ci/autoscaler/pull/385)]
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
|
||||||
|
- Use list format for pipelines [[#412](https://github.com/woodpecker-ci/autoscaler/pull/412)]
|
||||||
|
- [pre-commit.ci] pre-commit autoupdate [[#397](https://github.com/woodpecker-ci/autoscaler/pull/397)]
|
||||||
|
|
||||||
|
## [1.1.0](https://github.com/woodpecker-ci/autoscaler/releases/tag/1.1.0) - 2025-07-13
|
||||||
|
|
||||||
|
### ❤️ Thanks to all contributors! ❤️
|
||||||
|
|
||||||
|
@xoxys
|
||||||
|
|
||||||
|
### 📈 Enhancement
|
||||||
|
|
||||||
|
- Introduce global user-data flag [[#337](https://github.com/woodpecker-ci/autoscaler/pull/337)]
|
||||||
|
|
||||||
|
### 📦️ Dependency
|
||||||
|
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v2.2.2 [[#383](https://github.com/woodpecker-ci/autoscaler/pull/383)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-ready-release-go docker tag to v3.3.0 [[#377](https://github.com/woodpecker-ci/autoscaler/pull/377)]
|
||||||
|
- chore(deps): update docker.io/woodpeckerci/plugin-docker-buildx docker tag to v6.0.2 [[#376](https://github.com/woodpecker-ci/autoscaler/pull/376)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 6ae5c78 [[#382](https://github.com/woodpecker-ci/autoscaler/pull/382)]
|
||||||
|
- fix(deps): update module github.com/hetznercloud/hcloud-go/v2 to v2.22.0 [[#384](https://github.com/woodpecker-ci/autoscaler/pull/384)]
|
||||||
|
- fix(deps): update module golang.org/x/net to v0.42.0 [[#381](https://github.com/woodpecker-ci/autoscaler/pull/381)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.231.0 [[#380](https://github.com/woodpecker-ci/autoscaler/pull/380)]
|
||||||
|
- fix(deps): update golang deps non-major [[#379](https://github.com/woodpecker-ci/autoscaler/pull/379)]
|
||||||
|
- fix(deps): update module go.woodpecker-ci.org/woodpecker/v3 to v3.8.0 [[#378](https://github.com/woodpecker-ci/autoscaler/pull/378)]
|
||||||
|
- fix(deps): update golang deps non-major [[#375](https://github.com/woodpecker-ci/autoscaler/pull/375)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.229.0 [[#374](https://github.com/woodpecker-ci/autoscaler/pull/374)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.228.0 [[#373](https://github.com/woodpecker-ci/autoscaler/pull/373)]
|
||||||
|
- fix(deps): update module github.com/linode/linodego to v1.52.2 [[#372](https://github.com/woodpecker-ci/autoscaler/pull/372)]
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v2.2.1 [[#371](https://github.com/woodpecker-ci/autoscaler/pull/371)]
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v2.2.0 [[#370](https://github.com/woodpecker-ci/autoscaler/pull/370)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.227.0 [[#369](https://github.com/woodpecker-ci/autoscaler/pull/369)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.226.0 [[#368](https://github.com/woodpecker-ci/autoscaler/pull/368)]
|
||||||
|
- fix(deps): update module github.com/vultr/govultr/v3 to v3.21.0 [[#367](https://github.com/woodpecker-ci/autoscaler/pull/367)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to b7579e2 [[#366](https://github.com/woodpecker-ci/autoscaler/pull/366)]
|
||||||
|
- fix(deps): update golang deps non-major [[#365](https://github.com/woodpecker-ci/autoscaler/pull/365)]
|
||||||
|
- fix(deps): update module github.com/urfave/cli/v3 to v3.3.8 [[#364](https://github.com/woodpecker-ci/autoscaler/pull/364)]
|
||||||
|
- fix(deps): update golang deps non-major [[#363](https://github.com/woodpecker-ci/autoscaler/pull/363)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.225.0 [[#362](https://github.com/woodpecker-ci/autoscaler/pull/362)]
|
||||||
|
- fix(deps): update golang deps non-major [[#361](https://github.com/woodpecker-ci/autoscaler/pull/361)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to dcc06ee [[#360](https://github.com/woodpecker-ci/autoscaler/pull/360)]
|
||||||
|
- fix(deps): update module github.com/hetznercloud/hcloud-go/v2 to v2.21.1 [[#359](https://github.com/woodpecker-ci/autoscaler/pull/359)]
|
||||||
|
- fix(deps): update module github.com/linode/linodego to v1.52.1 [[#358](https://github.com/woodpecker-ci/autoscaler/pull/358)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to b6e5de4 [[#357](https://github.com/woodpecker-ci/autoscaler/pull/357)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 65e9200 [[#356](https://github.com/woodpecker-ci/autoscaler/pull/356)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.224.0 [[#355](https://github.com/woodpecker-ci/autoscaler/pull/355)]
|
||||||
|
- fix(deps): update golang deps non-major [[#354](https://github.com/woodpecker-ci/autoscaler/pull/354)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.222.0 [[#353](https://github.com/woodpecker-ci/autoscaler/pull/353)]
|
||||||
|
- fix(deps): update golang deps non-major [[#352](https://github.com/woodpecker-ci/autoscaler/pull/352)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.220.0 [[#351](https://github.com/woodpecker-ci/autoscaler/pull/351)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.219.0 [[#350](https://github.com/woodpecker-ci/autoscaler/pull/350)]
|
||||||
|
- chore(deps): update pre-commit hook igorshubovych/markdownlint-cli to v0.45.0 [[#349](https://github.com/woodpecker-ci/autoscaler/pull/349)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to ce4c2cf [[#345](https://github.com/woodpecker-ci/autoscaler/pull/345)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.218.0 [[#348](https://github.com/woodpecker-ci/autoscaler/pull/348)]
|
||||||
|
- chore(deps): update mstruebing/editorconfig-checker docker tag to v3.3.0 [[#346](https://github.com/woodpecker-ci/autoscaler/pull/346)]
|
||||||
|
- fix(deps): update golang deps non-major [[#347](https://github.com/woodpecker-ci/autoscaler/pull/347)]
|
||||||
|
- fix(deps): update golang deps non-major [[#344](https://github.com/woodpecker-ci/autoscaler/pull/344)]
|
||||||
|
- fix(deps): update golang deps non-major [[#343](https://github.com/woodpecker-ci/autoscaler/pull/343)]
|
||||||
|
- fix(deps): update golang deps non-major [[#342](https://github.com/woodpecker-ci/autoscaler/pull/342)]
|
||||||
|
- chore(deps): update docker.io/woodpeckerci/plugin-docker-buildx docker tag to v6 [[#340](https://github.com/woodpecker-ci/autoscaler/pull/340)]
|
||||||
|
- chore(deps): update pre-commit non-major [[#341](https://github.com/woodpecker-ci/autoscaler/pull/341)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.213.0 [[#335](https://github.com/woodpecker-ci/autoscaler/pull/335)]
|
||||||
|
- fix(deps): update module github.com/urfave/cli/v3 to v3.3.2 [[#334](https://github.com/woodpecker-ci/autoscaler/pull/334)]
|
||||||
|
- fix(deps): update module github.com/urfave/cli/v3 to v3.3.1 [[#333](https://github.com/woodpecker-ci/autoscaler/pull/333)]
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v2.1.5 [[#331](https://github.com/woodpecker-ci/autoscaler/pull/331)]
|
||||||
|
|
||||||
|
## [1.0.0](https://github.com/woodpecker-ci/autoscaler/releases/tag/1.0.0) - 2025-04-24
|
||||||
|
|
||||||
|
### ❤️ Thanks to all contributors! ❤️
|
||||||
|
|
||||||
|
@gsaslis, @xoxys
|
||||||
|
|
||||||
|
### 💥 Breaking changes
|
||||||
|
|
||||||
|
- fix(deps): update module github.com/urfave/cli/v2 to v3 [[#317](https://github.com/woodpecker-ci/autoscaler/pull/317)]
|
||||||
|
|
||||||
|
### 📚 Documentation
|
||||||
|
|
||||||
|
- Fix link to caddy docs [[#326](https://github.com/woodpecker-ci/autoscaler/pull/326)]
|
||||||
|
|
||||||
|
### 📦️ Dependency
|
||||||
|
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.212.0 [[#330](https://github.com/woodpecker-ci/autoscaler/pull/330)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-ready-release-go docker tag to v3.2.0 [[#328](https://github.com/woodpecker-ci/autoscaler/pull/328)]
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v2.1.2 [[#327](https://github.com/woodpecker-ci/autoscaler/pull/327)]
|
||||||
|
- fix(deps): update module go.woodpecker-ci.org/woodpecker/v3 to v3.5.2 [[#325](https://github.com/woodpecker-ci/autoscaler/pull/325)]
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v2.1.1 [[#324](https://github.com/woodpecker-ci/autoscaler/pull/324)]
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v2.1.0 [[#323](https://github.com/woodpecker-ci/autoscaler/pull/323)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 7e4ce0a [[#322](https://github.com/woodpecker-ci/autoscaler/pull/322)]
|
||||||
|
- fix(deps): update golang deps non-major [[#321](https://github.com/woodpecker-ci/autoscaler/pull/321)]
|
||||||
|
- fix(deps): update module github.com/vultr/govultr/v3 to v3.19.1 [[#320](https://github.com/woodpecker-ci/autoscaler/pull/320)]
|
||||||
|
- fix(deps): update golang deps non-major [[#319](https://github.com/woodpecker-ci/autoscaler/pull/319)]
|
||||||
|
- fix(deps): update module golang.org/x/oauth2 to v0.29.0 [[#318](https://github.com/woodpecker-ci/autoscaler/pull/318)]
|
||||||
|
- fix(deps): update golang deps non-major [[#316](https://github.com/woodpecker-ci/autoscaler/pull/316)]
|
||||||
|
- fix(deps): update golang deps non-major [[#315](https://github.com/woodpecker-ci/autoscaler/pull/315)]
|
||||||
|
- fix(deps): update module go.woodpecker-ci.org/woodpecker/v3 to v3.5.0 [[#314](https://github.com/woodpecker-ci/autoscaler/pull/314)]
|
||||||
|
- chore(deps): update dependency go to v1.24.2 [[#313](https://github.com/woodpecker-ci/autoscaler/pull/313)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.211.0 [[#312](https://github.com/woodpecker-ci/autoscaler/pull/312)]
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v2 [[#309](https://github.com/woodpecker-ci/autoscaler/pull/309)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-ready-release-go docker tag to v3.1.4 [[#307](https://github.com/woodpecker-ci/autoscaler/pull/307)]
|
||||||
|
- fix(deps): update module github.com/vultr/govultr/v3 to v3.18.0 [[#308](https://github.com/woodpecker-ci/autoscaler/pull/308)]
|
||||||
|
- fix(deps): update golang deps non-major [[#306](https://github.com/woodpecker-ci/autoscaler/pull/306)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/config to v1.29.11 [[#305](https://github.com/woodpecker-ci/autoscaler/pull/305)]
|
||||||
|
- chore(deps): update docker.io/woodpeckerci/plugin-docker-buildx docker tag to v5.2.2 [[#302](https://github.com/woodpecker-ci/autoscaler/pull/302)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/config to v1.29.10 [[#304](https://github.com/woodpecker-ci/autoscaler/pull/304)]
|
||||||
|
- chore(deps): update pre-commit hook adrienverge/yamllint to v1.37.0 [[#303](https://github.com/woodpecker-ci/autoscaler/pull/303)]
|
||||||
|
- chore(deps): update pre-commit non-major [[#300](https://github.com/woodpecker-ci/autoscaler/pull/300)]
|
||||||
|
- fix(deps): update module github.com/rs/zerolog to v1.34.0 [[#301](https://github.com/woodpecker-ci/autoscaler/pull/301)]
|
||||||
|
- fix(deps): update module github.com/hetznercloud/hcloud-go/v2 to v2.20.1 [[#299](https://github.com/woodpecker-ci/autoscaler/pull/299)]
|
||||||
|
- fix(deps): update golang deps non-major [[#298](https://github.com/woodpecker-ci/autoscaler/pull/298)]
|
||||||
|
- fix(deps): update module go.woodpecker-ci.org/woodpecker/v3 to v3.4.0 [[#297](https://github.com/woodpecker-ci/autoscaler/pull/297)]
|
||||||
|
- chore(deps): update mstruebing/editorconfig-checker docker tag to v3.2.1 [[#296](https://github.com/woodpecker-ci/autoscaler/pull/296)]
|
||||||
|
- chore(deps): update pre-commit hook adrienverge/yamllint to v1.36.1 [[#295](https://github.com/woodpecker-ci/autoscaler/pull/295)]
|
||||||
|
- chore(deps): update pre-commit non-major [[#294](https://github.com/woodpecker-ci/autoscaler/pull/294)]
|
||||||
|
- fix(deps): update golang deps non-major [[#293](https://github.com/woodpecker-ci/autoscaler/pull/293)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.209.0 [[#292](https://github.com/woodpecker-ci/autoscaler/pull/292)]
|
||||||
|
- fix(deps): update module github.com/vultr/govultr/v3 to v3.16.1 [[#291](https://github.com/woodpecker-ci/autoscaler/pull/291)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.208.0 [[#290](https://github.com/woodpecker-ci/autoscaler/pull/290)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 054e65f [[#289](https://github.com/woodpecker-ci/autoscaler/pull/289)]
|
||||||
|
- fix(deps): update golang deps non-major [[#288](https://github.com/woodpecker-ci/autoscaler/pull/288)]
|
||||||
|
- fix(deps): update golang deps non-major [[#287](https://github.com/woodpecker-ci/autoscaler/pull/287)]
|
||||||
|
- fix(deps): update golang deps non-major [[#286](https://github.com/woodpecker-ci/autoscaler/pull/286)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to dead583 [[#284](https://github.com/woodpecker-ci/autoscaler/pull/284)]
|
||||||
|
- fix(deps): update golang deps non-major [[#282](https://github.com/woodpecker-ci/autoscaler/pull/282)]
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
|
||||||
|
- Bump golangci-lint to v2 [[#311](https://github.com/woodpecker-ci/autoscaler/pull/311)]
|
||||||
|
- Run tests also on makefile changes [[#310](https://github.com/woodpecker-ci/autoscaler/pull/310)]
|
||||||
|
- [pre-commit.ci] pre-commit autoupdate [[#285](https://github.com/woodpecker-ci/autoscaler/pull/285)]
|
||||||
|
|
||||||
|
## [0.6.0](https://github.com/woodpecker-ci/autoscaler/releases/tag/0.6.0) - 2025-02-27
|
||||||
|
|
||||||
|
### ❤️ Thanks to all contributors! ❤️
|
||||||
|
|
||||||
|
@xoxys
|
||||||
|
|
||||||
|
### 📈 Enhancement
|
||||||
|
|
||||||
|
- Add hcloud server type fallback list [[#275](https://github.com/woodpecker-ci/autoscaler/pull/275)]
|
||||||
|
|
||||||
|
### 📦️ Dependency
|
||||||
|
|
||||||
|
- fix(deps): update golang deps non-major [[#281](https://github.com/woodpecker-ci/autoscaler/pull/281)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.204.0 [[#280](https://github.com/woodpecker-ci/autoscaler/pull/280)]
|
||||||
|
- fix(deps): update module golang.org/x/oauth2 to v0.27.0 [[#279](https://github.com/woodpecker-ci/autoscaler/pull/279)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to aa4b98e [[#276](https://github.com/woodpecker-ci/autoscaler/pull/276)]
|
||||||
|
|
||||||
|
## [0.5.1](https://github.com/woodpecker-ci/autoscaler/releases/tag/0.5.1) - 2025-02-21
|
||||||
|
|
||||||
|
### ❤️ Thanks to all contributors! ❤️
|
||||||
|
|
||||||
|
@henkka
|
||||||
|
|
||||||
|
### 📚 Documentation
|
||||||
|
|
||||||
|
- docs: fix typos [[#259](https://github.com/woodpecker-ci/autoscaler/pull/259)]
|
||||||
|
|
||||||
|
### 📦️ Dependency
|
||||||
|
|
||||||
|
- fix(deps): update module go.woodpecker-ci.org/woodpecker/v2 to v3 [[#266](https://github.com/woodpecker-ci/autoscaler/pull/266)]
|
||||||
|
- fix(deps): update golang deps non-major [[#265](https://github.com/woodpecker-ci/autoscaler/pull/265)]
|
||||||
|
- chore(deps): update golang docker tag to v1.24 [[#273](https://github.com/woodpecker-ci/autoscaler/pull/273)]
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v1.64.5 [[#271](https://github.com/woodpecker-ci/autoscaler/pull/271)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to eff6e97 [[#274](https://github.com/woodpecker-ci/autoscaler/pull/274)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 939b2ce [[#270](https://github.com/woodpecker-ci/autoscaler/pull/270)]
|
||||||
|
- chore(deps): update docker.io/golang docker tag to v1.24 [[#272](https://github.com/woodpecker-ci/autoscaler/pull/272)]
|
||||||
|
- chore(deps): update pre-commit non-major [[#264](https://github.com/woodpecker-ci/autoscaler/pull/264)]
|
||||||
|
- chore(deps): update docker.io/woodpeckerci/plugin-docker-buildx docker tag to v5.2.1 [[#263](https://github.com/woodpecker-ci/autoscaler/pull/263)]
|
||||||
|
- chore(deps): update mstruebing/editorconfig-checker docker tag to v3.2.0 [[#269](https://github.com/woodpecker-ci/autoscaler/pull/269)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to f9890c6 [[#267](https://github.com/woodpecker-ci/autoscaler/pull/267)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-ready-release-go docker tag to v3.1.3 [[#262](https://github.com/woodpecker-ci/autoscaler/pull/262)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.201.0 [[#261](https://github.com/woodpecker-ci/autoscaler/pull/261)]
|
||||||
|
- fix(deps): update module github.com/hetznercloud/hcloud-go/v2 to v2.18.0 [[#260](https://github.com/woodpecker-ci/autoscaler/pull/260)]
|
||||||
|
- fix(deps): update golang deps non-major [[#257](https://github.com/woodpecker-ci/autoscaler/pull/257)]
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
|
||||||
|
- [pre-commit.ci] pre-commit autoupdate [[#268](https://github.com/woodpecker-ci/autoscaler/pull/268)]
|
||||||
|
|
||||||
|
## [0.5.0](https://github.com/woodpecker-ci/autoscaler/releases/tag/0.5.0) - 2025-01-17
|
||||||
|
|
||||||
|
### ❤️ Thanks to all contributors! ❤️
|
||||||
|
|
||||||
|
@xoxys
|
||||||
|
|
||||||
|
### ✨ Features
|
||||||
|
|
||||||
|
- Add Scaleway provider [[#252](https://github.com/woodpecker-ci/autoscaler/pull/252)]
|
||||||
|
- Add Vultr provider [[#251](https://github.com/woodpecker-ci/autoscaler/pull/251)]
|
||||||
|
|
||||||
|
### 📦️ Dependency
|
||||||
|
|
||||||
|
- chore(deps): update dependency go to v1.23.5 [[#255](https://github.com/woodpecker-ci/autoscaler/pull/255)]
|
||||||
|
|
||||||
|
## [0.4.0](https://github.com/woodpecker-ci/autoscaler/releases/tag/0.4.0) - 2025-01-16
|
||||||
|
|
||||||
|
### ❤️ Thanks to all contributors! ❤️
|
||||||
|
|
||||||
|
@keslerm, @pat-s, @xoxys
|
||||||
|
|
||||||
|
### 📈 Enhancement
|
||||||
|
|
||||||
|
- Wait for AWS instance availablity before returning [[#227](https://github.com/woodpecker-ci/autoscaler/pull/227)]
|
||||||
|
|
||||||
|
### 📦️ Dependency
|
||||||
|
|
||||||
|
- fix(deps): update golang deps non-major [[#254](https://github.com/woodpecker-ci/autoscaler/pull/254)]
|
||||||
|
- fix(deps): update golang deps non-major [[#253](https://github.com/woodpecker-ci/autoscaler/pull/253)]
|
||||||
|
- fix(deps): update golang deps non-major [[#248](https://github.com/woodpecker-ci/autoscaler/pull/248)]
|
||||||
|
- fix(deps): update module go.woodpecker-ci.org/woodpecker/v2 to v2.8.3 [[#247](https://github.com/woodpecker-ci/autoscaler/pull/247)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/config to v1.28.10 [[#245](https://github.com/woodpecker-ci/autoscaler/pull/245)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 7588d65 [[#244](https://github.com/woodpecker-ci/autoscaler/pull/244)]
|
||||||
|
- chore(deps): update mstruebing/editorconfig-checker docker tag to v3.1.2 [[#246](https://github.com/woodpecker-ci/autoscaler/pull/246)]
|
||||||
|
- fix(deps): update golang deps non-major [[#243](https://github.com/woodpecker-ci/autoscaler/pull/243)]
|
||||||
|
- fix(deps): update golang deps non-major [[#242](https://github.com/woodpecker-ci/autoscaler/pull/242)]
|
||||||
|
- fix(deps): update golang deps non-major [[#241](https://github.com/woodpecker-ci/autoscaler/pull/241)]
|
||||||
|
- fix(deps): update module golang.org/x/oauth2 to v0.25.0 [[#239](https://github.com/woodpecker-ci/autoscaler/pull/239)]
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v1.63.4 [[#238](https://github.com/woodpecker-ci/autoscaler/pull/238)]
|
||||||
|
- fix(deps): update module go.woodpecker-ci.org/woodpecker/v2 to v2.8.2 [[#236](https://github.com/woodpecker-ci/autoscaler/pull/236)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 7d7fa50 [[#235](https://github.com/woodpecker-ci/autoscaler/pull/235)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-ready-release-go docker tag to v3.1.1 [[#237](https://github.com/woodpecker-ci/autoscaler/pull/237)]
|
||||||
|
- fix(deps): update golang deps non-major [[#234](https://github.com/woodpecker-ci/autoscaler/pull/234)]
|
||||||
|
- fix(deps): update golang deps non-major [[#233](https://github.com/woodpecker-ci/autoscaler/pull/233)]
|
||||||
|
- fix(deps): update module github.com/hetznercloud/hcloud-go/v2 to v2.17.1 [[#232](https://github.com/woodpecker-ci/autoscaler/pull/232)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.198.0 [[#231](https://github.com/woodpecker-ci/autoscaler/pull/231)]
|
||||||
|
- fix(deps): update golang deps non-major [[#229](https://github.com/woodpecker-ci/autoscaler/pull/229)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 4a55095 [[#228](https://github.com/woodpecker-ci/autoscaler/pull/228)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.196.0 [[#221](https://github.com/woodpecker-ci/autoscaler/pull/221)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-ready-release-go docker tag to v3.1.0 [[#220](https://github.com/woodpecker-ci/autoscaler/pull/220)]
|
||||||
|
- chore(deps): update docker.io/woodpeckerci/plugin-docker-buildx docker tag to v5.1.0 [[#219](https://github.com/woodpecker-ci/autoscaler/pull/219)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 43b7b7c [[#218](https://github.com/woodpecker-ci/autoscaler/pull/218)]
|
||||||
|
- fix(deps): update module golang.org/x/net to v0.32.0 [[#217](https://github.com/woodpecker-ci/autoscaler/pull/217)]
|
||||||
|
- chore(deps): update dependency go to v1.23.4 [[#216](https://github.com/woodpecker-ci/autoscaler/pull/216)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-ready-release-go docker tag to v3 [[#215](https://github.com/woodpecker-ci/autoscaler/pull/215)]
|
||||||
|
- fix(deps): update golang deps non-major [[#213](https://github.com/woodpecker-ci/autoscaler/pull/213)]
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
|
||||||
|
- Fix deprecated editorconfig filename [[#250](https://github.com/woodpecker-ci/autoscaler/pull/250)]
|
||||||
|
- Include tags from AWS config in instance creation [[#223](https://github.com/woodpecker-ci/autoscaler/pull/223)]
|
||||||
|
- Make sure to use the AWS Region when specified [[#224](https://github.com/woodpecker-ci/autoscaler/pull/224)]
|
||||||
|
- Rename linter [[#240](https://github.com/woodpecker-ci/autoscaler/pull/240)]
|
||||||
|
|
||||||
|
## [0.3.1](https://github.com/woodpecker-ci/autoscaler/releases/tag/0.3.1) - 2024-11-30
|
||||||
|
|
||||||
|
### ❤️ Thanks to all contributors! ❤️
|
||||||
|
|
||||||
|
@henkka, @pat-s, @xoxys
|
||||||
|
|
||||||
|
### 📦️ Dependency
|
||||||
|
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v1.62.2 [[#211](https://github.com/woodpecker-ci/autoscaler/pull/211)]
|
||||||
|
- fix(deps): update module go.woodpecker-ci.org/woodpecker/v2 to v2.8.0 [[#210](https://github.com/woodpecker-ci/autoscaler/pull/210)]
|
||||||
|
- fix(deps): update module github.com/aws/aws-sdk-go-v2/service/ec2 to v1.194.0 [[#208](https://github.com/woodpecker-ci/autoscaler/pull/208)]
|
||||||
|
- fix(deps): update module github.com/stretchr/testify to v1.10.0 [[#207](https://github.com/woodpecker-ci/autoscaler/pull/207)]
|
||||||
|
- chore(deps): update pre-commit hook igorshubovych/markdownlint-cli to v0.43.0 [[#206](https://github.com/woodpecker-ci/autoscaler/pull/206)]
|
||||||
|
- fix(deps): update module github.com/hetznercloud/hcloud-go/v2 to v2.17.0 [[#205](https://github.com/woodpecker-ci/autoscaler/pull/205)]
|
||||||
|
- fix(deps): update golang deps non-major [[#203](https://github.com/woodpecker-ci/autoscaler/pull/203)]
|
||||||
|
- fix(deps): update golang deps non-major [[#201](https://github.com/woodpecker-ci/autoscaler/pull/201)]
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v1.62.0 [[#200](https://github.com/woodpecker-ci/autoscaler/pull/200)]
|
||||||
|
- fix(deps): update golang deps non-major [[#197](https://github.com/woodpecker-ci/autoscaler/pull/197)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-ready-release-go docker tag to v2.1.1 [[#199](https://github.com/woodpecker-ci/autoscaler/pull/199)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 2d47ceb [[#198](https://github.com/woodpecker-ci/autoscaler/pull/198)]
|
||||||
|
- fix(deps): update module go.woodpecker-ci.org/woodpecker/v2 to v2.7.2 [[#196](https://github.com/woodpecker-ci/autoscaler/pull/196)]
|
||||||
|
- chore(deps): update node.js to v22 [[#193](https://github.com/woodpecker-ci/autoscaler/pull/193)]
|
||||||
|
- fix(deps): update module github.com/hetznercloud/hcloud-go/v2 to v2.15.0 [[#194](https://github.com/woodpecker-ci/autoscaler/pull/194)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to f66d83c [[#188](https://github.com/woodpecker-ci/autoscaler/pull/188)]
|
||||||
|
- chore(deps): update pre-commit hook pre-commit/pre-commit-hooks to v5 [[#187](https://github.com/woodpecker-ci/autoscaler/pull/187)]
|
||||||
|
- fix(deps): update golang deps non-major [[#186](https://github.com/woodpecker-ci/autoscaler/pull/186)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-ready-release-go docker tag to v2 [[#185](https://github.com/woodpecker-ci/autoscaler/pull/185)]
|
||||||
|
- chore(deps): update golang docker tag to v1.23 [[#181](https://github.com/woodpecker-ci/autoscaler/pull/181)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-ready-release-go docker tag to v1.2.0 [[#182](https://github.com/woodpecker-ci/autoscaler/pull/182)]
|
||||||
|
- fix(deps): update golang deps non-major [[#183](https://github.com/woodpecker-ci/autoscaler/pull/183)]
|
||||||
|
- chore(deps): update pre-commit non-major [[#184](https://github.com/woodpecker-ci/autoscaler/pull/184)]
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
|
||||||
|
- docs: add AWS as supported cloud provider [[#209](https://github.com/woodpecker-ci/autoscaler/pull/209)]
|
||||||
|
- ci: remove `renovate` branch triggers [[#204](https://github.com/woodpecker-ci/autoscaler/pull/204)]
|
||||||
|
- [pre-commit.ci] pre-commit autoupdate [[#195](https://github.com/woodpecker-ci/autoscaler/pull/195)]
|
||||||
|
- Bump buildx plugin image for pipeline [[#192](https://github.com/woodpecker-ci/autoscaler/pull/192)]
|
||||||
|
- [pre-commit.ci] pre-commit autoupdate [[#176](https://github.com/woodpecker-ci/autoscaler/pull/176)]
|
||||||
|
|
||||||
|
## [0.3.0](https://github.com/woodpecker-ci/autoscaler/releases/tag/0.3.0) - 2024-09-20
|
||||||
|
|
||||||
|
### ❤️ Thanks to all contributors! ❤️
|
||||||
|
|
||||||
|
@anbraten, @hhamalai, @qwerty287, @xoxys
|
||||||
|
|
||||||
|
### ✨ Features
|
||||||
|
|
||||||
|
- Add AWS provider [[#118](https://github.com/woodpecker-ci/autoscaler/pull/118)]
|
||||||
|
- Reactivate agents and prevent draining recently active agents [[#163](https://github.com/woodpecker-ci/autoscaler/pull/163)]
|
||||||
|
- Add agent idle timeout [[#162](https://github.com/woodpecker-ci/autoscaler/pull/162)]
|
||||||
|
- Allow to filter for specific tasks [[#134](https://github.com/woodpecker-ci/autoscaler/pull/134)]
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Fix drain agents [[#156](https://github.com/woodpecker-ci/autoscaler/pull/156)]
|
||||||
|
- Return error on unknown server types [[#151](https://github.com/woodpecker-ci/autoscaler/pull/151)]
|
||||||
|
|
||||||
|
### 📈 Enhancement
|
||||||
|
|
||||||
|
- Allow to remove an agent as soon as it connected once, but has no more tasks left [[#92](https://github.com/woodpecker-ci/autoscaler/pull/92)]
|
||||||
|
- Improve error handling [[#155](https://github.com/woodpecker-ci/autoscaler/pull/155)]
|
||||||
|
- Use docker gpg key from download.docker.com [[#154](https://github.com/woodpecker-ci/autoscaler/pull/154)]
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 701f63a [[#178](https://github.com/woodpecker-ci/autoscaler/pull/178)]
|
||||||
|
- chore(deps): update mstruebing/editorconfig-checker docker tag to v3.0.3 [[#179](https://github.com/woodpecker-ci/autoscaler/pull/179)]
|
||||||
|
- fix(deps): update golang deps non-major [[#173](https://github.com/woodpecker-ci/autoscaler/pull/173)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 778ce7b [[#174](https://github.com/woodpecker-ci/autoscaler/pull/174)]
|
||||||
|
- chore(deps): update golang deps non-major [[#170](https://github.com/woodpecker-ci/autoscaler/pull/170)]
|
||||||
|
- fix(deps): update go.woodpecker-ci.org/woodpecker/v2 digest to 987c201 [[#169](https://github.com/woodpecker-ci/autoscaler/pull/169)]
|
||||||
|
- [pre-commit.ci] pre-commit autoupdate [[#160](https://github.com/woodpecker-ci/autoscaler/pull/160)]
|
||||||
|
- fix(deps): update module github.com/linode/linodego to v1.36.1 [[#161](https://github.com/woodpecker-ci/autoscaler/pull/161)]
|
||||||
|
- fix(deps): update golang deps non-major [[#159](https://github.com/woodpecker-ci/autoscaler/pull/159)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 7f521ea [[#158](https://github.com/woodpecker-ci/autoscaler/pull/158)]
|
||||||
|
- fix(deps): update golang deps non-major [[#150](https://github.com/woodpecker-ci/autoscaler/pull/150)]
|
||||||
|
- [pre-commit.ci] pre-commit autoupdate [[#149](https://github.com/woodpecker-ci/autoscaler/pull/149)]
|
||||||
|
- Fix deprecations and run on renovate branches [[#147](https://github.com/woodpecker-ci/autoscaler/pull/147)]
|
||||||
|
- chore(deps): update mstruebing/editorconfig-checker docker tag to v3 [[#138](https://github.com/woodpecker-ci/autoscaler/pull/138)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-docker-buildx docker tag to v4 [[#143](https://github.com/woodpecker-ci/autoscaler/pull/143)]
|
||||||
|
- fix(deps): update golang deps non-major [[#142](https://github.com/woodpecker-ci/autoscaler/pull/142)]
|
||||||
|
- chore(deps): update golang docker tag to v1.22.3 [[#144](https://github.com/woodpecker-ci/autoscaler/pull/144)]
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v1.58.1 [[#145](https://github.com/woodpecker-ci/autoscaler/pull/145)]
|
||||||
|
- chore(deps): update pre-commit non-major [[#139](https://github.com/woodpecker-ci/autoscaler/pull/139)]
|
||||||
|
- fix(deps): update module golang.org/x/oauth2 to v0.20.0 [[#140](https://github.com/woodpecker-ci/autoscaler/pull/140)]
|
||||||
|
- fix(deps): update module github.com/urfave/cli/v2 to v2.27.2 [[#136](https://github.com/woodpecker-ci/autoscaler/pull/136)]
|
||||||
|
- fix(deps): update module github.com/linode/linodego to v1.33.0 [[#135](https://github.com/woodpecker-ci/autoscaler/pull/135)]
|
||||||
|
- fix(deps): update module github.com/hetznercloud/hcloud-go/v2 to v2.7.2 [[#132](https://github.com/woodpecker-ci/autoscaler/pull/132)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-ready-release-go docker tag to v1.1.1 [[#131](https://github.com/woodpecker-ci/autoscaler/pull/131)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to fe59bbe [[#130](https://github.com/woodpecker-ci/autoscaler/pull/130)]
|
||||||
|
- chore(deps): update pre-commit hook pre-commit/pre-commit-hooks to v4.6.0 [[#128](https://github.com/woodpecker-ci/autoscaler/pull/128)]
|
||||||
|
- chore(deps): update golang docker tag to v1.22.2 [[#127](https://github.com/woodpecker-ci/autoscaler/pull/127)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to c0f41cb [[#126](https://github.com/woodpecker-ci/autoscaler/pull/126)]
|
||||||
|
- fix(deps): update golang deps non-major [[#125](https://github.com/woodpecker-ci/autoscaler/pull/125)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-docker-buildx docker tag to v3.2.1 [[#124](https://github.com/woodpecker-ci/autoscaler/pull/124)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to a685a6e [[#122](https://github.com/woodpecker-ci/autoscaler/pull/122)]
|
||||||
|
- chore(deps): update pre-commit hook golangci/golangci-lint to v1.57.2 [[#123](https://github.com/woodpecker-ci/autoscaler/pull/123)]
|
||||||
|
- fix(deps): update module github.com/hetznercloud/hcloud-go/v2 to v2.7.0 [[#121](https://github.com/woodpecker-ci/autoscaler/pull/121)]
|
||||||
|
- chore(deps): update pre-commit non-major [[#120](https://github.com/woodpecker-ci/autoscaler/pull/120)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to a85f2c6 [[#119](https://github.com/woodpecker-ci/autoscaler/pull/119)]
|
||||||
|
- fix(deps): update golang deps non-major [[#116](https://github.com/woodpecker-ci/autoscaler/pull/116)]
|
||||||
|
|
||||||
|
## [0.2.0](https://github.com/woodpecker-ci/autoscaler/releases/tag/0.2.0) - 2024-03-17
|
||||||
|
|
||||||
|
### ❤️ Thanks to all contributors! ❤️
|
||||||
|
|
||||||
|
@6543, @guisea, @maltejur, @pat-s, @qwerty287, @xoxys
|
||||||
|
|
||||||
|
### ✨ Features
|
||||||
|
|
||||||
|
- Add linode provider [[#15](https://github.com/woodpecker-ci/autoscaler/pull/15)]
|
||||||
|
|
||||||
|
### 📚 Documentation
|
||||||
|
|
||||||
|
- Document `WOODPECKER_PROVIDER` [[#19](https://github.com/woodpecker-ci/autoscaler/pull/19)]
|
||||||
|
|
||||||
|
### 📈 Enhancement
|
||||||
|
|
||||||
|
- Ignore WaitingOnDeps for agent calculation [[#14](https://github.com/woodpecker-ci/autoscaler/pull/14)]
|
||||||
|
- don't require amd64 CPU architecture [[#10](https://github.com/woodpecker-ci/autoscaler/pull/10)]
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Update hetznercloud provider [[#12](https://github.com/woodpecker-ci/autoscaler/pull/12)]
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
|
||||||
|
- Temp disable linode provider [[#115](https://github.com/woodpecker-ci/autoscaler/pull/115)]
|
||||||
|
- Enable more linters and cleanup code [[#114](https://github.com/woodpecker-ci/autoscaler/pull/114)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-docker-buildx docker tag to v3.2.0 [[#113](https://github.com/woodpecker-ci/autoscaler/pull/113)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to c7f7c64 [[#112](https://github.com/woodpecker-ci/autoscaler/pull/112)]
|
||||||
|
- fix(deps): update module github.com/linode/linodego to v1.30.0 [[#111](https://github.com/woodpecker-ci/autoscaler/pull/111)]
|
||||||
|
- chore(deps): update golang docker tag to v1.22.1 [[#110](https://github.com/woodpecker-ci/autoscaler/pull/110)]
|
||||||
|
- fix(deps): update golang deps non-major [[#109](https://github.com/woodpecker-ci/autoscaler/pull/109)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 814bf88 [[#108](https://github.com/woodpecker-ci/autoscaler/pull/108)]
|
||||||
|
- fix(deps): update module github.com/linode/linodego to v1.29.0 [[#107](https://github.com/woodpecker-ci/autoscaler/pull/107)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to ec58324 [[#106](https://github.com/woodpecker-ci/autoscaler/pull/106)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-docker-buildx docker tag to v3.1.0 [[#105](https://github.com/woodpecker-ci/autoscaler/pull/105)]
|
||||||
|
- chore(deps): update golang docker tag [[#101](https://github.com/woodpecker-ci/autoscaler/pull/101)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 2c58cdc [[#100](https://github.com/woodpecker-ci/autoscaler/pull/100)]
|
||||||
|
- fix(deps): update golang deps non-major [[#99](https://github.com/woodpecker-ci/autoscaler/pull/99)]
|
||||||
|
- fix(deps): update module github.com/rs/zerolog to v1.32.0 [[#98](https://github.com/woodpecker-ci/autoscaler/pull/98)]
|
||||||
|
- [pre-commit.ci] pre-commit autoupdate [[#97](https://github.com/woodpecker-ci/autoscaler/pull/97)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-docker-buildx docker tag to v3.0.1 [[#96](https://github.com/woodpecker-ci/autoscaler/pull/96)]
|
||||||
|
- fix(deps): update module go.woodpecker-ci.org/woodpecker/v2 to v2.3.0 [[#95](https://github.com/woodpecker-ci/autoscaler/pull/95)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-ready-release-go docker tag to v1.1.0 [[#89](https://github.com/woodpecker-ci/autoscaler/pull/89)]
|
||||||
|
- fix(deps): update module go.woodpecker-ci.org/woodpecker/v2 to v2.2.2 [[#87](https://github.com/woodpecker-ci/autoscaler/pull/87)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-docker-buildx docker tag to v3 [[#85](https://github.com/woodpecker-ci/autoscaler/pull/85)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 1b97071 [[#86](https://github.com/woodpecker-ci/autoscaler/pull/86)]
|
||||||
|
- Use cleartext username [[#84](https://github.com/woodpecker-ci/autoscaler/pull/84)]
|
||||||
|
- chore(deps): update golang docker tag to v1.21.6 [[#83](https://github.com/woodpecker-ci/autoscaler/pull/83)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to db7319d [[#82](https://github.com/woodpecker-ci/autoscaler/pull/82)]
|
||||||
|
- fix(deps): update golang deps non-major [[#81](https://github.com/woodpecker-ci/autoscaler/pull/81)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-docker-buildx docker tag to v2.3.0 [[#80](https://github.com/woodpecker-ci/autoscaler/pull/80)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to be819d1 [[#79](https://github.com/woodpecker-ci/autoscaler/pull/79)]
|
||||||
|
- fix(deps): update module github.com/urfave/cli/v2 to v2.27.1 [[#78](https://github.com/woodpecker-ci/autoscaler/pull/78)]
|
||||||
|
- [pre-commit.ci] pre-commit autoupdate [[#77](https://github.com/woodpecker-ci/autoscaler/pull/77)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 02704c9 [[#76](https://github.com/woodpecker-ci/autoscaler/pull/76)]
|
||||||
|
- fix(deps): update module go.woodpecker-ci.org/woodpecker to v2 [[#75](https://github.com/woodpecker-ci/autoscaler/pull/75)]
|
||||||
|
- fix(deps): update module github.com/urfave/cli/v2 to v2.27.0 [[#74](https://github.com/woodpecker-ci/autoscaler/pull/74)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to dc181d7 [[#71](https://github.com/woodpecker-ci/autoscaler/pull/71)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to aacd6d4 [[#69](https://github.com/woodpecker-ci/autoscaler/pull/69)]
|
||||||
|
- fix(deps): update module github.com/hetznercloud/hcloud-go/v2 to v2.5.1 [[#68](https://github.com/woodpecker-ci/autoscaler/pull/68)]
|
||||||
|
- chore(deps): update golang docker tag to v1.21.5 [[#67](https://github.com/woodpecker-ci/autoscaler/pull/67)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to f3f8817 [[#66](https://github.com/woodpecker-ci/autoscaler/pull/66)]
|
||||||
|
- fix(deps): update module github.com/urfave/cli/v2 to v2.26.0 [[#65](https://github.com/woodpecker-ci/autoscaler/pull/65)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 6522937 [[#64](https://github.com/woodpecker-ci/autoscaler/pull/64)]
|
||||||
|
- fix(deps): update golang deps non-major [[#63](https://github.com/woodpecker-ci/autoscaler/pull/63)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-ready-release-go docker tag to v1.0.3 [[#62](https://github.com/woodpecker-ci/autoscaler/pull/62)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-docker-buildx docker tag to v2.2.1 [[#61](https://github.com/woodpecker-ci/autoscaler/pull/61)]
|
||||||
|
- fix(deps): update golang deps non-major [[#60](https://github.com/woodpecker-ci/autoscaler/pull/60)]
|
||||||
|
- chore(deps): update golang docker tag to v1.21.4 [[#59](https://github.com/woodpecker-ci/autoscaler/pull/59)]
|
||||||
|
- fix(deps): update golang.org/x/exp digest to 9a3e603 [[#58](https://github.com/woodpecker-ci/autoscaler/pull/58)]
|
||||||
|
- Add linters and `pre-commit` [[#57](https://github.com/woodpecker-ci/autoscaler/pull/57)]
|
||||||
|
- fix(deps): update module go.woodpecker-ci.org/woodpecker to v1 [[#54](https://github.com/woodpecker-ci/autoscaler/pull/54)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-ready-release-go docker tag to v1 [[#53](https://github.com/woodpecker-ci/autoscaler/pull/53)]
|
||||||
|
- Go vanity urls for autoscaler [[#48](https://github.com/woodpecker-ci/autoscaler/pull/48)]
|
||||||
|
- fix(deps): update module github.com/woodpecker-ci/woodpecker to v1.0.4 [[#47](https://github.com/woodpecker-ci/autoscaler/pull/47)]
|
||||||
|
- chore(deps): update woodpeckerci/plugin-ready-release-go docker tag to v0.7.0 [[#45](https://github.com/woodpecker-ci/autoscaler/pull/45)]
|
||||||
|
- renovate: use org config [[#43](https://github.com/woodpecker-ci/autoscaler/pull/43)]
|
||||||
|
- Fix indentation in cloudconfig multiline string [[#42](https://github.com/woodpecker-ci/autoscaler/pull/42)]
|
||||||
|
- Update woodpeckerci/plugin-docker-buildx Docker tag to v2.2.0 [[#41](https://github.com/woodpecker-ci/autoscaler/pull/41)]
|
||||||
|
- Update Node.js to v21 [[#40](https://github.com/woodpecker-ci/autoscaler/pull/40)]
|
||||||
|
- Update module github.com/woodpecker-ci/woodpecker to v1.0.3 [[#39](https://github.com/woodpecker-ci/autoscaler/pull/39)]
|
||||||
|
- Update module github.com/hetznercloud/hcloud-go/v2 to v2.4.0 [[#38](https://github.com/woodpecker-ci/autoscaler/pull/38)]
|
||||||
|
- Update golang Docker tag to v1.21.3 [[#37](https://github.com/woodpecker-ci/autoscaler/pull/37)]
|
||||||
|
- Update module golang.org/x/net to v0.17.0 [[#36](https://github.com/woodpecker-ci/autoscaler/pull/36)]
|
||||||
|
- Update mstruebing/editorconfig-checker Docker tag to v2.7.2 [[#35](https://github.com/woodpecker-ci/autoscaler/pull/35)]
|
||||||
|
- Update module github.com/woodpecker-ci/woodpecker to v1 [[#34](https://github.com/woodpecker-ci/autoscaler/pull/34)]
|
||||||
|
- Update Node.js to v20 [[#33](https://github.com/woodpecker-ci/autoscaler/pull/33)]
|
||||||
|
- Update module golang.org/x/oauth2 to v0.13.0 [[#32](https://github.com/woodpecker-ci/autoscaler/pull/32)]
|
||||||
|
- Update module github.com/hetznercloud/hcloud-go/v2 to v2.3.0 [[#27](https://github.com/woodpecker-ci/autoscaler/pull/27)]
|
||||||
|
- Update golang Docker tag to v1.21.2 [[#30](https://github.com/woodpecker-ci/autoscaler/pull/30)]
|
||||||
|
- Update golang.org/x/exp digest to 7918f67 [[#29](https://github.com/woodpecker-ci/autoscaler/pull/29)]
|
||||||
|
- Update module golang.org/x/net to v0.16.0 [[#31](https://github.com/woodpecker-ci/autoscaler/pull/31)]
|
||||||
|
- Update module github.com/rs/zerolog to v1.31.0 [[#28](https://github.com/woodpecker-ci/autoscaler/pull/28)]
|
||||||
|
- Update golang Docker tag [[#26](https://github.com/woodpecker-ci/autoscaler/pull/26)]
|
||||||
|
- Update module github.com/urfave/cli/v2 to v2.25.7 [[#22](https://github.com/woodpecker-ci/autoscaler/pull/22)]
|
||||||
|
- Update golang.org/x/exp digest to 9212866 [[#21](https://github.com/woodpecker-ci/autoscaler/pull/21)]
|
||||||
|
- Add renovate [[#20](https://github.com/woodpecker-ci/autoscaler/pull/20)]
|
||||||
|
|
||||||
|
## [0.1.0](https://github.com/woodpecker-ci/autoscaler/releases/tag/0.1.0) - 2023-07-28
|
||||||
|
|
||||||
|
### ❤️ Thanks to all contributors! ❤️
|
||||||
|
|
||||||
|
@anbraten, @xoxys
|
||||||
|
|
||||||
|
### ✨ Features
|
||||||
|
|
||||||
|
- Refactor project structure and improve agent calculation [[#3](https://github.com/woodpecker-ci/autoscaler/pull/3)]
|
||||||
|
|
||||||
|
### 📈 Enhancement
|
||||||
|
|
||||||
|
- Renamings and failing on parsing errors [[#9](https://github.com/woodpecker-ci/autoscaler/pull/9)]
|
||||||
|
- Add release helper [[#7](https://github.com/woodpecker-ci/autoscaler/pull/7)]
|
||||||
|
- Add hetznercloud network, firewall and ssh-key options [[#4](https://github.com/woodpecker-ci/autoscaler/pull/4)]
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
|
||||||
|
- Allow to set agent min-time-alive as user [[#5](https://github.com/woodpecker-ci/autoscaler/pull/5)]
|
||||||
|
- Add ci test workflow [[#6](https://github.com/woodpecker-ci/autoscaler/pull/6)]
|
||||||
|
- Add container image [[#1](https://github.com/woodpecker-ci/autoscaler/pull/1)]
|
||||||
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
FROM golang:1.23-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN GOMAXPROCS=1 go build -p 1 -o woodpecker-autoscaler ./cmd/woodpecker-autoscaler
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /build/woodpecker-autoscaler /usr/local/bin/
|
||||||
|
|
||||||
|
ENTRYPOINT ["woodpecker-autoscaler"]
|
||||||
201
LICENSE
Normal file
201
LICENSE
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
133
Makefile
Normal file
133
Makefile
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# renovate: datasource=github-releases depName=mvdan/gofumpt
|
||||||
|
GOFUMPT_VERSION := v0.10.0
|
||||||
|
# renovate: datasource=github-releases depName=golangci/golangci-lint
|
||||||
|
GOLANGCI_LINT_VERSION := v2.12.2
|
||||||
|
|
||||||
|
GO_PACKAGES ?= $(shell go list ./... | grep -v /vendor/)
|
||||||
|
|
||||||
|
TARGETOS ?= linux
|
||||||
|
TARGETARCH ?= amd64
|
||||||
|
|
||||||
|
VERSION ?= next
|
||||||
|
CI_COMMIT_SHA ?= $(shell git rev-parse HEAD)
|
||||||
|
|
||||||
|
# it's a tagged release
|
||||||
|
ifneq ($(CI_COMMIT_TAG),)
|
||||||
|
VERSION := $(CI_COMMIT_TAG:v%=%)
|
||||||
|
else
|
||||||
|
# append commit-sha to next version
|
||||||
|
ifeq ($(VERSION),next)
|
||||||
|
VERSION := $(shell echo "next-$(shell echo ${CI_COMMIT_SHA} | cut -c -10)")
|
||||||
|
endif
|
||||||
|
# append commit-sha to release branch version
|
||||||
|
ifeq ($(shell echo ${CI_COMMIT_BRANCH} | cut -c -9),release/v)
|
||||||
|
VERSION := $(shell echo "$(shell echo ${CI_COMMIT_BRANCH} | cut -c 10-)-$(shell echo ${CI_COMMIT_SHA} | cut -c -10)")
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
LDFLAGS := -s -w -extldflags "-static" -X go.woodpecker-ci.org/autoscaler/version.Version=${VERSION}
|
||||||
|
CGO_ENABLED := 0
|
||||||
|
|
||||||
|
HAS_GO = $(shell hash go > /dev/null 2>&1 && echo "GO" || echo "NOGO" )
|
||||||
|
ifeq ($(HAS_GO),GO)
|
||||||
|
CGO_CFLAGS ?= $(shell go env CGO_CFLAGS)
|
||||||
|
endif
|
||||||
|
CGO_CFLAGS ?=
|
||||||
|
|
||||||
|
# If the first argument is "in_docker"...
|
||||||
|
ifeq (in_docker,$(firstword $(MAKECMDGOALS)))
|
||||||
|
# use the rest as arguments for "in_docker"
|
||||||
|
MAKE_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
|
||||||
|
# Ignore the next args
|
||||||
|
$(eval $(MAKE_ARGS):;@:)
|
||||||
|
|
||||||
|
in_docker:
|
||||||
|
@[ "1" -eq "$(shell docker image ls woodpecker/make:local -a | wc -l)" ] && docker buildx build -f ./docker/Dockerfile.make -t woodpecker/make:local --load . || echo reuse existing docker image
|
||||||
|
@echo run in docker:
|
||||||
|
@docker run -it \
|
||||||
|
--user $(shell id -u):$(shell id -g) \
|
||||||
|
-e VERSION="$(VERSION)" \
|
||||||
|
-e CI_COMMIT_SHA="$(CI_COMMIT_SHA)" \
|
||||||
|
-e TARGETOS="$(TARGETOS)" \
|
||||||
|
-e TARGETARCH="$(TARGETARCH)" \
|
||||||
|
-e CGO_ENABLED="$(CGO_ENABLED)" \
|
||||||
|
-e GOPATH=/tmp/go \
|
||||||
|
-e HOME=/tmp/home \
|
||||||
|
-v $(PWD):/build --rm woodpecker/make:local make $(MAKE_ARGS)
|
||||||
|
else
|
||||||
|
|
||||||
|
# Proceed with normal make
|
||||||
|
|
||||||
|
##@ General
|
||||||
|
|
||||||
|
.PHONY: all
|
||||||
|
all: help
|
||||||
|
|
||||||
|
# The help target prints out all targets with their descriptions organized
|
||||||
|
# beneath their categories. The categories are represented by '##@' and the
|
||||||
|
# target descriptions by '##'. The awk commands is responsible for reading the
|
||||||
|
# entire set of makefiles included in this invocation, looking for lines of the
|
||||||
|
# file as xyz: ## something, and then pretty-format the target and help. Then,
|
||||||
|
# if there's a line with ##@ something, that gets pretty-printed as a category.
|
||||||
|
# More info on the usage of ANSI control characters for terminal formatting:
|
||||||
|
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
|
||||||
|
# More info on the awk command:
|
||||||
|
# http://linuxcommand.org/lc3_adv_awk.php
|
||||||
|
|
||||||
|
.PHONY: help
|
||||||
|
help: ## Display this help.
|
||||||
|
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||||
|
|
||||||
|
.PHONY: version
|
||||||
|
version: ## Print the current version
|
||||||
|
@echo ${VERSION}
|
||||||
|
|
||||||
|
format: install-tools ## Format source code
|
||||||
|
@gofumpt -extra -w .
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean: ## Clean build artifacts
|
||||||
|
go clean -i ./...
|
||||||
|
rm -rf build
|
||||||
|
@[ "1" != "$(shell docker image ls woodpecker/make:local -a | wc -l)" ] && docker image rm woodpecker/make:local || echo no docker image to clean
|
||||||
|
|
||||||
|
|
||||||
|
install-tools: ## Install development tools
|
||||||
|
@hash golangci-lint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
|
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION); \
|
||||||
|
fi ; \
|
||||||
|
hash lint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
|
go install github.com/rs/zerolog/cmd/lint@latest; \
|
||||||
|
fi ; \
|
||||||
|
hash gofumpt > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
|
go install mvdan.cc/gofumpt@$(GOFUMPT_VERSION); \
|
||||||
|
fi ; \
|
||||||
|
hash mockery > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
|
go install github.com/vektra/mockery/v3@latest; \
|
||||||
|
fi ; \
|
||||||
|
|
||||||
|
##@ Test
|
||||||
|
|
||||||
|
.PHONY: lint
|
||||||
|
lint: install-tools ## Lint code
|
||||||
|
@echo "Running golangci-lint"
|
||||||
|
golangci-lint run
|
||||||
|
@echo "Running zerolog linter"
|
||||||
|
lint go.woodpecker-ci.org/autoscaler/cmd/woodpecker-autoscaler
|
||||||
|
|
||||||
|
test-autoscaler: ## Test autoscaler code
|
||||||
|
go test -race -cover -coverprofile autoscaler-coverage.out -timeout 30s ${GO_PACKAGES}
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
test: test-autoscaler ## Run all tests
|
||||||
|
|
||||||
|
.PHONY: generate
|
||||||
|
generate:
|
||||||
|
mockery
|
||||||
|
|
||||||
|
##@ Build
|
||||||
|
|
||||||
|
build:
|
||||||
|
CGO_ENABLED=${CGO_ENABLED} GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags '${LDFLAGS}' -o dist/woodpecker-autoscaler go.woodpecker-ci.org/autoscaler/cmd/woodpecker-autoscaler
|
||||||
|
|
||||||
|
endif
|
||||||
105
README.md
Normal file
105
README.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# Timeweb Cloud Autoscaler Provider for Woodpecker CI
|
||||||
|
|
||||||
|
Динамический autoscaler для Woodpecker CI с поддержкой хостера Timeweb Cloud.
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────┐
|
||||||
|
│ Woodpecker Server │◄──── постоянно запущен на VDS
|
||||||
|
│ (gRPC + Web UI) │
|
||||||
|
└──────────┬───────────┘
|
||||||
|
│
|
||||||
|
▼ gRPC
|
||||||
|
┌──────────────────────┐
|
||||||
|
│ Woodpecker Agent │◄──── постоянно запущен на VDS (можно оставить 1 статический)
|
||||||
|
│ (Docker Compose) │
|
||||||
|
└──────────────────────┘
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ Woodpecker Autoscaler │◄──── постоянно запущен
|
||||||
|
│ (форк woodpecker-ci/autoscaler + │
|
||||||
|
│ провайдер timewebcloud) │
|
||||||
|
└──────────┬───────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼ HTTP / JWT
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ Timeweb Cloud API │
|
||||||
|
│ api.timeweb.cloud │
|
||||||
|
│ - CreateServer │
|
||||||
|
│ - DeleteServer │
|
||||||
|
│ - ListServers │
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. Появляется задача в очереди Woodpecker
|
||||||
|
2. Autoscaler создает новый VDS через Timeweb API
|
||||||
|
3. VDS загружается, cloud-init устанавливает Docker и запускает Agent
|
||||||
|
4. Agent подключается к Server по gRPC и забирает задачу
|
||||||
|
5. По завершении задачи и истечении idle-timeout, autoscaler удаляет VDS
|
||||||
|
```
|
||||||
|
|
||||||
|
## Сборка
|
||||||
|
|
||||||
|
Требования: Go 1.23+
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -o woodpecker-autoscaler ./cmd/woodpecker-autoscaler
|
||||||
|
```
|
||||||
|
|
||||||
|
При сборке на машине с ограниченной памятью используйте:
|
||||||
|
```bash
|
||||||
|
GOMAXPROCS=1 go build -p 1 -o woodpecker-autoscaler ./cmd/woodpecker-autoscaler
|
||||||
|
```
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export WOODPECKER_SERVER=https://your-woodpecker-server
|
||||||
|
export WOODPECKER_TOKEN=your-token
|
||||||
|
export WOODPECKER_PROVIDER=timewebcloud
|
||||||
|
export WOODPECKER_TIMEWEBCLOUD_API_TOKEN=your-jwt-token
|
||||||
|
export WOODPECKER_TIMEWEBCLOUD_OS_ID=123
|
||||||
|
export WOODPECKER_TIMEWEBCLOUD_PRESET_ID=456
|
||||||
|
export WOODPECKER_TIMEWEBCLOUD_AVAILABILITY_ZONE=msk-1
|
||||||
|
|
||||||
|
./woodpecker-autoscaler
|
||||||
|
```
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
twcloud-scaler/
|
||||||
|
├── agents.md # Контекст для AI-агентов
|
||||||
|
├── README.md # Этот файл
|
||||||
|
├── go.mod # Модуль Go
|
||||||
|
├── woodpecker-autoscaler # Собранный бинарник
|
||||||
|
├── cmd/
|
||||||
|
│ └── woodpecker-autoscaler/
|
||||||
|
│ ├── main.go # Точка входа
|
||||||
|
│ └── flags.go # Глобальные CLI-флаги
|
||||||
|
├── providers/
|
||||||
|
│ └── timewebcloud/
|
||||||
|
│ ├── provider.go # Реализация Provider interface
|
||||||
|
│ ├── flags.go # CLI-флаги провайдера
|
||||||
|
│ └── api/
|
||||||
|
│ └── client.go # Минимальный HTTP-клиент для Timeweb API
|
||||||
|
├── engine/ # Ядро autoscaler (из upstream)
|
||||||
|
├── config/ # Конфигурация (из upstream)
|
||||||
|
├── server/ # Woodpecker API client (из upstream)
|
||||||
|
└── utils/ # Утилиты (из upstream)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Что реализовано
|
||||||
|
|
||||||
|
- [x] Провайдер `timewebcloud` для Woodpecker Autoscaler
|
||||||
|
- [x] Создание VDS (`DeployAgent`) с cloud-init
|
||||||
|
- [x] Удаление VDS (`RemoveAgent`) по имени агента
|
||||||
|
- [x] Список развернутых агентов (`ListDeployedAgentNames`) с фильтрацией по префиксу
|
||||||
|
- [x] Минимальный HTTP-клиент для Timeweb Cloud API (вместо сломанного SDK)
|
||||||
|
- [x] Интеграция в main.go
|
||||||
|
- [x] Успешная сборка бинарника
|
||||||
|
|
||||||
|
## Ссылки
|
||||||
|
- [Woodpecker Autoscaler](https://github.com/woodpecker-ci/autoscaler)
|
||||||
|
- [Timeweb Cloud API Docs](https://timeweb.cloud/api-docs)
|
||||||
151
agents.md
Normal file
151
agents.md
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# Project: Woodpecker CI Autoscaler — Timeweb Cloud Provider
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Add a Timeweb Cloud provider to the Woodpecker CI autoscaler so that:
|
||||||
|
1. The Woodpecker server runs permanently on one VDS.
|
||||||
|
2. When a CI job appears, the autoscaler dynamically creates a new VDS on Timeweb Cloud.
|
||||||
|
3. The VDS is bootstrapped via cloud-init, connects to the server as an agent, and runs the job.
|
||||||
|
4. After the job finishes and the idle timeout expires, the VDS is destroyed.
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
### Current Setup
|
||||||
|
- Woodpecker server and agent run permanently on a single VDS via Docker Compose.
|
||||||
|
- The goal is to move to a dynamic model where agents are created on demand.
|
||||||
|
|
||||||
|
### Woodpecker CI Autoscaler Architecture
|
||||||
|
- **Repository**: `woodpecker-ci/autoscaler` (separate from the main `woodpecker-ci/woodpecker` repo).
|
||||||
|
- **Language**: Go.
|
||||||
|
- **Provider Interface** (3 methods):
|
||||||
|
```go
|
||||||
|
type Provider interface {
|
||||||
|
DeployAgent(context.Context, *woodpecker.Agent) error
|
||||||
|
RemoveAgent(context.Context, *woodpecker.Agent) error
|
||||||
|
ListDeployedAgentNames(context.Context) ([]string, error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Provisioning Flow**:
|
||||||
|
1. Autoscaler monitors the Woodpecker queue.
|
||||||
|
2. When pending tasks exceed capacity, it calls `AgentCreate()` to get a token, then `DeployAgent()`.
|
||||||
|
3. `DeployAgent` creates a VM and passes cloud-init user-data.
|
||||||
|
4. The VM boots, installs Docker, and runs the Woodpecker agent container via docker compose.
|
||||||
|
5. The agent connects to the server via gRPC using the provided token.
|
||||||
|
6. On scale-down, `RemoveAgent()` terminates the VM, and the agent is deleted from Woodpecker.
|
||||||
|
- **Cloud-init**: The autoscaler generates a cloud-init YAML that installs Docker and starts the agent. Custom templates are supported via `WOODPECKER_PROVIDER_USERDATA` / `WOODPECKER_PROVIDER_USERDATA_FILE`.
|
||||||
|
- **Agent Environment Variables** (set in cloud-init):
|
||||||
|
- `WOODPECKER_SERVER` — gRPC address of the server.
|
||||||
|
- `WOODPECKER_AGENT_SECRET` — token generated by `AgentCreate()`.
|
||||||
|
- `WOODPECKER_MAX_WORKFLOWS` — parallelism per agent.
|
||||||
|
- `WOODPECKER_GRPC_SECURE` — TLS flag.
|
||||||
|
- **Configuration**: The autoscaler uses `urfave/cli` for CLI flags. Providers define their own flags (e.g., `--hetznercloud-api-token`).
|
||||||
|
- **Registration**: To add a new provider, you must:
|
||||||
|
1. Implement the `Provider` interface in a new package under `providers/<name>/`.
|
||||||
|
2. Create a `flags.go` file with CLI flags.
|
||||||
|
3. Import the package and add a case in `cmd/woodpecker-autoscaler/main.go`.
|
||||||
|
4. Append the provider's flags to the global app flags.
|
||||||
|
|
||||||
|
### Timeweb Cloud API
|
||||||
|
- **Public API**: Yes — `https://api.timeweb.cloud`.
|
||||||
|
- **Official Go SDK**: `github.com/timeweb-cloud/sdk-go` (OpenAPI-generated).
|
||||||
|
- **Authentication**: JWT Bearer token (`Authorization: Bearer <token>`).
|
||||||
|
- **VDS Lifecycle Endpoints**:
|
||||||
|
- Create: `POST /api/v1/servers`
|
||||||
|
- Delete: `DELETE /api/v1/servers/{server_id}`
|
||||||
|
- Get: `GET /api/v1/servers/{server_id}`
|
||||||
|
- List: `GET /api/v1/servers`
|
||||||
|
- Start: `POST /api/v1/servers/{server_id}/start`
|
||||||
|
- Shutdown: `POST /api/v1/servers/{server_id}/shutdown`
|
||||||
|
- Clone: `POST /api/v1/servers/{server_id}/clone`
|
||||||
|
- **Create Server Parameters**:
|
||||||
|
- `name` (required)
|
||||||
|
- `os_id` or `image_id`
|
||||||
|
- `preset_id` or `configuration` (CPU, RAM, disk)
|
||||||
|
- `ssh_keys_ids`
|
||||||
|
- `cloud_init` — **this is critical** for passing user-data.
|
||||||
|
- `availability_zone`
|
||||||
|
- `hostname`
|
||||||
|
- **Rate Limit**: 20 requests per second per endpoint.
|
||||||
|
- **Tags/Labels**: The API does not seem to have a native "label" or "tag" system for servers. We may need to track pool association by server name prefix or by storing state locally. **This is an open question.**
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: Project Setup
|
||||||
|
1. Fork / vendor `woodpecker-ci/autoscaler` as the base.
|
||||||
|
2. Add `github.com/timeweb-cloud/sdk-go` as a dependency.
|
||||||
|
3. Create the provider package: `providers/timewebcloud/`.
|
||||||
|
|
||||||
|
### Phase 2: Provider Implementation
|
||||||
|
1. **Struct & Constructor** (`provider.go`):
|
||||||
|
- Fields: API client, config, pool ID, default image/preset/zone.
|
||||||
|
- `New(ctx, cli.Command, *config.Config) (types.Provider, error)`.
|
||||||
|
2. **Flags** (`flags.go`):
|
||||||
|
- `--timewebcloud-api-token` (env: `WOODPECKER_TIMEWEBCLOUD_API_TOKEN`)
|
||||||
|
- `--timewebcloud-os-id` / `--timewebcloud-image-id`
|
||||||
|
- `--timewebcloud-preset-id` / `--timewebcloud-configuration`
|
||||||
|
- `--timewebcloud-availability-zone`
|
||||||
|
- `--timewebcloud-ssh-key-id`
|
||||||
|
- `--timewebcloud-hostname-prefix`
|
||||||
|
3. **DeployAgent**:
|
||||||
|
- Generate cloud-init user-data via `cloudinit.RenderUserDataTemplate()`.
|
||||||
|
- Call `CreateServer` with the agent name and user-data.
|
||||||
|
- Store the mapping `agent.Name -> server_id` (in memory or via naming convention).
|
||||||
|
4. **RemoveAgent**:
|
||||||
|
- Find server by agent name (list all servers and filter by name, or use a stored mapping).
|
||||||
|
- Call `DeleteServer`.
|
||||||
|
- Handle "not found" gracefully.
|
||||||
|
5. **ListDeployedAgentNames**:
|
||||||
|
- List all servers.
|
||||||
|
- Filter by name prefix (e.g., `pool-<pool-id>-agent-`).
|
||||||
|
- Return matching names.
|
||||||
|
|
||||||
|
### Phase 3: Integration
|
||||||
|
1. Import the provider in `main.go`.
|
||||||
|
2. Add `case "timewebcloud":` to `setupProvider()`.
|
||||||
|
3. Append `timewebcloud.ProviderFlags` to the global flags.
|
||||||
|
|
||||||
|
### Phase 4: Testing & Deployment
|
||||||
|
1. Build the binary.
|
||||||
|
2. Test locally or on a staging VDS:
|
||||||
|
- Start the autoscaler with `--provider=timewebcloud`.
|
||||||
|
- Trigger a CI job.
|
||||||
|
- Verify VDS creation, agent connection, job execution, and cleanup.
|
||||||
|
3. Update Docker Compose / deployment docs.
|
||||||
|
|
||||||
|
## Key Technical Decisions
|
||||||
|
|
||||||
|
### 1. How to Track Agent-to-Server Mapping?
|
||||||
|
**Options**:
|
||||||
|
- **A. Name Prefix Convention**: Name servers as `wp-<pool>-<agent-name>`. `ListDeployedAgentNames` filters by prefix. Simple, no state needed.
|
||||||
|
- **B. In-Memory Map**: Store `map[string]int` (agent name -> server ID) in the provider struct. Lost on restart.
|
||||||
|
- **C. Local State File**: Persist the map to disk. Survives restart.
|
||||||
|
- **D. API Metadata**: If Timeweb API supports tags/labels, use them. (Currently unclear.)
|
||||||
|
|
||||||
|
**Recommendation**: Start with **A** (name prefix) as the simplest and most robust approach. If Timeweb adds tags later, migrate to **D**.
|
||||||
|
|
||||||
|
### 2. How to Handle Server Readiness?
|
||||||
|
**Question**: After `CreateServer`, the server may take time to boot. Does `DeployAgent` need to wait?
|
||||||
|
**Answer**: No. The autoscaler engine only requires that the VM creation is initiated. The agent will connect when ready. The engine has `AgentInactivityTimeout` (default 10m) to clean up agents that never connect.
|
||||||
|
|
||||||
|
### 3. OS Image Selection
|
||||||
|
**Question**: What base image should be used for the agent VMs?
|
||||||
|
**Answer**: Ubuntu 22.04 LTS or Debian 12 (stable, good Docker support). The `os_id` must be fetched from Timeweb's API (`GetOsList`). Alternatively, a custom image with Docker pre-installed could speed up boot time.
|
||||||
|
|
||||||
|
### 4. SSH Keys
|
||||||
|
**Question**: Are SSH keys needed if we use cloud-init?
|
||||||
|
**Answer**: Cloud-init handles everything. SSH keys are optional but useful for debugging. The provider should allow configuring `ssh_keys_ids`.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
1. Does Timeweb Cloud API support assigning custom tags/labels to servers? (Affects `ListDeployedAgentNames` implementation.)
|
||||||
|
2. What is the typical boot time for a new VDS? (Affects `AgentInactivityTimeout` tuning.)
|
||||||
|
3. Does the `cloud_init` field in `CreateServer` accept standard cloud-init YAML? (Needs testing.)
|
||||||
|
4. Is there a way to use a custom image (snapshot) to pre-install Docker and reduce boot time?
|
||||||
|
5. What are the `os_id` values for Ubuntu/Debian? (Need to call `GetOsList`.)
|
||||||
|
6. Does Timeweb charge for stopped (but not deleted) servers? (Affects whether we should stop vs. delete.)
|
||||||
|
|
||||||
|
## References
|
||||||
|
- Woodpecker Autoscaler Repo: `https://github.com/woodpecker-ci/autoscaler`
|
||||||
|
- Provider Interface: `engine/types/provider.go`
|
||||||
|
- Hetzner Provider (reference): `providers/hetznercloud/`
|
||||||
|
- Cloud-init Render: `engine/inits/cloudinit/cloudinit.go`
|
||||||
|
- Timeweb Cloud Go SDK: `https://github.com/timeweb-cloud/sdk-go`
|
||||||
|
- Timeweb Cloud API Docs: `https://timeweb.cloud/api-docs`
|
||||||
2
checkmake.ini
Normal file
2
checkmake.ini
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[maxbodylength]
|
||||||
|
maxBodyLength = 12
|
||||||
19
config/config.go
Normal file
19
config/config.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
MinAgents int
|
||||||
|
MaxAgents int
|
||||||
|
WorkflowsPerAgent int
|
||||||
|
PoolID string
|
||||||
|
Image string
|
||||||
|
Environment map[string]string
|
||||||
|
GRPCAddress string
|
||||||
|
GRPCSecure bool
|
||||||
|
AgentInactivityTimeout time.Duration
|
||||||
|
AgentIdleTimeout time.Duration
|
||||||
|
UserData string
|
||||||
|
FilterLabels string
|
||||||
|
ExtraAgentLabels map[string]string
|
||||||
|
}
|
||||||
32
docker-compose.yml
Normal file
32
docker-compose.yml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
woodpecker-server:
|
||||||
|
image: woodpeckerci/woodpecker-server:next
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- woodpecker-server-data:/var/lib/woodpecker/
|
||||||
|
environment:
|
||||||
|
- WOODPECKER_OPEN=true
|
||||||
|
- WOODPECKER_ADMIN=pi3c83
|
||||||
|
- WOODPECKER_GRPC_ADDR=:9000
|
||||||
|
- WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
woodpecker-autoscaler:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
env_file: .env
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- woodpecker-server
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
woodpecker-server-data:
|
||||||
18
docker/Dockerfile
Normal file
18
docker/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
FROM --platform=$BUILDPLATFORM golang:1.26 AS build
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . .
|
||||||
|
ARG TARGETOS TARGETARCH
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
|
--mount=type=cache,target=/go/pkg \
|
||||||
|
make build
|
||||||
|
|
||||||
|
FROM --platform=$BUILDPLATFORM scratch
|
||||||
|
ENV GODEBUG=netdns=go
|
||||||
|
|
||||||
|
# copy certs from build image
|
||||||
|
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||||
|
# copy agent binary
|
||||||
|
COPY --from=build /src/dist/woodpecker-autoscaler /bin/
|
||||||
|
|
||||||
|
ENTRYPOINT ["/bin/woodpecker-autoscaler"]
|
||||||
27
docker/Dockerfile.make
Normal file
27
docker/Dockerfile.make
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# docker build --rm -f docker/Dockerfile.make -t woodpecker/make:local .
|
||||||
|
FROM docker.io/golang:1.26-alpine AS golang_image
|
||||||
|
FROM docker.io/node:24-alpine
|
||||||
|
|
||||||
|
RUN apk add --no-cache --update make gcc binutils-gold musl-dev && \
|
||||||
|
apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main protoc && \
|
||||||
|
corepack enable
|
||||||
|
|
||||||
|
# Build packages.
|
||||||
|
COPY --from=golang_image /usr/local/go /usr/local/go
|
||||||
|
COPY Makefile /
|
||||||
|
ENV PATH=$PATH:/usr/local/go/bin
|
||||||
|
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||||
|
ENV COREPACK_ENABLE_AUTO_PIN=0
|
||||||
|
|
||||||
|
# Cache tools
|
||||||
|
RUN GOBIN=/usr/local/go/bin make install-tools && \
|
||||||
|
rm -rf /Makefile
|
||||||
|
|
||||||
|
ENV GOPATH=/tmp/go
|
||||||
|
ENV HOME=/tmp/home
|
||||||
|
ENV PATH=$PATH:/usr/local/go/bin:/tmp/go/bin
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
RUN chmod -R 777 /root
|
||||||
|
|
||||||
|
CMD [ "/bin/sh" ]
|
||||||
92
docs/DEPLOY.md
Normal file
92
docs/DEPLOY.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Инструкция по развертыванию
|
||||||
|
|
||||||
|
## 1. Получение API-токена Timeweb Cloud
|
||||||
|
|
||||||
|
1. Войдите в панель управления Timeweb Cloud: https://timeweb.cloud
|
||||||
|
2. Перейдите в раздел **"API и Terraform"**.
|
||||||
|
3. Создайте новый токен:
|
||||||
|
- Задайте имя (например, `woodpecker-autoscaler`).
|
||||||
|
- Выберите права: управление серверами (Servers).
|
||||||
|
- Можно отключить подтверждение через Telegram для автоматизации.
|
||||||
|
- Сохраните токен — он отображается только один раз.
|
||||||
|
|
||||||
|
## 2. Определение параметров сервера
|
||||||
|
|
||||||
|
Проверенные параметры для CI-агента:
|
||||||
|
|
||||||
|
| Параметр | Значение | Описание |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| OS ID | `79` | Ubuntu 22.04 LTS |
|
||||||
|
| Preset ID | `2451` | Cloud-40 (2 CPU, 2GB RAM, 40GB NVMe, 800 руб/мес) |
|
||||||
|
| Availability Zone | `spb-1` | Санкт-Петербург (ru-1) |
|
||||||
|
|
||||||
|
Другие варианты:
|
||||||
|
- Ubuntu 24.04: `os_id=99`
|
||||||
|
- Debian 12: `os_id=95`
|
||||||
|
- Москва: `zone=msk-1`, `preset_id=4799` (Cloud MSK 40)
|
||||||
|
- Новосибирск: `zone=nsk-1`, `preset_id=4241` (Cloud NSK 40)
|
||||||
|
|
||||||
|
Для получения полного списка:
|
||||||
|
```bash
|
||||||
|
./timeweb-list
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Настройка Woodpecker Autoscaler
|
||||||
|
|
||||||
|
Создайте `.env` файл:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Woodpecker Server
|
||||||
|
WOODPECKER_SERVER=http://woodpecker-server:8000
|
||||||
|
WOODPECKER_TOKEN=your-woodpecker-admin-token
|
||||||
|
WOODPECKER_GRPC_ADDR=woodpecker-server:9000
|
||||||
|
WOODPECKER_GRPC_SECURE=false
|
||||||
|
|
||||||
|
# Autoscaler
|
||||||
|
WOODPECKER_POOL_ID=twcloud
|
||||||
|
WOODPECKER_MIN_AGENTS=0
|
||||||
|
WOODPECKER_MAX_AGENTS=5
|
||||||
|
WOODPECKER_WORKFLOWS_PER_AGENT=2
|
||||||
|
WOODPECKER_AGENT_IDLE_TIMEOUT=10m
|
||||||
|
WOODPECKER_RECONCILIATION_INTERVAL=1m
|
||||||
|
WOODPECKER_PROVIDER=timewebcloud
|
||||||
|
|
||||||
|
# Timeweb Cloud
|
||||||
|
WOODPECKER_TIMEWEBCLOUD_API_TOKEN=your-jwt-token
|
||||||
|
WOODPECKER_TIMEWEBCLOUD_OS_ID=79
|
||||||
|
WOODPECKER_TIMEWEBCLOUD_PRESET_ID=2451
|
||||||
|
WOODPECKER_TIMEWEBCLOUD_AVAILABILITY_ZONE=spb-1
|
||||||
|
# Опционально: SSH-ключи для доступа к агентам
|
||||||
|
# WOODPECKER_TIMEWEBCLOUD_SSH_KEY_IDS=1234
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Запуск через Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Просмотр логов:
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f woodpecker-autoscaler
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Проверка
|
||||||
|
|
||||||
|
1. Запушьте коммит в репозиторий, подключенный к Woodpecker.
|
||||||
|
2. В логах autoscaler должно появиться `create agent` и `create server`.
|
||||||
|
3. В панели Timeweb Cloud должен создаться новый сервер с именем `pool-twcloud-agent-<random>`.
|
||||||
|
4. Через 2-3 минуты агент подключится к серверу и заберет задачу.
|
||||||
|
5. После завершения задачи и истечения `AGENT_IDLE_TIMEOUT` сервер будет удален.
|
||||||
|
|
||||||
|
## 6. Тестирование API (опционально)
|
||||||
|
|
||||||
|
Для проверки корректности API-клиента:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Список ОС и тарифов
|
||||||
|
./timeweb-list
|
||||||
|
|
||||||
|
# Тест создания/удаления сервера
|
||||||
|
TIMEWEB_API_TOKEN=your-token ./timeweb-tester
|
||||||
|
```
|
||||||
400
engine/autoscaler.go
Normal file
400
engine/autoscaler.go
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"go.woodpecker-ci.org/autoscaler/config"
|
||||||
|
"go.woodpecker-ci.org/autoscaler/engine/types"
|
||||||
|
"go.woodpecker-ci.org/autoscaler/server"
|
||||||
|
"go.woodpecker-ci.org/autoscaler/utils"
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Autoscaler struct {
|
||||||
|
client server.Client
|
||||||
|
agents []*woodpecker.Agent
|
||||||
|
config *config.Config
|
||||||
|
provider types.Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAutoscaler creates a new Autoscaler instance.
|
||||||
|
// It takes in a Provider, Client and Config, and returns a configured
|
||||||
|
// Autoscaler struct.
|
||||||
|
func NewAutoscaler(p types.Provider, client server.Client, config *config.Config) Autoscaler {
|
||||||
|
return Autoscaler{
|
||||||
|
provider: p,
|
||||||
|
client: client,
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Autoscaler) loadAgents(_ context.Context) error {
|
||||||
|
a.agents = []*woodpecker.Agent{}
|
||||||
|
|
||||||
|
agents, err := a.client.AgentList()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("client.AgentList: %w", err)
|
||||||
|
}
|
||||||
|
r, err := regexp.Compile(fmt.Sprintf("pool-%s-agent-.*?", a.config.PoolID))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not create regex matcher for agent names by pool ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, agent := range agents {
|
||||||
|
if r.MatchString(agent.Name) {
|
||||||
|
a.agents = append(a.agents, agent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Autoscaler) getPoolAgents(excludeNoSchedule bool) []*woodpecker.Agent {
|
||||||
|
agents := make([]*woodpecker.Agent, 0)
|
||||||
|
for _, agent := range a.agents {
|
||||||
|
if excludeNoSchedule && agent.NoSchedule {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
agents = append(agents, agent)
|
||||||
|
}
|
||||||
|
return agents
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Autoscaler) createAgents(ctx context.Context, amount int) error {
|
||||||
|
suffixLength := 4
|
||||||
|
|
||||||
|
reactivatedAgents := 0
|
||||||
|
|
||||||
|
// try to re-activate agents that are in no-schedule state
|
||||||
|
for i := 0; i < amount; i++ {
|
||||||
|
for _, agent := range a.agents {
|
||||||
|
if agent.NoSchedule {
|
||||||
|
log.Info().Str("agent", agent.Name).Msg("reactivate agent")
|
||||||
|
agent.NoSchedule = false
|
||||||
|
_, err := a.client.AgentUpdate(agent)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("client.AgentUpdate: %w", err)
|
||||||
|
}
|
||||||
|
reactivatedAgents++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create new agents
|
||||||
|
for i := 0; i < amount-reactivatedAgents; i++ {
|
||||||
|
agent, err := a.client.AgentCreate(&woodpecker.Agent{
|
||||||
|
Name: fmt.Sprintf("pool-%s-agent-%s", a.config.PoolID, utils.RandomString(suffixLength)),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("client.AgentCreate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Str("agent", agent.Name).Msg("deploying agent")
|
||||||
|
|
||||||
|
err = a.provider.DeployAgent(ctx, agent)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("types.DeployAgent: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.agents = append(a.agents, agent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Autoscaler) drainAgents(_ context.Context, amount int) error {
|
||||||
|
for i := 0; i < amount; i++ {
|
||||||
|
for _, agent := range a.agents {
|
||||||
|
// agent is already marked for draining
|
||||||
|
if agent.NoSchedule {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// agent has recently done work => not ready for draining
|
||||||
|
if time.Since(time.Unix(agent.LastWork, 0)) < a.config.AgentIdleTimeout {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// agent has never contacted the server => not ready for draining
|
||||||
|
if agent.LastContact == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Str("agent", agent.Name).Msg("drain agent")
|
||||||
|
agent.NoSchedule = true
|
||||||
|
_, err := a.client.AgentUpdate(agent)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("client.AgentUpdate: %w", err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Autoscaler) isAgentIdle(agent *woodpecker.Agent) (bool, error) {
|
||||||
|
tasks, err := a.client.AgentTasksList(agent.ID)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("client.AgentTasksList: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// agent still has tasks => not idle
|
||||||
|
if len(tasks) > 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// agent has done work recently => not idle
|
||||||
|
if time.Since(time.Unix(agent.LastWork, 0)) < a.config.AgentIdleTimeout {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Autoscaler) removeAgent(ctx context.Context, agent *woodpecker.Agent, reason string) error {
|
||||||
|
isIdle, err := a.isAgentIdle(agent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !isIdle {
|
||||||
|
log.Info().Str("agent", agent.Name).Msg("agent is still processing workload")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Str("agent", agent.Name).Str("reason", reason).Msgf("removing agent")
|
||||||
|
|
||||||
|
err = a.provider.RemoveAgent(ctx, agent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.client.AgentDelete(agent.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("client.AgentDelete: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredAgents := make([]*woodpecker.Agent, 0)
|
||||||
|
for _, a := range a.agents {
|
||||||
|
if a.ID != agent.ID {
|
||||||
|
filteredAgents = append(filteredAgents, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.agents = filteredAgents
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Autoscaler) removeDrainedAgents(ctx context.Context) error {
|
||||||
|
for _, agent := range a.getPoolAgents(false) {
|
||||||
|
if !agent.NoSchedule {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := a.removeAgent(ctx, agent, "was drained")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Autoscaler) cleanupDanglingAgents(ctx context.Context) error {
|
||||||
|
woodpeckerAgents := a.getPoolAgents(false)
|
||||||
|
providerAgentNames, err := a.provider.ListDeployedAgentNames(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove agents that are not in the woodpecker agent list anymore
|
||||||
|
for _, agentName := range providerAgentNames {
|
||||||
|
found := false
|
||||||
|
for _, agent := range woodpeckerAgents {
|
||||||
|
if agent.Name == agentName {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
log.Info().Str("agent", agentName).Str("reason", "not found on woodpecker").Msg("remove agent")
|
||||||
|
if err := a.provider.RemoveAgent(ctx, &woodpecker.Agent{Name: agentName}); err != nil {
|
||||||
|
return fmt.Errorf("types.RemoveAgent: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove agent from providerAgentNames
|
||||||
|
_providerAgentNames := make([]string, 0)
|
||||||
|
for _, a := range providerAgentNames {
|
||||||
|
if a != agentName {
|
||||||
|
_providerAgentNames = append(_providerAgentNames, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
providerAgentNames = _providerAgentNames
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove agents that do not exist on the provider anymore
|
||||||
|
for _, agent := range woodpeckerAgents {
|
||||||
|
found := false
|
||||||
|
for _, agentName := range providerAgentNames {
|
||||||
|
if agent.Name == agentName {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
log.Info().Str("agent", agent.Name).Str("reason", "not found on provider").Msg("remove agent")
|
||||||
|
if err = a.client.AgentDelete(agent.ID); err != nil {
|
||||||
|
return fmt.Errorf("client.AgentDelete: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove agent from woodpeckerAgents
|
||||||
|
_woodpeckerAgents := make([]*woodpecker.Agent, 0)
|
||||||
|
for _, a := range a.agents {
|
||||||
|
if a.Name != agent.Name {
|
||||||
|
woodpeckerAgents = append(woodpeckerAgents, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a.agents = _woodpeckerAgents
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Autoscaler) cleanupStaleAgents(ctx context.Context) error {
|
||||||
|
// remove agents that haven't contacted the server for a while (including agents that never contacted the server)
|
||||||
|
for _, agent := range a.getPoolAgents(false) {
|
||||||
|
if agent.NoSchedule {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
lastContact := agent.LastContact
|
||||||
|
|
||||||
|
// if agent has never contacted the server, use the creation time
|
||||||
|
if lastContact == 0 {
|
||||||
|
lastContact = agent.Created
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Since(time.Unix(lastContact, 0)) > a.config.AgentInactivityTimeout {
|
||||||
|
err := a.removeAgent(ctx, agent, "hasn't connected to the server for a while")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Autoscaler) getQueueInfo(_ context.Context) (freeTasks, runningTasks, pendingTasks int, err error) {
|
||||||
|
queueInfo, err := a.client.QueueInfo()
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, fmt.Errorf("error from QueueInfo: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.config.FilterLabels == "" {
|
||||||
|
return queueInfo.Stats.Workers, queueInfo.Stats.Running, queueInfo.Stats.Pending, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
labelFilterKey, labelFilterValue, ok := strings.Cut(a.config.FilterLabels, "=")
|
||||||
|
if !ok {
|
||||||
|
return 0, 0, 0, fmt.Errorf("invalid labels filter: %s", a.config.FilterLabels)
|
||||||
|
}
|
||||||
|
|
||||||
|
running := countTasksByLabel(queueInfo.Running, labelFilterKey, labelFilterValue)
|
||||||
|
pending := countTasksByLabel(queueInfo.Pending, labelFilterKey, labelFilterValue)
|
||||||
|
|
||||||
|
return queueInfo.Stats.Workers, running, pending, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Autoscaler) calcAgents(ctx context.Context) (float64, error) {
|
||||||
|
freeTasks, runningTasks, pendingTasks, err := a.getQueueInfo(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("queue info: freeTasks = %v runningTasks = %v pendingTasks = %v", freeTasks, runningTasks, pendingTasks)
|
||||||
|
availableAgents := math.Ceil(float64(freeTasks+runningTasks) / float64((a.config.WorkflowsPerAgent)))
|
||||||
|
reqAgents := math.Ceil(float64(pendingTasks+runningTasks) / float64(a.config.WorkflowsPerAgent))
|
||||||
|
|
||||||
|
availablePoolAgents := len(a.getPoolAgents(true))
|
||||||
|
maxUp := float64(a.config.MaxAgents - availablePoolAgents)
|
||||||
|
maxDown := float64(availablePoolAgents - a.config.MinAgents)
|
||||||
|
|
||||||
|
reqPoolAgents := math.Ceil(reqAgents - (availableAgents + float64(availablePoolAgents)))
|
||||||
|
reqPoolAgents = math.Max(reqPoolAgents, -maxDown)
|
||||||
|
reqPoolAgents = math.Min(reqPoolAgents, maxUp)
|
||||||
|
|
||||||
|
log.Debug().Msgf("capacity info: agents = %v/%v pool = %v/%v limits = %v/%v", availableAgents, reqAgents, availablePoolAgents, reqPoolAgents, maxUp, maxDown)
|
||||||
|
|
||||||
|
return reqPoolAgents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconcile periodically checks the status of the agent pool and adjusts it to match
|
||||||
|
// the desired capacity based on the current queue state.
|
||||||
|
func (a *Autoscaler) Reconcile(ctx context.Context) error {
|
||||||
|
if err := a.loadAgents(ctx); err != nil {
|
||||||
|
return fmt.Errorf("loading agents failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reqPoolAgents, err := a.calcAgents(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("calculating agents failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if reqPoolAgents > 0 {
|
||||||
|
num := int(math.Abs(reqPoolAgents))
|
||||||
|
log.Debug().Msgf("starting %d additional agents", num)
|
||||||
|
|
||||||
|
if err := a.createAgents(ctx, num); err != nil {
|
||||||
|
return fmt.Errorf("creating agents failed: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if reqPoolAgents < 0 {
|
||||||
|
num := int(math.Abs(reqPoolAgents))
|
||||||
|
|
||||||
|
log.Debug().Msgf("checking %d agents if ready for draining", num)
|
||||||
|
if err := a.drainAgents(ctx, num); err != nil {
|
||||||
|
return fmt.Errorf("draining agents failed: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup agents that are only present at the provider or woodpecker
|
||||||
|
if err := a.cleanupDanglingAgents(ctx); err != nil {
|
||||||
|
return fmt.Errorf("cleaning up dangling agents failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup agents that haven't contacted the server for a while
|
||||||
|
if err := a.cleanupStaleAgents(ctx); err != nil {
|
||||||
|
return fmt.Errorf("cleaning up stale agents failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove agents that are drained
|
||||||
|
if err := a.removeDrainedAgents(ctx); err != nil {
|
||||||
|
return fmt.Errorf("removing drained agents failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func countTasksByLabel(jobs []woodpecker.Task, labelKey, labelValue string) int {
|
||||||
|
count := 0
|
||||||
|
for _, job := range jobs {
|
||||||
|
val, exists := job.Labels[labelKey]
|
||||||
|
if exists && val == labelValue {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
546
engine/autoscaler_test.go
Normal file
546
engine/autoscaler_test.go
Normal file
@@ -0,0 +1,546 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
|
"go.woodpecker-ci.org/autoscaler/config"
|
||||||
|
mocks_provider "go.woodpecker-ci.org/autoscaler/engine/types/mocks"
|
||||||
|
mocks_server "go.woodpecker-ci.org/autoscaler/server/mocks"
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockClient struct {
|
||||||
|
workers int
|
||||||
|
running int
|
||||||
|
pending int
|
||||||
|
waitingOnDeps int
|
||||||
|
woodpecker.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MockClient) QueueInfo() (*woodpecker.Info, error) {
|
||||||
|
info := &woodpecker.Info{}
|
||||||
|
|
||||||
|
info.Stats.Workers = m.workers
|
||||||
|
info.Stats.Running = m.running
|
||||||
|
info.Stats.Pending = m.pending
|
||||||
|
info.Stats.WaitingOnDeps = m.waitingOnDeps
|
||||||
|
|
||||||
|
info.Pending = []woodpecker.Task{
|
||||||
|
{
|
||||||
|
Labels: map[string]string{
|
||||||
|
"arch": "amd64",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
info.Running = []woodpecker.Task{
|
||||||
|
{
|
||||||
|
Labels: map[string]string{
|
||||||
|
"arch": "amd64",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_calcAgents(t *testing.T) {
|
||||||
|
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||||
|
|
||||||
|
t.Run("should create new agent (MinAgents)", func(t *testing.T) {
|
||||||
|
autoscaler := Autoscaler{client: &MockClient{
|
||||||
|
pending: 0,
|
||||||
|
}, config: &config.Config{
|
||||||
|
WorkflowsPerAgent: 1,
|
||||||
|
MaxAgents: 2,
|
||||||
|
MinAgents: 1,
|
||||||
|
}}
|
||||||
|
|
||||||
|
value, _ := autoscaler.calcAgents(t.Context())
|
||||||
|
assert.Equal(t, float64(1), value)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should create single agent", func(t *testing.T) {
|
||||||
|
autoscaler := Autoscaler{client: &MockClient{
|
||||||
|
pending: 2,
|
||||||
|
}, config: &config.Config{
|
||||||
|
WorkflowsPerAgent: 5,
|
||||||
|
MaxAgents: 3,
|
||||||
|
}}
|
||||||
|
|
||||||
|
value, _ := autoscaler.calcAgents(t.Context())
|
||||||
|
assert.Equal(t, float64(1), value)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should create multiple agents", func(t *testing.T) {
|
||||||
|
autoscaler := Autoscaler{client: &MockClient{
|
||||||
|
pending: 6,
|
||||||
|
}, config: &config.Config{
|
||||||
|
WorkflowsPerAgent: 5,
|
||||||
|
MaxAgents: 3,
|
||||||
|
}}
|
||||||
|
|
||||||
|
value, _ := autoscaler.calcAgents(t.Context())
|
||||||
|
assert.Equal(t, float64(2), value)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should create new agent (MaxAgents)", func(t *testing.T) {
|
||||||
|
autoscaler := Autoscaler{client: &MockClient{
|
||||||
|
pending: 2,
|
||||||
|
}, config: &config.Config{
|
||||||
|
WorkflowsPerAgent: 1,
|
||||||
|
MaxAgents: 2,
|
||||||
|
}, agents: []*woodpecker.Agent{
|
||||||
|
{Name: "pool-1-agent-1234"},
|
||||||
|
}}
|
||||||
|
|
||||||
|
value, _ := autoscaler.calcAgents(t.Context())
|
||||||
|
assert.Equal(t, float64(1), value)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should not create new agent (availableAgents)", func(t *testing.T) {
|
||||||
|
autoscaler := Autoscaler{client: &MockClient{
|
||||||
|
workers: 2,
|
||||||
|
pending: 2,
|
||||||
|
}, config: &config.Config{
|
||||||
|
WorkflowsPerAgent: 1,
|
||||||
|
MaxAgents: 2,
|
||||||
|
}}
|
||||||
|
|
||||||
|
value, _ := autoscaler.calcAgents(t.Context())
|
||||||
|
assert.Equal(t, float64(0), value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_getQueueInfo(t *testing.T) {
|
||||||
|
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||||
|
t.Run("should not filter", func(t *testing.T) {
|
||||||
|
autoscaler := Autoscaler{
|
||||||
|
client: &MockClient{
|
||||||
|
pending: 2,
|
||||||
|
},
|
||||||
|
config: &config.Config{},
|
||||||
|
}
|
||||||
|
|
||||||
|
free, running, pending, _ := autoscaler.getQueueInfo(t.Context())
|
||||||
|
assert.Equal(t, 0, free)
|
||||||
|
assert.Equal(t, 0, running)
|
||||||
|
assert.Equal(t, 2, pending)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should filter one by label", func(t *testing.T) {
|
||||||
|
autoscaler := Autoscaler{
|
||||||
|
client: &MockClient{
|
||||||
|
pending: 2,
|
||||||
|
},
|
||||||
|
config: &config.Config{
|
||||||
|
FilterLabels: "arch=amd64",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
free, running, pending, _ := autoscaler.getQueueInfo(t.Context())
|
||||||
|
assert.Equal(t, 0, free)
|
||||||
|
assert.Equal(t, 1, running)
|
||||||
|
assert.Equal(t, 1, pending)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should filter all by label", func(t *testing.T) {
|
||||||
|
autoscaler := Autoscaler{
|
||||||
|
client: &MockClient{
|
||||||
|
pending: 2,
|
||||||
|
},
|
||||||
|
config: &config.Config{
|
||||||
|
FilterLabels: "arch=arm64",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
free, running, pending, _ := autoscaler.getQueueInfo(t.Context())
|
||||||
|
assert.Equal(t, 0, free)
|
||||||
|
assert.Equal(t, 0, running)
|
||||||
|
assert.Equal(t, 0, pending)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_getPoolAgents(t *testing.T) {
|
||||||
|
autoscaler := Autoscaler{
|
||||||
|
agents: []*woodpecker.Agent{
|
||||||
|
{ID: 1, Name: "pool-1-agent-1", NoSchedule: false},
|
||||||
|
{ID: 2, Name: "pool-1-agent-2", NoSchedule: true},
|
||||||
|
{ID: 3, Name: "pool-1-agent-3", NoSchedule: false},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
agents := autoscaler.getPoolAgents(false)
|
||||||
|
assert.Equal(t, 3, len(agents))
|
||||||
|
|
||||||
|
agents = autoscaler.getPoolAgents(true)
|
||||||
|
assert.Equal(t, 2, len(agents))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_createAgents(t *testing.T) {
|
||||||
|
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||||
|
|
||||||
|
t.Run("should create a new agent", func(t *testing.T) {
|
||||||
|
ctx := t.Context()
|
||||||
|
client := mocks_server.NewMockClient(t)
|
||||||
|
provider := mocks_provider.NewMockProvider(t)
|
||||||
|
autoscaler := Autoscaler{
|
||||||
|
client: client,
|
||||||
|
provider: provider,
|
||||||
|
config: &config.Config{
|
||||||
|
PoolID: "1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
client.On("AgentCreate", mock.Anything).Return(&woodpecker.Agent{Name: "pool-1-agent-1"}, nil)
|
||||||
|
provider.On("DeployAgent", ctx, mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
err := autoscaler.createAgents(ctx, 1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should reuse an no-schedule agent first before creating a new one", func(t *testing.T) {
|
||||||
|
ctx := t.Context()
|
||||||
|
client := mocks_server.NewMockClient(t)
|
||||||
|
provider := mocks_provider.NewMockProvider(t)
|
||||||
|
autoscaler := Autoscaler{
|
||||||
|
client: client,
|
||||||
|
provider: provider,
|
||||||
|
agents: []*woodpecker.Agent{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
NoSchedule: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
config: &config.Config{
|
||||||
|
PoolID: "1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
client.On("AgentUpdate", mock.MatchedBy(func(agent *woodpecker.Agent) bool {
|
||||||
|
return agent.ID == 1 && agent.NoSchedule == false
|
||||||
|
})).Return(nil, nil)
|
||||||
|
client.On("AgentCreate", mock.Anything).Return(&woodpecker.Agent{Name: "pool-1-agent-1"}, nil)
|
||||||
|
provider.On("DeployAgent", ctx, mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
err := autoscaler.createAgents(ctx, 2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_cleanupDanglingAgents(t *testing.T) {
|
||||||
|
t.Run("should remove agent that is only present on woodpecker (not provider)", func(t *testing.T) {
|
||||||
|
ctx := t.Context()
|
||||||
|
client := mocks_server.NewMockClient(t)
|
||||||
|
provider := mocks_provider.NewMockProvider(t)
|
||||||
|
autoscaler := Autoscaler{
|
||||||
|
agents: []*woodpecker.Agent{
|
||||||
|
{ID: 1, Name: "pool-1-agent-1", NoSchedule: false},
|
||||||
|
},
|
||||||
|
provider: provider,
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
|
||||||
|
provider.On("ListDeployedAgentNames", mock.Anything).Return(nil, nil)
|
||||||
|
client.On("AgentDelete", int64(1)).Return(nil)
|
||||||
|
|
||||||
|
err := autoscaler.cleanupDanglingAgents(ctx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should remove agent that is only present on provider (not woodpecker)", func(t *testing.T) {
|
||||||
|
ctx := t.Context()
|
||||||
|
client := mocks_server.NewMockClient(t)
|
||||||
|
provider := mocks_provider.NewMockProvider(t)
|
||||||
|
autoscaler := Autoscaler{
|
||||||
|
agents: []*woodpecker.Agent{
|
||||||
|
{ID: 1, Name: "pool-1-agent-1", NoSchedule: false},
|
||||||
|
},
|
||||||
|
provider: provider,
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
|
||||||
|
provider.On("ListDeployedAgentNames", mock.Anything).Return([]string{"pool-1-agent-1", "pool-1-agent-2"}, nil)
|
||||||
|
provider.On("RemoveAgent", mock.Anything, mock.MatchedBy(func(agent *woodpecker.Agent) bool {
|
||||||
|
return agent.Name == "pool-1-agent-2"
|
||||||
|
})).Return(nil)
|
||||||
|
|
||||||
|
err := autoscaler.cleanupDanglingAgents(ctx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_cleanupStaleAgents(t *testing.T) {
|
||||||
|
t.Run("should remove agent that never connected (last contact = 0) in over 15 minutes", func(t *testing.T) {
|
||||||
|
ctx := t.Context()
|
||||||
|
client := mocks_server.NewMockClient(t)
|
||||||
|
provider := mocks_provider.NewMockProvider(t)
|
||||||
|
autoscaler := Autoscaler{
|
||||||
|
agents: []*woodpecker.Agent{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
Name: "active agent",
|
||||||
|
NoSchedule: false,
|
||||||
|
Created: time.Now().Add(-time.Minute * 20).Unix(), // created 20 minutes ago
|
||||||
|
LastContact: time.Now().Add(-time.Minute * 5).Unix(), // last contact 5 minutes ago
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
Name: "never contacted agent",
|
||||||
|
NoSchedule: false,
|
||||||
|
Created: time.Now().Add(-time.Minute * 20).Unix(), // created 20 minutes ago
|
||||||
|
LastContact: 0, // never contacted
|
||||||
|
},
|
||||||
|
},
|
||||||
|
provider: provider,
|
||||||
|
client: client,
|
||||||
|
config: &config.Config{
|
||||||
|
AgentInactivityTimeout: time.Minute * 15,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
client.On("AgentTasksList", int64(2)).Return(nil, nil)
|
||||||
|
client.On("AgentDelete", int64(2)).Return(nil)
|
||||||
|
provider.On("RemoveAgent", mock.Anything, mock.MatchedBy(func(agent *woodpecker.Agent) bool {
|
||||||
|
return agent.ID == 2
|
||||||
|
})).Return(nil)
|
||||||
|
|
||||||
|
err := autoscaler.cleanupStaleAgents(ctx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should remove agent that has lost connection for more than 15 minutes", func(t *testing.T) {
|
||||||
|
ctx := t.Context()
|
||||||
|
client := mocks_server.NewMockClient(t)
|
||||||
|
provider := mocks_provider.NewMockProvider(t)
|
||||||
|
autoscaler := Autoscaler{
|
||||||
|
agents: []*woodpecker.Agent{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
Name: "active agent",
|
||||||
|
NoSchedule: false,
|
||||||
|
Created: time.Now().Add(-time.Minute * 20).Unix(), // created 20 minutes ago
|
||||||
|
LastContact: time.Now().Add(-time.Minute * 5).Unix(), // last contact 5 minutes ago
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
Name: "stale agent",
|
||||||
|
NoSchedule: false,
|
||||||
|
Created: time.Now().Add(-time.Minute * 20).Unix(), // created 20 minutes ago
|
||||||
|
LastContact: time.Now().Add(-time.Minute * 20).Unix(), // last contact 20 minutes ago
|
||||||
|
},
|
||||||
|
},
|
||||||
|
provider: provider,
|
||||||
|
client: client,
|
||||||
|
config: &config.Config{
|
||||||
|
AgentInactivityTimeout: time.Minute * 15,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
client.On("AgentTasksList", int64(2)).Return(nil, nil)
|
||||||
|
client.On("AgentDelete", int64(2)).Return(nil)
|
||||||
|
provider.On("RemoveAgent", mock.Anything, mock.MatchedBy(func(agent *woodpecker.Agent) bool {
|
||||||
|
return agent.ID == 2
|
||||||
|
})).Return(nil)
|
||||||
|
|
||||||
|
err := autoscaler.cleanupStaleAgents(ctx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_isAgentIdle(t *testing.T) {
|
||||||
|
t.Run("should return false if agent has tasks", func(t *testing.T) {
|
||||||
|
client := mocks_server.NewMockClient(t)
|
||||||
|
autoscaler := Autoscaler{
|
||||||
|
client: client,
|
||||||
|
config: &config.Config{
|
||||||
|
AgentIdleTimeout: time.Minute * 15,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
client.On("AgentTasksList", int64(1)).Return([]*woodpecker.Task{
|
||||||
|
{ID: "1"},
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
idle, err := autoscaler.isAgentIdle(&woodpecker.Agent{
|
||||||
|
ID: 1,
|
||||||
|
Name: "pool-1-agent-1",
|
||||||
|
NoSchedule: false,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, idle)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return false if agent has done work recently", func(t *testing.T) {
|
||||||
|
client := mocks_server.NewMockClient(t)
|
||||||
|
autoscaler := Autoscaler{
|
||||||
|
client: client,
|
||||||
|
config: &config.Config{
|
||||||
|
AgentIdleTimeout: time.Minute * 15,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
client.On("AgentTasksList", int64(1)).Return(nil, nil)
|
||||||
|
|
||||||
|
idle, err := autoscaler.isAgentIdle(&woodpecker.Agent{
|
||||||
|
ID: 1,
|
||||||
|
Name: "pool-1-agent-1",
|
||||||
|
NoSchedule: false,
|
||||||
|
LastWork: time.Now().Add(-time.Minute * 10).Unix(),
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, idle)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return true if agent is idle", func(t *testing.T) {
|
||||||
|
client := mocks_server.NewMockClient(t)
|
||||||
|
autoscaler := Autoscaler{
|
||||||
|
client: client,
|
||||||
|
config: &config.Config{
|
||||||
|
AgentIdleTimeout: time.Minute * 15,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
client.On("AgentTasksList", int64(1)).Return(nil, nil) // no tasks
|
||||||
|
|
||||||
|
idle, err := autoscaler.isAgentIdle(&woodpecker.Agent{
|
||||||
|
ID: 1,
|
||||||
|
Name: "pool-1-agent-1",
|
||||||
|
NoSchedule: false,
|
||||||
|
LastWork: time.Now().Add(-time.Minute * 20).Unix(), // last work 20 minutes ago
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, idle)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_drainAgents(t *testing.T) {
|
||||||
|
t.Run("should drain agents and skip no-schedule ones", func(t *testing.T) {
|
||||||
|
ctx := t.Context()
|
||||||
|
client := mocks_server.NewMockClient(t)
|
||||||
|
provider := mocks_provider.NewMockProvider(t)
|
||||||
|
autoscaler := Autoscaler{
|
||||||
|
agents: []*woodpecker.Agent{
|
||||||
|
{ID: 1, Name: "pool-1-agent-1", NoSchedule: false, LastContact: time.Now().Add(-time.Minute * 2).Unix()},
|
||||||
|
{ID: 2, Name: "pool-1-agent-2", NoSchedule: true, LastContact: time.Now().Add(-time.Minute * 2).Unix()},
|
||||||
|
{ID: 3, Name: "pool-1-agent-3", NoSchedule: true, LastContact: time.Now().Add(-time.Minute * 2).Unix()},
|
||||||
|
{ID: 4, Name: "pool-1-agent-4", NoSchedule: false, LastContact: time.Now().Add(-time.Minute * 2).Unix()},
|
||||||
|
},
|
||||||
|
provider: provider,
|
||||||
|
client: client,
|
||||||
|
config: &config.Config{
|
||||||
|
AgentIdleTimeout: time.Minute * 15,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
client.On("AgentUpdate", mock.MatchedBy(func(agent *woodpecker.Agent) bool {
|
||||||
|
return (agent.ID == 1 || agent.ID == 4) && agent.NoSchedule == true
|
||||||
|
})).Return(nil, nil)
|
||||||
|
|
||||||
|
err := autoscaler.drainAgents(ctx, 2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, autoscaler.agents[0].NoSchedule)
|
||||||
|
assert.True(t, autoscaler.agents[3].NoSchedule)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should not remove an agent that never connected", func(t *testing.T) {
|
||||||
|
ctx := t.Context()
|
||||||
|
client := mocks_server.NewMockClient(t)
|
||||||
|
provider := mocks_provider.NewMockProvider(t)
|
||||||
|
autoscaler := Autoscaler{
|
||||||
|
agents: []*woodpecker.Agent{
|
||||||
|
{ID: 1, Name: "pool-1-agent-1", NoSchedule: false, LastContact: 0},
|
||||||
|
},
|
||||||
|
provider: provider,
|
||||||
|
client: client,
|
||||||
|
config: &config.Config{
|
||||||
|
AgentIdleTimeout: time.Minute * 15,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := autoscaler.drainAgents(ctx, 1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, autoscaler.agents[0].NoSchedule)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should not remove an agent that has recently done some work", func(t *testing.T) {
|
||||||
|
ctx := t.Context()
|
||||||
|
client := mocks_server.NewMockClient(t)
|
||||||
|
provider := mocks_provider.NewMockProvider(t)
|
||||||
|
autoscaler := Autoscaler{
|
||||||
|
agents: []*woodpecker.Agent{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
Name: "pool-1-agent-1",
|
||||||
|
NoSchedule: false,
|
||||||
|
LastContact: time.Now().Add(-time.Minute * 2).Unix(), // last contact 2 minutes ago
|
||||||
|
LastWork: time.Now().Add(-time.Minute * 5).Unix(), // last work 5 minutes ago
|
||||||
|
},
|
||||||
|
},
|
||||||
|
provider: provider,
|
||||||
|
client: client,
|
||||||
|
config: &config.Config{
|
||||||
|
AgentIdleTimeout: time.Minute * 15,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := autoscaler.drainAgents(ctx, 1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, autoscaler.agents[0].NoSchedule)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_removeDrainedAgents(t *testing.T) {
|
||||||
|
t.Run("should remove agent", func(t *testing.T) {
|
||||||
|
ctx := t.Context()
|
||||||
|
client := mocks_server.NewMockClient(t)
|
||||||
|
provider := mocks_provider.NewMockProvider(t)
|
||||||
|
autoscaler := Autoscaler{
|
||||||
|
agents: []*woodpecker.Agent{
|
||||||
|
{ID: 1, Name: "pool-1-agent-1", NoSchedule: false},
|
||||||
|
{ID: 2, Name: "pool-1-agent-2", NoSchedule: true},
|
||||||
|
{ID: 3, Name: "pool-1-agent-3", NoSchedule: false},
|
||||||
|
},
|
||||||
|
provider: provider,
|
||||||
|
client: client,
|
||||||
|
config: &config.Config{
|
||||||
|
AgentIdleTimeout: time.Minute * 15,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
client.On("AgentTasksList", int64(2)).Return(nil, nil)
|
||||||
|
provider.On("RemoveAgent", mock.Anything, mock.MatchedBy(func(agent *woodpecker.Agent) bool {
|
||||||
|
return agent.ID == 2
|
||||||
|
})).Return(nil)
|
||||||
|
client.On("AgentDelete", int64(2)).Return(nil)
|
||||||
|
|
||||||
|
err := autoscaler.removeDrainedAgents(ctx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should not remove agent with tasks", func(t *testing.T) {
|
||||||
|
ctx := t.Context()
|
||||||
|
client := mocks_server.NewMockClient(t)
|
||||||
|
provider := mocks_provider.NewMockProvider(t)
|
||||||
|
autoscaler := Autoscaler{
|
||||||
|
agents: []*woodpecker.Agent{
|
||||||
|
{ID: 1, Name: "pool-1-agent-1", NoSchedule: false},
|
||||||
|
{ID: 2, Name: "pool-1-agent-2", NoSchedule: true},
|
||||||
|
{ID: 3, Name: "pool-1-agent-3", NoSchedule: false},
|
||||||
|
},
|
||||||
|
provider: provider,
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
|
||||||
|
client.On("AgentTasksList", int64(2)).Return([]*woodpecker.Task{
|
||||||
|
{ID: "1"},
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
err := autoscaler.removeDrainedAgents(ctx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
9
engine/const.go
Normal file
9
engine/const.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
var (
|
||||||
|
LabelPrefix = "wp.autoscaler/"
|
||||||
|
LabelPool = fmt.Sprintf("%spool", LabelPrefix)
|
||||||
|
LabelImage = fmt.Sprintf("%simage", LabelPrefix)
|
||||||
|
)
|
||||||
116
engine/inits/cloudinit/cloudinit.go
Normal file
116
engine/inits/cloudinit/cloudinit.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package cloudinit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"go.woodpecker-ci.org/autoscaler/config"
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RenderUserDataTemplate renders the user data template for an Agent
|
||||||
|
// using the provided configuration.
|
||||||
|
func RenderUserDataTemplate(config *config.Config, agent *woodpecker.Agent, tmpl *template.Template) (string, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case tmpl != nil:
|
||||||
|
case config.UserData != "":
|
||||||
|
tmpl, err = template.New("user-data").Parse(config.UserData)
|
||||||
|
default:
|
||||||
|
tmpl, err = template.New("user-data").Parse(CloudInitUserDataUbuntuDefault)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("template.New.Parse %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
params := struct {
|
||||||
|
Image string
|
||||||
|
Environment map[string]string
|
||||||
|
}{
|
||||||
|
Image: config.Image,
|
||||||
|
Environment: map[string]string{
|
||||||
|
"WOODPECKER_SERVER": config.GRPCAddress,
|
||||||
|
"WOODPECKER_AGENT_SECRET": agent.Token,
|
||||||
|
"WOODPECKER_MAX_WORKFLOWS": fmt.Sprintf("%d", config.WorkflowsPerAgent),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.GRPCSecure {
|
||||||
|
params.Environment["WOODPECKER_GRPC_SECURE"] = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range config.Environment {
|
||||||
|
params.Environment[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
params.Environment["WOODPECKER_AGENT_LABELS"] = genExtraAgentLabels(config.ExtraAgentLabels)
|
||||||
|
|
||||||
|
var userData bytes.Buffer
|
||||||
|
if err := tmpl.Execute(&userData, params); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return userData.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func genExtraAgentLabels(conf map[string]string) string {
|
||||||
|
out := make([]string, 0, len(conf))
|
||||||
|
for k, v := range conf {
|
||||||
|
out = append(out, fmt.Sprintf("%s=%s", k, v))
|
||||||
|
}
|
||||||
|
return strings.Join(out, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
// editorconfig-checker-disable
|
||||||
|
var CloudInitUserDataUbuntuDefault = `
|
||||||
|
#cloud-config
|
||||||
|
|
||||||
|
package_reboot_if_required: false
|
||||||
|
package_update: true
|
||||||
|
package_upgrade: false
|
||||||
|
|
||||||
|
groups:
|
||||||
|
- docker
|
||||||
|
|
||||||
|
system_info:
|
||||||
|
default_user:
|
||||||
|
groups: [ docker ]
|
||||||
|
|
||||||
|
apt:
|
||||||
|
sources:
|
||||||
|
docker.list:
|
||||||
|
keyid: 9DC858229FC7DD38854AE2D88D81803C0EBFCD88
|
||||||
|
keyserver: https://download.docker.com/linux/ubuntu/gpg
|
||||||
|
source: deb [signed-by=$KEY_FILE] https://download.docker.com/linux/ubuntu $RELEASE stable
|
||||||
|
|
||||||
|
packages:
|
||||||
|
- docker-ce
|
||||||
|
- docker-compose-plugin
|
||||||
|
- binfmt-support
|
||||||
|
- qemu-user-static
|
||||||
|
|
||||||
|
write_files:
|
||||||
|
- path: /root/docker-compose.yml
|
||||||
|
content: |
|
||||||
|
# docker-compose.yml
|
||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
woodpecker-agent:
|
||||||
|
image: {{ .Image }}
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
environment:
|
||||||
|
{{- range $key, $value := .Environment }}
|
||||||
|
- {{ $key }}={{ $value }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
runcmd:
|
||||||
|
- sh -xc "cd /root; docker compose up -d"
|
||||||
|
|
||||||
|
final_message: "The system is finally up, after $UPTIME seconds"
|
||||||
|
` // editorconfig-checker-enable
|
||||||
65
engine/inits/cloudinit/cloudinit_test.go
Normal file
65
engine/inits/cloudinit/cloudinit_test.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package cloudinit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"go.woodpecker-ci.org/autoscaler/config"
|
||||||
|
"go.woodpecker-ci.org/autoscaler/engine/inits/cloudinit"
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testUserDataStr = `
|
||||||
|
image: {{ .Image }}
|
||||||
|
environment:
|
||||||
|
{{- range $key, $value := .Environment }}
|
||||||
|
- {{ $key }}={{ $value }}
|
||||||
|
{{- end }}
|
||||||
|
`
|
||||||
|
|
||||||
|
var testUserDataTmpl = template.Must(template.New("test").Parse(testUserDataStr))
|
||||||
|
|
||||||
|
func TestRenderUserDataTemplate(t *testing.T) {
|
||||||
|
config := &config.Config{
|
||||||
|
Image: "test-image",
|
||||||
|
GRPCAddress: "test-address",
|
||||||
|
GRPCSecure: false,
|
||||||
|
Environment: map[string]string{
|
||||||
|
"FOO": "bar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
agent := &woodpecker.Agent{
|
||||||
|
Token: "test-token",
|
||||||
|
}
|
||||||
|
|
||||||
|
userData, err := cloudinit.RenderUserDataTemplate(config, agent, testUserDataTmpl)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Contains(t, userData, "test-image")
|
||||||
|
assert.Contains(t, userData, "bar")
|
||||||
|
assert.Contains(t, userData, "WOODPECKER_SERVER=test-address")
|
||||||
|
assert.Contains(t, userData, "WOODPECKER_AGENT_SECRET=test-token")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderUserDataTemplate_Secure(t *testing.T) {
|
||||||
|
config := &config.Config{
|
||||||
|
GRPCSecure: true,
|
||||||
|
}
|
||||||
|
agent := &woodpecker.Agent{}
|
||||||
|
|
||||||
|
userData, err := cloudinit.RenderUserDataTemplate(config, agent, testUserDataTmpl)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Contains(t, userData, "WOODPECKER_GRPC_SECURE=true")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderUserDataTemplate_Error(t *testing.T) {
|
||||||
|
config := &config.Config{}
|
||||||
|
agent := &woodpecker.Agent{}
|
||||||
|
tmpl := template.Must(template.New("test").Parse("{{.Missing}}"))
|
||||||
|
|
||||||
|
_, err := cloudinit.RenderUserDataTemplate(config, agent, tmpl)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
215
engine/types/mocks/mock_Provider.go
Normal file
215
engine/types/mocks/mock_Provider.go
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
// Code generated by mockery; DO NOT EDIT.
|
||||||
|
// github.com/vektra/mockery
|
||||||
|
// template: testify
|
||||||
|
|
||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewMockProvider creates a new instance of MockProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||||
|
// The first argument is typically a *testing.T value.
|
||||||
|
func NewMockProvider(t interface {
|
||||||
|
mock.TestingT
|
||||||
|
Cleanup(func())
|
||||||
|
}) *MockProvider {
|
||||||
|
mock := &MockProvider{}
|
||||||
|
mock.Mock.Test(t)
|
||||||
|
|
||||||
|
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockProvider is an autogenerated mock type for the Provider type
|
||||||
|
type MockProvider struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockProvider_Expecter struct {
|
||||||
|
mock *mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_m *MockProvider) EXPECT() *MockProvider_Expecter {
|
||||||
|
return &MockProvider_Expecter{mock: &_m.Mock}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeployAgent provides a mock function for the type MockProvider
|
||||||
|
func (_mock *MockProvider) DeployAgent(context1 context.Context, agent *woodpecker.Agent) error {
|
||||||
|
ret := _mock.Called(context1, agent)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for DeployAgent")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if returnFunc, ok := ret.Get(0).(func(context.Context, *woodpecker.Agent) error); ok {
|
||||||
|
r0 = returnFunc(context1, agent)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockProvider_DeployAgent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeployAgent'
|
||||||
|
type MockProvider_DeployAgent_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeployAgent is a helper method to define mock.On call
|
||||||
|
// - context1 context.Context
|
||||||
|
// - agent *woodpecker.Agent
|
||||||
|
func (_e *MockProvider_Expecter) DeployAgent(context1 interface{}, agent interface{}) *MockProvider_DeployAgent_Call {
|
||||||
|
return &MockProvider_DeployAgent_Call{Call: _e.mock.On("DeployAgent", context1, agent)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockProvider_DeployAgent_Call) Run(run func(context1 context.Context, agent *woodpecker.Agent)) *MockProvider_DeployAgent_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
var arg0 context.Context
|
||||||
|
if args[0] != nil {
|
||||||
|
arg0 = args[0].(context.Context)
|
||||||
|
}
|
||||||
|
var arg1 *woodpecker.Agent
|
||||||
|
if args[1] != nil {
|
||||||
|
arg1 = args[1].(*woodpecker.Agent)
|
||||||
|
}
|
||||||
|
run(
|
||||||
|
arg0,
|
||||||
|
arg1,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockProvider_DeployAgent_Call) Return(err error) *MockProvider_DeployAgent_Call {
|
||||||
|
_c.Call.Return(err)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockProvider_DeployAgent_Call) RunAndReturn(run func(context1 context.Context, agent *woodpecker.Agent) error) *MockProvider_DeployAgent_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListDeployedAgentNames provides a mock function for the type MockProvider
|
||||||
|
func (_mock *MockProvider) ListDeployedAgentNames(context1 context.Context) ([]string, error) {
|
||||||
|
ret := _mock.Called(context1)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for ListDeployedAgentNames")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 []string
|
||||||
|
var r1 error
|
||||||
|
if returnFunc, ok := ret.Get(0).(func(context.Context) ([]string, error)); ok {
|
||||||
|
return returnFunc(context1)
|
||||||
|
}
|
||||||
|
if returnFunc, ok := ret.Get(0).(func(context.Context) []string); ok {
|
||||||
|
r0 = returnFunc(context1)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
|
||||||
|
r1 = returnFunc(context1)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockProvider_ListDeployedAgentNames_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListDeployedAgentNames'
|
||||||
|
type MockProvider_ListDeployedAgentNames_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListDeployedAgentNames is a helper method to define mock.On call
|
||||||
|
// - context1 context.Context
|
||||||
|
func (_e *MockProvider_Expecter) ListDeployedAgentNames(context1 interface{}) *MockProvider_ListDeployedAgentNames_Call {
|
||||||
|
return &MockProvider_ListDeployedAgentNames_Call{Call: _e.mock.On("ListDeployedAgentNames", context1)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockProvider_ListDeployedAgentNames_Call) Run(run func(context1 context.Context)) *MockProvider_ListDeployedAgentNames_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
var arg0 context.Context
|
||||||
|
if args[0] != nil {
|
||||||
|
arg0 = args[0].(context.Context)
|
||||||
|
}
|
||||||
|
run(
|
||||||
|
arg0,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockProvider_ListDeployedAgentNames_Call) Return(strings []string, err error) *MockProvider_ListDeployedAgentNames_Call {
|
||||||
|
_c.Call.Return(strings, err)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockProvider_ListDeployedAgentNames_Call) RunAndReturn(run func(context1 context.Context) ([]string, error)) *MockProvider_ListDeployedAgentNames_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveAgent provides a mock function for the type MockProvider
|
||||||
|
func (_mock *MockProvider) RemoveAgent(context1 context.Context, agent *woodpecker.Agent) error {
|
||||||
|
ret := _mock.Called(context1, agent)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for RemoveAgent")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if returnFunc, ok := ret.Get(0).(func(context.Context, *woodpecker.Agent) error); ok {
|
||||||
|
r0 = returnFunc(context1, agent)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockProvider_RemoveAgent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveAgent'
|
||||||
|
type MockProvider_RemoveAgent_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveAgent is a helper method to define mock.On call
|
||||||
|
// - context1 context.Context
|
||||||
|
// - agent *woodpecker.Agent
|
||||||
|
func (_e *MockProvider_Expecter) RemoveAgent(context1 interface{}, agent interface{}) *MockProvider_RemoveAgent_Call {
|
||||||
|
return &MockProvider_RemoveAgent_Call{Call: _e.mock.On("RemoveAgent", context1, agent)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockProvider_RemoveAgent_Call) Run(run func(context1 context.Context, agent *woodpecker.Agent)) *MockProvider_RemoveAgent_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
var arg0 context.Context
|
||||||
|
if args[0] != nil {
|
||||||
|
arg0 = args[0].(context.Context)
|
||||||
|
}
|
||||||
|
var arg1 *woodpecker.Agent
|
||||||
|
if args[1] != nil {
|
||||||
|
arg1 = args[1].(*woodpecker.Agent)
|
||||||
|
}
|
||||||
|
run(
|
||||||
|
arg0,
|
||||||
|
arg1,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockProvider_RemoveAgent_Call) Return(err error) *MockProvider_RemoveAgent_Call {
|
||||||
|
_c.Call.Return(err)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockProvider_RemoveAgent_Call) RunAndReturn(run func(context1 context.Context, agent *woodpecker.Agent) error) *MockProvider_RemoveAgent_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
13
engine/types/provider.go
Normal file
13
engine/types/provider.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Provider interface {
|
||||||
|
DeployAgent(context.Context, *woodpecker.Agent) error
|
||||||
|
RemoveAgent(context.Context, *woodpecker.Agent) error
|
||||||
|
ListDeployedAgentNames(context.Context) ([]string, error)
|
||||||
|
}
|
||||||
28
go.mod
Normal file
28
go.mod
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
module go.woodpecker-ci.org/autoscaler
|
||||||
|
|
||||||
|
go 1.26.0
|
||||||
|
|
||||||
|
toolchain go1.26.3
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/rs/zerolog v1.35.1
|
||||||
|
github.com/stretchr/testify v1.11.1
|
||||||
|
github.com/urfave/cli/v3 v3.9.0
|
||||||
|
go.woodpecker-ci.org/woodpecker/v3 v3.14.1
|
||||||
|
golang.org/x/net v0.54.0
|
||||||
|
golang.org/x/oauth2 v0.36.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
|
golang.org/x/sys v0.44.0 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
44
go.sum
Normal file
44
go.sum
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
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/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
||||||
|
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/urfave/cli/v3 v3.9.0 h1:AV9lIiPv3ukYnxunaCUsHnEozptYmDN2F0+yWqLMn/c=
|
||||||
|
github.com/urfave/cli/v3 v3.9.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||||
|
go.woodpecker-ci.org/woodpecker/v3 v3.14.1 h1:/HrTfGQOJxVHx5+ReCAf/4BBZe+HoE7s/rYgOzZlRi0=
|
||||||
|
go.woodpecker-ci.org/woodpecker/v3 v3.14.1/go.mod h1:b0Rz7zEMUG1T2C0djxV1euWkNlSDH5uqJB/5fCQtvBE=
|
||||||
|
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||||
|
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||||
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||||
|
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
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.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
222
providers/timewebcloud/api/client.go
Normal file
222
providers/timewebcloud/api/client.go
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultBaseURL = "https://api.timeweb.cloud"
|
||||||
|
|
||||||
|
// Client is a minimal HTTP client for the Timeweb Cloud API.
|
||||||
|
type Client struct {
|
||||||
|
baseURL string
|
||||||
|
token string
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new Timeweb Cloud API client.
|
||||||
|
func NewClient(token string) *Client {
|
||||||
|
return &Client{
|
||||||
|
baseURL: defaultBaseURL,
|
||||||
|
token: token,
|
||||||
|
client: &http.Client{Timeout: 30 * time.Second},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) request(ctx context.Context, method, path string, body []byte) (*http.Response, error) {
|
||||||
|
url := c.baseURL + path
|
||||||
|
var bodyReader io.Reader
|
||||||
|
if body != nil {
|
||||||
|
bodyReader = bytes.NewReader(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
return c.client.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateServerRequest is the payload for creating a server.
|
||||||
|
type CreateServerRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
OsID int32 `json:"os_id,omitempty"`
|
||||||
|
ImageID string `json:"image_id,omitempty"`
|
||||||
|
PresetID int32 `json:"preset_id,omitempty"`
|
||||||
|
Bandwidth int32 `json:"bandwidth,omitempty"`
|
||||||
|
SSHKeysIds []int32 `json:"ssh_keys_ids,omitempty"`
|
||||||
|
CloudInit string `json:"cloud_init,omitempty"`
|
||||||
|
AvailabilityZone string `json:"availability_zone,omitempty"`
|
||||||
|
Hostname string `json:"hostname,omitempty"`
|
||||||
|
Comment string `json:"comment,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateServerResponse is the response from creating a server.
|
||||||
|
type CreateServerResponse struct {
|
||||||
|
Server Server `json:"server"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server represents a Timeweb Cloud VDS.
|
||||||
|
type Server struct {
|
||||||
|
ID int32 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Location string `json:"location"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateServer creates a new cloud server.
|
||||||
|
func (c *Client) CreateServer(ctx context.Context, req CreateServerRequest) (*CreateServerResponse, error) {
|
||||||
|
payload, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.request(ctx, http.MethodPost, "/api/v1/servers", payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusCreated {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var result CreateServerResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteServer deletes a cloud server by ID.
|
||||||
|
func (c *Client) DeleteServer(ctx context.Context, id int32) error {
|
||||||
|
path := "/api/v1/servers/" + strconv.FormatInt(int64(id), 10)
|
||||||
|
resp, err := c.request(ctx, http.MethodDelete, path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetServersResponse is the response for listing servers.
|
||||||
|
type GetServersResponse struct {
|
||||||
|
Meta Meta `json:"meta"`
|
||||||
|
Servers []Server `json:"servers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Meta contains pagination info.
|
||||||
|
type Meta struct {
|
||||||
|
Total int32 `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetServers lists all cloud servers.
|
||||||
|
func (c *Client) GetServers(ctx context.Context) (*GetServersResponse, error) {
|
||||||
|
resp, err := c.request(ctx, http.MethodGet, "/api/v1/servers", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var result GetServersResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type OS struct {
|
||||||
|
ID int32 `json:"id"`
|
||||||
|
Family string `json:"family"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
VersionCodename string `json:"version_codename"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetOSListResponse struct {
|
||||||
|
OsList []OS `json:"servers_os"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetOSList(ctx context.Context) (*GetOSListResponse, error) {
|
||||||
|
resp, err := c.request(ctx, http.MethodGet, "/api/v1/os/servers", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var result GetOSListResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Preset struct {
|
||||||
|
ID int32 `json:"id"`
|
||||||
|
Location string `json:"location"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
CPU int32 `json:"cpu"`
|
||||||
|
CPUFrequency string `json:"cpu_frequency"`
|
||||||
|
RAM int32 `json:"ram"`
|
||||||
|
Disk int32 `json:"disk"`
|
||||||
|
DiskType string `json:"disk_type"`
|
||||||
|
Bandwidth int32 `json:"bandwidth"`
|
||||||
|
DescriptionShort string `json:"description_short"`
|
||||||
|
IsDedicatedCPU bool `json:"is_dedicated_cpu"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetPresetsResponse struct {
|
||||||
|
Presets []Preset `json:"server_presets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetServerPresets(ctx context.Context) (*GetPresetsResponse, error) {
|
||||||
|
resp, err := c.request(ctx, http.MethodGet, "/api/v1/presets/servers", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var result GetPresetsResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
47
providers/timewebcloud/flags.go
Normal file
47
providers/timewebcloud/flags.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package timewebcloud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const category = "Timeweb Cloud"
|
||||||
|
|
||||||
|
var ProviderFlags = []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "timewebcloud-api-token",
|
||||||
|
Usage: "Timeweb Cloud API JWT token",
|
||||||
|
Sources: cli.EnvVars("WOODPECKER_TIMEWEBCLOUD_API_TOKEN"),
|
||||||
|
Category: category,
|
||||||
|
},
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "timewebcloud-os-id",
|
||||||
|
Usage: "OS image ID to use for new servers (from GetOsList)",
|
||||||
|
Sources: cli.EnvVars("WOODPECKER_TIMEWEBCLOUD_OS_ID"),
|
||||||
|
Category: category,
|
||||||
|
},
|
||||||
|
&cli.IntFlag{
|
||||||
|
Name: "timewebcloud-preset-id",
|
||||||
|
Usage: "Pricing preset ID (from GetServersPresets)",
|
||||||
|
Sources: cli.EnvVars("WOODPECKER_TIMEWEBCLOUD_PRESET_ID"),
|
||||||
|
Category: category,
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "timewebcloud-availability-zone",
|
||||||
|
Usage: "Availability zone for new servers",
|
||||||
|
Sources: cli.EnvVars("WOODPECKER_TIMEWEBCLOUD_AVAILABILITY_ZONE"),
|
||||||
|
Category: category,
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "timewebcloud-ssh-key-ids",
|
||||||
|
Usage: "SSH key IDs to inject into new servers",
|
||||||
|
Sources: cli.EnvVars("WOODPECKER_TIMEWEBCLOUD_SSH_KEY_IDS"),
|
||||||
|
Category: category,
|
||||||
|
},
|
||||||
|
// TODO: Deprecated remove in v2.0
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "timewebcloud-user-data",
|
||||||
|
Usage: "timeweb cloud userdata template (deprecated)",
|
||||||
|
Sources: cli.EnvVars("WOODPECKER_TIMEWEBCLOUD_USERDATA"),
|
||||||
|
Category: category,
|
||||||
|
},
|
||||||
|
}
|
||||||
122
providers/timewebcloud/provider.go
Normal file
122
providers/timewebcloud/provider.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package timewebcloud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
|
||||||
|
"go.woodpecker-ci.org/autoscaler/config"
|
||||||
|
"go.woodpecker-ci.org/autoscaler/engine/inits/cloudinit"
|
||||||
|
"go.woodpecker-ci.org/autoscaler/engine/types"
|
||||||
|
"go.woodpecker-ci.org/autoscaler/providers/timewebcloud/api"
|
||||||
|
woodpecker "go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Provider struct {
|
||||||
|
name string
|
||||||
|
config *config.Config
|
||||||
|
client *api.Client
|
||||||
|
osID int32
|
||||||
|
presetID int32
|
||||||
|
availabilityZone string
|
||||||
|
sshKeyIDs []int32
|
||||||
|
userDataTemplate *template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(_ context.Context, c *cli.Command, config *config.Config) (types.Provider, error) {
|
||||||
|
p := &Provider{
|
||||||
|
name: "timewebcloud",
|
||||||
|
config: config,
|
||||||
|
client: api.NewClient(c.String("timewebcloud-api-token")),
|
||||||
|
osID: int32(c.Int("timewebcloud-os-id")),
|
||||||
|
presetID: int32(c.Int("timewebcloud-preset-id")),
|
||||||
|
availabilityZone: c.String("timewebcloud-availability-zone"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, raw := range c.StringSlice("timewebcloud-ssh-key-ids") {
|
||||||
|
id, err := strconv.ParseInt(raw, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: invalid ssh key id: %s", p.name, raw)
|
||||||
|
}
|
||||||
|
p.sshKeyIDs = append(p.sshKeyIDs, int32(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
if u := c.String("timewebcloud-user-data"); u != "" {
|
||||||
|
log.Warn().Msg("timewebcloud-user-data is deprecated, please use provider-user-data instead")
|
||||||
|
tmpl, err := template.New("user-data").Parse(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: template.New.Parse %w", p.name, err)
|
||||||
|
}
|
||||||
|
p.userDataTemplate = tmpl
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) DeployAgent(ctx context.Context, agent *woodpecker.Agent) error {
|
||||||
|
userData, err := cloudinit.RenderUserDataTemplate(p.config, agent, p.userDataTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: cloudinit.RenderUserDataTemplate: %w", p.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := api.CreateServerRequest{
|
||||||
|
Name: agent.Name,
|
||||||
|
OsID: p.osID,
|
||||||
|
PresetID: p.presetID,
|
||||||
|
CloudInit: userData,
|
||||||
|
AvailabilityZone: p.availabilityZone,
|
||||||
|
SSHKeysIds: p.sshKeyIDs,
|
||||||
|
Comment: fmt.Sprintf("woodpecker autoscaler agent for pool %s", p.config.PoolID),
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Str("agent", agent.Name).Msg("create agent")
|
||||||
|
|
||||||
|
_, err = p.client.CreateServer(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: CreateServer: %w", p.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) RemoveAgent(ctx context.Context, agent *woodpecker.Agent) error {
|
||||||
|
servers, err := p.client.GetServers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: GetServers: %w", p.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, server := range servers.Servers {
|
||||||
|
if server.Name == agent.Name {
|
||||||
|
log.Info().Str("agent", agent.Name).Int32("server_id", server.ID).Msg("delete agent")
|
||||||
|
if err := p.client.DeleteServer(ctx, server.ID); err != nil {
|
||||||
|
return fmt.Errorf("%s: DeleteServer: %w", p.name, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warn().Str("agent", agent.Name).Msg("agent not found, skipping deletion")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) ListDeployedAgentNames(ctx context.Context) ([]string, error) {
|
||||||
|
servers, err := p.client.GetServers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: GetServers: %w", p.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := fmt.Sprintf("pool-%s-agent-", p.config.PoolID)
|
||||||
|
var names []string
|
||||||
|
for _, server := range servers.Servers {
|
||||||
|
if strings.HasPrefix(server.Name, prefix) {
|
||||||
|
names = append(names, server.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return names, nil
|
||||||
|
}
|
||||||
3
release-config.ts
Normal file
3
release-config.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
commentOnReleasedPullRequests: false,
|
||||||
|
};
|
||||||
4
renovate.json
Normal file
4
renovate.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": ["github>woodpecker-ci/renovate-config"]
|
||||||
|
}
|
||||||
79
server/client.go
Normal file
79
server/client.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
"golang.org/x/net/proxy"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"go.woodpecker-ci.org/woodpecker/v3/woodpecker-go/woodpecker"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client interface {
|
||||||
|
woodpecker.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient returns a new client from the CLI context.
|
||||||
|
func NewClient(ctx context.Context, c *cli.Command) (Client, error) {
|
||||||
|
var (
|
||||||
|
skip = c.Bool("skip-verify")
|
||||||
|
socks = c.String("socks-proxy")
|
||||||
|
socksoff = c.Bool("socks-proxy-off")
|
||||||
|
serverToken = c.String("server-token")
|
||||||
|
serverURL = c.String("server-url")
|
||||||
|
)
|
||||||
|
serverURL = strings.TrimRight(serverURL, "/")
|
||||||
|
|
||||||
|
if len(serverURL) == 0 {
|
||||||
|
return nil, fmt.Errorf("please provide the woodpecker server address")
|
||||||
|
}
|
||||||
|
if len(serverToken) == 0 {
|
||||||
|
return nil, fmt.Errorf("please provide a woodpecker access token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt to find system CA certs
|
||||||
|
certs, err := x509.SystemCertPool()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("ca certs not found")
|
||||||
|
}
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
RootCAs: certs,
|
||||||
|
InsecureSkipVerify: skip,
|
||||||
|
}
|
||||||
|
|
||||||
|
config := new(oauth2.Config)
|
||||||
|
client := config.Client(
|
||||||
|
ctx,
|
||||||
|
&oauth2.Token{
|
||||||
|
AccessToken: serverToken,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
trans, _ := client.Transport.(*oauth2.Transport)
|
||||||
|
|
||||||
|
if len(socks) != 0 && !socksoff {
|
||||||
|
dialer, err := proxy.SOCKS5("tcp", socks, nil, proxy.Direct)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
trans.Base = &http.Transport{
|
||||||
|
TLSClientConfig: tlsConfig,
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
Dial: dialer.Dial,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
trans.Base = &http.Transport{
|
||||||
|
TLSClientConfig: tlsConfig,
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return woodpecker.NewClient(serverURL, client), nil
|
||||||
|
}
|
||||||
4982
server/mocks/mock_Client.go
Normal file
4982
server/mocks/mock_Client.go
Normal file
File diff suppressed because it is too large
Load Diff
18
utils/random.go
Normal file
18
utils/random.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RandomString generates a random string of length n using alphanumeric characters.
|
||||||
|
func RandomString(n int) string {
|
||||||
|
letterRunes := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||||
|
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
|
||||||
|
b := make([]rune, n)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = letterRunes[rng.Intn(len(letterRunes))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
42
utils/random_test.go
Normal file
42
utils/random_test.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package utils_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"go.woodpecker-ci.org/autoscaler/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRandomString(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
n int
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "zero length",
|
||||||
|
n: 0,
|
||||||
|
want: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "length 10",
|
||||||
|
n: 10,
|
||||||
|
want: 10,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
str := utils.RandomString(tt.n)
|
||||||
|
assert.Equal(t, tt.want, len(str))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("alphanumeric", func(t *testing.T) {
|
||||||
|
str1 := utils.RandomString(10)
|
||||||
|
for _, r := range str1 {
|
||||||
|
assert.Contains(t, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", string(r))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
39
utils/stringmaps.go
Normal file
39
utils/stringmaps.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SliceToMap converts a slice of strings in the format "key=value"
|
||||||
|
// into a string map, using the provided delimiter to split the pieces.
|
||||||
|
// Returns a map and nil error on success, or nil and an error if a
|
||||||
|
// slice element does not contain the delimiter.
|
||||||
|
func SliceToMap(list []string, del string) (map[string]string, error) {
|
||||||
|
m := make(map[string]string)
|
||||||
|
for _, e := range list {
|
||||||
|
before, after, _ := strings.Cut(e, del)
|
||||||
|
if before == "" || after == "" {
|
||||||
|
return nil, fmt.Errorf("could not split '%s' into key value pair with '=' delimiter", e)
|
||||||
|
}
|
||||||
|
m[strings.TrimSpace(before)] = strings.TrimSpace(after)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeMaps merges two string maps m1 and m2 into a new map.
|
||||||
|
// It copies all key-value pairs from m1 into the result.
|
||||||
|
// It then copies all key-value pairs from m2 into the result,
|
||||||
|
// overwriting any keys that are present in both m1 and m2.
|
||||||
|
// The merged map is returned.
|
||||||
|
func MergeMaps(m1, m2 map[string]string) map[string]string {
|
||||||
|
merged := make(map[string]string)
|
||||||
|
for k, v := range m1 {
|
||||||
|
merged[k] = v
|
||||||
|
}
|
||||||
|
for key, value := range m2 {
|
||||||
|
merged[key] = value
|
||||||
|
}
|
||||||
|
return merged
|
||||||
|
}
|
||||||
96
utils/stringmaps_test.go
Normal file
96
utils/stringmaps_test.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package utils_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"go.woodpecker-ci.org/autoscaler/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSliceToMap(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
input []string
|
||||||
|
del string
|
||||||
|
want map[string]string
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic",
|
||||||
|
input: []string{"key1=value1", "key2=value2"},
|
||||||
|
del: "=",
|
||||||
|
want: map[string]string{"key1": "value1", "key2": "value2"},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "whitespace",
|
||||||
|
input: []string{"key1 = value1", "key2=value2"},
|
||||||
|
del: "=",
|
||||||
|
want: map[string]string{"key1": "value1", "key2": "value2"},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing delimiter",
|
||||||
|
input: []string{"key1", "key2=value2"},
|
||||||
|
del: "=",
|
||||||
|
want: nil,
|
||||||
|
wantErr: assert.AnError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range testCases {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
actual, err := utils.SliceToMap(tt.input, tt.del)
|
||||||
|
if tt.wantErr != nil {
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.want, actual)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeMaps(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
m1 map[string]string
|
||||||
|
m2 map[string]string
|
||||||
|
want map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil maps",
|
||||||
|
m1: nil,
|
||||||
|
m2: nil,
|
||||||
|
want: map[string]string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty maps",
|
||||||
|
m1: map[string]string{},
|
||||||
|
m2: map[string]string{},
|
||||||
|
want: map[string]string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "overwrite",
|
||||||
|
m1: map[string]string{"key1": "value1", "key2": "value2"},
|
||||||
|
m2: map[string]string{"key2": "newvalue2", "key3": "value3"},
|
||||||
|
want: map[string]string{"key1": "value1", "key2": "newvalue2", "key3": "value3"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no overwrite",
|
||||||
|
m1: map[string]string{"key1": "value1", "key2": "value2"},
|
||||||
|
m2: map[string]string{"key3": "value3", "key4": "value4"},
|
||||||
|
want: map[string]string{"key1": "value1", "key2": "value2", "key3": "value3", "key4": "value4"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range testCases {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
merged := utils.MergeMaps(tt.m1, tt.m2)
|
||||||
|
assert.Equal(t, tt.want, merged)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
13
version/version.go
Normal file
13
version/version.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
// Version of Woodpecker Autoscaler, set with ldflags, from Git tag.
|
||||||
|
var Version string
|
||||||
|
|
||||||
|
// String returns the Version set at build time or "dev".
|
||||||
|
func String() string {
|
||||||
|
if Version == "" {
|
||||||
|
return "dev"
|
||||||
|
}
|
||||||
|
|
||||||
|
return Version
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user