Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add scaleway provider #56

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions cmd/woodpecker-autoscaler/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"go.woodpecker-ci.org/autoscaler/config"
"go.woodpecker-ci.org/autoscaler/engine"
"go.woodpecker-ci.org/autoscaler/providers/hetznercloud"
"go.woodpecker-ci.org/autoscaler/providers/scaleway"
"go.woodpecker-ci.org/autoscaler/server"
)

Expand All @@ -25,6 +26,13 @@ func setupProvider(ctx *cli.Context, config *config.Config) (engine.Provider, er
// Enable it again when the issue is fixed.
// case "linode":
// return linode.New(ctx, config)
case "scaleway":
scwCfg, err := scaleway.FromCLI(ctx)
if err != nil {
return nil, err
}

return scaleway.New(scwCfg, config)
Comment on lines +29 to +35
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was there a specific reason to use another structure to do the config setup for this provider? I would prefer to keep the setup for providers equal. Everything done by scaleway.FromCLI(ctx) should be handled by scaleway.New similar to e.g. https://github.com/woodpecker-ci/autoscaler/blob/main/providers/hetznercloud/provider.go#L43

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I initially decoupled the parsing from CLI and the config structure to allow using declarative file (e.g. a yaml to config the autoscaler) instead of the CLI. But it seems like we won't support configuration from file so I can remove that indeed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have some time to change it that would be great. If not we can also merge the current state and change it later.

case "":
return nil, fmt.Errorf("please select a provider")
}
Expand Down Expand Up @@ -77,6 +85,9 @@ func run(ctx *cli.Context) error {
return fmt.Errorf("can't parse reconciliation-interval: %w", err)
}

// Run a reconcile loop at start-up to avoid waiting 1m or more
autoscaler.Reconcile(ctx.Context)

for {
select {
case <-ctx.Done():
Expand Down Expand Up @@ -115,6 +126,7 @@ func main() {

// Register hetznercloud flags
app.Flags = append(app.Flags, hetznercloud.DriverFlags...)
app.Flags = append(app.Flags, scaleway.ProviderFlags...)
// Register linode flags
// TODO: Temp disabled due to the security issue https://github.com/woodpecker-ci/autoscaler/issues/91
// Enable it again when the issue is fixed.
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ module go.woodpecker-ci.org/autoscaler
go 1.21

require (
github.com/cenkalti/backoff/v4 v4.2.1
github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf
github.com/hetznercloud/hcloud-go/v2 v2.7.0
github.com/joho/godotenv v1.5.1
github.com/linode/linodego v1.32.0
github.com/rs/zerolog v1.32.0
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.25
github.com/stretchr/testify v1.9.0
github.com/urfave/cli/v2 v2.27.1
go.woodpecker-ci.org/woodpecker/v2 v2.4.1
Expand Down Expand Up @@ -35,5 +37,6 @@ require (
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/ini.v1 v1.66.6 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf h1:NrF81UtW8gG2LBGkXFQFqlfNnvMt9WdB46sfdJY4oqc=
github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo=
github.com/go-resty/resty/v2 v2.12.0 h1:rsVL8P90LFvkUYq/V5BTVe203WfRIU4gvcf+yfzJzGA=
Expand Down Expand Up @@ -50,6 +54,8 @@ github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.25 h1:/8rfZAdFfafRXOgz+ZpMZZWZ5pYggCY9t7e/BvjaBHM=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.25/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
Expand Down Expand Up @@ -122,5 +128,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI=
gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
91 changes: 91 additions & 0 deletions providers/scaleway/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package scaleway

import (
"errors"

"github.com/cenkalti/backoff/v4"
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
)

// Config is the Scaleway Provider specific configuration
//
// This is decoupled from the CLI interface for future-proofing reasons.
// Please, see ProviderFlags for information on how to configure the provider from the
// CLI or environment variables.
type Config struct {
// ApiToken of Scaleway IAM
//
// Creating a standalone IAM Applications is recommended to segregate
// permissions.
SecretKey string `json:"secret_key"`
AccessKey string `json:"access_key"`
DefaultProjectID string `json:"default_project_id"`
ClientRetry backoff.BackOff
InstancePool map[string]InstancePool `json:"instance_pool"`
}

// Locality defines a geographical area
//
// Scaleway Cloud has multiple Region that are made of several Zones.
// Exactly one of Zones or Region SHOULD be set,
// if both are set, use Region and ignore Zones
type Locality struct {
Zones []scw.Zone `json:"zones,omitempty"`
Region *scw.Region `json:"region,omitempty"`
}

// InstancePool is used as a template to spawn your instances
type InstancePool struct {
// Locality where your instances should live
// The Provider will try to spread your
// instances evenly among Locality.Zones if possible
Locality Locality `json:"locality"`
// ProjectID where resources should be applied
ProjectID *string `json:"project_id,omitempty"`
// Prefix is added before each instance name
Prefix string `json:"prefix"`
// Tags added to the placement group and its instances
Tags []string `json:"tags"`
// DynamicIPRequired: define if a dynamic IPv4 is required for the Instance.
DynamicIPRequired *bool `json:"dynamic_ip_required,omitempty"`
// RoutedIPEnabled: if true, configure the Instance, so it uses the new routed IP mode.
RoutedIPEnabled *bool `json:"routed_ip_enabled,omitempty"`
// CommercialType: define the Instance commercial type (i.e. GP1-S).
CommercialType string `json:"commercial_type,omitempty"`
// Image: instance image ID or label.
Image string `json:"image,omitempty"`
// EnableIPv6: true if IPv6 is enabled on the server.
EnableIPv6 bool `json:"enable_ipv6,omitempty"`
// PublicIPs to attach to your instance indexed per instance.IPType
PublicIPs map[instance.IPType]int `json:"public_ips,omitempty"`
// SecurityGroups to use per zone
SecurityGroups map[scw.Zone]string `json:"security_groups,omitempty"`
// Storage of the block storage associated with your Instances
// It should be a multiple of 512 bytes, in future version we could give
// more customisation over the volumes used by the agents
Storage scw.Size `json:"storage"`
}

func (l Locality) ResolveZones() ([]scw.Zone, error) {
if l.Region != nil {
if !l.Region.Exists() {
return nil, errors.New("you specified an invalid region: " + l.Region.String())
}

return l.Region.GetZones(), nil
}

zones := l.Zones
if len(zones) == 0 {
return nil, errors.New("you need to specify a valid locality")
}

for _, zone := range zones {
if !zone.Exists() {
return nil, errors.New("you specified a non-existing zone: " + zone.String())
}
}

return zones, nil
}
14 changes: 14 additions & 0 deletions providers/scaleway/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Package scaleway implements a way to use the Scaleway Cloud Provider for your
xoxys marked this conversation as resolved.
Show resolved Hide resolved
// Woodpecker CIs.
//
// This package contains subpackage per Scaleway Instance API version.
//
// # Limitations
//
// - As of now, we can only deploy instances in single-zones.
//
// Authors:
// - Enzo "raskyld" Nocera <[email protected]> [@[email protected]]
//
// [@[email protected]]: https://social.vivaldi.net/@raskyld
package scaleway
41 changes: 41 additions & 0 deletions providers/scaleway/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package scaleway

import (
"log/slog"

"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
)

type InstanceAlreadyExistsError struct {
inst *instance.Server
}

type InstanceDoesNotExists struct {
InstanceName string
Project string
Zones []scw.Zone
}

func (i InstanceAlreadyExistsError) Error() string {
return "instance already exists"
}

func (i InstanceAlreadyExistsError) LogValue() slog.Value {
return slog.GroupValue(slog.String("err", i.Error()),
slog.Group("instance", slog.String("id", i.inst.ID), slog.String("name", i.inst.Name),
slog.String("zone", i.inst.Zone.String()), slog.String("project", i.inst.Project)))
}

func (i InstanceDoesNotExists) Error() string {
return "instance does not exist"
}

func (i InstanceDoesNotExists) LogValue() slog.Value {
zones := make([]string, 0, len(i.Zones))
for _, zone := range i.Zones {
zones = append(zones, zone.String())
}

return slog.GroupValue(slog.String("name", i.InstanceName), slog.String("project", i.Project), slog.Any("zones", zones))
}
147 changes: 147 additions & 0 deletions providers/scaleway/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package scaleway

import (
"errors"
"os"

"github.com/scaleway/scaleway-sdk-go/scw"
"github.com/urfave/cli/v2"
)

const (
DefaultPool = "default"
DefaultAgentStorageGB = 25

category = "Scaleway"
flagPrefix = "scw"
envPrefix = "WOODPECKER_SCW"
)

var ProviderFlags = []cli.Flag{
&cli.StringFlag{
Name: flagPrefix + "-access-key",
Usage: "Scaleway IAM API Token Access Key",
EnvVars: []string{envPrefix + "_ACCESS_KEY"},
// NB(raskyld): We should recommend the usage of file-system to users
// Most container runtimes support mounting secrets into the fs
// natively.
FilePath: os.Getenv(envPrefix + "_ACCESS_KEY_FILE"),
Category: category,
},
&cli.StringFlag{
Name: flagPrefix + "-secret-key",
Usage: "Scaleway IAM API Token Secret Key",
EnvVars: []string{envPrefix + "_SECRET_KEY"},
// NB(raskyld): We should recommend the usage of file-system to users
// Most container runtimes support mounting secrets into the fs
// natively.
FilePath: os.Getenv(envPrefix + "_SECRET_KEY_FILE"),
Category: category,
},
// TODO(raskyld): implement multi-AZ
&cli.StringFlag{
Name: flagPrefix + "-zone",
Usage: "Scaleway Zone where to spawn instances",
EnvVars: []string{envPrefix + "_ZONE"},
Category: category,
Value: scw.ZoneFrPar2.String(),
},
&cli.StringFlag{
Name: flagPrefix + "-instance-type",
Usage: "Scaleway Instance type to spawn",
EnvVars: []string{envPrefix + "_INSTANCE_TYPE"},
Category: category,
},
&cli.StringSliceFlag{
Name: flagPrefix + "-tags",
Usage: "Comma separated list of tags to uniquely identify the instances spawned",
EnvVars: []string{envPrefix + "_TAGS"},
Category: category,
},
&cli.StringFlag{
Name: flagPrefix + "-project",
Usage: "Scaleway Project ID in which to spawn the instances",
EnvVars: []string{envPrefix + "_PROJECT"},
Category: category,
},
&cli.StringFlag{
Name: flagPrefix + "-prefix",
Usage: "Prefix prepended before any Scaleway resource name",
EnvVars: []string{envPrefix + "_PREFIX"},
Category: category,
Value: "wip-woodpecker-ci-autoscaler",
},
&cli.BoolFlag{
Name: flagPrefix + "-enable-ipv6",
Usage: "Enable IPv6 for the instances",
EnvVars: []string{envPrefix + "_ENABLE_IPV6"},
Category: category,
},
&cli.StringFlag{
Name: flagPrefix + "-image",
Usage: "The base image for your instance",
EnvVars: []string{envPrefix + "_IMAGE"},
Category: category,
Value: "ubuntu_jammy",
},
&cli.Uint64Flag{
Name: flagPrefix + "-storage-size",
Usage: "How much storage to provision for your agents in GB",
EnvVars: []string{envPrefix + "_STORAGE_SIZE"},
Category: category,
Value: DefaultAgentStorageGB,
},
}

func FromCLI(c *cli.Context) (Config, error) {
if !c.IsSet(flagPrefix + "-instance-type") {
return Config{}, errors.New("you must specify an instance type")
}

if !c.IsSet(flagPrefix + "-tags") {
return Config{}, errors.New("you must specify tags to apply to your resources")
}

if !c.IsSet(flagPrefix + "-project") {
return Config{}, errors.New("you must specify in which project resources should be spawned")
}

if !c.IsSet(flagPrefix + "-secret-key") {
return Config{}, errors.New("you must specify a secret key")
}

if !c.IsSet(flagPrefix + "-access-key") {
return Config{}, errors.New("you must specify an access key")
}

zone := scw.Zone(c.String(flagPrefix + "-zone"))
if !zone.Exists() {
return Config{}, errors.New(zone.String() + " is not a valid zone")
}

cfg := Config{
SecretKey: c.String(flagPrefix + "-secret-key"),
AccessKey: c.String(flagPrefix + "-access-key"),
DefaultProjectID: c.String(flagPrefix + "-project"),
}

cfg.InstancePool = map[string]InstancePool{
DefaultPool: {
Locality: Locality{
Zones: []scw.Zone{zone},
},
ProjectID: scw.StringPtr(c.String(flagPrefix + "-project")),
Prefix: c.String(flagPrefix + "-prefix"),
Tags: c.StringSlice(flagPrefix + "-tags"),
// We do not need stables IP for our JIT runners
DynamicIPRequired: scw.BoolPtr(true),
CommercialType: c.String(flagPrefix + "-instance-type"),
Image: c.String(flagPrefix + "-image"),
EnableIPv6: c.Bool(flagPrefix + "-enable-ipv6"),
//nolint:gomnd
Storage: scw.Size(c.Uint64(flagPrefix+"-storage-size") * 1e9),
},
}

return cfg, nil
}
Loading