Repository: drone/autoscaler Branch: master Commit: b22b87077a04 Files: 212 Total size: 731.3 KB Directory structure: gitextract_xkto3c2c/ ├── .drone.sh ├── .drone.yml ├── .github/ │ ├── issue_template.md │ └── pull_request_template.md ├── .gitignore ├── BUILDING ├── CHANGELOG.md ├── COPYRIGHT ├── Dockerfile ├── LICENSE.md ├── README.md ├── cmd/ │ └── drone-autoscaler/ │ └── main.go ├── config/ │ ├── config.go │ ├── load.go │ └── load_test.go ├── drivers/ │ ├── amazon/ │ │ ├── create.go │ │ ├── create_test.go │ │ ├── destroy.go │ │ ├── destroy_test.go │ │ ├── option.go │ │ ├── option_test.go │ │ ├── provider.go │ │ ├── provider_test.go │ │ ├── setup.go │ │ ├── setup_test.go │ │ ├── util.go │ │ └── util_test.go │ ├── azure/ │ │ ├── create.go │ │ ├── create_test.go │ │ ├── destroy.go │ │ ├── destroy_test.go │ │ ├── option.go │ │ ├── option_test.go │ │ ├── provider.go │ │ └── provider_test.go │ ├── digitalocean/ │ │ ├── create.go │ │ ├── create_test.go │ │ ├── destroy.go │ │ ├── destroy_test.go │ │ ├── option.go │ │ ├── option_test.go │ │ ├── provider.go │ │ ├── provider_test.go │ │ ├── setup.go │ │ ├── setup_test.go │ │ └── userdata.go │ ├── google/ │ │ ├── create.go │ │ ├── create_test.go │ │ ├── destroy.go │ │ ├── destroy_test.go │ │ ├── option.go │ │ ├── option_test.go │ │ ├── provider.go │ │ ├── provider_test.go │ │ ├── setup.go │ │ └── setup_test.go │ ├── hetznercloud/ │ │ ├── create.go │ │ ├── create_test.go │ │ ├── destroy.go │ │ ├── destroy_test.go │ │ ├── option.go │ │ ├── option_test.go │ │ ├── provider.go │ │ ├── provider_test.go │ │ ├── setup.go │ │ └── setup_test.go │ ├── internal/ │ │ └── userdata/ │ │ ├── userdata.go │ │ └── userdata_test.go │ ├── openstack/ │ │ ├── create.go │ │ ├── create_test.go │ │ ├── destroy.go │ │ ├── destroy_test.go │ │ ├── doc.go │ │ ├── option.go │ │ ├── option_test.go │ │ ├── provider.go │ │ ├── provider_test.go │ │ ├── setup.go │ │ ├── setup_test.go │ │ └── testdata/ │ │ ├── associateresp1.json │ │ ├── authresp1.json │ │ ├── fipresp1.json │ │ ├── flavorlistresp1.json │ │ ├── imagelistresp1.json │ │ ├── servercreateresp1.json │ │ ├── serverstatusresp1.json │ │ └── tokenresp1.json │ ├── packet/ │ │ ├── create.go │ │ ├── create_test.go │ │ ├── destroy.go │ │ ├── destroy_test.go │ │ ├── option.go │ │ ├── option_test.go │ │ ├── provider.go │ │ ├── provider_test.go │ │ ├── setup.go │ │ └── setup_test.go │ └── scaleway/ │ ├── create.go │ ├── create_test.go │ ├── destroy.go │ ├── destroy_test.go │ ├── option.go │ ├── option_test.go │ ├── provider.go │ ├── provider_test.go │ └── setup.go ├── engine/ │ ├── alloc.go │ ├── alloc_test.go │ ├── calc.go │ ├── calc_test.go │ ├── certs/ │ │ ├── cert.go │ │ └── cert_test.go │ ├── collect.go │ ├── collect_test.go │ ├── docker.go │ ├── engine.go │ ├── install.go │ ├── install_test.go │ ├── pinger.go │ ├── pinger_test.go │ ├── planner.go │ ├── planner_test.go │ ├── reaper.go │ ├── reaper_test.go │ ├── sort.go │ └── sort_test.go ├── engine.go ├── go.mod ├── go.sum ├── licenses/ │ ├── Polyform-Free-Trial.md │ └── Polyform-Small-Business.md ├── logger/ │ ├── context.go │ ├── context_test.go │ ├── history/ │ │ ├── history.go │ │ └── history_test.go │ ├── logger.go │ ├── logger_test.go │ ├── logrus.go │ ├── logrus_test.go │ └── request/ │ └── request.go ├── metrics/ │ ├── metrics.go │ ├── server_capacity.go │ ├── server_capacity_test.go │ ├── server_count.go │ ├── server_count_test.go │ ├── server_create.go │ ├── server_create_test.go │ ├── server_delete.go │ └── server_delete_test.go ├── mocks/ │ ├── mock_docker.go │ ├── mock_drone.go │ ├── mock_engine.go │ ├── mock_metrics.go │ ├── mock_provider.go │ ├── mock_server.go │ └── mocks.go ├── provider.go ├── server/ │ ├── auth.go │ ├── auth_test.go │ ├── engine.go │ ├── engine_test.go │ ├── healthz.go │ ├── healthz_test.go │ ├── metrics.go │ ├── metrics_test.go │ ├── servers.go │ ├── servers_test.go │ ├── varz.go │ ├── varz_test.go │ ├── version.go │ ├── version_test.go │ ├── web/ │ │ ├── handler.go │ │ ├── nocache.go │ │ ├── nocache_test.go │ │ ├── render.go │ │ ├── render_test.go │ │ ├── static/ │ │ │ ├── files/ │ │ │ │ ├── reset.css │ │ │ │ ├── style.css │ │ │ │ └── timeago.js │ │ │ ├── static.go │ │ │ └── static_gen.go │ │ └── template/ │ │ ├── files/ │ │ │ ├── index.tmpl │ │ │ └── logs.tmpl │ │ ├── server.go │ │ ├── template.go │ │ ├── template_gen.go │ │ └── testdata/ │ │ ├── logs.json │ │ ├── logs_empty.json │ │ ├── servers.json │ │ └── servers_empty.json │ ├── writer.go │ └── writer_test.go ├── server.go ├── slack/ │ ├── slack.go │ └── slack_test.go └── store/ ├── db.go ├── db_test.go ├── lock.go ├── migrate/ │ ├── migrate.go │ ├── mysql/ │ │ ├── ddl.go │ │ ├── ddl_gen.go │ │ └── files/ │ │ └── 001_create_table_servers.sql │ ├── postgres/ │ │ ├── ddl.go │ │ ├── ddl_gen.go │ │ └── files/ │ │ └── 001_create_table_servers.sql │ └── sqlite/ │ ├── ddl.go │ ├── ddl_gen.go │ └── files/ │ └── 001_create_table_servers.sql ├── servers.go ├── servers_test.go ├── util.go └── util_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .drone.sh ================================================ #!/bin/sh set -e set -x COMMIT="-X main.commit=${DRONE_COMMIT_SHA}" VERSION="-X main.version=${DRONE_TAG=latest}" go build \ -ldflags "-extldflags \"-static\" $COMMIT $VERSION" \ -o release/linux/amd64/drone-autoscaler \ github.com/drone/autoscaler/cmd/drone-autoscaler ================================================ FILE: .drone.yml ================================================ --- kind: pipeline name: default type: vm pool: use: ubuntu platform: os: linux arch: amd64 steps: - name: test pull: default image: golang volumes: - name: deps path: /go commands: - go get - go test -v -cover ./... - name: test_postgres pull: default image: golang volumes: - name: deps path: /go commands: - cd store - go test -v environment: DATABASE_CONFIG: host=postgres user=postgres password=password dbname=test sslmode=disable DATABASE_DRIVER: postgres - name: test_mysql pull: default image: golang volumes: - name: deps path: /go commands: - cd store - go test -v environment: DATABASE_CONFIG: "root:password@tcp(mysql:3306)/test?parseTime=true" DATABASE_DRIVER: mysql - name: build pull: default image: golang volumes: - name: deps path: /go commands: - sh .drone.sh - name: publish pull: default image: plugins/docker settings: auto_tag: true repo: drone/autoscaler password: from_secret: docker_password username: from_secret: docker_username when: event: - push - tag volumes: - name: deps temp: {} services: - name: postgres pull: default image: postgres:9 environment: POSTGRES_DB: test POSTGRES_PASSWORD: password - name: mysql pull: default image: mysql:5 environment: MYSQL_DATABASE: test MYSQL_ROOT_PASSWORD: password ... ================================================ FILE: .github/issue_template.md ================================================ ================================================ FILE: .github/pull_request_template.md ================================================ ================================================ FILE: .gitignore ================================================ ./drone-autoscaler NOTES.md release vendor *.sqlite *.sqlite3 *.bak *.out *.db *.env ================================================ FILE: BUILDING ================================================ 1. Install go 1.11 or later 2. Install dependencies: go get 3. Compile and test: go install ./... go test ./... ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [1.7.5] ### Fixed - ignore unset environment variables when configuring runners, by [@bradrydzewski](https://github.com/bradrydzewski). [d26b8e41](https://github.com/drone/autoscaler/commit/6db28505572d90df9a271404440789043c7b378b). ## [1.7.4] ### Fixed - support colon in map values sourced from environment variables, by [@UnAfraid](https://github.com/kelseyhightower/envconfig/pull/185) ## [1.7.3] ### Added - parameter to configure env file for remote runners. [#74](https://github.com/drone/autoscaler/pull/74). ### Fixed - the content-type should be set before the status is written to the http response. [89780e6](https://github.com/drone/autoscaler/commit/89780e6b9585e8116249524fe6fe6dffe3904fd6). - pending instance count is excluded from determining available capacity when reducing pool size. [d26b8e41](https://github.com/drone/autoscaler/commit/d26b8e41fd178595fd00d739d1d3b27f2a870314). - custom scopes not passed to google cloud configuration. [#79](https://github.com/drone/autoscaler/pull/79). ### Changed - docker.NewClient was deprecated; migrate to docker.NewClientWithOpts. [#72](https://github.com/drone/autoscaler/pull/72). ## [1.7.2] ### Fixed - captuare instance private IP when google compute private IP is enabled, by [ademariag](https://github.com/ademariag). [#68](https://github.com/drone/autoscaler/pull/68). ## [1.7.2] ### Added - support for aws fallback instance types, by [bradrydzewski](https://github.com/bradrydzewski). [d524689b].(https://github.com/drone/autoscaler/commit/d524689bbd1ed73ef8ee77cb3e0c5e6e6f786158). ## [1.7.1] ### Added - support for google compute private ip, by [swjclarke](https://github.com/swjclarke). - support for google compute service accounts, by [ademariag](https://github.com/ademariag). ### Fixed - google compute instance scopes being ignored, by [ademariag](https://github.com/ademariag). ## [1.7.0] ### Added - parameter to configure docker stop timeout duration. - parameter to configure aws volume iops, by [ttousai](https://github.com/ttousai). - parameter to configure gcp scopes, by [imranismail](https://github.com/imranismail). - metrics to track server boot errors - metrics to track server boot time - metrics to track server installation errors - metrics to track server installation time - metrics to track server creation errors - metrics to track server creation time ### Fixed - do not run docker stop if the instance was not created. - do not run docker stop if the instance was not assigned an IP. ## [1.6.1] ### Added - support for instance not found errors in gcp, by [frebib](https://github.com/frebib). ### Fixed - resume instance removal when autoscaler unexpectedly restarted, by [@bradrydzewski](https://github.com/bradrydzewski). ## [1.6.0] ### Changed - Use logrus for logging instead of zerolog, by [@bradrydzewski](https://github.com/bradrydzewski). ### Added - Read only user interface to visualize servers and logs, by [@bradrydzewski](https://github.com/bradrydzewski). - Support for configuring subnetworks with GCP, by [@nsigarora](https://github.com/nsigarora). - Support for handling ErrInstanceNotFound with Hetzner, by [@tboerger](https://github.com/tboerger). ## [1.5.0] ### Changed - Use the new Docker runner image and deprecate the agent, by [@bradrydzewski](https://github.com/bradrydzewski). - Enable Digital Ocean private IP addresses, by [@barrypeng6](https://github.com/barrypeng6). ## [1.4.3] ### Fixed - Expired context preventing database updates, by [@bradrydzewski](https://github.com/bradrydzewski). ## [1.4.2] ### Added - Log errors updating the instance state, by [@bradrydzewski](https://github.com/bradrydzewski). - Add mutex to database operations for sqlite, by [@bradrydzewski](https://github.com/bradrydzewski). ## [1.4.1] - 2019-10-10 ### Fixed - Support for arm machines on Scaleway, by [@tboerger](https://github.com/tboerger). ## [1.4.0] - 2019-09-23 ### Added - Ability to configure the reaper internal, by [@msaizar](https://github.com/msaizar). - Ability to configure the install check deadline, by [@bradrydzewski](https://github.com/bradrydzewski). - Ability to configure the install check interval, by [@bradrydzewski](https://github.com/bradrydzewski). ## [1.3.0] - 2019-09-11 ### Added - Added support for Scaleway, by [@frebib](https://github.com/frebib). [#45](https://github.com/drone/autoscaler/pull/45). ### Fixed - Fixed issue where non-existing instance could not be destroyed, by [@jlesage](https://github.com/jlesage). [#50](https://github.com/drone/autoscaler/pull/50). - Added timeout when attempting to ping the instance, by [@bradrydzewski](https://github.com/bradrydzewski). ## [1.2.2] - 2019-08-29 ### Added - Support for loading runner environment variables from file, by [@bradrydzewski](https://github.com/bradrydzewski). - Basic support for configuring windows agents, by [@bradrydzewski](https://github.com/bradrydzewski). ### Fixed - Pull garbage collector image before creating the container, by [@msaizar](https://github.com/msaizar). - Handle nil pointer caused by empty or missing interface in AWS driver, by [@bradrydzewski](https://github.com/bradrydzewski). ## [1.2.1] - 2019-08-14 ### Added - Added postgres driver, by [@mmuehlberger](https://github.com/mmuehlberger). - Support for capacity buffer, by [@jones2026](https://github.com/jones2026). [#39](https://github.com/drone/autoscaler/pull/39). ### Fixed - Close docker client after server ping, by [@msaizar](https://github.com/msaizar), [#42](https://github.com/drone/autoscaler/pull/42). ## [1.2.0] - 2019-07-29 ### Added - Support for agent label assignment and matching, by [@logikone](https://github.com/logikone). - Allow Hetzner to choose datacenter when none specified, by [@tboerger](https://github.com/tboerger). ### Fixed - Upgraded zerolog to fix duplicate keys in json output, by [@krtx](https://github.com/krtx). ## [1.1.0] - 2019-05-29 ### Added - Create AWS instances with Name tag set to agent unique id, from [@bradrydzewski](https://github.com/bradrydzewski). - Handle AWS instance not found errors, from [@andy-trimble](https://github.com/andy-trimble). - Remove hard-coded DNS servers from the default Docker configuration, from [jones2026](https://github.com/jones2026). ## [1.0.0] - 2019-05-06 ### Added - Optional support for watchtower from [@bradrydzewski](https://github.com/bradrydzewski). - Optional support for drone/gc from [@bradrydzewski](https://github.com/bradrydzewski). - Update the default agent image to 1.0 stable, from [@bradrydzewski](https://github.com/bradrydzewski). - Configure agent environment variables from [@bradrydzewski](https://github.com/bradrydzewski). - Configure agent host volume mounts from [@patrickjahns](https://github.com/patrickjahns). - Update Digital Ocean default image from [@jlesage](https://github.com/jlesage). - Fix problems using custom Digital Ocean image from [@jlesage](https://github.com/jlesage). ================================================ FILE: COPYRIGHT ================================================ Copyright 2018 Drone.IO Inc Use of this software is governed by the Polyform License that can be found in the LICENSE file. ================================================ FILE: Dockerfile ================================================ FROM alpine:3.20 as alpine RUN apk add -U --no-cache ca-certificates FROM alpine:3.20 EXPOSE 8080 80 443 VOLUME /data ENV GODEBUG netdns=go ENV XDG_CACHE_HOME /data ENV DRONE_DATABASE_DRIVER sqlite3 ENV DRONE_DATABASE_DATASOURCE /data/database.sqlite?cache=shared&mode=rwc&_busy_timeout=9999999 COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ ADD release/linux/amd64/drone-autoscaler /bin/ ENTRYPOINT ["/bin/drone-autoscaler"] ================================================ FILE: LICENSE.md ================================================ [Polyform-Small-Business-1.0.0](https://polyformproject.org/licenses/small-business/1.0.0) OR [Polyform-Free-Trial-1.0.0](https://polyformproject.org/licenses/free-trial/1.0.0) ================================================ FILE: README.md ================================================ [![Build Status](https://cloud.drone.io/api/badges/drone/autoscaler/status.svg)](https://cloud.drone.io/drone/autoscaler) Drone Autoscale is a lightweight daemon that elastically increases and decreases your compute resources based on your build volume. Integrates with [DigitalOcean](https://m.do.co/c/00500d28741b), [Amazon Web services](http://autoscale.drone.io/intro/amazon/), Hetzner and more. Documentation:
https://autoscale.drone.io Technical Support:
https://discourse.drone.io Issue Tracker and Roadmap:
https://trello.com/b/ttae5E5o/drone ================================================ FILE: cmd/drone-autoscaler/main.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package main import ( "context" "errors" "net/http" "net/url" "os" "github.com/drone/autoscaler" "github.com/drone/autoscaler/config" "github.com/drone/autoscaler/drivers/amazon" "github.com/drone/autoscaler/drivers/digitalocean" "github.com/drone/autoscaler/drivers/google" "github.com/drone/autoscaler/drivers/hetznercloud" "github.com/drone/autoscaler/drivers/openstack" "github.com/drone/autoscaler/drivers/packet" "github.com/drone/autoscaler/drivers/scaleway" "github.com/drone/autoscaler/engine" "github.com/drone/autoscaler/logger" "github.com/drone/autoscaler/logger/history" "github.com/drone/autoscaler/logger/request" "github.com/drone/autoscaler/metrics" "github.com/drone/autoscaler/server" "github.com/drone/autoscaler/server/web" "github.com/drone/autoscaler/server/web/static" "github.com/drone/autoscaler/slack" "github.com/drone/autoscaler/store" "github.com/drone/drone-go/drone" "github.com/drone/signal" "github.com/99designs/basicauth-go" "github.com/go-chi/chi" "github.com/sirupsen/logrus" "golang.org/x/crypto/acme/autocert" "golang.org/x/oauth2" "golang.org/x/sync/errgroup" _ "github.com/go-sql-driver/mysql" _ "github.com/joho/godotenv/autoload" _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" ) var ( source = "https://github.com/drone/autoscaler.git" version string commit string ) func main() { conf := config.MustLoad() setupLogging(conf) provider, err := setupProvider(conf) if err != nil { logrus.WithError(err). Fatalln("Invalid or missing hosting provider") } // instruments the provider with prometheus metrics. provider = metrics.ServerCreate(provider) provider = metrics.ServerDelete(provider) db, err := store.Connect( conf.Database.Driver, conf.Database.Datasource, conf.Database.MaxIdle, conf.Database.MaxLifetime, ) if err != nil { logrus.WithError(err). Fatalln("Cannot establish database connection") } mu := store.NewLocker(conf.Database.Driver) servers := store.NewServerStore(db, mu) // instruments the provider with slack notifications // instance creation and termination events. if conf.Slack.Webhook != "" { servers = slack.New(conf, servers) } servers = metrics.ServerCount(servers) defer db.Close() client := setupClient(conf) enginex := engine.New( client, conf, servers, provider, metrics.New(), ) // // Setup the router // r := chi.NewRouter() r.Use(request.Logger) // middleware to require basic authentication. auth := basicauth.New(conf.UI.Realm, map[string][]string{ conf.UI.Username: {conf.UI.Password}, }) r.Route(conf.HTTP.Root, func(root chi.Router) { // handler to serve static assets for the dashboard. fs := http.FileServer(static.New()) root.Handle("/", http.RedirectHandler("/ui", http.StatusSeeOther)) root.Get("/metrics", server.HandleMetrics(conf.Prometheus.AuthToken)) root.Get("/version", server.HandleVersion(source, version, commit)) root.Get("/healthz", server.HandleHealthz()) root.Get("/varz", server.HandleVarz(enginex)) root.Handle("/static/*", http.StripPrefix("/static/", fs)) if conf.UI.Password != "" { // register the history handler history := history.New() logrus.AddHook(history) root.Route("/ui", func(ui chi.Router) { ui.Use(auth) ui.Get("/", web.HandleServers(servers)) ui.Get("/logs", web.HandleLogging(history)) }) } root.Route("/api", func(api chi.Router) { api.Use(server.CheckDrone(conf)) api.Post("/pause", server.HandleEnginePause(enginex)) api.Post("/resume", server.HandleEngineResume(enginex)) api.Get("/servers", server.HandleServerList(servers)) api.Post("/servers", server.HandleServerCreate(servers, conf)) api.Get("/servers/{name}", server.HandleServerFind(servers)) api.Delete("/servers/{name}", server.HandleServerDelete(servers)) }) }) // // starts the web server. // srv := &http.Server{ Handler: r, } ctx := context.Background() ctx = signal.WithContextFunc(ctx, func() { logrus.Println("Program terminating, interrupt received") srv.Shutdown(ctx) }) var g errgroup.Group g.Go(func() error { if conf.TLS.Autocert { return srv.Serve( autocert.NewListener(conf.HTTP.Host), ) } else if conf.TLS.Cert != "" { return srv.ListenAndServeTLS( conf.TLS.Cert, conf.TLS.Key, ) } srv.Addr = conf.HTTP.Port logrus.WithField("addr", conf.HTTP.Port). Infoln("starting the server") return srv.ListenAndServe() }) // // starts the auto-scaler routine. // g.Go(func() error { enginex.Start(ctx) return nil }) if err := g.Wait(); err != nil { // terminate with non-zero exit code on error logrus.WithError(err).Fatalln("Program terminated") } else { logrus.Println("Program terminated") } } // helper funciton configures the logging. func setupLogging(c config.Config) { logger.Default = logger.Logrus( logrus.NewEntry( logrus.StandardLogger(), ), ) if c.Logs.Debug { logrus.SetLevel(logrus.DebugLevel) } if c.Logs.Trace { logrus.SetLevel(logrus.TraceLevel) } if c.Logs.Pretty == false { logrus.SetFormatter(&logrus.JSONFormatter{}) } } // helper function configures the drone client. func setupClient(c config.Config) drone.Client { config := new(oauth2.Config) auther := config.Client( oauth2.NoContext, &oauth2.Token{ AccessToken: c.Server.Token, }, ) uri := new(url.URL) uri.Scheme = c.Server.Proto uri.Host = c.Server.Host return drone.NewClient(uri.String(), auther) } // helper function configures the hosting provider. func setupProvider(c config.Config) (autoscaler.Provider, error) { switch { case c.Google.Project != "": return google.New( google.WithDiskSize(c.Google.DiskSize), google.WithDiskType(c.Google.DiskType), google.WithMachineImage(c.Google.MachineImage), google.WithMachineType(c.Google.MachineType), google.WithLabels(c.Google.Labels), google.WithNetwork(c.Google.Network), google.WithSubnetwork(c.Google.Subnetwork), google.WithStackType(c.Google.StackType), google.WithPrivateIP(c.Google.PrivateIP), google.WithServiceAccountEmail(c.Google.ServiceAccountEmail), google.WithProject(c.Google.Project), google.WithTags(c.Google.Tags...), google.WithScopes(c.Google.Scopes...), google.WithUserData(c.Google.UserData), google.WithUserDataFile(c.Google.UserDataFile), google.WithZones(c.Google.Zone...), google.WithUserDataKey(c.Google.UserDataKey), google.WithRateLimit(c.Google.RateLimit), ) case c.DigitalOcean.Token != "": return digitalocean.New( digitalocean.WithSSHKey(c.DigitalOcean.SSHKey), digitalocean.WithImage(c.DigitalOcean.Image), digitalocean.WithRegion(c.DigitalOcean.Region), digitalocean.WithSize(c.DigitalOcean.Size), digitalocean.WithFirewall(c.DigitalOcean.Firewall), digitalocean.WithUserDataFile(c.DigitalOcean.UserDataFile), digitalocean.WithUserData(c.DigitalOcean.UserData), digitalocean.WithToken(c.DigitalOcean.Token), digitalocean.WithPrivateIP(c.DigitalOcean.PrivateIP), digitalocean.WithTags(c.DigitalOcean.Tags...), ), nil case c.Scaleway.AccessKey != "": return scaleway.New( scaleway.WithAccessKey(c.Scaleway.AccessKey), scaleway.WithSecretKey(c.Scaleway.SecretKey), scaleway.WithOrganisationID(c.Scaleway.OrganisationID), scaleway.WithZone(c.Scaleway.Zone), scaleway.WithSize(c.Scaleway.Size), scaleway.WithImage(c.Scaleway.Image), scaleway.WithDynamicIP(c.Scaleway.DynamicIP), scaleway.WithTags(c.Scaleway.Tags...), scaleway.WithUserData(c.Scaleway.UserData), scaleway.WithUserDataFile(c.Scaleway.UserDataFile), ) case c.HetznerCloud.Token != "": return hetznercloud.New( hetznercloud.WithDatacenter(c.HetznerCloud.Datacenter), hetznercloud.WithImage(c.HetznerCloud.Image), hetznercloud.WithUserDataFile(c.HetznerCloud.UserDataFile), hetznercloud.WithUserData(c.HetznerCloud.UserData), hetznercloud.WithServerType(c.HetznerCloud.Type), hetznercloud.WithSSHKey(c.HetznerCloud.SSHKey), hetznercloud.WithToken(c.HetznerCloud.Token), ), nil case c.Packet.APIKey != "": return packet.New( packet.WithAPIKey(c.Packet.APIKey), packet.WithFacility(c.Packet.Facility), packet.WithProject(c.Packet.ProjectID), packet.WithPlan(c.Packet.Plan), packet.WithOS(c.Packet.OS), packet.WithSSHKey(c.Packet.SSHKey), packet.WithUserData(c.Packet.UserData), packet.WithUserDataFile(c.Packet.UserDataFile), packet.WithHostname(c.Packet.Hostname), packet.WithTags(c.Packet.Tags...), ), nil case os.Getenv("AWS_ACCESS_KEY_ID") != "" || os.Getenv("AWS_IAM") != "": return amazon.New( amazon.WithDeviceName(c.Amazon.DeviceName), amazon.WithImage(c.Amazon.Image), amazon.WithRegion(c.Amazon.Region), amazon.WithRetries(c.Amazon.Retries), amazon.WithPrivateIP(c.Amazon.PrivateIP), amazon.WithSSHKey(c.Amazon.SSHKey), amazon.WithSecurityGroup(c.Amazon.SecurityGroup...), amazon.WithSize(c.Amazon.Instance), amazon.WithSizeAlt(c.Amazon.InstanceAlt), amazon.WithSubnets(append([]string{c.Amazon.SubnetID}, c.Amazon.SubnetIDsAlt...)), amazon.WithTags(c.Amazon.Tags), amazon.WithUserData(c.Amazon.UserData), amazon.WithUserDataFile(c.Amazon.UserDataFile), amazon.WithVolumeSize(c.Amazon.VolumeSize), amazon.WithVolumeType(c.Amazon.VolumeType), amazon.WithVolumeIops(c.Amazon.VolumeIops), amazon.WithVolumeThroughput(c.Amazon.VolumeThroughput), amazon.WithIamProfileArn(c.Amazon.IamProfileArn), amazon.WithMarketType(c.Amazon.MarketType), amazon.WithInstanceMetadataTokens(c.Amazon.IMDSTokens), ), nil case os.Getenv("OS_USERNAME") != "": return openstack.New( openstack.WithImage(c.OpenStack.Image), openstack.WithRegion(c.OpenStack.Region), openstack.WithFlavor(c.OpenStack.Flavor), openstack.WithNetwork(c.OpenStack.Network), openstack.WithFloatingIpPool(c.OpenStack.Pool), openstack.WithSSHKey(c.OpenStack.SSHKey), openstack.WithSecurityGroup(c.OpenStack.SecurityGroup...), openstack.WithMetadata(c.OpenStack.Metadata), openstack.WithUserData(c.OpenStack.UserData), openstack.WithUserDataFile(c.OpenStack.UserDataFile), ) default: return nil, errors.New("missing provider configuration") } } ================================================ FILE: config/config.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package config import "time" // TODO change DRONE_HTTP_PORT to DRONE_HTTP_BIND type ( // Config stores the configuration settings. Config struct { Interval time.Duration `default:"5m"` CapacityBuffer int `default:"0" split_words:"true"` Timeout struct { Stop time.Duration `envconfig:"DRONE_TIMEOUT_STOP" default:"1h"` } Slack struct { Webhook string Create bool `default:"true"` Destroy bool `default:"true"` Error bool `default:"true"` } Logs struct { Debug bool `default:"true"` Trace bool Pretty bool } Pool struct { Min int `default:"2"` Max int `default:"4"` MinAge time.Duration `default:"55m" split_words:"true"` } Check struct { Interval time.Duration `envconfig:"DRONE_INSTALL_CHECK_INTERVAL" default:"1m"` Deadline time.Duration `envconfig:"DRONE_INSTALL_CHECK_DEADLINE" default:"30m"` } Server struct { Host string Proto string Token string } Agent struct { Token string Image string `default:"drone/drone-runner-docker:1"` Concurrency int `default:"2"` OS string `default:"linux"` Arch string `default:"amd64"` Version string Kernel string EnvironFile string `envconfig:"DRONE_AGENT_ENV_FILE"` Environ []string Volumes []string Ports []string `envconfig:"DRONE_AGENT_PUBLISHED_PORTS"` Labels map[string]string `envconfig:"DRONE_AGENT_LABELS"` NamePrefix string `envconfig:"DRONE_AGENT_NAME_PREFIX" default:"agent-"` } Runner Runner GC struct { Enabled bool `envconfig:"DRONE_GC_ENABLED"` Image string `envconfig:"DRONE_GC_IMAGE" default:"drone/gc"` Debug bool `envconfig:"DRONE_GC_DEBUG"` Images []string `envconfig:"DRONE_GC_IGNORE_IMAGES"` Interval time.Duration `envconfig:"DRONE_GC_INTERVAL" default:"30m"` Cache string `envconfig:"DRONE_GC_CACHE" default:"10gb"` } Reaper struct { Enabled bool `envconfig:"DRONE_REAPER_ENABLED", default:"false"` Interval time.Duration `envconfig:"DRONE_REAPER_INTERVAL" default:"1h"` } Pinger struct { Enabled bool `envconfig:"DRONE_PINGER_ENABLED", default:"false"` Interval time.Duration `envconfig:"DRONE_PINGER_INTERVAL" default:"10m"` } Watchtower struct { Enabled bool `envconfig:"DRONE_WATCHTOWER_ENABLED"` SignalEnabled bool `envconfig:"DRONE_WATCHTOWER_SIGNAL_ENABLED" default:"true"` Signal string `envconfig:"DRONE_WATCHTOWER_STOP_SIGNAL" default:"SIGHUP"` Image string `envconfig:"DRONE_WATCHTOWER_IMAGE" default:"webhippie/watchtower"` Interval int `envconfig:"DRONE_WATCHTOWER_INTERVAL" default:"300"` Timeout time.Duration `envconfig:"DRONE_WATCHTOWER_TIMEOUT" default:"120m"` } HTTP struct { Proto string `envconfig:"DRONE_HTTP_PROTO" default:"http"` Host string `envconfig:"DRONE_HTTP_HOST"` Port string `envconfig:"DRONE_HTTP_PORT" default:":8080"` Root string `envconfig:"DRONE_HTTP_ROOT" default:"/"` } UI struct { Username string `envconfig:"DRONE_UI_USERNAME"` Password string `envconfig:"DRONE_UI_PASSWORD"` Realm string `envconfig:"DRONE_UI_REALM" default:"Autoscaler"` } TLS struct { Autocert bool Cert string Key string } Prometheus struct { AuthToken string `split_words:"true"` } Database struct { Driver string `default:"sqlite3"` Datasource string `default:"database.sqlite?cache=shared&mode=rwc&_busy_timeout=9999999"` MaxIdle int `envconfig:"DRONE_DATABASE_MAX_IDLE" default:"0"` MaxLifetime time.Duration `envconfig:"DRONE_DATABASE_MAX_LIFETIME"` } Amazon struct { DeviceName string `envconfig:"DRONE_AMAZON_DEVICE_NAME"` Image string `envconfig:"DRONE_AMAZON_IMAGE"` Instance string `envconfig:"DRONE_AMAZON_INSTANCE"` InstanceAlt string `envconfig:"DRONE_AMAZON_INSTANCE_ALT"` PrivateIP bool `split_words:"true"` Region string Retries int SSHKey string SubnetID string `split_words:"true"` SubnetIDsAlt []string `envconfig:"DRONE_AMAZON_SUBNET_IDS_ALT"` // In the same manner as InstanceAlt, allows fallback to other subnets if provisioning in the main one fails SecurityGroup []string `split_words:"true"` Tags map[string]string UserData string `envconfig:"DRONE_AMAZON_USERDATA"` UserDataFile string `envconfig:"DRONE_AMAZON_USERDATA_FILE"` VolumeSize int64 `envconfig:"DRONE_AMAZON_VOLUME_SIZE"` VolumeType string `envconfig:"DRONE_AMAZON_VOLUME_TYPE"` VolumeIops int64 `envconfig:"DRONE_AMAZON_VOLUME_IOPS"` VolumeThroughput int64 `envconfig:"DRONE_AMAZON_VOLUME_THROUGHPUT"` IamProfileArn string `envconfig:"DRONE_AMAZON_IAM_PROFILE_ARN"` MarketType string `envconfig:"DRONE_AMAZON_MARKET_TYPE"` IMDSTokens string `envconfig:"DRONE_AMAZON_IMDS_TOKENS"` } DigitalOcean struct { Token string Image string Region string SSHKey string Size string Firewall string Tags []string PrivateIP bool `split_words:"true"` UserData string `envconfig:"DRONE_DIGITALOCEAN_USERDATA"` UserDataFile string `envconfig:"DRONE_DIGITALOCEAN_USERDATA_FILE"` } Google struct { MachineType string `envconfig:"DRONE_GOOGLE_MACHINE_TYPE"` MachineImage string `envconfig:"DRONE_GOOGLE_MACHINE_IMAGE"` Network string `envconfig:"DRONE_GOOGLE_NETWORK"` Subnetwork string `envconfig:"DRONE_GOOGLE_SUBNETWORK"` StackType string `envconfig:"DRONE_GOOGLE_STACK_TYPE"` Labels map[string]string `envconfig:"DRONE_GOOGLE_LABELS"` Scopes []string `envconfig:"DRONE_GOOGLE_SCOPES"` ServiceAccountEmail string `envconfig:"DRONE_GOOGLE_SERVICE_ACCOUNT_EMAIL"` DiskSize int64 `envconfig:"DRONE_GOOGLE_DISK_SIZE"` DiskType string `envconfig:"DRONE_GOOGLE_DISK_TYPE"` Project string `envconfig:"DRONE_GOOGLE_PROJECT"` PrivateIP bool `split_words:"true"` Tags []string `envconfig:"DRONE_GOOGLE_TAGS"` UserData string `envconfig:"DRONE_GOOGLE_USERDATA"` UserDataFile string `envconfig:"DRONE_GOOGLE_USERDATA_FILE"` Zone []string `envconfig:"DRONE_GOOGLE_ZONE"` UserDataKey string `envconfig:"DRONE_GOOGLE_USERDATA_KEY" default:"user-data"` RateLimit int `envconfig:"DRONE_GOOGLE_READ_RATELIMIT" default:"25"` } HetznerCloud struct { Datacenter string Image string SSHKey int Token string Type string UserData string `envconfig:"DRONE_HETZNERCLOUD_USERDATA"` UserDataFile string `envconfig:"DRONE_HETZNERCLOUD_USERDATA_FILE"` } Packet struct { APIKey string Facility string Plan string OS string ProjectID string `split_words:"true"` Tags []string SSHKey string UserData string `envconfig:"DRONE_PACKET_USERDATA"` UserDataFile string `envconfig:"DRONE_PACKET_USERDATA_FILE"` Hostname string } OpenStack struct { Region string `envconfig:"OS_REGION_NAME"` Image string Flavor string Network string Pool string `envconfig:"DRONE_OPENSTACK_IP_POOL"` SecurityGroup []string `split_words:"true"` SSHKey string Metadata map[string]string UserData string `envconfig:"DRONE_OPENSTACK_USERDATA"` UserDataFile string `envconfig:"DRONE_OPENSTACK_USERDATA_FILE"` } Scaleway struct { AccessKey string `split_words:"true"` SecretKey string `split_words:"true"` OrganisationID string `split_words:"true"` Zone string Size string Image string DynamicIP bool `split_words:"true"` Tags []string UserData string `envconfig:"DRONE_SCALEWAY_USERDATA"` UserDataFile string `envconfig:"DRONE_SCALEWAY_USERDATA_FILE"` } } Runner struct { Volumes string Devices string Privileged string EnvFile string } ) ================================================ FILE: config/load.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package config import ( "fmt" "os" "strings" "github.com/drone/envconfig" "github.com/joho/godotenv" ) // legacy environment variables. the key is the legacy // variable name, and the value is the new variable name. var legacy = map[string]string{ "DRONE_ENABLE_PINGER": "DRONE_PINGER_ENABLED", "DRONE_ENABLE_REAPER": "DRONE_REAPER_ENABLED", } func init() { // loop through legacy environment variable and, if set // rewrite to the new variable name. for k, v := range legacy { if s, ok := os.LookupEnv(k); ok { os.Setenv(v, s) } } } // Load loads the configuration from the environment. func Load() (Config, error) { config := Config{} if err := envconfig.Process("DRONE", &config); err != nil { return config, err } if path := config.Agent.EnvironFile; path != "" { envs, _ := godotenv.Read(path) for k, v := range envs { config.Agent.Environ = append( config.Agent.Environ, fmt.Sprintf("%s=%s", k, v), ) } } // If environment variables don't contain `=`, we consider that it's an environment name, we fetch and expose the value for i, env := range config.Agent.Environ { if !strings.Contains(env, "=") { config.Agent.Environ[i] = fmt.Sprintf("%s=%s", env, os.Getenv(env)) } } godotenv.Load() return config, nil } // MustLoad loads the configuration from the environmnet // and panics if an error is encountered. func MustLoad() Config { config, err := Load() if err != nil { panic(err) } return config } ================================================ FILE: config/load_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package config import ( "encoding/json" "io/ioutil" "os" "reflect" "testing" "time" "github.com/kr/pretty" ) func TestDefaults(t *testing.T) { conf := MustLoad() if got, want := conf.Logs.Debug, true; got != want { t.Errorf("Want default DRONE_LOGS_DEBUG of %v, got %v", want, got) } if got, want := conf.Interval, time.Minute*5; got != want { t.Errorf("Want default DRONE_INTERVAL of %s, got %s", want, got) } if got, want := conf.CapacityBuffer, 0; got != want { t.Errorf("Want default DRONE_CAPACITY_BUFFER of %d, got %d", want, got) } if got, want := conf.Pool.Max, 4; got != want { t.Errorf("Want default DRONE_POOL_MIN of %d, got %d", want, got) } if got, want := conf.Pool.Min, 2; got != want { t.Errorf("Want default DRONE_POOL_MAX of %d, got %d", want, got) } if got, want := conf.Pool.MinAge, time.Minute*55; got != want { t.Errorf("Want default DRONE_POOL_MIN_AGE of %d, got %d", want, got) } if got, want := conf.Check.Interval, time.Minute; got != want { t.Errorf("Want default DRONE_INSTALL_CHECK_INTERVAL of %s, got %s", want, got) } if got, want := conf.Check.Deadline, time.Minute*30; got != want { t.Errorf("Want default DRONE_INSTALL_CHECK_DEADLINE of %s, got %s", want, got) } if got, want := conf.HTTP.Port, ":8080"; got != want { t.Errorf("Want default DRONE_HTTP_PORT of %s, got %s", want, got) } if got, want := conf.HTTP.Root, "/"; got != want { t.Errorf("Want default DRONE_HTTP_ROOT of %s, got %s", want, got) } if got, want := conf.Database.Driver, "sqlite3"; got != want { t.Errorf("Want default DRONE_DATABASE_DRIVER of %s, got %s", want, got) } if got, want := conf.Database.Datasource, "database.sqlite?cache=shared&mode=rwc&_busy_timeout=9999999"; got != want { t.Errorf("Want default DRONE_DATABASE_DATASOURCE of %s, got %s", want, got) } if got, want := conf.Agent.Concurrency, 2; got != want { t.Errorf("Want default DRONE_AGENT_CONCURRENCY of %d, got %d", want, got) } if got, want := conf.Agent.Image, "drone/drone-runner-docker:1"; got != want { t.Errorf("Want default DRONE_AGENT_IMAGE of %s, got %s", want, got) } } func TestLoad(t *testing.T) { environ := map[string]string{ "DRONE_INTERVAL": "1m", "DRONE_SLACK_WEBHOOK": "https://hooks.slack.com/services/XXX/YYY/ZZZ", "DRONE_SLACK_CREATE": "false", "DRONE_SLACK_DESTROY": "false", "DRONE_LOGS_DEBUG": "true", "DRONE_LOGS_COLOR": "true", "DRONE_LOGS_PRETTY": "true", "DRONE_CAPACITY_BUFFER": "3", "DRONE_POOL_MIN_AGE": "1h", "DRONE_POOL_MIN": "1", "DRONE_POOL_MAX": "5", "DRONE_SERVER_HOST": "drone.company.com", "DRONE_SERVER_PROTO": "http", "DRONE_SERVER_TOKEN": "633eb230f5", "DRONE_HTTP_HOST": "autoscaler.drone.company.com", "DRONE_HTTP_PORT": "633eb230f5", "DRONE_HTTP_ROOT": "/autoscaler", "DRONE_AGENT_TOKEN": "f5064039f5", "DRONE_AGENT_IMAGE": "drone/drone-runner-docker:latest", "DRONE_AGENT_CONCURRENCY": "2", "DRONE_AGENT_ARCH": "arm64", "DRONE_TLS_AUTOCERT": "true", "DRONE_TLS_CERT": "/path/to/cert.crt", "DRONE_TLS_KEY": "/path/to/cert.key", "DRONE_PROMETHEUS_AUTH_TOKEN": "b359e05e8", "DRONE_DATABASE_DRIVER": "mysql", "DRONE_DATABASE_DATASOURCE": "user:password@/dbname", "DRONE_DIGITALOCEAN_TOKEN": "2573633eb", "DRONE_DIGITALOCEAN_IMAGE": "docker-18-04", "DRONE_DIGITALOCEAN_REGION": "ncy1", "DRONE_DIGITALOCEAN_SSHKEY": "/path/to/ssh/key", "DRONE_DIGITALOCEAN_SIZE": "s-1vcpu-1gb", "DRONE_DIGITALOCEAN_IPV6": "true", "DRONE_DIGITALOCEAN_PRIVATE_IP": "false", "DRONE_DIGITALOCEAN_FIREWALL": "", "DRONE_DIGITALOCEAN_TAGS": "drone,agent,prod", "DRONE_DIGITALOCEAN_USERDATA": "#cloud-init", "DRONE_DIGITALOCEAN_USERDATA_FILE": "/path/to/cloud/init.yml", "DRONE_GOOGLE_ZONE": "us-central1-b,us-central1-a", "DRONE_GOOGLE_MACHINE_TYPE": "f1-micro", "DRONE_GOOGLE_MACHINE_IMAGE": "ubuntu-1510-wily-v20151114", "DRONE_GOOGLE_DISK_TYPE": "pd-standard", "DRONE_GOOGLE_NETWORK": "default", "DRONE_GOOGLE_SUBNETWORK": "", "DRONE_GOOGLE_PRIVATE_IP": "false", "DRONE_GOOGLE_PREEMPTIBLE": "true", "DRONE_GOOGLE_SCOPES": "devstorage.read_only,pubsub", "DRONE_GOOGLE_DISK_SIZE": "10", "DRONE_GOOGLE_PROJECT": "project-foo", "DRONE_GOOGLE_TAGS": "drone,agent,prod", "DRONE_GOOGLE_USERDATA": "#cloud-init", "DRONE_GOOGLE_USERDATA_FILE": "/path/to/cloud/init.yml", "DRONE_GOOGLE_READ_RATELIMIT": "20", "DRONE_AMAZON_IMAGE": "ami-07f84a50d2dec2fa4", "DRONE_AMAZON_INSTANCE": "t3.medium", "DRONE_AMAZON_PRIVATE_IP": "true", "DRONE_AMAZON_RETRIES": "1", "DRONE_AMAZON_REGION": "us-east-2", "DRONE_AMAZON_SSHKEY": "id_rsa", "DRONE_AMAZON_SUBNET_ID": "subnet-0b32177f", "DRONE_AMAZON_SUBNET_IDS_ALT": "subnet-abcd,subnet-efgh", "DRONE_AMAZON_SECURITY_GROUP": "sg-770eabe1", "DRONE_AMAZON_TAGS": "os:linux,arch:amd64", "DRONE_AMAZON_USERDATA": "#cloud-init", "DRONE_AMAZON_USERDATA_FILE": "/path/to/cloud/init.yml", "DRONE_HETZNERCLOUD_TOKEN": "12345678", "DRONE_HETZNERCLOUD_IMAGE": "ubuntu-16.04", "DRONE_HETZNERCLOUD_DATACENTER": "nbg1-dc3", "DRONE_HETZNERCLOUD_SSHKEY": "12345", "DRONE_HETZNERCLOUD_TYPE": "cx11", "DRONE_HETZNERCLOUD_USERDATA": "#cloud-init", "DRONE_HETZNERCLOUD_USERDATA_FILE": "/path/to/cloud/init.yml", "DRONE_PACKET_APIKEY": "12345678", "DRONE_PACKET_FACILITY": "facility", "DRONE_PACKET_PROJECT_ID": "project", "DRONE_PACKET_PLAN": "plan", "DRONE_PACKET_OS": "ubuntu", "DRONE_PACKET_SSHKEY": "id_rsa", "DRONE_PACKET_USERDATA": "#cloud-init", "DRONE_PACKET_USERDATA_FILE": "/path/to/cloud/init.yml", "DRONE_PACKET_HOSTNAME": "agent", "DRONE_PACKET_TAGS": "drone,agent,prod", "DRONE_OPENSTACK_NETWORK": "my-subnet-1", "DRONE_OPENSTACK_IP_POOL": "ext-ips-1", "DRONE_OPENSTACK_SSHKEY": "drone-ci", "DRONE_OPENSTACK_SECURITY_GROUP": "secgrp-feedface", "DRONE_OPENSTACK_FLAVOR": "t1.medium", "DRONE_OPENSTACK_IMAGE": "ubuntu-16.04-server-latest", "DRONE_OPENSTACK_METADATA": "name:agent,owner:drone-ci", "DRONE_OPENSTACK_USERDATA": "#cloud-init", "DRONE_OPENSTACK_USERDATA_FILE": "/path/to/cloud/init.yml", "OS_REGION_NAME": "sto-01", "DRONE_WATCHTOWER_SIGNAL_ENABLED": "false", "DRONE_WATCHTOWER_STOP_SIGNAL": "", } defer func() { // reset the environment. for k := range environ { os.Unsetenv(k) } }() // set test environment variables for k, v := range environ { os.Setenv(k, v) } a := MustLoad() b := Config{} err := json.Unmarshal(jsonConfig, &b) if err != nil { t.Error(err) return } if !reflect.DeepEqual(a, b) { t.Errorf("configuration mismatch") pretty.Ldiff(t, a, b) } } var jsonConfig = []byte(`{ "Interval": 60000000000, "CapacityBuffer": 3, "Timeout": { "Stop": 3600000000000 }, "Slack": { "Webhook": "https://hooks.slack.com/services/XXX/YYY/ZZZ", "Create": false, "Destroy": false, "Error": true }, "Logs": { "Color": true, "Debug": true, "Pretty": true }, "Pool": { "Min": 1, "Max": 5, "MinAge": 3600000000000 }, "Server": { "Host": "drone.company.com", "Proto": "http", "Token": "633eb230f5" }, "Agent": { "OS": "linux", "Arch": "arm64", "Token": "f5064039f5", "Image": "drone/drone-runner-docker:latest", "Concurrency": 2, "KeepaliveTime": 360000000000, "KeepaliveTimeout": 30000000000, "NamePrefix": "agent-" }, "HTTP": { "Proto": "http", "Host": "autoscaler.drone.company.com", "Port": "633eb230f5", "Root": "/autoscaler" }, "UI": { "Realm": "Autoscaler" }, "TLS": { "Autocert": true, "Cert": "/path/to/cert.crt", "Key": "/path/to/cert.key" }, "Prometheus": { "AuthToken": "b359e05e8" }, "Database": { "Driver": "mysql", "Datasource": "user:password@/dbname" }, "DigitalOcean": { "Token": "2573633eb", "Image": "docker-18-04", "Region": "ncy1", "SSHKey": "/path/to/ssh/key", "Size": "s-1vcpu-1gb", "Tags": [ "drone", "agent", "prod" ], "UserData": "#cloud-init", "UserDataFile": "/path/to/cloud/init.yml" }, "Amazon": { "Image": "ami-07f84a50d2dec2fa4", "Instance": "t3.medium", "PrivateIP": true, "Retries": 1, "Region": "us-east-2", "SSHKey": "id_rsa", "SubnetID": "subnet-0b32177f", "SubnetIDsAlt": [ "subnet-abcd", "subnet-efgh" ], "SecurityGroup": [ "sg-770eabe1" ], "tags": { "os": "linux", "arch": "amd64" }, "UserData": "#cloud-init", "UserDataFile": "/path/to/cloud/init.yml" }, "Google": { "Zone": ["us-central1-b","us-central1-a"], "MachineType": "f1-micro", "MachineImage": "ubuntu-1510-wily-v20151114", "DiskType": "pd-standard", "Address": "", "Network": "default", "Subnetwork": "", "Preemptible": true, "Scopes": [ "devstorage.read_only", "pubsub" ], "DiskSize": 10, "Project": "project-foo", "Tags": [ "drone", "agent", "prod" ], "UserData": "#cloud-init", "UserDataFile": "/path/to/cloud/init.yml", "UserDataKey": "user-data", "RateLimit": 20 }, "HetznerCloud": { "Token": "12345678", "Image": "ubuntu-16.04", "Datacenter": "nbg1-dc3", "SSHKey": 12345, "Type": "cx11", "UserData": "#cloud-init", "UserDataFile": "/path/to/cloud/init.yml" }, "Packet": { "APIKey": "12345678", "Facility": "facility", "ProjectID": "project", "Plan": "plan", "OS": "ubuntu", "SSHKey": "id_rsa", "UserData": "#cloud-init", "UserDataFile": "/path/to/cloud/init.yml", "Hostname": "agent", "Tags": [ "drone", "agent", "prod" ] }, "OpenStack": { "Region": "sto-01", "Image": "ubuntu-16.04-server-latest", "Flavor": "t1.medium", "Network": "my-subnet-1", "Pool": "ext-ips-1", "SecurityGroup": [ "secgrp-feedface" ], "SSHKey": "drone-ci", "Metadata": { "name": "agent", "owner": "drone-ci" }, "UserData": "#cloud-init", "UserDataFile": "/path/to/cloud/init.yml" }, "Watchtower": { "Image": "webhippie/watchtower", "Interval": 300, "Timeout": 7200000000000 }, "GC": { "Image": "drone/gc", "Interval": 1800000000000, "Cache": "10gb" }, "Reaper": { "Interval": 3600000000000 }, "Pinger": { "Interval": 600000000000 }, "Check": { "Interval": 60000000000, "Deadline": 1800000000000 } }`) func TestLoadEnvVariables(t *testing.T) { f, err := ioutil.TempFile("", "autoscaler-env-file-test") if err != nil { t.Error(err) } f.WriteString("ENV_FROM_FILE=FILE_VALUE") defer os.Remove(f.Name()) environ := map[string]string{ "ENV_FROM_HOST": "HOST_VALUE", "DRONE_AGENT_ENVIRON": `ENV=VALUE,ENV_FROM_HOST`, "DRONE_AGENT_ENV_FILE": f.Name(), } defer func() { // reset the environment. for k := range environ { os.Unsetenv(k) } }() // set test environment variables for k, v := range environ { os.Setenv(k, v) } a := MustLoad() want := []string{ "ENV=VALUE", "ENV_FROM_HOST=HOST_VALUE", "ENV_FROM_FILE=FILE_VALUE", } if got, want := len(a.Agent.Environ), len(want); got != want { t.Errorf("Should have an environment of length %d, got %d", want, got) } for i := range a.Agent.Environ { if got, wantV := a.Agent.Environ[i], want[i]; got != wantV { t.Errorf("Wanted environ %s at index %d, got %s", wantV, i, got) } } } ================================================ FILE: drivers/amazon/create.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package amazon import ( "bytes" "context" "encoding/base64" "fmt" "time" "github.com/drone/autoscaler" "github.com/drone/autoscaler/logger" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" ) type attemptOverrides struct { attempt int size string subnet string } func (p *provider) Create(ctx context.Context, opts autoscaler.InstanceCreateOpts) (*autoscaler.Instance, error) { attemptOverrides := attemptOverrides{ attempt: 1, size: p.size, } tryCreateInAllSubnets := func() (*autoscaler.Instance, error) { var ( instance *autoscaler.Instance err error ) for _, subnet := range p.subnets { attemptOverrides.subnet = subnet instance, err = p.create(ctx, opts, attemptOverrides) // if the instance was provisioned (with or without errors), return the instance. if instance != nil { return instance, err } attemptOverrides.attempt++ } return nil, fmt.Errorf("failed to create instance in all subnets: %w", err) } instance, err := tryCreateInAllSubnets() // if the instance was provisioned (with or without errors), return the instance. if instance != nil { return instance, err } // if the instance was not provisioned, and fallback // parameters were provided, retry using the fallback if p.sizeAlt != "" { attemptOverrides.size = p.sizeAlt instance, err = tryCreateInAllSubnets() } // if there is no fallback logic do not retry return instance, err } func (p *provider) create(ctx context.Context, opts autoscaler.InstanceCreateOpts, overrides attemptOverrides) (*autoscaler.Instance, error) { p.init.Do(func() { p.setup(ctx) }) buf := new(bytes.Buffer) err := p.userdata.Execute(buf, &opts) if err != nil { return nil, err } client := p.getClient() var iamProfile *ec2.IamInstanceProfileSpecification if p.iamProfileArn != "" { iamProfile = &ec2.IamInstanceProfileSpecification{ Arn: &p.iamProfileArn, } } var marketOptions *ec2.InstanceMarketOptionsRequest if p.spotInstance == true { marketOptions = &ec2.InstanceMarketOptionsRequest{ MarketType: aws.String("spot"), } } tags := createCopy(p.tags) tags["Name"] = opts.Name var metadataOptions *ec2.InstanceMetadataOptionsRequest if p.imdsTokens != "" { metadataOptions = &ec2.InstanceMetadataOptionsRequest{ HttpTokens: aws.String(p.imdsTokens), } } in := &ec2.RunInstancesInput{ KeyName: aws.String(p.key), ImageId: aws.String(p.image), InstanceType: aws.String(overrides.size), MinCount: aws.Int64(1), MaxCount: aws.Int64(1), InstanceMarketOptions: marketOptions, IamInstanceProfile: iamProfile, UserData: aws.String(base64.StdEncoding.EncodeToString(buf.Bytes())), MetadataOptions: metadataOptions, NetworkInterfaces: []*ec2.InstanceNetworkInterfaceSpecification{ { AssociatePublicIpAddress: aws.Bool(!p.privateIP), DeviceIndex: aws.Int64(0), SubnetId: aws.String(overrides.subnet), Groups: aws.StringSlice(p.groups), }, }, TagSpecifications: []*ec2.TagSpecification{ { ResourceType: aws.String("instance"), Tags: convertTags(tags), }, { ResourceType: aws.String("volume"), Tags: convertTags(tags), }, }, BlockDeviceMappings: []*ec2.BlockDeviceMapping{ { DeviceName: aws.String(p.deviceName), Ebs: &ec2.EbsBlockDevice{ VolumeSize: aws.Int64(p.volumeSize), VolumeType: aws.String(p.volumeType), DeleteOnTermination: aws.Bool(true), }, }, }, } if p.volumeType == "io1" || p.volumeType == "io2" || p.volumeType == "gp3" { for _, blockDeviceMapping := range in.BlockDeviceMappings { if p.volumeIops > 0 { blockDeviceMapping.Ebs.Iops = aws.Int64(p.volumeIops) } } } if p.volumeType == "gp3" { for _, blockDeviceMapping := range in.BlockDeviceMappings { if p.volumeThroughput > 0 { blockDeviceMapping.Ebs.Throughput = aws.Int64(p.volumeThroughput) } } } logger := logger.FromContext(ctx). WithField("attempt", overrides.attempt). WithField("size", overrides.size). WithField("subnet", overrides.subnet). WithField("region", p.region). WithField("image", p.image). WithField("name", opts.Name) logger.Debug("instance create") results, err := client.RunInstances(in) if err != nil { logger.WithError(err). Error("instance create failed") return nil, err } amazonInstance := results.Instances[0] instance := &autoscaler.Instance{ Provider: autoscaler.ProviderAmazon, ID: *amazonInstance.InstanceId, Name: opts.Name, Size: *amazonInstance.InstanceType, Region: *amazonInstance.Placement.AvailabilityZone, Image: *amazonInstance.ImageId, } logger.WithField("name", instance.Name). Infoln("instance create success") // poll the amazon endpoint for server updates // and exit when a network address is allocated. interval := time.Duration(0) poller: for { select { case <-ctx.Done(): logger.WithField("name", instance.Name). Debugln("instance network deadline exceeded") return instance, ctx.Err() case <-time.After(interval): interval = time.Minute logger.WithField("name", instance.Name). Debugln("check instance network") desc, err := client.DescribeInstances( &ec2.DescribeInstancesInput{ InstanceIds: []*string{ amazonInstance.InstanceId, }, }, ) if err != nil { logger.WithError(err). Warnln("instance details failed") continue } if len(desc.Reservations) == 0 { logger.Warnln("empty reservations in details") continue } if len(desc.Reservations[0].Instances) == 0 { logger.Warnln("empty instances in reservations") continue } amazonInstance = desc.Reservations[0].Instances[0] if p.privateIP { if amazonInstance.PrivateIpAddress != nil { instance.Address = *amazonInstance.PrivateIpAddress break poller } } if amazonInstance.PublicIpAddress != nil { instance.Address = *amazonInstance.PublicIpAddress break poller } } } logger. WithField("name", instance.Name). WithField("ip", instance.Address). Debugln("instance network ready") return instance, nil } ================================================ FILE: drivers/amazon/create_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package amazon ================================================ FILE: drivers/amazon/destroy.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package amazon import ( "context" "os" "github.com/drone/autoscaler" "github.com/drone/autoscaler/logger" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/ec2" ) func (p *provider) Destroy(ctx context.Context, instance *autoscaler.Instance) error { if os.Getenv("DRONE_FLAG_ALTERNATE_DESTROY") == "true" { return p.destroy2(ctx, instance) } logger := logger.FromContext(ctx). WithField("id", instance.ID). WithField("ip", instance.Address). WithField("name", instance.Name). WithField("zone", instance.Region) logger.Debugln("terminate instance") input := &ec2.TerminateInstancesInput{ InstanceIds: []*string{ aws.String(instance.ID), }, } _, err := p.getClient().TerminateInstances(input) if awsErr, ok := err.(awserr.Error); ok { switch awsErr.Code() { case ec2.UnsuccessfulInstanceCreditSpecificationErrorCodeInvalidInstanceIdMalformed: fallthrough case ec2.UnsuccessfulInstanceCreditSpecificationErrorCodeInvalidInstanceIdNotFound: logger.Debugln("instance does not exist") return autoscaler.ErrInstanceNotFound } } if err != nil { logger.WithError(err). Errorln("cannot terminate instance") return err } logger.Debugln("terminated") return nil } func (p *provider) destroy2(ctx context.Context, instance *autoscaler.Instance) error { logger := logger.FromContext(ctx). WithField("id", instance.ID). WithField("ip", instance.Address). WithField("name", instance.Name). WithField("zone", instance.Region) logger.Debugln("terminate instance") input := &ec2.TerminateInstancesInput{ InstanceIds: []*string{ aws.String(instance.ID), }, } _, err := p.getClient().TerminateInstances(input) if err == nil { logger.Debugln("terminated") return nil } // if terminate instance returns an error indicating // the instance no longer exists, return a not found // error. if awsErr, ok := err.(awserr.Error); ok { switch awsErr.Code() { case ec2.UnsuccessfulInstanceCreditSpecificationErrorCodeInvalidInstanceIdMalformed: logger.Debugln("instance does not exist") return autoscaler.ErrInstanceNotFound case ec2.UnsuccessfulInstanceCreditSpecificationErrorCodeInvalidInstanceIdNotFound: logger.Debugln("instance does not exist") return autoscaler.ErrInstanceNotFound } } if err != nil { logger.WithError(err). Errorln("cannot terminate instance") } logger.Debugln("describe instance") describe := &ec2.DescribeInstancesInput{ InstanceIds: []*string{ aws.String(instance.ID), }, } _, desErr := p.getClient().DescribeInstances(describe) // if we are able to describe the instance it confirms the // instance still exists and could not be terminated. Return // an error so that the instance is flagged as being in an // error state and requires manual attention. if desErr == nil { logger.Errorln("describe instance was successful. instance still exists") return err } // if the ware unable to describe the instance because the // instance no longer exists, we can return a not found error. // this will result in the instance being deleted from the // system, since we will have confirmed it no longer exists. if awsErr, ok := desErr.(awserr.Error); ok { switch awsErr.Code() { case ec2.UnsuccessfulInstanceCreditSpecificationErrorCodeInvalidInstanceIdMalformed: logger.Debugln("instance does not exist") return autoscaler.ErrInstanceNotFound case ec2.UnsuccessfulInstanceCreditSpecificationErrorCodeInvalidInstanceIdNotFound: logger.Debugln("instance does not exist") return autoscaler.ErrInstanceNotFound } } // otherwise we return the original error returned when // attempting to delete the instance. return err } ================================================ FILE: drivers/amazon/destroy_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package amazon import ( "context" "os" "testing" "github.com/drone/autoscaler" "github.com/h2non/gock" ) func TestDestroy(t *testing.T) { defer gock.Off() os.Setenv("AWS_ACCESS_KEY_ID", "your_access_key_id") os.Setenv("AWS_SECRET_ACCESS_KEY", "your_secret_access_key") defer func() { os.Unsetenv("AWS_ACCESS_KEY_ID") os.Unsetenv("AWS_SECRET_ACCESS_KEY") }() gock.New("https://ec2.us-east-1.amazonaws.com"). Post("/"). Reply(200) mockContext := context.TODO() mockInstance := &autoscaler.Instance{ ID: "i-1234567890abcdef0", } p := New( WithRegion("us-east-1"), ).(*provider) p.retries = 1 err := p.Destroy(mockContext, mockInstance) if err != nil { t.Error(err) } } func TestDestroyDeleteError(t *testing.T) { mockContext := context.TODO() mockInstance := &autoscaler.Instance{ ID: "i-1234567890abcdef0", } p := New( WithRegion("us-east-1"), ).(*provider) p.retries = 1 err := p.Destroy(mockContext, mockInstance) if err == nil { t.Errorf("Expect error returned from aws") } } func TestDestroyNotFound(t *testing.T) { defer gock.Off() os.Setenv("AWS_ACCESS_KEY_ID", "your_access_key_id") os.Setenv("AWS_SECRET_ACCESS_KEY", "your_secret_access_key") defer func() { os.Unsetenv("AWS_ACCESS_KEY_ID") os.Unsetenv("AWS_SECRET_ACCESS_KEY") }() gock.New("https://ec2.us-east-1.amazonaws.com"). Post("/"). Reply(400). BodyString(`InvalidInstanceID.NotFoundThe instance ID 'i-1a2b3c4d' does not existea966190-f9aa-478e-9ede-example`) mockContext := context.TODO() mockInstance := &autoscaler.Instance{ ID: "i-1234567890abcdef0", } p := New( WithRegion("us-east-1"), ).(*provider) p.retries = 1 err := p.Destroy(mockContext, mockInstance) if err == nil { t.Errorf("Expect error returned from aws") } if err != autoscaler.ErrInstanceNotFound { t.Errorf("Expect instance not found returned from aws") } } ================================================ FILE: drivers/amazon/option.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package amazon import ( "io/ioutil" "github.com/drone/autoscaler/drivers/internal/userdata" ) // Option configures a Digital Ocean provider option. type Option func(*provider) // WithDeviceName returns an option to set the device name. func WithDeviceName(n string) Option { return func(p *provider) { p.deviceName = n } } // WithImage returns an option to set the image. func WithImage(image string) Option { return func(p *provider) { p.image = image } } // WithPrivateIP returns an option to set the private IP address. func WithPrivateIP(private bool) Option { return func(p *provider) { p.privateIP = private } } // WithRetries returns an option to set the retry count. func WithRetries(retries int) Option { return func(p *provider) { p.retries = retries } } // WithRegion returns an option to set the target region. func WithRegion(region string) Option { return func(p *provider) { p.region = region } } // WithSecurityGroup returns an option to set the instance size. func WithSecurityGroup(group ...string) Option { return func(p *provider) { p.groups = group } } // WithSize returns an option to set the instance size. func WithSize(size string) Option { return func(p *provider) { p.size = size } } // WithSizeAlt returns an option to set the alternate instance // size. If instance creation fails, the system will attempt to // provision a second instance using the alternate size. func WithSizeAlt(size string) Option { return func(p *provider) { p.sizeAlt = size } } // WithSSHKey returns an option to set the ssh key. func WithSSHKey(key string) Option { return func(p *provider) { p.key = key } } // WithSubnets returns an option to set the subnet ids. func WithSubnets(ids []string) Option { return func(p *provider) { p.subnets = ids } } // WithTags returns an option to set the image. func WithTags(tags map[string]string) Option { return func(p *provider) { p.tags = tags } } // WithUserData returns an option to set the cloud-init // template from text. func WithUserData(text string) Option { return func(p *provider) { if text != "" { p.userdata = userdata.Parse(text) } } } // WithUserDataFile returns an option to set the cloud-init // template from file. func WithUserDataFile(filepath string) Option { return func(p *provider) { if filepath != "" { b, err := ioutil.ReadFile(filepath) if err != nil { panic(err) } p.userdata = userdata.Parse(string(b)) } } } // WithVolumeSize returns an option to set the volume size // in gigabytes. func WithVolumeSize(s int64) Option { return func(p *provider) { p.volumeSize = s } } // WithVolumeType returns an option to set the volume type. func WithVolumeType(t string) Option { return func(p *provider) { p.volumeType = t } } // WithVolumeIops returns an option to set the volume iops. func WithVolumeIops(i int64) Option { return func(p *provider) { p.volumeIops = i } } // WithVolumeThroughput returns an option to set the volume throughput. func WithVolumeThroughput(i int64) Option { return func(p *provider) { p.volumeThroughput = i } } // WithIamProfileArn returns an option to set the iam profile arn. func WithIamProfileArn(t string) Option { return func(p *provider) { p.iamProfileArn = t } } // WithInstanceMetadataTokens returns an option to set the instance metadata service tokens requiment. func WithInstanceMetadataTokens(t string) Option { return func(p *provider) { p.imdsTokens = t } } // WithMarketType returns an option to set the instance market type. func WithMarketType(t string) Option { return func(p *provider) { p.spotInstance = t == "spot" } } ================================================ FILE: drivers/amazon/option_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package amazon import "testing" func TestOptions(t *testing.T) { p := New( WithDeviceName("/dev/sda2"), WithImage("ami-0aab355e1bfa1e72e"), WithPrivateIP(true), WithRegion("us-west-2"), WithRetries(10), WithSecurityGroup("sg-770eabe1"), WithSize("t3.2xlarge"), WithSSHKey("id_rsa"), WithSubnets([]string{"subnet-0b32177f"}), WithTags(map[string]string{"foo": "bar", "baz": "qux"}), WithVolumeSize(64), WithVolumeType("io1"), ).(*provider) if got, want := p.deviceName, "/dev/sda2"; got != want { t.Errorf("Want device name %q, got %q", want, got) } if got, want := p.image, "ami-0aab355e1bfa1e72e"; got != want { t.Errorf("Want image %q, got %q", want, got) } if got, want := p.region, "us-west-2"; got != want { t.Errorf("Want region %q, got %q", want, got) } if got, want := p.size, "t3.2xlarge"; got != want { t.Errorf("Want size %q, got %q", want, got) } if got, want := p.key, "id_rsa"; got != want { t.Errorf("Want key %q, got %q", want, got) } if got, want := p.groups[0], "sg-770eabe1"; got != want { t.Errorf("Want security groups %q, got %q", want, got) } if got, want := p.subnets, []string{"subnet-0b32177f"}; len(got) != 1 || got[0] != want[0] { t.Errorf("Want subnet %q, got %q", want, got) } if got, want := p.retries, 10; got != want { t.Errorf("Want %d retries, got %d", want, got) } if got, want := p.privateIP, true; got != want { t.Errorf("Want %v privateIP, got %v", want, got) } if got, want := len(p.tags), 2; got != want { t.Errorf("Want %d tags, got %d", want, got) } if got, want := p.volumeSize, int64(64); got != want { t.Errorf("Want volume size %d, got %d", want, got) } if got, want := p.volumeType, "io1"; got != want { t.Errorf("Want volume type %q, got %q", want, got) } } ================================================ FILE: drivers/amazon/provider.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package amazon import ( "sync" "text/template" "github.com/drone/autoscaler" "github.com/drone/autoscaler/drivers/internal/userdata" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/ec2" ) type provider struct { init sync.Once deviceName string volumeSize int64 volumeType string volumeIops int64 volumeThroughput int64 retries int key string region string image string privateIP bool userdata *template.Template size string sizeAlt string subnets []string groups []string tags map[string]string iamProfileArn string spotInstance bool imdsTokens string } func (p *provider) getClient() *ec2.EC2 { config := aws.NewConfig() config = config.WithRegion(p.region) config = config.WithMaxRetries(p.retries) session, _ := session.NewSession(config) return ec2.New(session) } // New returns a new Digital Ocean provider. func New(opts ...Option) autoscaler.Provider { p := new(provider) for _, opt := range opts { opt(p) } if p.retries == 0 { p.retries = 10 } if p.region == "" { p.region = "us-east-1" } if p.size == "" { p.size = "t3.medium" } if p.image == "" { p.image = defaultImage(p.region) } if p.deviceName == "" { p.deviceName = "/dev/sda1" } if p.volumeSize == 0 { p.volumeSize = 32 } if p.volumeType == "" { p.volumeType = "gp2" } if (p.volumeType == "io1" || p.volumeType == "io2") && p.volumeIops == 0 { p.volumeIops = 100 } if p.volumeType == "gp3" && p.volumeIops == 0 { p.volumeIops = 3000 // 3000 is the minimum for gp3 } if p.volumeType == "gp3" && p.volumeThroughput == 0 { p.volumeThroughput = 125 // 125 is the minimum for gp3 } if p.userdata == nil { p.userdata = userdata.T } return p } ================================================ FILE: drivers/amazon/provider_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package amazon ================================================ FILE: drivers/amazon/setup.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package amazon import ( "context" "errors" "github.com/drone/autoscaler/logger" "github.com/aws/aws-sdk-go/service/ec2" "golang.org/x/sync/errgroup" ) func (p *provider) setup(ctx context.Context) error { var g errgroup.Group if p.key == "" { g.Go(func() error { return p.setupKeypair(ctx) }) } if len(p.subnets) == 0 { // TODO: find or create subnet } if len(p.groups) == 0 { // TODO: find or create security groups } return g.Wait() } func (p *provider) setupKeypair(ctx context.Context) error { logger := logger.FromContext(ctx) logger.Debugln("finding default ssh key") opts := new(ec2.DescribeKeyPairsInput) keys, err := p.getClient().DescribeKeyPairs(opts) if err != nil { return err } index := map[string]string{} for _, key := range keys.KeyPairs { index[*key.KeyName] = *key.KeyFingerprint } // if the account has multiple keys configured we will // attempt to use an existing key based on naming convention. for _, name := range []string{"drone", "id_rsa_drone"} { fingerprint, ok := index[name] if !ok { continue } p.key = name logger. WithField("name", name). WithField("fingerprint", fingerprint). Debugln("using default ssh key") return nil } // if there were no matches but the account has at least // one keypair already created we will select the first // in the list. if len(keys.KeyPairs) > 0 { key := keys.KeyPairs[0] p.key = *key.KeyName logger. WithField("name", *key.KeyName). WithField("fingerprint", *key.KeyFingerprint). Debugln("using default ssh key") return nil } return errors.New("No matching keys") } ================================================ FILE: drivers/amazon/setup_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package amazon ================================================ FILE: drivers/amazon/util.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package amazon import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" ) // helper function converts an array of tags in string // format to an array of ec2 tags. func convertTags(in map[string]string) []*ec2.Tag { var out []*ec2.Tag for k, v := range in { out = append(out, &ec2.Tag{ Key: aws.String(k), Value: aws.String(v), }) } return out } // helper function creates a copy of map[string]string func createCopy(in map[string]string) map[string]string { out := map[string]string{} for k, v := range in { out[k] = v } return out } // helper function returns the default image based on the // selected region. func defaultImage(region string) string { return images[region] } // static ami id list for Ubuntu Server 20.04 LTS // source: https://cloud-images.ubuntu.com/locator/ // filters: // - Cloud: Amazon AWS, Amazon GovCloud, Amazon AWS China // - Version: 20.04 // - Instance Type: hvm-ssd var images = map[string]string{ // AWS Regions: Ubuntu Server 20.04 LTS // Upstream release version: 20220706 "af-south-1": "ami-0f5298ccab965edeb", "ap-east-1": "ami-0dfad1f1f65cd083b", "ap-northeast-1": "ami-0986c991cc80c6ad9", "ap-northeast-2": "ami-0565d651769eb3de5", "ap-northeast-3": "ami-0e6078093a109801c", "ap-south-1": "ami-0325e3016099f9112", "ap-southeast-1": "ami-0eaf04122a1ae7b3b", "ap-southeast-2": "ami-048a2d001938101dd", "ap-southeast-3": "ami-09915141a4f1dafdd", "ca-central-1": "ami-04a579d2f00bb4001", "eu-central-1": "ami-06cac34c3836ff90b", "eu-north-1": "ami-0ede84a5f28ec932a", "eu-south-1": "ami-0a39f417b8836bc59", "eu-west-1": "ami-0141514361b6a3c1b", "eu-west-2": "ami-014b642f603e350c3", "eu-west-3": "ami-0d0b8d91779dec1e5", "me-south-1": "ami-0c769d841005394ee", "sa-east-1": "ami-088afbba294231fe0", "us-east-1": "ami-0070c5311b7677678", "us-east-2": "ami-07f84a50d2dec2fa4", "us-west-1": "ami-040a251ee9d7d1a9b", "us-west-2": "ami-0aab355e1bfa1e72e", // AWS GovCloud (US): Ubuntu Server 20.04 LTS // Upstream release version: 20220627.1 "us-gov-east-1": "ami-0d8ee446ec886f5cf", "us-gov-west-1": "ami-0cbaf57cea1d72aec", // AWS China: Ubuntu Server 20.04 LTS // Upstream release version: 20210720 "cn-north-1": "ami-0741e7b8b4fb0001c", "cn-northwest-1": "ami-0883e8062ff31f727", } ================================================ FILE: drivers/amazon/util_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package amazon import ( "reflect" "testing" "github.com/kr/pretty" ) func TestConvertTags(t *testing.T) { a := map[string]string{"foo": "bar", "baz": "qux"} b := map[string]string{} tags := convertTags(a) if got, want := len(tags), 2; got != want { t.Errorf("Want %d tags, got %d", want, got) } for _, tag := range tags { b[*tag.Key] = *tag.Value } if !reflect.DeepEqual(a, b) { t.Errorf("unexpected tag conversion") pretty.Ldiff(t, a, b) } } ================================================ FILE: drivers/azure/create.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package azure ================================================ FILE: drivers/azure/create_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package azure ================================================ FILE: drivers/azure/destroy.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package azure ================================================ FILE: drivers/azure/destroy_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package azure ================================================ FILE: drivers/azure/option.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package azure ================================================ FILE: drivers/azure/option_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package azure ================================================ FILE: drivers/azure/provider.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package azure ================================================ FILE: drivers/azure/provider_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package azure ================================================ FILE: drivers/digitalocean/create.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package digitalocean import ( "bytes" "context" "strconv" "time" "github.com/drone/autoscaler" "github.com/drone/autoscaler/logger" "github.com/digitalocean/godo" ) func (p *provider) Create(ctx context.Context, opts autoscaler.InstanceCreateOpts) (*autoscaler.Instance, error) { p.init.Do(func() { p.setup(ctx) }) buf := new(bytes.Buffer) err := p.userdata.Execute(buf, &opts) if err != nil { return nil, err } req := &godo.DropletCreateRequest{ Name: opts.Name, Region: p.region, Size: p.size, Tags: p.tags, IPv6: false, PrivateNetworking: p.privateIP, UserData: buf.String(), SSHKeys: []godo.DropletCreateSSHKey{ {Fingerprint: p.key}, }, Image: godo.DropletCreateImage{ Slug: p.image, }, } logger := logger.FromContext(ctx). WithField("region", req.Region). WithField("image", req.Image.Slug). WithField("size", req.Size). WithField("name", req.Name) logger.Debugln("instance create") client := newClient(ctx, p.token) droplet, _, err := client.Droplets.Create(ctx, req) if err != nil { logger.WithError(err). Errorln("cannot create instance") return nil, err } if p.firewall != "" { _, err := client.Firewalls.AddDroplets(ctx, p.firewall, droplet.ID) if err != nil { logger.WithError(err). Errorln("cannot assign instance to firewall") return nil, err } } instance := &autoscaler.Instance{ Provider: autoscaler.ProviderDigitalOcean, ID: strconv.Itoa(droplet.ID), Name: droplet.Name, Size: req.Size, Region: req.Region, Image: req.Image.Slug, } logger.WithField("name", instance.Name). Infoln("instance created") // poll the digitalocean endpoint for server updates // and exit when a network address is allocated. interval := time.Duration(0) poller: for { select { case <-ctx.Done(): logger.WithField("name", instance.Name). Debugln("cannot ascertain network") return instance, ctx.Err() case <-time.After(interval): interval = time.Minute logger.WithField("name", instance.Name). Debugln("find instance network") droplet, _, err = client.Droplets.Get(ctx, droplet.ID) if err != nil { logger.WithError(err). Errorln("cannot find instance") return instance, err } for _, network := range droplet.Networks.V4 { if network.Type == "public" { instance.Address = network.IPAddress } } if instance.Address != "" { break poller } } } logger. WithField("name", instance.Name). WithField("ip", instance.Address). Debugln("instance network ready") return instance, nil } ================================================ FILE: drivers/digitalocean/create_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package digitalocean import ( "context" "testing" "time" "github.com/digitalocean/godo" "github.com/drone/autoscaler" "github.com/h2non/gock" ) func TestCreate(t *testing.T) { defer gock.Off() gock.New("https://api.digitalocean.com"). Post("/v2/droplets"). Reply(200). BodyString(respDropletCreate) gock.New("https://api.digitalocean.com"). Get("/v2/droplets/3164494"). Reply(200). BodyString(respDropletDesc) p := New( WithSSHKey("58:8e:30:66:fc:e2:ff:ad:4f:6f:02:4b:af:28:0d:c7"), WithToken("77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3"), ).(*provider) p.init.Do(func() {}) // prevent init function instance, err := p.Create(context.TODO(), autoscaler.InstanceCreateOpts{Name: "agent1"}) if err != nil { t.Error(err) } if !gock.IsDone() { t.Errorf("Expected http requests not detected") } t.Run("Attributes", testInstance(instance)) } func TestCreate_CreateError(t *testing.T) { defer gock.Off() gock.New("https://api.digitalocean.com"). Post("/v2/droplets"). Reply(500) p := New( WithSSHKey("58:8e:30:66:fc:e2:ff:ad:4f:6f:02:4b:af:28:0d:c7"), WithToken("77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3"), ).(*provider) p.init.Do(func() {}) // prevent init function _, err := p.Create(context.TODO(), autoscaler.InstanceCreateOpts{Name: "agent1"}) if err == nil { t.Errorf("Expect error returned from digital ocean") } else if _, ok := err.(*godo.ErrorResponse); !ok { t.Errorf("Expect ErrorResponse digital ocean") } if !gock.IsDone() { t.Errorf("Expected http requests not detected") } } func TestCreate_DescribeError(t *testing.T) { defer gock.Off() gock.New("https://api.digitalocean.com"). Post("/v2/droplets"). Reply(200). BodyString(respDropletCreate) gock.New("https://api.digitalocean.com"). Get("/v2/droplets/3164494"). Reply(500) p := New( WithSSHKey("58:8e:30:66:fc:e2:ff:ad:4f:6f:02:4b:af:28:0d:c7"), WithToken("77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3"), ).(*provider) p.init.Do(func() {}) // prevent init function instance, err := p.Create(context.TODO(), autoscaler.InstanceCreateOpts{Name: "agent1"}) if err == nil { t.Errorf("Expect error returned from digital ocean") } else if _, ok := err.(*godo.ErrorResponse); !ok { t.Errorf("Expect ErrorResponse digital ocean") } if !gock.IsDone() { t.Errorf("Expected http requests not detected") } t.Run("Attributes", testInstance(instance)) } func TestCreate_DescribeTimeout(t *testing.T) { defer gock.Off() gock.New("https://api.digitalocean.com"). Post("/v2/droplets"). Reply(200). BodyString(respDropletCreate) gock.New("https://api.digitalocean.com"). Get("/v2/droplets/3164494"). Reply(200). BodyString(respDropletCreate) // no network data p := New( WithSSHKey("58:8e:30:66:fc:e2:ff:ad:4f:6f:02:4b:af:28:0d:c7"), WithToken("77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3"), ).(*provider) p.init.Do(func() {}) // prevent init function ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() instance, err := p.Create(ctx, autoscaler.InstanceCreateOpts{Name: "agent1"}) if err == nil { t.Errorf("Expected context deadline exceeded, got nil") } else if err.Error() != "context deadline exceeded" { t.Errorf("Expected context deadline exceeded, got %s", err) } if !gock.IsDone() { t.Errorf("Expected http requests not detected") } t.Run("Attributes", testInstance(instance)) } func testInstance(instance *autoscaler.Instance) func(t *testing.T) { return func(t *testing.T) { if instance == nil { t.Errorf("Expect non-nil instance even if error") } if got, want := instance.ID, "3164494"; got != want { t.Errorf("Want droplet ID %v, got %v", want, got) } if got, want := instance.Image, "docker-18-04"; got != want { t.Errorf("Want droplet Image %v, got %v", want, got) } if got, want := instance.Name, "example.com"; got != want { t.Errorf("Want droplet Name %v, got %v", want, got) } if got, want := instance.Region, "nyc1"; got != want { t.Errorf("Want droplet Region %v, got %v", want, got) } if got, want := instance.Provider, autoscaler.ProviderDigitalOcean; got != want { t.Errorf("Want droplet Provider %v, got %v", want, got) } } } func testInstanceAddress(instance *autoscaler.Instance) func(t *testing.T) { return func(t *testing.T) { if instance == nil { t.Errorf("Expect non-nil instance even if error") } if got, want := instance.Address, "104.131.186.241"; got != want { t.Errorf("Want droplet Address %v, got %v", want, got) } } } // sample response for POST /v2/droplets const respDropletCreate = ` { "droplet": { "id": 3164494, "name": "example.com", "memory": 1024, "vcpus": 1, "disk": 25, "locked": true, "status": "new", "kernel": { "id": 2233, "name": "Ubuntu 14.04 x64 vmlinuz-3.13.0-37-generic", "version": "3.13.0-37-generic" }, "created_at": "2014-11-14T16:36:31Z", "features": [ "virtio" ], "backup_ids": [ ], "snapshot_ids": [ ], "image": { }, "volume_ids": [ ], "size": { }, "size_slug": "s-1vcpu-1gb", "networks": { }, "region": { }, "tags": [ "web" ] }, "links": { "actions": [ { "id": 36805096, "rel": "create", "href": "https:\/\/api.digitalocean.com\/v2\/actions\/36805096" } ] } } ` // sample response for POST /v2/droplets/:id const respDropletDesc = ` { "droplet": { "id": 3164494, "name": "example.com", "memory": 1024, "vcpus": 1, "disk": 25, "locked": true, "status": "new", "kernel": { "id": 2233, "name": "Ubuntu 14.04 x64 vmlinuz-3.13.0-37-generic", "version": "3.13.0-37-generic" }, "created_at": "2014-11-14T16:36:31Z", "features": [ "virtio" ], "backup_ids": [ ], "snapshot_ids": [ ], "image": { }, "volume_ids": [ ], "size": { }, "size_slug": "s-1vcpu-1gb", "networks": { "v4": [ { "ip_address": "104.131.186.241", "netmask": "255.255.240.0", "gateway": "104.131.176.1", "type": "public" } ] }, "region": { }, "tags": [ "web" ] }, "links": { "actions": [ { "id": 36805096, "rel": "create", "href": "https:\/\/api.digitalocean.com\/v2\/actions\/36805096" } ] } } ` ================================================ FILE: drivers/digitalocean/destroy.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package digitalocean import ( "context" "strconv" "github.com/drone/autoscaler" "github.com/drone/autoscaler/logger" ) func (p *provider) Destroy(ctx context.Context, instance *autoscaler.Instance) error { logger := logger.FromContext(ctx). WithField("region", instance.Region). WithField("image", instance.Image). WithField("size", instance.Size). WithField("name", instance.Name) client := newClient(ctx, p.token) id, err := strconv.Atoi(instance.ID) if err != nil { return err } _, res, err := client.Droplets.Get(ctx, id) if err != nil && res.StatusCode == 404 { logger.WithError(err). Warnln("droplet does not exist") return autoscaler.ErrInstanceNotFound } else if err != nil { logger.WithError(err). Errorln("cannot find droplet") return err } logger.Debugln("deleting droplet") _, err = client.Droplets.Delete(ctx, id) if err != nil { logger.WithError(err). Errorln("deleting droplet failed") return err } logger.Debugln("droplet deleted") return nil } ================================================ FILE: drivers/digitalocean/destroy_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package digitalocean import ( "context" "strconv" "testing" "github.com/digitalocean/godo" "github.com/drone/autoscaler" "github.com/golang/mock/gomock" "github.com/h2non/gock" ) func TestDestroy(t *testing.T) { defer gock.Off() gock.New("https://api.digitalocean.com"). Get("/v2/droplets/3164494"). Reply(200). BodyString(respDropletCreate) gock.New("https://api.digitalocean.com"). Delete("/v2/droplets/3164494"). Reply(204) mockContext := context.TODO() mockInstance := &autoscaler.Instance{ ID: "3164494", } p := New( WithSSHKey("58:8e:30:66:fc:e2:ff:ad:4f:6f:02:4b:af:28:0d:c7"), WithToken("77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3"), ) err := p.Destroy(mockContext, mockInstance) if err != nil { t.Error(err) } if !gock.IsDone() { t.Errorf("Expected http requests not detected") } } func TestDestroyDeleteError(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() defer gock.Off() gock.New("https://api.digitalocean.com"). Get("/v2/droplets/3164494"). Reply(200). BodyString(respDropletCreate) gock.New("https://api.digitalocean.com"). Delete("/v2/droplets/3164494"). Reply(500) mockContext := context.TODO() mockInstance := &autoscaler.Instance{ ID: "3164494", } p := New( WithSSHKey("58:8e:30:66:fc:e2:ff:ad:4f:6f:02:4b:af:28:0d:c7"), WithToken("77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3"), ) err := p.Destroy(mockContext, mockInstance) if err == nil { t.Errorf("Expect error returned from digital ocean") } else if _, ok := err.(*godo.ErrorResponse); !ok { t.Errorf("Expect ErrorResponse digital ocean") } if !gock.IsDone() { t.Errorf("Expected http requests not detected") } } func TestDestroyFindError(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() defer gock.Off() gock.New("https://api.digitalocean.com"). Get("/v2/droplets/3164494"). Reply(500) mockContext := context.TODO() mockInstance := &autoscaler.Instance{ ID: "3164494", } p := New( WithSSHKey("58:8e:30:66:fc:e2:ff:ad:4f:6f:02:4b:af:28:0d:c7"), WithToken("77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3"), ) err := p.Destroy(mockContext, mockInstance) if err == nil { t.Errorf("Expect error returned from digital ocean") } else if _, ok := err.(*godo.ErrorResponse); !ok { t.Errorf("Expect ErrorResponse digital ocean") } if !gock.IsDone() { t.Errorf("Expected http requests not detected") } } func TestDestroyNotFound(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() defer gock.Off() gock.New("https://api.digitalocean.com"). Get("/v2/droplets/3164494"). Reply(404) mockContext := context.TODO() mockInstance := &autoscaler.Instance{ ID: "3164494", } p := New( WithSSHKey("58:8e:30:66:fc:e2:ff:ad:4f:6f:02:4b:af:28:0d:c7"), WithToken("77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3"), ) err := p.Destroy(mockContext, mockInstance) if err == nil { t.Errorf("Expect error returned from digital ocean") } else if err != autoscaler.ErrInstanceNotFound { t.Errorf("Expect ErrInstanceNotFound") } if !gock.IsDone() { t.Errorf("Expected http requests not detected") } } func TestDestroyInvalidInput(t *testing.T) { i := &autoscaler.Instance{} p := provider{} err := p.Destroy(context.TODO(), i) if _, ok := err.(*strconv.NumError); !ok { t.Errorf("Expected invalid or missing ID error") } } ================================================ FILE: drivers/digitalocean/option.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package digitalocean import ( "io/ioutil" "github.com/drone/autoscaler/drivers/internal/userdata" ) // Option configures a Digital Ocean provider option. type Option func(*provider) // WithImage returns an option to set the image. func WithImage(image string) Option { return func(p *provider) { p.image = image } } // WithRegion returns an option to set the target region. func WithRegion(region string) Option { return func(p *provider) { p.region = region } } // WithSize returns an option to set the instance size. func WithSize(size string) Option { return func(p *provider) { p.size = size } } // WithSSHKey returns an option to set the ssh key. func WithSSHKey(key string) Option { return func(p *provider) { p.key = key } } // WithTags returns an option to set the image. func WithTags(tags ...string) Option { return func(p *provider) { p.tags = tags } } // WithToken returns an option to set the auth token. func WithToken(token string) Option { return func(p *provider) { p.token = token } } // WithFirewall returns an option to set the droplet firewall. func WithFirewall(firewall string) Option { return func(p *provider) { p.firewall = firewall } } // WithPrivateIP returns an option to set the private IP address. func WithPrivateIP(private bool) Option { return func(p *provider) { p.privateIP = private } } // WithUserData returns an option to set the cloud-init // template from text. func WithUserData(text string) Option { return func(p *provider) { if text != "" { p.userdata = userdata.Parse(text) } } } // WithUserDataFile returns an option to set the cloud-init // template from file. func WithUserDataFile(filepath string) Option { return func(p *provider) { if filepath != "" { b, err := ioutil.ReadFile(filepath) if err != nil { panic(err) } p.userdata = userdata.Parse(string(b)) } } } ================================================ FILE: drivers/digitalocean/option_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package digitalocean import "testing" func TestOptions(t *testing.T) { p := New( WithImage("ubuntu-18-04-x64"), WithRegion("nyc3"), WithSize("s-8vcpu-32gb"), WithSSHKey("58:8e:30:66:fc:e2:ff:ad:4f:6f:02:4b:af:28:0d:c7"), WithTags("drone", "agent"), WithFirewall("f33e7128-f3e7-4229-b6cc-a4751381a104"), WithToken("77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3"), WithPrivateIP(false), ).(*provider) if got, want := p.image, "ubuntu-18-04-x64"; got != want { t.Errorf("Want image %q, got %q", want, got) } if got, want := p.region, "nyc3"; got != want { t.Errorf("Want region %q, got %q", want, got) } if got, want := p.size, "s-8vcpu-32gb"; got != want { t.Errorf("Want size %q, got %q", want, got) } if got, want := p.key, "58:8e:30:66:fc:e2:ff:ad:4f:6f:02:4b:af:28:0d:c7"; got != want { t.Errorf("Want key %q, got %q", want, got) } if got, want := p.token, "77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3"; got != want { t.Errorf("Want token %q, got %q", want, got) } if got, want := p.firewall, "f33e7128-f3e7-4229-b6cc-a4751381a104"; got != want { t.Errorf("Want token %q, got %q", want, got) } if got, want := p.privateIP, false; got != want { t.Errorf("Want %v privateIP, got %v", want, got) } if got, want := len(p.tags), 2; got != want { t.Errorf("Want %d tags, got %d", want, got) } } ================================================ FILE: drivers/digitalocean/provider.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package digitalocean import ( "context" "sync" "text/template" "github.com/drone/autoscaler" "github.com/digitalocean/godo" "golang.org/x/oauth2" ) // provider implements a DigitalOcean provider. type provider struct { init sync.Once key string region string token string size string image string firewall string privateIP bool userdata *template.Template tags []string } // New returns a new Digital Ocean provider. func New(opts ...Option) autoscaler.Provider { p := new(provider) for _, opt := range opts { opt(p) } if p.region == "" { p.region = "nyc1" } if p.size == "" { p.size = "s-2vcpu-4gb" } if p.image == "" { p.image = "docker-18-04" } if p.userdata == nil { p.userdata = userdataT } return p } // helper function returns a new digitalocean client. func newClient(ctx context.Context, token string) *godo.Client { return godo.NewClient( oauth2.NewClient(ctx, oauth2.StaticTokenSource( &oauth2.Token{ AccessToken: token, }, )), ) } ================================================ FILE: drivers/digitalocean/provider_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package digitalocean import "testing" func TestDefaults(t *testing.T) { p := New().(*provider) if got, want := p.image, "docker-18-04"; got != want { t.Errorf("Want image %q, got %q", want, got) } if got, want := p.region, "nyc1"; got != want { t.Errorf("Want region %q, got %q", want, got) } if got, want := p.size, "s-2vcpu-4gb"; got != want { t.Errorf("Want size %q, got %q", want, got) } if got, want := p.key, ""; got != want { t.Errorf("Want key %q, got %q", want, got) } if got, want := p.token, ""; got != want { t.Errorf("Want token %q, got %q", want, got) } if got, want := len(p.tags), 0; got != want { t.Errorf("Want %d tags, got %d", want, got) } } ================================================ FILE: drivers/digitalocean/setup.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package digitalocean import ( "context" "errors" "github.com/digitalocean/godo" "github.com/drone/autoscaler/logger" "golang.org/x/sync/errgroup" ) func (p *provider) setup(ctx context.Context) error { var g errgroup.Group if p.key == "" { g.Go(func() error { return p.setupKeypair(ctx) }) } return g.Wait() } func (p *provider) setupKeypair(ctx context.Context) error { logger := logger.FromContext(ctx) logger.Debugln("finding default ssh key") client := newClient(ctx, p.token) keys, _, err := client.Keys.List(ctx, &godo.ListOptions{}) if err != nil { return err } index := map[string]string{} for _, key := range keys { index[key.Name] = key.Fingerprint } // if the account has multiple keys configured we will // attempt to use an existing key based on naming convention. for _, name := range []string{"drone", "id_rsa_drone"} { fingerprint, ok := index[name] if !ok { continue } p.key = fingerprint logger. WithField("name", name). WithField("fingerprint", fingerprint). Debugln("using default ssh key") return nil } // if there were no matches but the account has at least // one keypair already created we will select the first // in the list. if len(keys) > 0 { key := keys[0] p.key = key.Fingerprint logger. WithField("name", key.Name). WithField("fingerprint", key.Fingerprint). Debugln("using default ssh key") return nil } return errors.New("No matching keys") } ================================================ FILE: drivers/digitalocean/setup_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package digitalocean import ( "context" "testing" "github.com/h2non/gock" ) func TestSetupKey_Single(t *testing.T) { defer gock.Off() gock.New("https://api.digitalocean.com"). Get("/v2/account/keys"). Reply(200). BodyString(respSingleKey) p := New( WithToken("77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3"), ).(*provider) err := p.setup(context.TODO()) if err != nil { t.Error(err) } if got, want := p.key, "3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa"; got != want { t.Errorf("Want fingerprint %s, got %s", want, got) } } func TestSetupKey_FoundMatch(t *testing.T) { defer gock.Off() gock.New("https://api.digitalocean.com"). Get("/v2/account/keys"). Reply(200). BodyString(respMultiKey) p := New( WithToken("77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3"), ).(*provider) err := p.setup(context.TODO()) if err != nil { t.Error(err) } if got, want := p.key, "00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff"; got != want { t.Errorf("Want fingerprint %s, got %s", want, got) } if !gock.IsDone() { t.Errorf("Expected http requests not detected") } } func TestSetupKey_NoMatch(t *testing.T) { defer gock.Off() gock.New("https://api.digitalocean.com"). Get("/v2/account/keys"). Reply(200). BodyString(respMultiKeyNoMatch) p := New( WithToken("77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3"), ).(*provider) err := p.setup(context.TODO()) if err != nil { t.Error(err) } if got, want := p.key, "3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa"; got != want { t.Errorf("Want fingerprint %s, got %s", want, got) } if !gock.IsDone() { t.Errorf("Expected http requests not detected") } } var respSingleKey = ` { "ssh_keys": [ { "id": 512189, "fingerprint": "3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa", "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDDHr/jh2Jy4yALcK4JyWbVkPRaWmhck3IgCoeOO3z1e2dBowLh64QAM+Qb72pxekALga2oi4GvT+TlWNhzPH4V example", "name": "My SSH Public Key" } ], "links": { }, "meta": { "total": 1 } } ` var respMultiKey = ` { "ssh_keys": [ { "id": 512189, "fingerprint": "3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa", "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDDHr/jh2Jy4yALcK4JyWbVkPRaWmhck3IgCoeOO3z1e2dBowLh64QAM+Qb72pxekALga2oi4GvT+TlWNhzPH4V example", "name": "My SSH Public Key" }, { "id": 513199, "fingerprint": "00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff", "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDDHr/jh2Jy4yALcK4JyWbVkPRaWmhck3IgCoeOO3z1e2dBowLh64QAM+Qb72pxekALga2oi4GvT+TlWNhzPH4V example", "name": "id_rsa_drone" } ], "links": { }, "meta": { "total": 2 } } ` var respMultiKeyNoMatch = ` { "ssh_keys": [ { "id": 512189, "fingerprint": "3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa", "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDDHr/jh2Jy4yALcK4JyWbVkPRaWmhck3IgCoeOO3z1e2dBowLh64QAM+Qb72pxekALga2oi4GvT+TlWNhzPH4V example", "name": "My SSH Public Key" }, { "id": 513199, "fingerprint": "00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff", "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDDHr/jh2Jy4yALcK4JyWbVkPRaWmhck3IgCoeOO3z1e2dBowLh64QAM+Qb72pxekALga2oi4GvT+TlWNhzPH4V example", "name": "My SSH Public Key2" } ], "links": { }, "meta": { "total": 2 } } ` ================================================ FILE: drivers/digitalocean/userdata.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package digitalocean import "github.com/drone/autoscaler/drivers/internal/userdata" var userdataT = userdata.Parse(`#cloud-config write_files: - path: /etc/systemd/system/docker.service.d/override.conf content: | [Service] ExecStart= ExecStart=/usr/bin/dockerd - path: /etc/default/docker content: | DOCKER_OPTS="" - path: /etc/docker/daemon.json content: | { "dns": [ "8.8.8.8", "8.8.4.4" ], "hosts": [ "0.0.0.0:2376", "unix:///var/run/docker.sock" ], "tls": true, "tlsverify": true, "tlscacert": "/etc/docker/ca.pem", "tlscert": "/etc/docker/server-cert.pem", "tlskey": "/etc/docker/server-key.pem" } - path: /etc/docker/ca.pem encoding: b64 content: {{ .CACert | base64 }} - path: /etc/docker/server-cert.pem encoding: b64 content: {{ .TLSCert | base64 }} - path: /etc/docker/server-key.pem encoding: b64 content: {{ .TLSKey | base64 }} runcmd: - [ systemctl, daemon-reload ] - [ systemctl, restart, docker ] `) ================================================ FILE: drivers/google/create.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package google import ( "bytes" "context" "fmt" "math/rand" "strings" "github.com/drone/autoscaler" "github.com/drone/autoscaler/logger" "google.golang.org/api/compute/v1" "google.golang.org/api/googleapi" ) func (p *provider) Create(ctx context.Context, opts autoscaler.InstanceCreateOpts) (*autoscaler.Instance, error) { p.init.Do(func() { p.setup(ctx) }) buf := new(bytes.Buffer) err := p.userdata.Execute(buf, &opts) if err != nil { return nil, err } name := strings.ToLower(opts.Name) // select random zone from the list zone := p.zones[rand.Intn(len(p.zones))] logger := logger.FromContext(ctx). WithField("zone", zone). WithField("image", p.image). WithField("size", p.size). WithField("name", opts.Name) logger.Debugln("instance insert") networkConfig := []*compute.AccessConfig{} if !p.privateIP { networkConfig = []*compute.AccessConfig{ { Name: "External NAT", Type: "ONE_TO_ONE_NAT", }, } } in := &compute.Instance{ Name: name, Zone: fmt.Sprintf("projects/%s/zones/%s", p.project, zone), MinCpuPlatform: "Automatic", MachineType: fmt.Sprintf("projects/%s/zones/%s/machineTypes/%s", p.project, zone, p.size), Metadata: &compute.Metadata{ Items: []*compute.MetadataItems{ { Key: p.userdataKey, Value: googleapi.String(buf.String()), }, }, }, Tags: &compute.Tags{ Items: p.tags, }, Disks: []*compute.AttachedDisk{ { Type: "PERSISTENT", Boot: true, Mode: "READ_WRITE", AutoDelete: true, DeviceName: name, InitializeParams: &compute.AttachedDiskInitializeParams{ SourceImage: fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s", p.image), DiskType: fmt.Sprintf("projects/%s/zones/%s/diskTypes/%s", p.project, zone, p.diskType), DiskSizeGb: p.diskSize, }, }, }, CanIpForward: false, NetworkInterfaces: []*compute.NetworkInterface{ { Network: p.network, Subnetwork: p.subnetwork, StackType: p.stackType, AccessConfigs: networkConfig, }, }, Labels: p.labels, Scheduling: &compute.Scheduling{ Preemptible: false, OnHostMaintenance: "MIGRATE", AutomaticRestart: googleapi.Bool(true), }, DeletionProtection: false, ServiceAccounts: []*compute.ServiceAccount{ { Scopes: p.scopes, Email: p.serviceAccountEmail, }, }, } // Cannot add this in the same way as v4 access configs since the instance creation // fails if any v6 access configs are specified for an instance with IPV4_ONLY stack type if p.stackType == "IPV4_IPV6" { in.NetworkInterfaces[0].Ipv6AccessConfigs = []*compute.AccessConfig{ { Name: "external-ipv6", Type: "DIRECT_IPV6", NetworkTier: "PREMIUM", }, } } op, err := p.service.Instances.Insert(p.project, zone, in).Do() if err != nil { logger.WithError(err). Errorln("instance insert failed") return nil, err } logger.Debugln("pending instance insert operation") err = p.waitZoneOperation(ctx, op.Name, zone) if err != nil { logger.WithError(err). Errorln("instance insert operation failed") return nil, err } logger.Debugln("instance insert operation complete") resp, err := p.service.Instances.Get(p.project, zone, name).Do() if err != nil { logger.WithError(err). Errorln("cannot get instance details") return nil, err } address := resp.NetworkInterfaces[0].NetworkIP if !p.privateIP { address = resp.NetworkInterfaces[0].AccessConfigs[0].NatIP } instance := &autoscaler.Instance{ Provider: autoscaler.ProviderGoogle, ID: name, Name: opts.Name, Image: p.image, Region: zone, Size: p.size, Address: address, ServiceAccountEmail: p.serviceAccountEmail, Scopes: p.scopes, } logger. WithField("name", instance.Name). WithField("ip", instance.Address). Debugln("instance inserted") return instance, nil } ================================================ FILE: drivers/google/create_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package google import ( "context" "net/http" "testing" "github.com/drone/autoscaler" "github.com/h2non/gock" "google.golang.org/api/compute/v1" "google.golang.org/api/googleapi" ) func TestCreate(t *testing.T) { defer gock.Off() gock.New("https://compute.googleapis.com"). Post("/compute/v1/projects/my-project/zones/us-central1-a/instances"). JSON(insertInstanceMock). Reply(200). BodyString(`{ "name": "operation-name" }`) gock.New("https://compute.googleapis.com"). Get("/compute/v1/projects/my-project/zones/us-central1-a/instances/agent-807jvfwj"). Reply(200). BodyString(`{ "networkInterfaces": [ { "accessConfigs": [ { "natIP": "1.2.3.4" } ] } ] }`) gock.New("https://compute.googleapis.com"). Get("/compute/v1/projects/my-project/zones/us-central1-a/operations/operation-name"). Reply(200). BodyString(`{ "status": "DONE" }`) v, err := New( WithClient(http.DefaultClient), WithZones("us-central1-a"), WithProject("my-project"), WithUserData("#cloud-init"), ) if err != nil { t.Error(err) return } p := v.(*provider) p.init.Do(func() {}) instance, err := p.Create(context.TODO(), autoscaler.InstanceCreateOpts{Name: "agent-807jVFwj"}) if err != nil { t.Error(err) } if want, got := instance.Address, "1.2.3.4"; got != want { t.Errorf("Want instance IP %q, got %q", want, got) } if want, got := instance.Image, "ubuntu-os-cloud/global/images/ubuntu-2004-focal-v20220712"; got != want { t.Errorf("Want instance ID %q, got %q", want, got) } if want, got := instance.ID, "agent-807jvfwj"; got != want { t.Errorf("Want instance ID %q, got %q", want, got) } if want, got := instance.Name, "agent-807jVFwj"; got != want { t.Errorf("Want instance Name %q, got %q", want, got) } if want, got := instance.Provider, autoscaler.ProviderGoogle; got != want { t.Errorf("Want google Provider type") } if want, got := instance.Region, "us-central1-a"; got != want { t.Errorf("Want instance Region %q, got %q", want, got) } if want, got := instance.Size, "n1-standard-1"; got != want { t.Errorf("Want instance Size %q, got %q", want, got) } if want, got := instance.ServiceAccountEmail, "default"; got != want { t.Errorf("Want service account email %q, got %q", want, got) } } func TestCreateWithMultiZones(t *testing.T) { defer gock.Off() gock.New("https://compute.googleapis.com"). Post("/compute/v1/projects/my-project/zones/us-central1-b/instances"). JSON(insertInstanceMockB). Reply(200). BodyString(`{ "name": "operation-name" }`) gock.New("https://compute.googleapis.com"). Get("/compute/v1/projects/my-project/zones/us-central1-b/instances/agent-807jvfwj"). Reply(200). BodyString(`{ "networkInterfaces": [ { "accessConfigs": [ { "natIP": "1.2.3.4" } ] } ] }`) gock.New("https://compute.googleapis.com"). Get("/compute/v1/projects/my-project/zones/us-central1-b/operations/operation-name"). Reply(200). BodyString(`{ "status": "DONE" }`) v, err := New( WithClient(http.DefaultClient), WithZones("us-central1-b"), WithProject("my-project"), WithUserData("#cloud-init"), ) if err != nil { t.Error(err) return } p := v.(*provider) p.init.Do(func() {}) instance, err := p.Create(context.TODO(), autoscaler.InstanceCreateOpts{Name: "agent-807jVFwj"}) if err != nil { t.Error(err) } if want, got := instance.Region, "us-central1-b"; got != want { t.Errorf("Want region %q, got %q", want, got) } } var insertInstanceMock = &compute.Instance{ Name: "agent-807jvfwj", Zone: "projects/my-project/zones/us-central1-a", MinCpuPlatform: "Automatic", MachineType: "projects/my-project/zones/us-central1-a/machineTypes/n1-standard-1", Metadata: &compute.Metadata{ Items: []*compute.MetadataItems{ { Key: "user-data", Value: googleapi.String(`#cloud-init`), }, }, }, Tags: &compute.Tags{ Items: []string{"allow-docker"}, }, Disks: []*compute.AttachedDisk{ { Type: "PERSISTENT", Boot: true, Mode: "READ_WRITE", AutoDelete: true, DeviceName: "agent-807jvfwj", InitializeParams: &compute.AttachedDiskInitializeParams{ SourceImage: "https://www.googleapis.com/compute/v1/projects/ubuntu-os-cloud/global/images/ubuntu-2004-focal-v20220712", DiskType: "projects/my-project/zones/us-central1-a/diskTypes/pd-standard", DiskSizeGb: 50, }, }, }, CanIpForward: false, NetworkInterfaces: []*compute.NetworkInterface{ { Network: "global/networks/default", StackType: "IPV4_ONLY", AccessConfigs: []*compute.AccessConfig{ { Name: "External NAT", Type: "ONE_TO_ONE_NAT", }, }, }, }, Labels: map[string]string{}, Scheduling: &compute.Scheduling{ Preemptible: false, OnHostMaintenance: "MIGRATE", AutomaticRestart: googleapi.Bool(true), }, DeletionProtection: false, ServiceAccounts: []*compute.ServiceAccount{ { Email: "default", Scopes: []string{ "https://www.googleapis.com/auth/devstorage.read_only", "https://www.googleapis.com/auth/logging.write", "https://www.googleapis.com/auth/monitoring.write", "https://www.googleapis.com/auth/trace.append", }, }, }, } var insertInstanceMockB = &compute.Instance{ Name: "agent-807jvfwj", Zone: "projects/my-project/zones/us-central1-b", MinCpuPlatform: "Automatic", MachineType: "projects/my-project/zones/us-central1-b/machineTypes/n1-standard-1", Metadata: &compute.Metadata{ Items: []*compute.MetadataItems{ { Key: "user-data", Value: googleapi.String(`#cloud-init`), }, }, }, Tags: &compute.Tags{ Items: []string{"allow-docker"}, }, Disks: []*compute.AttachedDisk{ { Type: "PERSISTENT", Boot: true, Mode: "READ_WRITE", AutoDelete: true, DeviceName: "agent-807jvfwj", InitializeParams: &compute.AttachedDiskInitializeParams{ SourceImage: "https://www.googleapis.com/compute/v1/projects/ubuntu-os-cloud/global/images/ubuntu-2004-focal-v20220712", DiskType: "projects/my-project/zones/us-central1-b/diskTypes/pd-standard", DiskSizeGb: 50, }, }, }, CanIpForward: false, NetworkInterfaces: []*compute.NetworkInterface{ { Network: "global/networks/default", StackType: "IPV4_ONLY", AccessConfigs: []*compute.AccessConfig{ { Name: "External NAT", Type: "ONE_TO_ONE_NAT", }, }, }, }, Labels: map[string]string{}, Scheduling: &compute.Scheduling{ Preemptible: false, OnHostMaintenance: "MIGRATE", AutomaticRestart: googleapi.Bool(true), }, DeletionProtection: false, ServiceAccounts: []*compute.ServiceAccount{ { Email: "default", Scopes: []string{ "https://www.googleapis.com/auth/devstorage.read_only", "https://www.googleapis.com/auth/logging.write", "https://www.googleapis.com/auth/monitoring.write", "https://www.googleapis.com/auth/trace.append", }, }, }, } ================================================ FILE: drivers/google/destroy.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package google import ( "context" "net/http" "github.com/drone/autoscaler" "google.golang.org/api/googleapi" ) func (p *provider) Destroy(ctx context.Context, instance *autoscaler.Instance) error { // An instance's Region is actually a Zone in the google provider op, err := p.service.Instances.Delete(p.project, instance.Region, instance.ID).Do() if err != nil { // https://github.com/googleapis/google-api-go-client/blob/master/googleapi/googleapi.go#L135 if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == http.StatusNotFound { return autoscaler.ErrInstanceNotFound } return err } return p.waitZoneOperation(ctx, op.Name, instance.Region) } ================================================ FILE: drivers/google/destroy_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package google import ( "context" "net/http" "testing" "github.com/drone/autoscaler" "github.com/h2non/gock" ) func TestDestroy(t *testing.T) { defer gock.Off() gock.New("https://compute.googleapis.com"). Delete("/compute/v1/projects/my-project/zones/us-central1-a/instances/my-instance"). Reply(200). BodyString(`{ "name": "operation-name" }`) gock.New("https://compute.googleapis.com"). Get("/compute/v1/projects/my-project/zones/us-central1-a/operations/operation-name"). Reply(200). BodyString(`{ "status": "DONE" }`) mockContext := context.TODO() mockInstance := &autoscaler.Instance{ ID: "my-instance", Region: "us-central1-a", } p, err := New( WithClient(http.DefaultClient), WithZones("us-central1-a"), WithProject("my-project"), ) if err != nil { t.Error(err) return } err = p.Destroy(mockContext, mockInstance) if err != nil { t.Error(err) } } func TestDestroy_Error(t *testing.T) { defer gock.Off() gock.New("https://compute.googleapis.com"). Delete("/compute/v1/projects/my-project/zones/us-central1-a/instances/my-instance"). Reply(404) mockContext := context.TODO() mockInstance := &autoscaler.Instance{ ID: "my-instance", Region: "us-central1-a", } p, err := New( WithClient(http.DefaultClient), WithZones("us-central1-a"), WithProject("my-project"), ) if err != nil { t.Error(err) return } err = p.Destroy(mockContext, mockInstance) if err == nil { t.Errorf("Expect error deleting server") } } ================================================ FILE: drivers/google/option.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package google import ( "io/ioutil" "net/http" "time" "github.com/drone/autoscaler/drivers/internal/userdata" "golang.org/x/time/rate" "google.golang.org/api/compute/v1" ) // Option configures a Digital Ocean provider option. type Option func(*provider) // WithClient returns an option to set the default http // Client used with the Google Compute provider. func WithClient(client *http.Client) Option { return func(p *provider) { service, err := compute.New(client) if err != nil { panic(err) } p.service = service } } // WithDiskSize returns an option to set the instance disk // size in gigabytes. func WithDiskSize(diskSize int64) Option { return func(p *provider) { p.diskSize = diskSize } } // WithDiskType returns an option to set the instance disk type. func WithDiskType(diskType string) Option { return func(p *provider) { p.diskType = diskType } } // WithLabels returns an option to set the metadata labels. func WithLabels(labels map[string]string) Option { return func(p *provider) { p.labels = labels } } // WithMachineImage returns an option to set the image. func WithMachineImage(image string) Option { return func(p *provider) { p.image = image } } // WithMachineType returns an option to set the instance type. func WithMachineType(size string) Option { return func(p *provider) { p.size = size } } // WithNetwork returns an option to set the network. func WithNetwork(network string) Option { return func(p *provider) { p.network = network } } // WithSubNetwork returns an option to set the subnetwork. func WithSubnetwork(subnetwork string) Option { return func(p *provider) { p.subnetwork = subnetwork } } // WithStackType returns an option to set the stack type for the instance. func WithStackType(stackType string) Option { return func(p *provider) { p.stackType = stackType } } // WithPrivateIP returns an option to set the private IP address. func WithPrivateIP(private bool) Option { return func(p *provider) { p.privateIP = private } } // WithProject returns an option to set the project. func WithProject(project string) Option { return func(p *provider) { p.project = project } } // WithTags returns an option to set the resource tags. func WithTags(tags ...string) Option { return func(p *provider) { p.tags = tags } } // WithUserData returns an option to set the cloud-init // template from text. func WithUserData(text string) Option { return func(p *provider) { if text != "" { p.userdata = userdata.Parse(text) } } } // WithUserDataFile returns an option to set the cloud-init // template from file. func WithUserDataFile(filepath string) Option { return func(p *provider) { if filepath != "" { b, err := ioutil.ReadFile(filepath) if err != nil { panic(err) } p.userdata = userdata.Parse(string(b)) } } } // WithUserDataKey allows to set the user data key for Google Cloud Platform // This allows user to set either user-data or a startup script func WithUserDataKey(text string) Option { return func(p *provider) { if text != "" { p.userdataKey = text } } } // WithZone returns an option to set the target zone. func WithZones(zones ...string) Option { return func(p *provider) { p.zones = zones } } // WithScopes returns an option to set the scopes. func WithScopes(scopes ...string) Option { return func(p *provider) { p.scopes = scopes } } // WithServiceAccountEmail returns an option to set the ServiceAccountEmail. func WithServiceAccountEmail(email string) Option { return func(p *provider) { p.serviceAccountEmail = email } } func WithRateLimit(limitAmount int) Option { return func(p *provider) { limit := rate.Every(1 * time.Second / time.Duration(limitAmount)) p.rateLimiter = rate.NewLimiter(limit, 1) } } ================================================ FILE: drivers/google/option_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package google import ( "net/http" "reflect" "testing" ) func TestOptions(t *testing.T) { v, err := New( WithClient(http.DefaultClient), WithDiskSize(100), WithDiskType("local-ssd"), WithMachineImage("ubuntu-1604-lts"), WithMachineType("c3.large"), WithNetwork("global/defaults/foo"), WithPrivateIP(false), WithServiceAccountEmail("default"), WithProject("my-project"), WithTags("drone", "agent"), WithZones("us-central1-f"), WithScopes("scope1", "scope2"), WithUserDataKey("test-key"), ) if err != nil { t.Error(err) return } p := v.(*provider) if got, want := p.diskSize, int64(100); got != want { t.Errorf("Want diskSize %d, got %d", want, got) } if got, want := p.diskType, "local-ssd"; got != want { t.Errorf("Want diskType %s, got %s", want, got) } if got, want := p.image, "ubuntu-1604-lts"; got != want { t.Errorf("Want image %q, got %q", want, got) } if got, want := p.network, "global/defaults/foo"; got != want { t.Errorf("Want network %q, got %q", want, got) } if got, want := p.privateIP, false; got != want { t.Errorf("Want %v privateIP, got %v", want, got) } if got, want := p.project, "my-project"; got != want { t.Errorf("Want project %q, got %q", want, got) } if got, want := p.size, "c3.large"; got != want { t.Errorf("Want size %q, got %q", want, got) } if got, want := len(p.tags), 2; got != want { t.Errorf("Want %d tags, got %d", want, got) } if got, want := p.zones, []string{"us-central1-f"}; !reflect.DeepEqual(want, got) { t.Errorf("Want zone %q, got %q", want, got) } if got, want := len(p.scopes), 2; got != want { t.Errorf("Want %d scopes, got %d", want, got) } if got, want := p.serviceAccountEmail, "default"; got != want { t.Errorf("Want service account name %q, got %q", want, got) } if got, want := p.userdataKey, "test-key"; got != want { t.Errorf("Want userdata key %q, got %q", want, got) } } ================================================ FILE: drivers/google/provider.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package google import ( "context" "errors" "net/http" "sync" "text/template" "time" "github.com/drone/autoscaler" "github.com/drone/autoscaler/drivers/internal/userdata" "golang.org/x/oauth2" "golang.org/x/oauth2/google" "golang.org/x/time/rate" compute "google.golang.org/api/compute/v1" "google.golang.org/api/googleapi" ) var ( defaultTags = []string{ "allow-docker", } defaultScopes = []string{ "https://www.googleapis.com/auth/devstorage.read_only", "https://www.googleapis.com/auth/logging.write", "https://www.googleapis.com/auth/monitoring.write", "https://www.googleapis.com/auth/trace.append", } ) // provider implements a Google Cloud Platform provider. type provider struct { init sync.Once diskSize int64 diskType string image string labels map[string]string network string subnetwork string stackType string project string privateIP bool scopes []string serviceAccountEmail string size string tags []string zones []string userdata *template.Template userdataKey string rateLimiter *rate.Limiter service *compute.Service } // New returns a new Google Cloud Platform provider. func New(opts ...Option) (autoscaler.Provider, error) { p := new(provider) for _, opt := range opts { opt(p) } if p.diskSize == 0 { p.diskSize = 50 } if p.diskType == "" { p.diskType = "pd-standard" } if len(p.zones) == 0 { p.zones = []string{"us-central1-a"} } if p.size == "" { p.size = "n1-standard-1" } if p.image == "" { p.image = "ubuntu-os-cloud/global/images/ubuntu-2004-focal-v20220712" } if p.network == "" { p.network = "global/networks/default" } if p.stackType == "" { p.stackType = "IPV4_ONLY" } if p.userdata == nil { p.userdata = userdata.T } if p.userdataKey == "" { p.userdataKey = "user-data" } if len(p.tags) == 0 { p.tags = defaultTags } if len(p.scopes) == 0 { p.scopes = defaultScopes } if p.serviceAccountEmail == "" { p.serviceAccountEmail = "default" } if p.rateLimiter == nil { // If unspecified, set to the max read rate limit for the API 25/s // Source: https://cloud.google.com/compute/docs/api-rate-limits p.rateLimiter = rate.NewLimiter(rate.Every(time.Second/25), 1) } if p.service == nil { client, err := google.DefaultClient(oauth2.NoContext, compute.ComputeScope) if err != nil { return nil, err } p.service, err = compute.New(client) if err != nil { return nil, err } } return p, nil } func (p *provider) waitZoneOperation(ctx context.Context, name string, zone string) error { for { if p.rateLimiter.Allow() { op, err := p.service.ZoneOperations.Get(p.project, zone, name).Do() if err != nil { if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == http.StatusNotFound { return autoscaler.ErrInstanceNotFound } return err } if op.Error != nil { return errors.New(op.Error.Errors[0].Message) } if op.Status == "DONE" { return nil } } time.Sleep(time.Second) } } func (p *provider) waitGlobalOperation(ctx context.Context, name string) error { for { if p.rateLimiter.Allow() { op, err := p.service.GlobalOperations.Get(p.project, name).Do() if err != nil { return err } if op.Error != nil { return errors.New(op.Error.Errors[0].Message) } if op.Status == "DONE" { return nil } } time.Sleep(time.Second) } } ================================================ FILE: drivers/google/provider_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package google import ( "net/http" "reflect" "testing" "github.com/drone/autoscaler/drivers/internal/userdata" ) func TestDefaults(t *testing.T) { v, err := New( WithClient(http.DefaultClient), ) if err != nil { t.Error(err) return } p := v.(*provider) if got, want := p.diskSize, int64(50); got != want { t.Errorf("Want diskSize %d, got %d", want, got) } if got, want := p.diskType, "pd-standard"; got != want { t.Errorf("Want diskType %s, got %s", want, got) } if got, want := p.image, "ubuntu-os-cloud/global/images/ubuntu-2004-focal-v20220712"; got != want { t.Errorf("Want image %q, got %q", want, got) } if got, want := p.network, "global/networks/default"; got != want { t.Errorf("Want network %q, got %q", want, got) } if !reflect.DeepEqual(p.scopes, defaultScopes) { t.Errorf("Want default scopes") } if got, want := p.size, "n1-standard-1"; got != want { t.Errorf("Want size %q, got %q", want, got) } if !reflect.DeepEqual(p.tags, defaultTags) { t.Errorf("Want default tags") } if p.userdata != userdata.T { t.Errorf("Want default userdata template") } if p.userdataKey != "user-data" { t.Errorf("Want default userdata key") } if got, want := p.zones, []string{"us-central1-a"}; !reflect.DeepEqual(got, want) { t.Errorf("Want region %q, got %q", want, got) } } ================================================ FILE: drivers/google/setup.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package google import ( "context" "reflect" "github.com/drone/autoscaler/logger" compute "google.golang.org/api/compute/v1" ) func (p *provider) setup(ctx context.Context) error { if reflect.DeepEqual(p.tags, defaultTags) { return p.setupFirewall(ctx) } return nil } func (p *provider) setupFirewall(ctx context.Context) error { logger := logger.FromContext(ctx) logger.Debugln("finding default firewall rules") _, err := p.service.Firewalls.Get(p.project, "default-allow-docker").Context(ctx).Do() if err == nil { logger.Debugln("found default firewall rule") return nil } rule := &compute.Firewall{ Allowed: []*compute.FirewallAllowed{ { IPProtocol: "tcp", Ports: []string{"2376"}, }, }, Direction: "INGRESS", Name: "default-allow-docker", Network: p.network, Priority: 1000, SourceRanges: []string{"0.0.0.0/0"}, TargetTags: []string{"allow-docker"}, } op, err := p.service.Firewalls.Insert(p.project, rule).Context(ctx).Do() if err != nil { logger.WithError(err). Errorln("cannot create firewall operation") return err } err = p.waitGlobalOperation(ctx, op.Name) if err != nil { logger.WithError(err). Errorln("cannot create firewall rule") } return err } ================================================ FILE: drivers/google/setup_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package google import ( "context" "net/http" "testing" "github.com/h2non/gock" compute "google.golang.org/api/compute/v1" ) func TestSetupFirewall(t *testing.T) { defer gock.Off() gock.New("https://compute.googleapis.com"). Get("/compute/v1/projects/my-project/global/firewalls/default-allow-docker"). Reply(404) gock.New("https://compute.googleapis.com"). Post("/compute/v1/projects/my-project/global/firewalls"). JSON(createFirewallMock). Reply(200). BodyString(`{ "name": "operation-name" }`) gock.New("https://compute.googleapis.com"). Get("/compute/v1/projects/my-project/global/operations/operation-name"). Reply(200). BodyString(`{ "status": "DONE" }`) p, err := New( WithClient(http.DefaultClient), WithZones("us-central1-a"), WithProject("my-project"), ) if err != nil { t.Error(err) return } err = p.(*provider).setupFirewall(context.TODO()) if err != nil { t.Error(err) } } func TestSetupFirewall_Exists(t *testing.T) { defer gock.Off() gock.New("https://compute.googleapis.com"). Get("/compute/v1/projects/my-project/global/firewalls/default-allow-docker"). Reply(200). BodyString(findFirewallRes) p, err := New( WithClient(http.DefaultClient), WithZones("us-central1-a"), WithProject("my-project"), ) if err != nil { t.Error(err) return } err = p.(*provider).setupFirewall(context.TODO()) if err != nil { t.Error(err) } } var createFirewallMock = &compute.Firewall{ Allowed: []*compute.FirewallAllowed{ { IPProtocol: "tcp", Ports: []string{"2376"}, }, }, Direction: "INGRESS", Name: "default-allow-docker", Network: "global/networks/default", Priority: 1000, SourceRanges: []string{"0.0.0.0/0"}, TargetTags: []string{"allow-docker"}, } var findFirewallRes = ` { "allowed": [ { "IPProtocol": "tcp", "ports": [ "2376" ] } ], "creationTimestamp": "2018-03-10T11:31:09.445-08:00", "description": "", "direction": "INGRESS", "id": "3206167972979853122", "kind": "compute#firewall", "name": "default-allow-docker", "network": "projects/my-project/global/networks/default", "priority": 1000, "selfLink": "projects/my-project/global/firewalls/default-allow-docker", "sourceRanges": [ "0.0.0.0/0" ], "targetTags": [ "allow-docker" ] } ` ================================================ FILE: drivers/hetznercloud/create.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package hetznercloud import ( "bytes" "context" "strconv" "github.com/drone/autoscaler" "github.com/drone/autoscaler/logger" "github.com/hetznercloud/hcloud-go/hcloud" ) func (p *provider) Create(ctx context.Context, opts autoscaler.InstanceCreateOpts) (*autoscaler.Instance, error) { p.init.Do(func() { p.setup(ctx) }) buf := new(bytes.Buffer) err := p.userdata.Execute(buf, &opts) if err != nil { return nil, err } req := hcloud.ServerCreateOpts{ Name: opts.Name, UserData: buf.String(), ServerType: &hcloud.ServerType{ Name: p.serverType, }, Image: &hcloud.Image{ Name: p.image, }, SSHKeys: []*hcloud.SSHKey{ { ID: p.key, }, }, } datacenter := "unknown" if p.datacenter != "" { req.Datacenter = &hcloud.Datacenter{ Name: p.datacenter, } datacenter = p.datacenter } logger := logger.FromContext(ctx). WithField("datacenter", datacenter). WithField("image", req.Image.Name). WithField("serverType", req.ServerType.Name). WithField("name", req.Name) logger.Debugln("instance create") resp, _, err := p.client.Server.Create(ctx, req) if err != nil { logger.WithError(err). Errorln("cannot create instance") return nil, err } logger. WithField("name", req.Name). Infoln("instance created") return &autoscaler.Instance{ Provider: autoscaler.ProviderHetznerCloud, ID: strconv.Itoa(resp.Server.ID), Name: resp.Server.Name, Address: resp.Server.PublicNet.IPv4.IP.String(), Size: req.ServerType.Name, Region: datacenter, Image: req.Image.Name, }, nil } ================================================ FILE: drivers/hetznercloud/create_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package hetznercloud import ( "context" "testing" "github.com/drone/autoscaler" "github.com/h2non/gock" ) func TestCreate(t *testing.T) { defer gock.Off() gock.New("https://api.hetzner.cloud"). Post("/v1/servers"). Reply(200). BodyString(respInstanceCreate) p := New( WithToken("LRK9DAWQ1ZAEFSrCNEEzLCUwhYX1U3g7wMg4dTlkkDC96fyDuyJ39nVbVjCKSDfj"), ).(*provider) p.init.Do(func() {}) // pre-initialize instance, err := p.Create(context.TODO(), autoscaler.InstanceCreateOpts{Name: "agent1"}) if err != nil { t.Error(err) } t.Run("Attributes", testInstance(instance)) } func TestCreate_CreateError(t *testing.T) { defer gock.Off() gock.New("https://api.hetzner.cloud"). Post("/v1/servers"). Reply(500) p := New( WithToken("LRK9DAWQ1ZAEFSrCNEEzLCUwhYX1U3g7wMg4dTlkkDC96fyDuyJ39nVbVjCKSDfj"), ).(*provider) p.init.Do(func() {}) // pre-initialize _, err := p.Create(context.TODO(), autoscaler.InstanceCreateOpts{Name: "agent1"}) if err == nil { t.Errorf("Expect error returned from hetzner cloud") } } func testInstance(instance *autoscaler.Instance) func(t *testing.T) { return func(t *testing.T) { if instance == nil { t.Errorf("Expect non-nil instance even if error") } if got, want := instance.ID, "544037"; got != want { t.Errorf("Want instance ID %v, got %v", want, got) } if got, want := instance.Image, "ubuntu-20.04"; got != want { t.Errorf("Want instance Image %v, got %v", want, got) } if got, want := instance.Name, "test"; got != want { t.Errorf("Want instance Name %v, got %v", want, got) } if got, want := instance.Region, "unknown"; got != want { t.Errorf("Want instance Region %v, got %v", want, got) } if got, want := instance.Provider, autoscaler.ProviderHetznerCloud; got != want { t.Errorf("Want instance Provider %v, got %v", want, got) } } } func testInstanceAddress(instance *autoscaler.Instance) func(t *testing.T) { return func(t *testing.T) { if instance == nil { t.Errorf("Expect non-nil instance even if error") } if got, want := instance.Address, "195.201.93.137"; got != want { t.Errorf("Want instance Address %v, got %v", want, got) } } } // sample response for POST /v1/servers const respInstanceCreate = ` { "server": { "id": 544037, "name": "test", "status": "initializing", "created": "2018-03-02T08:44:07+00:00", "public_net": { "ipv4": { "ip": "195.201.93.137", "blocked": false, "dns_ptr": "static.137.93.201.195.clients.your-server.de" }, "ipv6": { "ip": "2a01:4f8:1c0c:6996::/64", "blocked": false, "dns_ptr": [] }, "floating_ips": [] }, "server_type": { "id": 1, "name": "cx11", "description": "CX11", "cores": 1, "memory": 2.0, "disk": 20, "prices": [ { "location": "fsn1", "price_hourly": { "net": "0.0040000000", "gross": "0.0047600000000000" }, "price_monthly": { "net": "2.4900000000", "gross": "2.9631000000000000" } }, { "location": "nbg1", "price_hourly": { "net": "0.0040000000", "gross": "0.0047600000000000" }, "price_monthly": { "net": "2.4900000000", "gross": "2.9631000000000000" } } ], "storage_type": "local" }, "datacenter": { "id": 2, "name": "nbg1-dc3", "description": "Nuremberg 1 DC 3", "location": { "id": 2, "name": "nbg1", "description": "Nuremberg DC Park 1", "country": "DE", "city": "Nuremberg", "latitude": 49.452102, "longitude": 11.076665 }, "server_types": { "supported": [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ], "available": [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] } }, "image": { "id": 1, "type": "system", "status": "available", "name": "ubuntu-20.04", "description": "Ubuntu 20.04", "image_size": null, "disk_size": 5, "created": "2018-01-15T11:34:45+00:00", "created_from": null, "bound_to": null, "os_flavor": "ubuntu", "os_version": "20.04", "rapid_deploy": true }, "iso": null, "rescue_enabled": false, "locked": false, "backup_window": null, "outgoing_traffic": 0, "ingoing_traffic": 0, "included_traffic": 21990232555520 }, "action": { "id": 279192, "command": "create_server", "status": "running", "progress": 0, "started": "2018-03-02T08:44:07+00:00", "finished": null, "resources": [ { "id": 544037, "type": "server" } ], "error": null }, "root_password": null } ` ================================================ FILE: drivers/hetznercloud/destroy.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package hetznercloud import ( "context" "strconv" "github.com/drone/autoscaler" "github.com/drone/autoscaler/logger" "github.com/hetznercloud/hcloud-go/hcloud" ) func (p *provider) Destroy(ctx context.Context, instance *autoscaler.Instance) error { logger := logger.FromContext(ctx). WithField("region", instance.Region). WithField("image", instance.Image). WithField("size", instance.Size). WithField("name", instance.Name) id, err := strconv.Atoi(instance.ID) if err != nil { return err } logger.Debugln("deleting instance") _, err = p.client.Server.Delete(ctx, &hcloud.Server{ID: id}) if err != nil { if err.Error() == "hcloud: server responded with status code 404" { logger.WithError(err). Debugln("instance does not exist") return autoscaler.ErrInstanceNotFound } logger.WithError(err). Errorln("deleting instance failed") return err } logger.Debugln("instance deleted") return nil } ================================================ FILE: drivers/hetznercloud/destroy_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package hetznercloud import ( "context" "strconv" "testing" "github.com/drone/autoscaler" "github.com/h2non/gock" ) func TestDestroy(t *testing.T) { defer gock.Off() gock.New("https://api.hetzner.cloud"). Delete("/v1/servers/3164494"). Reply(200) mockContext := context.TODO() mockInstance := &autoscaler.Instance{ ID: "3164494", } p := New( WithToken("LRK9DAWQ1ZAEFSrCNEEzLCUwhYX1U3g7wMg4dTlkkDC96fyDuyJ39nVbVjCKSDfj"), ) err := p.Destroy(mockContext, mockInstance) if err != nil { t.Error(err) } } func TestDestroyDeleteError(t *testing.T) { defer gock.Off() gock.New("https://api.hetzner.cloud"). Delete("/v1/servers/3164494"). Reply(500) mockContext := context.TODO() mockInstance := &autoscaler.Instance{ ID: "3164494", } p := New( WithToken("LRK9DAWQ1ZAEFSrCNEEzLCUwhYX1U3g7wMg4dTlkkDC96fyDuyJ39nVbVjCKSDfj"), ) err := p.Destroy(mockContext, mockInstance) if err == nil { t.Errorf("Expect error returned from hetzner cloud") } } func TestDestroyNotFound(t *testing.T) { defer gock.Off() gock.New("https://api.hetzner.cloud"). Delete("/v1/servers/3164494"). Reply(404). BodyString(destroyNotFoundResponse) mockContext := context.TODO() mockInstance := &autoscaler.Instance{ ID: "3164494", } p := New( WithToken("LRK9DAWQ1ZAEFSrCNEEzLCUwhYX1U3g7wMg4dTlkkDC96fyDuyJ39nVbVjCKSDfj"), ) err := p.Destroy(mockContext, mockInstance) if err == nil { t.Errorf("Expect error returned from hetzner cloud") } if err != autoscaler.ErrInstanceNotFound { t.Errorf("Expect instance not found returned from hetzner cloud") } } func TestDestroyInvalidInput(t *testing.T) { i := &autoscaler.Instance{} p := provider{} err := p.Destroy(context.TODO(), i) if _, ok := err.(*strconv.NumError); !ok { t.Errorf("Expected invalid or missing ID error") } } var destroyNotFoundResponse = `{ "error": { "message": "server with ID '3164494' not found", "code": "not_found", "details": null } }` ================================================ FILE: drivers/hetznercloud/option.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package hetznercloud import ( "io/ioutil" "github.com/drone/autoscaler/drivers/internal/userdata" "github.com/hetznercloud/hcloud-go/hcloud" ) // Option configures a Digital Ocean provider option. type Option func(*provider) // WithClient returns an option to set the Hetzner client. func WithClient(client *hcloud.Client) Option { return func(p *provider) { p.client = client } } // WithDatacenter returns an option to set the datacenter. func WithDatacenter(datacenter string) Option { return func(p *provider) { p.datacenter = datacenter } } // WithImage returns an option to set the image. func WithImage(image string) Option { return func(p *provider) { p.image = image } } // WithServerType returns an option to set the server type. func WithServerType(serverType string) Option { return func(p *provider) { p.serverType = serverType } } // WithSSHKey returns an option to set the ssh key. func WithSSHKey(key int) Option { return func(p *provider) { p.key = key } } // WithToken returns an option to set the auth token. func WithToken(token string) Option { return WithClient( hcloud.NewClient( hcloud.WithToken( token, ), ), ) } // WithUserData returns an option to set the cloud-init // template from text. func WithUserData(text string) Option { return func(p *provider) { if text != "" { p.userdata = userdata.Parse(text) } } } // WithUserDataFile returns an option to set the cloud-init // template from file. func WithUserDataFile(filepath string) Option { return func(p *provider) { if filepath != "" { b, err := ioutil.ReadFile(filepath) if err != nil { panic(err) } p.userdata = userdata.Parse(string(b)) } } } ================================================ FILE: drivers/hetznercloud/option_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package hetznercloud import "testing" func TestOptions(t *testing.T) { p := New( WithImage("ubuntu-17.04"), WithDatacenter("fsn1-dc8"), WithServerType("cx20"), WithSSHKey(23234), ).(*provider) if got, want := p.image, "ubuntu-17.04"; got != want { t.Errorf("Want image %q, got %q", want, got) } if got, want := p.datacenter, "fsn1-dc8"; got != want { t.Errorf("Want region %q, got %q", want, got) } if got, want := p.serverType, "cx20"; got != want { t.Errorf("Want serverType %q, got %q", want, got) } if got, want := p.key, 23234; got != want { t.Errorf("Want key %d, got %d", want, got) } } ================================================ FILE: drivers/hetznercloud/provider.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package hetznercloud import ( "sync" "text/template" "github.com/drone/autoscaler" "github.com/drone/autoscaler/drivers/internal/userdata" "github.com/hetznercloud/hcloud-go/hcloud" ) // provider implement a Hetzner Cloud provider. type provider struct { init sync.Once token string datacenter string serverType string image string userdata *template.Template key int client *hcloud.Client } // New returns a new Digital Ocean provider. func New(opts ...Option) autoscaler.Provider { p := new(provider) for _, opt := range opts { opt(p) } if p.serverType == "" { p.serverType = "cx11" } if p.image == "" { p.image = "ubuntu-20.04" } if p.userdata == nil { p.userdata = userdata.T } return p } ================================================ FILE: drivers/hetznercloud/provider_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package hetznercloud import "testing" func TestDefaults(t *testing.T) { p := New().(*provider) if got, want := p.image, "ubuntu-20.04"; got != want { t.Errorf("Want image %q, got %q", want, got) } if got, want := p.datacenter, ""; got != want { t.Errorf("Want datacenter %q, got %q", want, got) } if got, want := p.serverType, "cx11"; got != want { t.Errorf("Want server type %q, got %q", want, got) } } ================================================ FILE: drivers/hetznercloud/setup.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package hetznercloud import ( "context" "errors" "github.com/drone/autoscaler/logger" "github.com/hetznercloud/hcloud-go/hcloud" "golang.org/x/sync/errgroup" ) func (p *provider) setup(ctx context.Context) error { var g errgroup.Group if p.key == 0 { g.Go(func() error { return p.setupKeypair(ctx) }) } return g.Wait() } func (p *provider) setupKeypair(ctx context.Context) error { logger := logger.FromContext(ctx) logger.Debugln("finding default ssh key") keys, _, err := p.client.SSHKey.List(ctx, hcloud.SSHKeyListOpts{}) if err != nil { return err } index := map[string]*hcloud.SSHKey{} for _, key := range keys { index[key.Name] = key } // if the account has multiple keys configured we will // attempt to use an existing key based on naming convention. for _, name := range []string{"drone", "id_rsa_drone"} { key, ok := index[name] if !ok { continue } p.key = key.ID logger. WithField("name", name). WithField("fingerprint", key.Fingerprint). Debugln("using default ssh key") return nil } // if there were no matches but the account has at least // one keypair already created we will select the first // in the list. if len(keys) > 0 { key := keys[0] p.key = key.ID logger. WithField("name", key.Name). WithField("fingerprint", key.Fingerprint). Debugln("using default ssh key") return nil } return errors.New("No matching keys") } ================================================ FILE: drivers/hetznercloud/setup_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package hetznercloud import ( "context" "testing" "github.com/h2non/gock" ) func TestSetupKey_ChooseFirst(t *testing.T) { defer gock.Off() gock.New("https://api.hetzner.cloud"). Get("/v1/ssh_keys"). Reply(200). BodyString(respSingleKey) p := New( WithToken("LRK9DAWQ1ZAEFSrCNEEzLCUwhYX1U3g7wMg4dTlkkDC96fyDuyJ39nVbVjCKSDfj"), ).(*provider) err := p.setup(context.TODO()) if err != nil { t.Error(err) } if got, want := p.key, 2323; got != want { t.Errorf("Want key id %d, got %d", want, got) } } func TestSetupKey_ChooseMatch(t *testing.T) { defer gock.Off() gock.New("https://api.hetzner.cloud"). Get("/v1/ssh_keys"). Reply(200). BodyString(respMultiKey) p := New( WithToken("LRK9DAWQ1ZAEFSrCNEEzLCUwhYX1U3g7wMg4dTlkkDC96fyDuyJ39nVbVjCKSDfj"), ).(*provider) err := p.setup(context.TODO()) if err != nil { t.Error(err) } if got, want := p.key, 2324; got != want { t.Errorf("Want key id %d, got %d", want, got) } } const respSingleKey = ` { "ssh_keys": [ { "id": 2323, "name": "My ssh key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f", "public_key": "ssh-rsa AAAjjk76kgf...Xt" } ] } ` const respMultiKey = ` { "ssh_keys": [ { "id": 2323, "name": "My ssh key", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f", "public_key": "ssh-rsa AAAjjk76kgf...Xt" }, { "id": 2324, "name": "drone", "fingerprint": "b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f", "public_key": "ssh-rsa AAAjjk76kgf...Xt" } ] } ` ================================================ FILE: drivers/internal/userdata/userdata.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package userdata import ( "encoding/base64" "text/template" "github.com/drone/funcmap" ) var funcs = map[string]interface{}{ "base64": func(src []byte) string { return base64.StdEncoding.EncodeToString(src) }, } // Parse parses the userdata template. func Parse(text string) *template.Template { if decoded, err := base64.StdEncoding.DecodeString(text); err == nil { return template.Must( template.New("_").Funcs(funcs).Funcs(funcmap.Funcs).Parse(string(decoded)), ) } return template.Must( template.New("_").Funcs(funcs).Funcs(funcmap.Funcs).Parse(text), ) } // T is the default userdata template. var T = Parse(`#cloud-config apt_reboot_if_required: false package_update: false package_upgrade: false apt: sources: docker.list: source: deb [arch=amd64] https://download.docker.com/linux/ubuntu $RELEASE stable keyid: 0EBFCD88 packages: - docker-ce write_files: - path: /etc/systemd/system/docker.service.d/override.conf content: | [Service] ExecStart= ExecStart=/usr/bin/dockerd - path: /etc/default/docker content: | DOCKER_OPTS="" - path: /etc/docker/daemon.json content: | { "hosts": [ "0.0.0.0:2376", "unix:///var/run/docker.sock" ], "tls": true, "tlsverify": true, "tlscacert": "/etc/docker/ca.pem", "tlscert": "/etc/docker/server-cert.pem", "tlskey": "/etc/docker/server-key.pem" } - path: /etc/docker/ca.pem encoding: b64 content: {{ .CACert | base64 }} - path: /etc/docker/server-cert.pem encoding: b64 content: {{ .TLSCert | base64 }} - path: /etc/docker/server-key.pem encoding: b64 content: {{ .TLSKey | base64 }} runcmd: - [ systemctl, daemon-reload ] - [ systemctl, restart, docker ] `) ================================================ FILE: drivers/internal/userdata/userdata_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package userdata import ( "bytes" "fmt" "testing" "github.com/drone/autoscaler" ) func TestUserdata(t *testing.T) { buf := new(bytes.Buffer) err := T.Execute(buf, &autoscaler.InstanceCreateOpts{ Name: "agent-123456", CACert: []byte(dummyCA), TLSKey: []byte(dummykey), TLSCert: []byte(dummyCert), }) if err != nil { t.Error(err) return } } func TestUserdataFuncmap(t *testing.T) { buf := new(bytes.Buffer) err := UD.Execute(buf, &map[string]interface{}{ "Content": "foo", }) fmt.Println(buf.String()) if err != nil { t.Error(err) return } if buf.String() != UDExpected { t.Errorf("expected '%s', got '%s'", UDExpected, buf.String()) } } var dummyCA = `-----BEGIN CERTIFICATE----- MIIGOTCCBCGgAwIBAgIJAOE/vJd8EB24MA0GCSqGSIb3DQEBBQUAMIGyMQswCQYD VQQGEwJGUjEPMA0GA1UECAwGQWxzYWNlMRMwEQYDVQQHDApTdHJhc2JvdXJnMRgw FgYDVQQKDA93d3cuZnJlZWxhbi5vcmcxEDAOBgNVBAsMB2ZyZWVsYW4xLTArBgNV BAMMJEZyZWVsYW4gU2FtcGxlIENlcnRpZmljYXRlIEF1dGhvcml0eTEiMCAGCSqG KvbxUcDaVvXB0EU0bg== -----END CERTIFICATE-----` var dummykey = `-----BEGIN RSA PRIVATE KEY----- MIIJKQIBAAKCAgEA3W29+ID6194bH6ejLrIC4hb2Ugo8v6ZC+Mrck2dNYMNPjcOK ABvxxEtBamnSaeU/IY7FC/giN622LEtV/3oDcrua0+yWuVafyxmZyTKUb4/GUgaf RQPf/eiX9urWurtIK7XgNGFNUjYPq4dSJQPPhwCHE/LKAykWnZBXRrX0Dq4XyApN ku0IpjIjEXH+8ixE12wH8wt7DEvdO7T3N3CfUbaITl1qBX+Nm2Z6q4Ag/u5rl8NJ v3TGd3xXD9yQIjmugNgxNiwAZzhJs/ZJy++fPSJ1XQxbd9qPghgGoe/ff6G7 -----END RSA PRIVATE KEY-----` var dummyCert = `-----BEGIN CERTIFICATE----- MIIGJzCCBA+gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBsjELMAkGA1UEBhMCRlIx d3d3LmZyZWVsYW4ub3JnMRAwDgYDVQQLDAdmcmVlbGFuMS0wKwYDVQQDDCRGcmVl bGFuIFNhbXBsZSBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxIjAgBgkqhkiG9w0BCQEW E2NvbnRhY3RAZnJlZWxhbi5vcmcwHhcNMTIwNDI3MTAzMTE4WhcNMjIwNDI1MTAz DiH5uEqBXExjrj0FslxcVKdVj5glVcSmkLwZKbEU1OKwleT/iXFhvooWhQ== -----END CERTIFICATE-----` var UD = Parse(`#cloud-config apt_reboot_if_required: package_update: false package_upgrade: false write_files: - path: /etc/systemd/system/docker.service.d/override.conf content: | {{nindent .Content 6 }} `) var UDExpected = `#cloud-config apt_reboot_if_required: package_update: false package_upgrade: false write_files: - path: /etc/systemd/system/docker.service.d/override.conf content: | foo ` ================================================ FILE: drivers/openstack/create.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package openstack import ( "bytes" "context" "github.com/drone/autoscaler" "github.com/drone/autoscaler/logger" "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips" "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs" "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" "github.com/gophercloud/gophercloud/pagination" ) // Create creates an OpenStack instance func (p *provider) Create(ctx context.Context, opts autoscaler.InstanceCreateOpts) (*autoscaler.Instance, error) { p.init.Do(func() { _ = p.setup(ctx) }) buf := new(bytes.Buffer) err := p.userdata.Execute(buf, &opts) if err != nil { return nil, err } logger := logger.FromContext(ctx). WithField("region", p.region). WithField("image", p.image). WithField("flavor", p.flavor). WithField("network", p.network). WithField("pool", p.pool). WithField("name", opts.Name) logger.Debugln("instance create") nets := make([]servers.Network, 0) if p.network != "" { network, err := networks.Get(p.networkClient, p.network).Extract() if err != nil { logger.WithError(err). Debugln("failed to find network") return nil, err } nets = append(nets, servers.Network{ UUID: network.ID, }) } serverCreateOpts := servers.CreateOpts{ Name: opts.Name, ImageRef: p.image, FlavorRef: p.flavor, Networks: nets, UserData: buf.Bytes(), ServiceClient: p.computeClient, Metadata: p.metadata, SecurityGroups: p.groups, } createOpts := keypairs.CreateOptsExt{ CreateOptsBuilder: serverCreateOpts, KeyName: p.key, } server, err := servers.Create(p.computeClient, createOpts).Extract() if err != nil { logger.WithError(err). Debugln("failed to create server") return nil, err } err = servers.WaitForStatus(p.computeClient, server.ID, "ACTIVE", 300) if err != nil { logger.WithError(err). Debugln("failed waiting for server") return nil, err } instance := &autoscaler.Instance{ Provider: autoscaler.ProviderOpenStack, ID: server.ID, Name: server.Name, Region: p.region, Image: p.image, Size: p.flavor, } if p.network != "" { network, err := networks.Get(p.networkClient, p.network).Extract() if err != nil { logger.WithError(err). Debugln("failed to find network") return nil, err } if err := servers.ListAddresses(p.computeClient, server.ID).EachPage(func(page pagination.Page) (bool, error) { result, err := servers.ExtractAddresses(page) if err != nil { return false, err } for name, addresses := range result { if name == network.Name { for _, address := range addresses { instance.Address = address.Address return true, nil } } } return false, nil }); err != nil { logger.WithError(err). Debugln("failed to fetch address") return nil, err } } if p.pool != "" { ip, err := floatingips.Create(p.computeClient, floatingips.CreateOpts{ Pool: p.pool, }).Extract() if err != nil { logger.WithError(err). Debugln("failed to create floating ip") return nil, err } if err := floatingips.AssociateInstance(p.computeClient, server.ID, floatingips.AssociateOpts{ FloatingIP: ip.IP, }).ExtractErr(); err != nil { logger.WithError(err). Debugln("failed to associate floating ip") return nil, err } instance.Address = ip.IP } logger. WithField("name", instance.Name). WithField("ip", instance.Address). Debugln("instance network ready") return instance, nil } ================================================ FILE: drivers/openstack/create_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package openstack import ( "context" "os" "testing" "github.com/drone/autoscaler" "github.com/h2non/gock" ) func TestCreate(t *testing.T) { defer gock.Off() setupEnv(t) authResp1 := helperLoad(t, "authresp1.json") gock.New("http://ops.my.cloud"). Get("/identity"). Reply(300). SetHeader("Content-Type", "application/json"). BodyString(string(authResp1)) tokenResp1 := helperLoad(t, "tokenresp1.json") gock.New("http://ops.my.cloud"). Post("/identity/v3/auth/tokens"). Reply(201). SetHeader("Content-Type", "application/json"). SetHeader("X-Subject-Token", authToken). BodyString(string(tokenResp1)) authResp2 := helperLoad(t, "authresp1.json") gock.New("http://ops.my.cloud"). Get("/identity"). Reply(300). SetHeader("Content-Type", "application/json"). BodyString(string(authResp2)) tokenResp2 := helperLoad(t, "tokenresp1.json") gock.New("http://ops.my.cloud"). Post("/identity/v3/auth/tokens"). Reply(201). SetHeader("Content-Type", "application/json"). SetHeader("X-Subject-Token", authToken). BodyString(string(tokenResp2)) fipResp1 := helperLoad(t, "fipresp1.json") gock.New("http://ops.my.cloud"). Post("/compute/v2.1/os-floating-ips"). MatchHeader("X-Auth-Token", authToken). Reply(200). SetHeader("Content-Type", "application/json"). BodyString(string(fipResp1)) imageListResp := helperLoad(t, "imagelistresp1.json") gock.New("http://ops.my.cloud"). Get("/compute/v2.1/images/detail"). MatchHeader("X-Auth-Token", authToken). Reply(200). SetHeader("Content-Type", "application/json"). BodyString(string(imageListResp)) flavorListResp1 := helperLoad(t, "flavorlistresp1.json") gock.New("http://ops.my.cloud"). Get("/compute/v2.1/flavors/detail"). MatchHeader("X-Auth-Token", authToken). Reply(200). SetHeader("Content-Type", "application/json"). BodyString(string(flavorListResp1)) serverCreateResp1 := helperLoad(t, "servercreateresp1.json") gock.New("http://ops.my.cloud"). Post("/compute/v2.1/servers"). MatchHeader("X-Auth-Token", authToken). Reply(202). SetHeader("Content-Type", "application/json"). BodyString(string(serverCreateResp1)) serverStatusResp1 := helperLoad(t, "serverstatusresp1.json") gock.New("http://ops.my.cloud"). Get("/compute/v2.1/servers/56046f6d-3184-495b-938b-baa450db970d"). MatchHeader("X-Auth-Token", authToken). Reply(200). SetHeader("Content-Type", "application/json"). BodyString(string(serverStatusResp1)) associateResp1 := helperLoad(t, "associateresp1.json") gock.New("http://ops.my.cloud"). Post("/compute/v2.1/servers/56046f6d-3184-495b-938b-baa450db970d/action"). MatchHeader("X-Auth-Token", authToken). BodyString(string("{\"addFloatingIp\":{\"address\":\"172.24.4.5\"}}")). Reply(202). SetHeader("Content-Type", "application/json"). BodyString(string(associateResp1)) v, err := New( WithRegion("RegionOne"), WithFlavor("m1.small"), WithImage("ubuntu-16.04-server-latest"), WithFloatingIpPool("public"), WithSSHKey("drone-ci-key"), ) if err != nil { t.Error(err) return } p := v.(*provider) p.init.Do(func() {}) // prevent init function instance, err := p.Create(context.TODO(), autoscaler.InstanceCreateOpts{Name: "agent-RjISb5v1"}) if err != nil { t.Error(err) } if !gock.IsDone() { t.Error("Not all expected http requests completed") } t.Run("Instance Attributes", testInstance(instance)) } func TestAuthFail(t *testing.T) { defer gock.Off() setupEnv(t) err := os.Setenv("OS_PASSWORD", "BAADF00D") if err != nil { t.Error("Unable to set OS_PASSWORD") } authResp1 := helperLoad(t, "authresp1.json") gock.New("http://ops.my.cloud"). Get("/identity"). Reply(300). SetHeader("Content-Type", "application/json"). BodyString(string(authResp1)) gock.New("http://ops.my.cloud"). Post("/identity/v3/auth/tokens"). Reply(401) _, err = New( WithRegion("RegionOne"), WithFlavor("m1.small"), WithImage("ubuntu-16.04-server-latest"), WithFloatingIpPool("public"), WithSSHKey("drone-ci-key"), ) if err == nil { t.Error("Expected authentication error from OpenStack") } if !gock.IsDone() { t.Error("Not all expected http requests completed") } } func TestCreateFail(t *testing.T) { defer gock.Off() setupEnv(t) authResp1 := helperLoad(t, "authresp1.json") gock.New("http://ops.my.cloud"). Get("/identity"). Reply(300). SetHeader("Content-Type", "application/json"). BodyString(string(authResp1)) tokenResp1 := helperLoad(t, "tokenresp1.json") gock.New("http://ops.my.cloud"). Post("/identity/v3/auth/tokens"). Reply(201). SetHeader("Content-Type", "application/json"). SetHeader("X-Subject-Token", authToken). BodyString(string(tokenResp1)) authResp2 := helperLoad(t, "authresp1.json") gock.New("http://ops.my.cloud"). Get("/identity"). Reply(300). SetHeader("Content-Type", "application/json"). BodyString(string(authResp2)) tokenResp2 := helperLoad(t, "tokenresp1.json") gock.New("http://ops.my.cloud"). Post("/identity/v3/auth/tokens"). Reply(201). SetHeader("Content-Type", "application/json"). SetHeader("X-Subject-Token", authToken). BodyString(string(tokenResp2)) imageListResp := helperLoad(t, "imagelistresp1.json") gock.New("http://ops.my.cloud"). Get("/compute/v2.1/images/detail"). MatchHeader("X-Auth-Token", authToken). Reply(200). SetHeader("Content-Type", "application/json"). BodyString(string(imageListResp)) flavorListResp1 := helperLoad(t, "flavorlistresp1.json") gock.New("http://ops.my.cloud"). Get("/compute/v2.1/flavors/detail"). MatchHeader("X-Auth-Token", authToken). Reply(200). SetHeader("Content-Type", "application/json"). BodyString(string(flavorListResp1)) gock.New("http://ops.my.cloud"). Post("/compute/v2.1/servers"). MatchHeader("X-Auth-Token", authToken). Reply(500) v, err := New( WithRegion("RegionOne"), WithFlavor("m1.small"), WithImage("ubuntu-16.04-server-latest"), WithFloatingIpPool("public"), WithSSHKey("drone-ci-key"), ) if err != nil { t.Error(err) return } p := v.(*provider) p.init.Do(func() {}) // prevent init function _, err = p.Create(context.TODO(), autoscaler.InstanceCreateOpts{Name: "agent-RjISb5v1"}) if err == nil { t.Error("Expected error creating instance") } if !gock.IsDone() { t.Error("Not all expected http requests completed") } } func setupEnv(t *testing.T) { err := os.Setenv("OS_AUTH_URL", "http://ops.my.cloud/identity") if err != nil { t.Error("Unable to set OS_AUTH_URL") } err = os.Setenv("OS_USERNAME", "admin") if err != nil { t.Error("Unable to set OS_USERNAME") } err = os.Setenv("OS_PASSWORD", "admin") if err != nil { t.Error("Unable to set OS_USERNAME") } err = os.Setenv("OS_DOMAIN_NAME", "demo") if err != nil { t.Error("Unable to set OS_DOMAIN_NAME") } } func testInstance(instance *autoscaler.Instance) func(t *testing.T) { return func(t *testing.T) { if instance == nil { t.Errorf("Expect non-nil instance even if error") } if want, got := instance.Address, "172.24.4.5"; got != want { t.Errorf("Want instance IP %q, got %q", want, got) } if want, got := instance.Image, "4ef19958-ee2d-44a7-a100-de0b8afdbc8e"; got != want { t.Errorf("Want instance ID %q, got %q", want, got) } if want, got := instance.ID, "56046f6d-3184-495b-938b-baa450db970d"; got != want { t.Errorf("Want instance ID %q, got %q", want, got) } if want, got := instance.Name, "agent-RjISb5v1"; got != want { t.Errorf("Want instance Name %q, got %q", want, got) } if want, got := instance.Provider, autoscaler.ProviderOpenStack; got != want { t.Errorf("Want OpenStack Provider type") } if want, got := instance.Region, "RegionOne"; got != want { t.Errorf("Want instance Region %q, got %q", want, got) } if want, got := instance.Size, "29e3cce3-d771-4220-80fe-3edf0e8dd466"; got != want { t.Errorf("Want instance Size %q, got %q", want, got) } } } ================================================ FILE: drivers/openstack/destroy.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package openstack import ( "context" "fmt" "github.com/drone/autoscaler" "github.com/drone/autoscaler/logger" "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips" "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" "github.com/gophercloud/gophercloud/pagination" ) func (p *provider) Destroy(ctx context.Context, instance *autoscaler.Instance) error { logger := logger.FromContext(ctx). WithField("region", instance.Region). WithField("image", instance.Image). WithField("flavor", instance.Size). WithField("name", instance.Name) logger.Debugln("deleting instance") err := p.deleteFloatingIps(instance) if err != nil { logger.WithError(err). Debugln("failed to delete floating ips") return err } err = servers.Delete(p.computeClient, instance.ID).ExtractErr() if err == nil { logger.Debugln("instance deleted") return nil } if err.Error() == "Resource not found" { logger.WithError(err). Debugln("instance does not exist") return autoscaler.ErrInstanceNotFound } logger.WithError(err). Errorln("attempting to force delete") err = servers.ForceDelete(p.computeClient, instance.ID).ExtractErr() if err == nil { logger.Debugln("instance deleted") return nil } if err.Error() == "Resource not found" { logger.WithError(err). Debugln("instance does not exist") return autoscaler.ErrInstanceNotFound } logger.WithError(err). Errorln("force-deleting instance failed") return err } func (p *provider) deleteFloatingIps(instance *autoscaler.Instance) error { return floatingips.List(p.computeClient).EachPage(func(page pagination.Page) (bool, error) { ips, err := floatingips.ExtractFloatingIPs(page) if err != nil { return false, err } for _, ip := range ips { if ip.InstanceID == instance.ID { if err := floatingips.DisassociateInstance(p.computeClient, instance.ID, floatingips.DisassociateOpts{ FloatingIP: ip.IP, }).ExtractErr(); err != nil { return false, fmt.Errorf("failed to disassociate floating ip: %s", err) } if err := floatingips.Delete(p.computeClient, ip.ID).ExtractErr(); err != nil { return false, fmt.Errorf("failed to delete floating ip: %s", err) } } } return true, nil }) } ================================================ FILE: drivers/openstack/destroy_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package openstack import ( "context" "testing" "github.com/drone/autoscaler" "github.com/h2non/gock" ) func TestDestroy(t *testing.T) { defer gock.Off() setupEnv(t) authResp1 := helperLoad(t, "authresp1.json") gock.New("http://ops.my.cloud"). Get("/identity"). Reply(300). SetHeader("Content-Type", "application/json"). BodyString(string(authResp1)) tokenResp1 := helperLoad(t, "tokenresp1.json") gock.New("http://ops.my.cloud"). Post("/identity/v3/auth/tokens"). Reply(201). SetHeader("Content-Type", "application/json"). SetHeader("X-Subject-Token", authToken). BodyString(string(tokenResp1)) authResp2 := helperLoad(t, "authresp1.json") gock.New("http://ops.my.cloud"). Get("/identity"). Reply(300). SetHeader("Content-Type", "application/json"). BodyString(string(authResp2)) tokenResp2 := helperLoad(t, "tokenresp1.json") gock.New("http://ops.my.cloud"). Post("/identity/v3/auth/tokens"). Reply(201). SetHeader("Content-Type", "application/json"). SetHeader("X-Subject-Token", authToken). BodyString(string(tokenResp2)) fipResp1 := helperLoad(t, "fipresp1.json") gock.New("http://ops.my.cloud"). MatchHeader("X-Auth-Token", authToken). Get("/compute/v2.1/os-floating-ips"). Reply(200). SetHeader("Content-Type", "application/json"). BodyString(string(fipResp1)) gock.New("http://ops.my.cloud"). MatchHeader("X-Auth-Token", authToken). Delete("/compute/v2.1/servers/56046f6d-3184-495b-938b-baa450db970d"). Reply(204) imageListResp := helperLoad(t, "imagelistresp1.json") gock.New("http://ops.my.cloud"). Get("/compute/v2.1/images/detail"). MatchHeader("X-Auth-Token", authToken). Reply(200). SetHeader("Content-Type", "application/json"). BodyString(string(imageListResp)) flavorListResp1 := helperLoad(t, "flavorlistresp1.json") gock.New("http://ops.my.cloud"). Get("/compute/v2.1/flavors/detail"). MatchHeader("X-Auth-Token", authToken). Reply(200). SetHeader("Content-Type", "application/json"). BodyString(string(flavorListResp1)) mockContext := context.TODO() mockInstance := &autoscaler.Instance{ ID: "56046f6d-3184-495b-938b-baa450db970d", Address: "172.24.4.5", } v, err := New( WithRegion("RegionOne"), WithFlavor("m1.small"), WithImage("ubuntu-16.04-server-latest"), WithFloatingIpPool("public"), WithSSHKey("drone-ci-key"), ) if err != nil { t.Error(err) return } p := v.(*provider) p.init.Do(func() {}) // err = p.Destroy(mockContext, mockInstance) if err != nil { t.Error(err) } if !gock.IsDone() { t.Error("Not all expected http requests completed") } } ================================================ FILE: drivers/openstack/doc.go ================================================ /* Package openstack contains a autoscaler driver for OpenStack Configuration: Authenticate with the usual OpenStack environment variables. (Not all of these may be necessary: see https://github.com/gophercloud/gophercloud/blob/master/openstack/auth_env.go) OS_AUTH_URL=https://my.openstack.cloud:5000 OS_ENDPOINT_TYPE=publicURL OS_IDENTITY_API_VERSION=2 OS_PASSWORD= OS_DOMAIN_ID=default OS_REGION_NAME=my-region OS_TENANT_ID=my-tenant-id OS_TENANT_NAME=my-tenant-name OS_USERNAME=my-username Configure driver with: DRONE_OPENSTACK_SSHKEY=drone-key-name DRONE_OPENSTACK_SECURITY_GROUP=my-security-group # Pool for floating ips DRONE_OPENSTACK_IP_POOL=my-ip-pool DRONE_OPENSTACK_FLAVOR=v1-standard-2 DRONE_OPENSTACK_IMAGE=ubuntu-16.04-server-latest DRONE_OPENSTACK_METADATA=name:agent,owner:drone-ci */ package openstack ================================================ FILE: drivers/openstack/option.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package openstack import ( "io/ioutil" "github.com/drone/autoscaler/drivers/internal/userdata" "github.com/gophercloud/gophercloud" ) type Option func(*provider) // WithImage returns an option to set the instance image. func WithImage(image string) Option { return func(p *provider) { p.image = image } } // WithRegion returns an option to set the OpenStack target region. func WithRegion(region string) Option { return func(p *provider) { p.region = region } } // WithFlavor returns an option to set the instance flavor. func WithFlavor(flavor string) Option { return func(p *provider) { p.flavor = flavor } } // WithSecurityGroup returns an option to set the instance security groups. func WithSecurityGroup(group ...string) Option { return func(p *provider) { p.groups = group } } // WithComputeClient returns an option to set the // GopherCloud ServiceClient. func WithComputeClient(computeClient *gophercloud.ServiceClient) Option { return func(p *provider) { p.computeClient = computeClient } } // WithNetworkClient returns an option to set the // GopherCloud ServiceClient. func WithNetworkClient(networkClient *gophercloud.ServiceClient) Option { return func(p *provider) { p.networkClient = networkClient } } // WithSSHKey returns an option to set the ssh key. func WithSSHKey(key string) Option { return func(p *provider) { p.key = key } } // WithNetwork returns an option to set the network id. func WithNetwork(id string) Option { return func(p *provider) { p.network = id } } func WithFloatingIpPool(pool string) Option { return func(p *provider) { p.pool = pool } } // WithMetadata returns an option to set the instance metadata. func WithMetadata(metadata map[string]string) Option { return func(p *provider) { p.metadata = metadata } } // WithUserData returns an option to set the cloud-init // template from text. func WithUserData(text string) Option { return func(p *provider) { if text != "" { p.userdata = userdata.Parse(text) } } } // WithUserDataFile returns an option to set the cloud-init // template from file. func WithUserDataFile(filepath string) Option { return func(p *provider) { if filepath != "" { b, err := ioutil.ReadFile(filepath) if err != nil { panic(err) } p.userdata = userdata.Parse(string(b)) } } } ================================================ FILE: drivers/openstack/option_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package openstack import ( "testing" "github.com/gophercloud/gophercloud" ) func TestOptions(t *testing.T) { v, err := New( WithComputeClient(&gophercloud.ServiceClient{}), WithNetworkClient(&gophercloud.ServiceClient{}), WithFloatingIpPool("ext-ips-1"), WithFlavor("053dc448-045b-4c15-a4a0-1908b6b9310d"), WithSecurityGroup("drone-ci"), WithSSHKey("drone-ci"), WithRegion("sto-01"), WithImage("0e9fe318-568f-417e-b2c1-f1218aa2712f"), WithMetadata(map[string]string{"foo": "bar", "baz": "qux"}), WithNetwork("c7d172c8-96e6-40ab-aaaa-4a555e247c73"), ) if err != nil { t.Error(err) return } p := v.(*provider) if got, want := p.pool, "ext-ips-1"; got != want { t.Errorf("Want pool %q, got %q", want, got) } if got, want := p.region, "sto-01"; got != want { t.Errorf("Want region %q, got %q", want, got) } if got, want := p.flavor, "053dc448-045b-4c15-a4a0-1908b6b9310d"; got != want { t.Errorf("Want flavor %q, got %q", want, got) } if got, want := p.image, "0e9fe318-568f-417e-b2c1-f1218aa2712f"; got != want { t.Errorf("Want image %q, got %q", want, got) } if got, want := p.network, "c7d172c8-96e6-40ab-aaaa-4a555e247c73"; got != want { t.Errorf("Want network %q, got %q", want, got) } if got, want := p.key, "drone-ci"; got != want { t.Errorf("Want key %q, got %q", want, got) } if got, want := len(p.metadata), 2; got != want { t.Errorf("Want %d tags, got %d", want, got) } if got, want := p.metadata["foo"], "bar"; got != want { t.Errorf("Want foo=%q metadata, got foo=%q", want, got) } if got, want := p.metadata["baz"], "qux"; got != want { t.Errorf("Want baz=%q metadata, got baz=%q", want, got) } } ================================================ FILE: drivers/openstack/provider.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package openstack import ( "regexp" "sync" "text/template" "github.com/drone/autoscaler" "github.com/drone/autoscaler/drivers/internal/userdata" "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack" "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" "github.com/gophercloud/gophercloud/openstack/compute/v2/images" "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" ) // provider implements an OpenStack provider type provider struct { init sync.Once key string region string image string flavor string network string pool string userdata *template.Template groups []string metadata map[string]string computeClient *gophercloud.ServiceClient networkClient *gophercloud.ServiceClient } // New returns a new OpenStack provider. func New(opts ...Option) (autoscaler.Provider, error) { p := new(provider) for _, opt := range opts { opt(p) } if p.userdata == nil { p.userdata = userdata.T } if p.computeClient == nil { authOpts, err := openstack.AuthOptionsFromEnv() if err != nil { return nil, err } authClient, err := openstack.AuthenticatedClient(authOpts) if err != nil { return nil, err } p.computeClient, err = openstack.NewComputeV2(authClient, gophercloud.EndpointOpts{ Region: p.region, }) if err != nil { return nil, err } } if p.networkClient == nil { authOpts, err := openstack.AuthOptionsFromEnv() if err != nil { return nil, err } authClient, err := openstack.AuthenticatedClient(authOpts) if err != nil { return nil, err } p.networkClient, err = openstack.NewNetworkV2(authClient, gophercloud.EndpointOpts{ Region: p.region, }) if err != nil { return nil, err } } if p.image != "" && !isUUID(p.image) { uuid, err := images.IDFromName(p.computeClient, p.image) if err != nil { return nil, err } p.image = uuid } if p.flavor != "" && !isUUID(p.flavor) { uuid, err := flavors.IDFromName(p.computeClient, p.flavor) if err != nil { return nil, err } p.flavor = uuid } if p.network != "" && !isUUID(p.network) { uuid, err := networks.IDFromName(p.networkClient, p.network) if err != nil { return nil, err } p.network = uuid } return p, nil } func isUUID(uuid string) bool { r := regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$") return r.MatchString(uuid) } ================================================ FILE: drivers/openstack/provider_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package openstack import ( "testing" "github.com/gophercloud/gophercloud" ) func TestDefaults(t *testing.T) { v, err := New( WithComputeClient(&gophercloud.ServiceClient{}), WithNetworkClient(&gophercloud.ServiceClient{}), ) if err != nil { t.Error(err) return } p := v.(*provider) // Add tests if we set some actual defaults in the future. _ = p } ================================================ FILE: drivers/openstack/setup.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package openstack import ( "context" "errors" "github.com/drone/autoscaler/logger" "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs" "golang.org/x/sync/errgroup" ) func (p *provider) setup(ctx context.Context) error { var g errgroup.Group if p.key == "" { g.Go(func() error { return p.findKeyPair(ctx) }) } return g.Wait() } func (p *provider) findKeyPair(ctx context.Context) error { logger := logger.FromContext(ctx) logger.Debugln("finding default ssh key") allPages, err := keypairs.List(p.computeClient).AllPages() if err != nil { return err } keys, err := keypairs.ExtractKeyPairs(allPages) if err != nil { return err } index := map[string]keypairs.KeyPair{} for _, key := range keys { index[key.Name] = key } // if the account has multiple keys configured we will // attempt to use an existing key based on naming convention. for _, name := range []string{"drone", "id_rsa_drone"} { key, ok := index[name] if !ok { continue } p.key = key.Name logger. WithField("name", name). WithField("fingerprint", key.Fingerprint). Debugln("using default ssh key") return nil } // if there were no matches but the account has at least // one keypair already created we will select the first // in the list. if len(keys) > 0 { key := keys[0] p.key = key.Name logger. WithField("name", key.Name). WithField("fingerprint", key.Fingerprint). Debugln("using default ssh key") return nil } return errors.New("no matching keys") } ================================================ FILE: drivers/openstack/setup_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package openstack import ( "io/ioutil" "path/filepath" "testing" ) func helperLoad(t *testing.T, name string) []byte { path := filepath.Join("testdata", name) // relative path bytes, err := ioutil.ReadFile(path) if err != nil { t.Fatal(err) } return bytes } const authToken = "gAAAAABb1tQPtYVBv68airR0dgKC2vXpkLNfEHx0w1EL89dOOjKrtdYHR7IZrDd4VjwZapC5Sri4CndpPscw-nHoh0VQsrvFjtuvT6M64RdrrOljmJbvP0o7PbV713-Pi8OpRIfunvsQFnEQ2DxDH56QC6fsLEcF14VtogOQwTRBod0SkeOCpi4" ================================================ FILE: drivers/openstack/testdata/associateresp1.json ================================================ ================================================ FILE: drivers/openstack/testdata/authresp1.json ================================================ { "versions": { "values": [ { "status": "stable", "updated": "2018-10-15T00:00:00Z", "media-types": [ { "base": "application/json", "type": "application/vnd.openstack.identity-v3+json" } ], "id": "v3.11", "links": [ { "href": "http://ops.my.cloud/identity/v3/", "rel": "self" } ] } ] } } ================================================ FILE: drivers/openstack/testdata/fipresp1.json ================================================ { "floating_ip": { "instance_id": null, "ip": "172.24.4.5", "fixed_ip": null, "id": "0f013e62-42b1-461c-af7c-8aa3c705ff29", "pool": "public" } } ================================================ FILE: drivers/openstack/testdata/flavorlistresp1.json ================================================ { "flavors": [ { "name": "m1.tiny", "links": [ { "href": "http://ops.my.cloud/compute/v2.1/flavors/20f8acd8-5660-45d2-a176-1dafe98d591f", "rel": "self" }, { "href": "http://ops.my.cloud/compute/flavors/20f8acd8-5660-45d2-a176-1dafe98d591f", "rel": "bookmark" } ], "ram": 512, "OS-FLV-DISABLED:disabled": false, "vcpus": 1, "swap": "", "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 1, "id": "20f8acd8-5660-45d2-a176-1dafe98d591f" }, { "name": "m1.small", "links": [ { "href": "http://ops.my.cloud/compute/v2.1/flavors/29e3cce3-d771-4220-80fe-3edf0e8dd466", "rel": "self" }, { "href": "http://ops.my.cloud/compute/flavors/29e3cce3-d771-4220-80fe-3edf0e8dd466", "rel": "bookmark" } ], "ram": 2048, "OS-FLV-DISABLED:disabled": false, "vcpus": 1, "swap": "", "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 20, "id": "29e3cce3-d771-4220-80fe-3edf0e8dd466" }, { "name": "m1.medium", "links": [ { "href": "http://ops.my.cloud/compute/v2.1/flavors/2fe9e665-cf0f-4bff-a2a7-a0c19b15da7b", "rel": "self" }, { "href": "http://ops.my.cloud/compute/flavors/2fe9e665-cf0f-4bff-a2a7-a0c19b15da7b", "rel": "bookmark" } ], "ram": 4096, "OS-FLV-DISABLED:disabled": false, "vcpus": 2, "swap": "", "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 40, "id": "2fe9e665-cf0f-4bff-a2a7-a0c19b15da7b" }, { "name": "m1.large", "links": [ { "href": "http://ops.my.cloud/compute/v2.1/flavors/43832d64-56ed-401f-953c-d4d4c156f33a", "rel": "self" }, { "href": "http://ops.my.cloud/compute/flavors/43832d64-56ed-401f-953c-d4d4c156f33a", "rel": "bookmark" } ], "ram": 8192, "OS-FLV-DISABLED:disabled": false, "vcpus": 4, "swap": "", "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 80, "id": "43832d64-56ed-401f-953c-d4d4c156f33a" }, { "name": "m1.xlarge", "links": [ { "href": "http://ops.my.cloud/compute/v2.1/flavors/618945e9-beb4-4f20-88fd-e044a228156f", "rel": "self" }, { "href": "http://ops.my.cloud/compute/flavors/618945e9-beb4-4f20-88fd-e044a228156f", "rel": "bookmark" } ], "ram": 16384, "OS-FLV-DISABLED:disabled": false, "vcpus": 8, "swap": "", "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 160, "id": "618945e9-beb4-4f20-88fd-e044a228156f" }, { "name": "cirros256", "links": [ { "href": "http://ops.my.cloud/compute/v2.1/flavors/67a446b2-53c3-460b-a2da-533ce9a0c527", "rel": "self" }, { "href": "http://ops.my.cloud/compute/flavors/67a446b2-53c3-460b-a2da-533ce9a0c527", "rel": "bookmark" } ], "ram": 256, "OS-FLV-DISABLED:disabled": false, "vcpus": 1, "swap": "", "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 0, "id": "67a446b2-53c3-460b-a2da-533ce9a0c527" }, { "name": "ds512M", "links": [ { "href": "http://ops.my.cloud/compute/v2.1/flavors/c7d172c8-96e6-40ab-aaaa-4a555e247c73", "rel": "self" }, { "href": "http://ops.my.cloud/compute/flavors/c7d172c8-96e6-40ab-aaaa-4a555e247c73", "rel": "bookmark" } ], "ram": 512, "OS-FLV-DISABLED:disabled": false, "vcpus": 1, "swap": "", "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 5, "id": "c7d172c8-96e6-40ab-aaaa-4a555e247c73" }, { "name": "ds1G", "links": [ { "href": "http://ops.my.cloud/compute/v2.1/flavors/c3a01655-1b6a-44aa-b095-cc28dd407e70", "rel": "self" }, { "href": "http://ops.my.cloud/compute/flavors/c3a01655-1b6a-44aa-b095-cc28dd407e70", "rel": "bookmark" } ], "ram": 1024, "OS-FLV-DISABLED:disabled": false, "vcpus": 1, "swap": "", "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 10, "id": "c3a01655-1b6a-44aa-b095-cc28dd407e70" }, { "name": "ds2G", "links": [ { "href": "http://ops.my.cloud/compute/v2.1/flavors/053dc448-045b-4c15-a4a0-1908b6b9310d", "rel": "self" }, { "href": "http://ops.my.cloud/compute/flavors/053dc448-045b-4c15-a4a0-1908b6b9310d", "rel": "bookmark" } ], "ram": 2048, "OS-FLV-DISABLED:disabled": false, "vcpus": 2, "swap": "", "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 10, "id": "053dc448-045b-4c15-a4a0-1908b6b9310d" }, { "name": "ds4G", "links": [ { "href": "http://ops.my.cloud/compute/v2.1/flavors/0e9fe318-568f-417e-b2c1-f1218aa2712f", "rel": "self" }, { "href": "http://ops.my.cloud/compute/flavors/0e9fe318-568f-417e-b2c1-f1218aa2712f", "rel": "bookmark" } ], "ram": 4096, "OS-FLV-DISABLED:disabled": false, "vcpus": 4, "swap": "", "os-flavor-access:is_public": true, "rxtx_factor": 1.0, "OS-FLV-EXT-DATA:ephemeral": 0, "disk": 20, "id": "0e9fe318-568f-417e-b2c1-f1218aa2712f" } ] } ================================================ FILE: drivers/openstack/testdata/imagelistresp1.json ================================================ { "images": [ { "status": "ACTIVE", "updated": "2018-10-26T14:29:41Z", "links": [ { "href": "http://ops.my.cloud/compute/v2.1/images/ee7d6850-0592-4036-bd6e-198b41df7381", "rel": "self" }, { "href": "http://ops.my.cloud/compute/images/ee7d6850-0592-4036-bd6e-198b41df7381", "rel": "bookmark" }, { "href": "http://ops.my.cloud/image/images/ee7d6850-0592-4036-bd6e-198b41df7381", "type": "application/vnd.openstack.image", "rel": "alternate" } ], "id": "ee7d6850-0592-4036-bd6e-198b41df7381", "OS-EXT-IMG-SIZE:size": 74448896, "name": "rancheros-v1.4.1", "created": "2018-10-26T14:29:39Z", "minDisk": 0, "progress": 100, "minRam": 0, "metadata": { "description": "RancherOS v1.4.1" } }, { "status": "ACTIVE", "updated": "2018-10-23T13:20:03Z", "links": [ { "href": "http://ops.my.cloud/compute/v2.1/images/4ef19958-ee2d-44a7-a100-de0b8afdbc8e", "rel": "self" }, { "href": "http://ops.my.cloud/compute/images/4ef19958-ee2d-44a7-a100-de0b8afdbc8e", "rel": "bookmark" }, { "href": "http://ops.my.cloud/image/images/4ef19958-ee2d-44a7-a100-de0b8afdbc8e", "type": "application/vnd.openstack.image", "rel": "alternate" } ], "id": "4ef19958-ee2d-44a7-a100-de0b8afdbc8e", "OS-EXT-IMG-SIZE:size": 296943616, "name": "ubuntu-16.04-server-latest", "created": "2018-10-23T13:19:58Z", "minDisk": 0, "progress": 100, "minRam": 0, "metadata": { "description": "Ubuntu 16.04 LTS" } }, { "status": "ACTIVE", "updated": "2018-10-22T12:03:52Z", "links": [ { "href": "http://ops.my.cloud/compute/v2.1/images/7fd93141-c387-4859-bc79-b92fac420473", "rel": "self" }, { "href": "http://ops.my.cloud/compute/images/7fd93141-c387-4859-bc79-b92fac420473", "rel": "bookmark" }, { "href": "http://ops.my.cloud/image/images/7fd93141-c387-4859-bc79-b92fac420473", "type": "application/vnd.openstack.image", "rel": "alternate" } ], "id": "7fd93141-c387-4859-bc79-b92fac420473", "OS-EXT-IMG-SIZE:size": 13267968, "name": "cirros-0.3.5-x86_64-disk", "created": "2018-10-22T12:03:51Z", "minDisk": 0, "progress": 100, "minRam": 0, "metadata": {} } ] } ================================================ FILE: drivers/openstack/testdata/servercreateresp1.json ================================================ { "server": { "OS-EXT-STS:task_state": null, "addresses": { "private": [ { "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7a:f3:1f", "version": 4, "addr": "10.0.0.14", "OS-EXT-IPS:type": "fixed" } ] }, "links": [ { "href": "http://ops.my.cloud/compute/v2.1/servers/56046f6d-3184-495b-938b-baa450db970d", "rel": "self" }, { "href": "http://ops.my.cloud/compute/servers/56046f6d-3184-495b-938b-baa450db970d", "rel": "bookmark" } ], "image": { "id": "4ef19958-ee2d-44a7-a100-de0b8afdbc8e", "links": [ { "href": "http://ops.my.cloud/compute/images/4ef19958-ee2d-44a7-a100-de0b8afdbc8e", "rel": "bookmark" } ] }, "OS-EXT-STS:vm_state": "active", "OS-EXT-SRV-ATTR:instance_name": "instance-0000000d", "OS-SRV-USG:launched_at": "2018-10-29T09:37:05.000000", "flavor": { "id": "2", "links": [ { "href": "http://ops.my.cloud/compute/flavors/2", "rel": "bookmark" } ] }, "id": "56046f6d-3184-495b-938b-baa450db970d", "security_groups": [ { "name": "drone-agent" } ], "user_id": "898384bb1b5e4d5a9ff816f7ea911943", "OS-DCF:diskConfig": "MANUAL", "accessIPv4": "", "accessIPv6": "", "progress": 0, "OS-EXT-STS:power_state": 1, "OS-EXT-AZ:availability_zone": "nova", "config_drive": "", "status": "ACTIVE", "updated": "2018-10-29T09:37:06Z", "hostId": "1e678c454d7593d464d1a0c1c15111119ae841d11d3f7ba66f9aaee9", "OS-EXT-SRV-ATTR:host": "devstack", "OS-SRV-USG:terminated_at": null, "key_name": "drone-ci-key", "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", "name": "agent-RjISb5v1", "created": "2018-10-29T09:37:01Z", "tenant_id": "661f707340b0486caf878f9cc2bc1fab", "os-extended-volumes:volumes_attached": [], "metadata": { "owner": "drone-ci", "name": "agent" } } } ================================================ FILE: drivers/openstack/testdata/serverstatusresp1.json ================================================ { "server": { "OS-EXT-STS:task_state": null, "addresses": { "private": [ { "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7a:f3:1f", "version": 4, "addr": "10.0.0.14", "OS-EXT-IPS:type": "fixed" } ] }, "links": [ { "href": "http://ops.my.cloud/compute/v2.1/servers/56046f6d-3184-495b-938b-baa450db970d", "rel": "self" }, { "href": "http://ops.my.cloud/compute/servers/56046f6d-3184-495b-938b-baa450db970d", "rel": "bookmark" } ], "image": { "id": "4ef19958-ee2d-44a7-a100-de0b8afdbc8e", "links": [ { "href": "http://ops.my.cloud/compute/images/4ef19958-ee2d-44a7-a100-de0b8afdbc8e", "rel": "bookmark" } ] }, "OS-EXT-STS:vm_state": "active", "OS-EXT-SRV-ATTR:instance_name": "instance-0000000d", "OS-SRV-USG:launched_at": "2018-10-29T09:37:05.000000", "flavor": { "id": "2", "links": [ { "href": "http://ops.my.cloud/compute/flavors/2", "rel": "bookmark" } ] }, "id": "56046f6d-3184-495b-938b-baa450db970d", "security_groups": [ { "name": "drone-agent" } ], "user_id": "898384bb1b5e4d5a9ff816f7ea911943", "OS-DCF:diskConfig": "MANUAL", "accessIPv4": "", "accessIPv6": "", "progress": 0, "OS-EXT-STS:power_state": 1, "OS-EXT-AZ:availability_zone": "nova", "config_drive": "", "status": "ACTIVE", "updated": "2018-10-29T09:37:06Z", "hostId": "1e678c454d7593d464d1a0c1c15111119ae841d11d3f7ba66f9aaee9", "OS-EXT-SRV-ATTR:host": "devstack", "OS-SRV-USG:terminated_at": null, "key_name": "drone-ci-key", "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", "name": "agent-RjISb5v1", "created": "2018-10-29T09:37:01Z", "tenant_id": "661f707340b0486caf878f9cc2bc1fab", "os-extended-volumes:volumes_attached": [], "metadata": { "owner": "drone-ci", "name": "agent" } } } ================================================ FILE: drivers/openstack/testdata/tokenresp1.json ================================================ { "token": { "is_domain": false, "methods": [ "password" ], "roles": [ { "id": "ed9e3145d25241579d09ee3aec891bb8", "name": "reader" }, { "id": "74de3a0909ff455dbe3b4d3e8605c517", "name": "admin" }, { "id": "702053a51c4543029dd6bc3a6141f5ae", "name": "member" } ], "expires_at": "2018-10-29T10:34:07.000000Z", "project": { "domain": { "id": "default", "name": "Default" }, "id": "661f707340b0486caf878f9cc2bc1fab", "name": "demo" }, "catalog": [ { "endpoints": [ { "url": "http://ops.my.cloud/identity", "interface": "admin", "region": "RegionOne", "region_id": "RegionOne", "id": "4433d85dd7f64f74afdb920bc17d92b6" }, { "url": "http://ops.my.cloud/identity", "interface": "public", "region": "RegionOne", "region_id": "RegionOne", "id": "ad24909f182a46e78238ea2611ece548" } ], "type": "identity", "id": "092f43883e8944c6807ff71d59b52b94", "name": "keystone" }, { "endpoints": [ { "url": "http://ops.my.cloud/compute/v2.1", "interface": "public", "region": "RegionOne", "region_id": "RegionOne", "id": "2df37853876f471e9e65aaced4b5d799" } ], "type": "compute", "id": "0a8db85efccc4c59bb023c78a932b1fb", "name": "nova" }, { "endpoints": [ { "url": "http://ops.my.cloud:9696/", "interface": "public", "region": "RegionOne", "region_id": "RegionOne", "id": "c82f85c8599e4c379829d5d03245c3e1" } ], "type": "network", "id": "473577f6c81d4dccbaad596da46d9d23", "name": "neutron" }, { "endpoints": [ { "url": "http://ops.my.cloud/placement", "interface": "public", "region": "RegionOne", "region_id": "RegionOne", "id": "710613f4370f4985931621f746b2bdac" } ], "type": "placement", "id": "549aa251294d4b0d8e8e875b65fdb038", "name": "placement" }, { "endpoints": [ { "url": "http://ops.my.cloud/compute/v2/661f707340b0486caf878f9cc2bc1fab", "interface": "public", "region": "RegionOne", "region_id": "RegionOne", "id": "ad8fc82f56a543b7a45faa64642218f8" } ], "type": "compute_legacy", "id": "6ee1053e7751458280cd8312548ad788", "name": "nova_legacy" }, { "endpoints": [ { "url": "http://ops.my.cloud/image", "interface": "public", "region": "RegionOne", "region_id": "RegionOne", "id": "b9cb65ade0774693aad0ed409d5956bf" } ], "type": "image", "id": "ea4559f84b804a679aba2a165671bb5e", "name": "glance" } ], "user": { "password_expires_at": null, "domain": { "id": "default", "name": "Default" }, "id": "898384bb1b5e4d5a9ff816f7ea911943", "name": "admin" }, "audit_ids": [ "Ga_Iq4XvQ26BMLL2k8Mk2Q" ], "issued_at": "2018-10-29T09:34:07.000000Z" } } ================================================ FILE: drivers/packet/create.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package packet import ( "bytes" "context" "time" "github.com/drone/autoscaler" "github.com/drone/autoscaler/logger" "github.com/packethost/packngo" ) func (p *provider) Create(ctx context.Context, opts autoscaler.InstanceCreateOpts) (*autoscaler.Instance, error) { p.init.Do(func() { p.setup(ctx) }) buf := new(bytes.Buffer) err := p.userdata.Execute(buf, &opts) if err != nil { return nil, err } logger := logger.FromContext(ctx). WithField("project", p.project). WithField("facility", p.facility). WithField("billing", p.billing). WithField("plan", p.plan). WithField("os", p.os). WithField("hostname", p.hostname) cr := &packngo.DeviceCreateRequest{ HostName: p.hostname, Facility: p.facility, Plan: p.plan, OS: p.os, ProjectID: p.project, BillingCycle: p.billing, UserData: buf.String(), } logger.Debugln("instance create") d, _, err := p.client.Devices.Create(cr) if err != nil { logger.WithError(err). Errorln("cannot create instance") return nil, err } instance := &autoscaler.Instance{ Provider: autoscaler.ProviderPacket, ID: d.ID, Name: opts.Name, Image: d.OS.Slug, Region: d.Facility.Code, Size: d.Plan.Slug, } // poll the packet endpoint for server updates // and exit when a network address is allocated. interval := time.Duration(0) poller: for { select { case <-ctx.Done(): logger.WithField("name", instance.Name). Debugln("cannot ascertain network") return instance, ctx.Err() case <-time.After(interval): interval = time.Minute logger.WithField("name", instance.Name). Debugln("find instance network") d, _, err := p.client.Devices.Get(d.ID) if err != nil { logger.WithError(err). Errorln("cannot find instance") return instance, err } if d.State == "active" { for _, ip := range d.Network { if ip.Public && ip.AddressFamily == 4 { instance.Address = ip.Address } } if instance.Address != "" { break poller } } } } logger. WithField("name", instance.Name). WithField("ip", instance.Address). Debugln("instance network ready") return instance, nil } ================================================ FILE: drivers/packet/create_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package packet import ( "context" "testing" "time" "github.com/drone/autoscaler" "github.com/h2non/gock" "github.com/packethost/packngo" ) func TestCreate(t *testing.T) { defer gock.Off() gock.New(baseURL). MatchHeader("X-Auth-Token", apiKey). Post(createDevice). Reply(200). BodyString(respCreate) gock.New(baseURL). MatchHeader("X-Auth-Token", apiKey). Get(getDevice). MatchParam("include", "facility"). Reply(200). Delay(10 * time.Second). BodyString(respCreate) instance, err := prov.Create(context.TODO(), autoscaler.InstanceCreateOpts{Name: prov.os}) if err != nil { t.Error(err) t.FailNow() } t.Run("Attributes", testInstance(instance)) } func testInstance(instance *autoscaler.Instance) func(t *testing.T) { return func(t *testing.T) { if instance == nil { t.Errorf("Expect non-nil instance even if error") } if got, want := instance.ID, instanceID; got != want { t.Errorf("Want ID %v, got %v", want, got) } if got, want := instance.Image, prov.os; got != want { t.Errorf("Want Image %v, got %v", want, got) } if got, want := instance.Name, prov.os; got != want { t.Errorf("Want Name %v, got %v", want, got) } if got, want := instance.Region, prov.facility; got != want { t.Errorf("Want Region %v, got %v", want, got) } if got, want := instance.Provider, autoscaler.ProviderPacket; got != want { t.Errorf("Want Provider %v, got %v", want, got) } } } func TestCreate_Timeout(t *testing.T) { defer gock.Off() gock.New(baseURL). MatchHeader("X-Auth-Token", apiKey). Post(getDevice). Reply(200). BodyString(respCreateInactive) gock.New(baseURL). MatchHeader("X-Auth-Token", apiKey). Get(getDevice). Reply(200). BodyString(respCreateInactive) ctx, cancel := context.WithCancel(context.Background()) cancel() if _, err := prov.Create(ctx, autoscaler.InstanceCreateOpts{Name: prov.os}); err != context.Canceled { t.Errorf("Expected error creating a device") } } func TestCreate_Erro(t *testing.T) { defer gock.Off() gock.New(baseURL). MatchHeader("X-Auth-Token", apiKey). Post(getDevice). Reply(400) _, err := prov.Create(context.Background(), autoscaler.InstanceCreateOpts{Name: prov.os}) if err == nil { t.Errorf("Expect error returned when creatiung the device") } else if _, ok := err.(*packngo.ErrorResponse); !ok { t.Errorf("Expect error to be of type ErrorResponse") } } func TestCreate_WaitToBecomeActive(t *testing.T) { defer gock.Off() gock.New(baseURL). MatchHeader("X-Auth-Token", apiKey). Post(getDevice). Reply(200). BodyString(respCreateInactive) gock.New(baseURL). MatchHeader("X-Auth-Token", apiKey). Get(getDevice). Reply(200). BodyString(respCreateInactive) wait := make(chan struct{}) go func() { prov.Create(context.Background(), autoscaler.InstanceCreateOpts{Name: prov.os}) close(wait) }() select { case <-wait: t.Errorf("Expected device creation to block when device is not set to active") case <-time.After(50 * time.Millisecond): } } ================================================ FILE: drivers/packet/destroy.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package packet import ( "context" "github.com/drone/autoscaler" ) func (p *provider) Destroy(ctx context.Context, instance *autoscaler.Instance) error { _, err := p.client.Devices.Delete(instance.ID) return err } ================================================ FILE: drivers/packet/destroy_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package packet import ( "context" "reflect" "testing" "github.com/drone/autoscaler" "github.com/h2non/gock" "github.com/packethost/packngo" ) func TestDestroyError(t *testing.T) { defer gock.Off() gock.New(baseURL). Delete(getDevice + "/" + instanceID). Reply(400) err := prov.Destroy(context.Background(), &autoscaler.Instance{ID: instanceID}) if err == nil { t.Errorf("Expect error when deleting a device") } else if _, ok := err.(*packngo.ErrorResponse); !ok { t.Errorf("expected: %s , got: %s ", reflect.TypeOf(&packngo.ErrorResponse{}), reflect.TypeOf(err)) } } ================================================ FILE: drivers/packet/option.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package packet import ( "io/ioutil" "github.com/drone/autoscaler/drivers/internal/userdata" ) // Option configures a Digital Ocean provider option. type Option func(*provider) // WithAPIKey returns an option to set the api key. func WithAPIKey(apikey string) Option { return func(p *provider) { p.apikey = apikey } } // WithFacility returns an option to set the target facility. func WithFacility(facility string) Option { return func(p *provider) { p.facility = facility } } // WithPlan returns an option to set the plan. func WithPlan(plan string) Option { return func(p *provider) { p.plan = plan } } // WithOS returns an option to set the operating system. func WithOS(os string) Option { return func(p *provider) { p.os = os } } // WithProject returns an option to set the project id. func WithProject(project string) Option { return func(p *provider) { p.project = project } } // WithSSHKey returns an option to set the ssh key. func WithSSHKey(sshkey string) Option { return func(p *provider) { p.sshkey = sshkey } } // WithHostname returns an option to set the hostname func WithHostname(hostname string) Option { return func(p *provider) { if hostname != "" { p.hostname = hostname } } } // WithTags returns an option to set the image. func WithTags(tags ...string) Option { return func(p *provider) { p.tags = tags } } // WithUserData returns an option to set the cloud-init // template from text. func WithUserData(text string) Option { return func(p *provider) { if text != "" { p.userdata = userdata.Parse(text) } } } // WithUserDataFile returns an option to set the cloud-init // template from file. func WithUserDataFile(filepath string) Option { return func(p *provider) { if filepath != "" { b, err := ioutil.ReadFile(filepath) if err != nil { panic(err) } p.userdata = userdata.Parse(string(b)) } } } ================================================ FILE: drivers/packet/option_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package packet import "testing" func TestOptions(t *testing.T) { p := New( WithAPIKey("my_authentication_token"), WithFacility("sjc1"), WithOS("ubuntu_16_10"), WithPlan("baremetal_1"), WithProject("my_project"), WithSSHKey("id_rsa"), WithHostname("agent-abcdef"), WithTags("drone", "agent"), ).(*provider) if got, want := p.apikey, "my_authentication_token"; got != want { t.Errorf("Want api key %q, got %q", want, got) } if got, want := p.facility, "sjc1"; got != want { t.Errorf("Want facility %q, got %q", want, got) } if got, want := p.os, "ubuntu_16_10"; got != want { t.Errorf("Want os %q, got %q", want, got) } if got, want := p.plan, "baremetal_1"; got != want { t.Errorf("Want plan %q, got %q", want, got) } if got, want := p.project, "my_project"; got != want { t.Errorf("Want project %q, got %q", want, got) } if got, want := p.sshkey, "id_rsa"; got != want { t.Errorf("Want sshkey %q, got %q", want, got) } if got, want := p.hostname, "agent-abcdef"; got != want { t.Errorf("Want hostname %q, got %q", want, got) } if got, want := len(p.tags), 2; got != want { t.Errorf("Want %d tags, got %d", want, got) } } ================================================ FILE: drivers/packet/provider.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package packet import ( "sync" "text/template" "github.com/drone/autoscaler" "github.com/drone/autoscaler/drivers/internal/userdata" "github.com/packethost/packngo" ) const consumerToken = "24e70949af5ecd17fe8e867b335fc88e7de8bd4ad617c0403d8769a376ddea72" // provider implements a Packet.net provider. type provider struct { init sync.Once apikey string billing string facility string os string plan string project string sshkey string hostname string tags []string userdata *template.Template client *packngo.Client } // New returns a new Packet.net provider. func New(opts ...Option) autoscaler.Provider { p := new(provider) for _, opt := range opts { opt(p) } if p.facility == "" { p.facility = "ewr1" } if p.os == "" { p.os = "ubuntu_18_04" } if p.plan == "" { p.plan = "baremetal_0" } if p.billing == "" { p.billing = "hourly" } if p.userdata == nil { p.userdata = userdata.T } if p.client == nil { p.client = packngo.NewClient( consumerToken, p.apikey, nil) } return p } ================================================ FILE: drivers/packet/provider_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package packet import ( "testing" "github.com/drone/autoscaler/drivers/internal/userdata" ) func TestDefaults(t *testing.T) { p := New().(*provider) if got, want := p.plan, "baremetal_0"; got != want { t.Errorf("Want plan %q, got %q", want, got) } if got, want := p.facility, "ewr1"; got != want { t.Errorf("Want region %q, got %q", want, got) } if got, want := p.billing, "hourly"; got != want { t.Errorf("Want billing %q, got %q", want, got) } if got, want := p.os, "ubuntu_18_04"; got != want { t.Errorf("Want os %q, got %q", want, got) } if p.userdata != userdata.T { t.Errorf("Want default userdata template") } } ================================================ FILE: drivers/packet/setup.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package packet import ( "context" "errors" "github.com/drone/autoscaler/logger" "github.com/packethost/packngo" "golang.org/x/sync/errgroup" ) func (p *provider) setup(ctx context.Context) error { var g errgroup.Group if p.sshkey == "" { g.Go(func() error { return p.setupKeypair(ctx) }) } return g.Wait() } // helper funciton to ascertain the ID of an existing SSH // key to use when provisioning instances. This is only // necessary when the user has not provided the ID. func (p *provider) setupKeypair(ctx context.Context) error { logger := logger.FromContext(ctx) logger.Debugln("finding default ssh key") keys, _, err := p.client.SSHKeys.List() if err != nil { return err } index := map[string]packngo.SSHKey{} for _, key := range keys { index[key.Label] = key } // if the account has multiple keys configured we will // attempt to use an existing key based on naming convention. for _, name := range []string{"drone", "id_rsa_drone"} { key, ok := index[name] if !ok { continue } p.sshkey = key.Key logger. WithField("id", key.ID). WithField("label", key.Key). WithField("fingerprint", key.FingerPrint). Debugln("using default ssh key") return nil } // if there were no matches but the account has at least // one keypair already created we will select the first // in the list. if len(keys) > 0 { key := keys[0] p.sshkey = key.ID logger. WithField("id", key.ID). WithField("label", key.Label). WithField("fingerprint", key.FingerPrint). Debugln("using default ssh key") return nil } return errors.New("No matching keys") } ================================================ FILE: drivers/packet/setup_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package packet import ( "context" "os" "testing" "github.com/h2non/gock" ) const ( baseURL = "https://api.packet.net/" getDevice = "/devices" getSSH = "/ssh-keys" projectID = "x" createDevice = "/projects/" + projectID + getDevice instanceID = "92b0facf-189e-4bbf-81a8-bc56c0c4dc88" apiKey = "apiKey" sshKey = "sshKey" hostname = "hostname" tag = "tag" ) var ( prov *provider respCreate string respCreateInactive string respSSHKeys string ) func TestMain(m *testing.M) { prov = New( WithProject(projectID), WithTags(tag), WithHostname(hostname), WithAPIKey(apiKey), ).(*provider) respCreate = ` { "id": "` + instanceID + `", "state": "active", "tags": ["` + tag + `"], "hostname": "` + hostname + `", "operating_system": { "slug": "` + prov.os + `" }, "facility": { "code": "ewr1" }, "ip_addresses": [ { "address_family": 4, "public": true, "address": "147.75.77.155" } ], "plan": { "slug": "baremetal_0" } } ` respCreateInactive = ` { "id": "` + instanceID + `", "state": "inactive", "tags": ["` + tag + `"], "hostname": "` + hostname + `", "operating_system": { "slug": "` + prov.os + `" }, "facility": { "code": "ewr1" }, "ip_addresses": [ { "address_family": 4, "public": true, "address": "147.75.77.155" } ], "plan": { "slug": "baremetal_0" } } ` respSSHKeys = ` { "ssh_keys": [ { "id": "` + sshKey + `", "label": "label", "key": "key", "fingerprint": "fingerprint" } ] } ` os.Exit(m.Run()) } func TestSetup_Keypair(t *testing.T) { defer gock.Off() gock.New(baseURL). MatchHeader("X-Auth-Token", apiKey). Get(getSSH). Reply(200). BodyString(respSSHKeys) if err := prov.setupKeypair(context.Background()); err != nil { t.Error(err) t.FailNow() } if prov.sshkey != sshKey { t.Errorf("expected: %s, got: %s", sshKey, prov.sshkey) } } ================================================ FILE: drivers/scaleway/create.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package scaleway import ( "bytes" "context" "errors" "time" "github.com/drone/autoscaler" "github.com/drone/autoscaler/logger" "github.com/scaleway/scaleway-sdk-go/api/instance/v1" "github.com/scaleway/scaleway-sdk-go/scw" ) func (p *provider) Create(ctx context.Context, opts autoscaler.InstanceCreateOpts) (*autoscaler.Instance, error) { p.init.Do(func() { p.setup(ctx) }) api := instance.NewAPI(p.client) req := &instance.CreateServerRequest{ Name: opts.Name, DynamicIPRequired: scw.BoolPtr(p.dynamicIP), CommercialType: p.size, Image: p.image, Tags: p.tags, SecurityGroup: p.securityGroup, } logger := logger.FromContext(ctx). WithField("datacenter", string(p.zone)). WithField("image", req.Image). WithField("size", req.CommercialType). WithField("name", req.Name) logger.Infoln("instance create") resp, err := api.CreateServer(req) if err != nil { logger.WithError(err). Errorln("cannot create instance") return nil, err } buf := new(bytes.Buffer) err = p.userdata.Execute(buf, &opts) if err != nil { return nil, err } err = api.SetServerUserData(&instance.SetServerUserDataRequest{ Zone: req.Zone, ServerID: resp.Server.ID, Key: "cloud-init", Content: buf, }) if err != nil { return nil, err } logger.WithField("name", req.Name). Debugln("powering instance on") server, err := serverPowerAction(api, ctx, instance.ServerActionPoweron, resp.Server.ID) if err != nil { logger.WithError(err). Errorln("cannot power on instance") return nil, err } if server.State != instance.ServerStateRunning { return nil, errors.New("instance in invalid state: " + string(server.State)) } logger.WithField("name", req.Name). Infoln("instance created") ip := server.PublicIP if ip == nil { return nil, errors.New("server not assigned ip") } return &autoscaler.Instance{ Provider: autoscaler.ProviderScaleway, ID: server.ID, Name: server.Name, Address: ip.Address.String(), Region: string(req.Zone), Image: req.Image, Size: req.CommercialType, }, nil } func serverPowerAction(api *instance.API, ctx context.Context, action instance.ServerAction, serverID string) (*instance.Server, error) { saReq := &instance.ServerActionRequest{ ServerID: serverID, Action: action, } gsReq := &instance.GetServerRequest{ ServerID: serverID, } terminal := map[instance.ServerState]struct{}{ instance.ServerStateStopped: {}, instance.ServerStateStoppedInPlace: {}, instance.ServerStateLocked: {}, instance.ServerStateRunning: {}, } // Call to power the server on _, err := api.ServerAction(saReq, scw.WithContext(ctx)) if err != nil { return nil, err } var complete bool var server *instance.Server // Wait for context end, or poll every 3 seconds for // server status, until it is powered on for !complete { select { case <-ctx.Done(): return nil, ctx.Err() case <-time.After(3 * time.Second): gsResp, err := api.GetServer(gsReq, scw.WithContext(ctx)) if err != nil { return server, err } if _, complete = terminal[gsResp.Server.State]; complete { server = gsResp.Server break } } } if server == nil { return nil, errors.New("server is nil") } return server, nil } ================================================ FILE: drivers/scaleway/create_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package scaleway ================================================ FILE: drivers/scaleway/destroy.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package scaleway import ( "context" "github.com/drone/autoscaler" "github.com/drone/autoscaler/logger" "github.com/scaleway/scaleway-sdk-go/api/instance/v1" "github.com/scaleway/scaleway-sdk-go/scw" ) func (p *provider) Destroy(ctx context.Context, inst *autoscaler.Instance) error { p.init.Do(func() { p.setup(ctx) }) logger := logger.FromContext(ctx). WithField("datacenter", inst.Region). WithField("image", inst.Image). WithField("size", inst.Size). WithField("name", inst.Name) api := instance.NewAPI(p.client) srvReq := &instance.GetServerRequest{ ServerID: inst.ID, } _, err := api.GetServer(srvReq, scw.WithContext(ctx)) if err != nil { scwErr, ok := err.(*scw.ResponseError) if ok && scwErr.StatusCode == 404 { return autoscaler.ErrInstanceNotFound } else { logger.WithError(err). Errorln("cannot get server") return err } } // Issue "terminate" action, instead of DeleteServer, as terminate // cleans up volumes and IP addresses attached, too req := &instance.ServerActionRequest{ ServerID: inst.ID, Action: instance.ServerActionTerminate, } logger.Debugln("terminating server") _, err = api.ServerAction(req, scw.WithContext(ctx)) if err != nil { logger.WithError(err). Errorln("terminating server failed") return err } logger.Infoln("server terminated") return err } ================================================ FILE: drivers/scaleway/destroy_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package scaleway ================================================ FILE: drivers/scaleway/option.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package scaleway import ( "io/ioutil" "github.com/drone/autoscaler/drivers/internal/userdata" "github.com/scaleway/scaleway-sdk-go/scw" ) // Option configures a Scaleway provider option. type Option func(*provider) error // WithAccessKey returns an option to set the user access key func WithAccessKey(accessKey string) Option { return func(p *provider) error { p.accessKey = accessKey return nil } } // WithSecretKey returns an option to set the user secret key func WithSecretKey(secretKey string) Option { return func(p *provider) error { p.secretKey = secretKey return nil } } // WithOrganisationID returns an option to set the user organisation id func WithOrganisationID(orgId string) Option { return func(p *provider) error { p.orgID = orgId return nil } } // WithImage returns an option to set the image. func WithImage(image string) Option { return func(p *provider) error { p.image = image return nil } } // WithDynamicIP returns an option to enable a dynamic IP. func WithDynamicIP(dynamicIP bool) Option { return func(p *provider) error { p.dynamicIP = dynamicIP return nil } } // WithTags returns an option to set the server tags. func WithTags(tags ...string) Option { return func(p *provider) error { p.tags = tags return nil } } // WithZone returns an option to set the target zone. func WithZone(name string) Option { return func(p *provider) error { if name == "" { return nil } zone, err := scw.ParseZone(name) if err != nil { return err } p.zone = zone return nil } } // WithSize returns an option to set the instance size. func WithSize(size string) Option { return func(p *provider) error { p.size = size return nil } } // WithUserData returns an option to set the cloud-init // template from text. func WithUserData(text string) Option { return func(p *provider) error { if text != "" { p.userdata = userdata.Parse(text) } return nil } } // WithUserDataFile returns an option to set the cloud-init // template from file. func WithUserDataFile(filepath string) Option { return func(p *provider) error { if filepath != "" { b, err := ioutil.ReadFile(filepath) if err != nil { return err } p.userdata = userdata.Parse(string(b)) } return nil } } ================================================ FILE: drivers/scaleway/option_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package scaleway ================================================ FILE: drivers/scaleway/provider.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package scaleway import ( "sync" "text/template" "github.com/drone/autoscaler/drivers/internal/userdata" "github.com/scaleway/scaleway-sdk-go/scw" "github.com/drone/autoscaler" ) // provider implements a Scaleway provider. type provider struct { init sync.Once accessKey string secretKey string orgID string securityGroup string dynamicIP bool zone scw.Zone // fr-par-1 or nl-ams-1 size string image string tags []string userdata *template.Template client *scw.Client } // New returns a new Scaleway provider. func New(opts ...Option) (autoscaler.Provider, error) { p := new(provider) for _, opt := range opts { err := opt(p) if err != nil { return nil, err } } if p.zone == "" { p.zone = scw.ZoneFrPar1 } if p.size == "" { p.size = "dev1-l" } if p.image == "" { p.image = "ubuntu-bionic" } if p.userdata == nil { p.userdata = userdata.T } return p, nil } ================================================ FILE: drivers/scaleway/provider_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package scaleway ================================================ FILE: drivers/scaleway/setup.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package scaleway import ( "context" "github.com/scaleway/scaleway-sdk-go/scw" "golang.org/x/sync/errgroup" ) func (p *provider) setup(ctx context.Context) error { var g errgroup.Group if p.client == nil { g.Go(func() error { return p.newClient(ctx) }) } return g.Wait() } func (p *provider) newClient(ctx context.Context) error { client, err := scw.NewClient( scw.WithDefaultOrganizationID(p.orgID), scw.WithAuth(p.accessKey, p.secretKey), scw.WithDefaultZone(p.zone), ) if err != nil { return err } p.client = client return nil } ================================================ FILE: engine/alloc.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package engine import ( "context" "sync" "time" "github.com/drone/autoscaler" "github.com/drone/autoscaler/engine/certs" "github.com/drone/autoscaler/logger" "github.com/drone/autoscaler/metrics" ) type allocator struct { wg sync.WaitGroup servers autoscaler.ServerStore provider autoscaler.Provider metrics metrics.Collector } func (a *allocator) Allocate(ctx context.Context) error { logger := logger.FromContext(ctx) servers, err := a.servers.ListState(ctx, autoscaler.StatePending) if err != nil { return err } for _, server := range servers { server.State = autoscaler.StateCreating err = a.servers.Update(ctx, server) if err != nil { logger.WithError(err). WithField("server", server.Name). WithField("state", "creating"). Errorln("failed to update server state") return err } a.wg.Add(1) go func(server *autoscaler.Server) { a.allocate(ctx, server) a.wg.Done() }(server) } return nil } func (a *allocator) allocate(ctx context.Context, server *autoscaler.Server) error { logger := logger.FromContext(ctx) defer func() { if err := recover(); err != nil { logger.WithError(err.(error)). WithField("server", server.Name). Errorln("unexpected panic") } }() ca, err := certs.GenerateCA() if err != nil { return err } cert, err := certs.GenerateCert(server.Name, ca) if err != nil { return err } ctx, cancel := context.WithTimeout(ctx, time.Hour) defer cancel() opts := autoscaler.InstanceCreateOpts{ Name: server.Name, CAKey: ca.Key, CACert: ca.Cert, TLSKey: cert.Key, TLSCert: cert.Cert, } start := time.Now() instance, err := a.provider.Create(ctx, opts) if err != nil { a.metrics.IncrServerCreateError() logger.WithError(err). WithField("server", server.Name). Errorln("failed to provision server") server.Error = err.Error() server.State = autoscaler.StateError } else { a.metrics.TrackServerCreateTime(start) logger.WithField("server", server.Name). Debugln("provisioned server") server.State = autoscaler.StateCreated } if instance != nil { server.ID = instance.ID server.Address = instance.Address server.Image = instance.Image server.Provider = instance.Provider server.Region = instance.Region server.Size = instance.Size server.CACert = opts.CACert server.CAKey = opts.CAKey server.TLSCert = opts.TLSCert server.TLSKey = opts.TLSKey server.Started = time.Now().Unix() } err = a.servers.Update(ctx, server) if err != nil { a.metrics.IncrServerCreateError() logger.WithError(err). WithField("server", server.Name). Errorln("failed to update server state") return err } return nil } ================================================ FILE: engine/alloc_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package engine import ( "context" "errors" "testing" "github.com/drone/autoscaler" "github.com/drone/autoscaler/metrics" "github.com/drone/autoscaler/mocks" "github.com/golang/mock/gomock" ) func TestAllocate(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() mockctx := context.Background() mockInstance := &autoscaler.Instance{} mockServers := []*autoscaler.Server{ {State: autoscaler.StatePending}, } store := mocks.NewMockServerStore(controller) store.EXPECT().ListState(mockctx, autoscaler.StatePending).Return(mockServers, nil) store.EXPECT().Update(mockctx, mockServers[0]).Return(nil) store.EXPECT().Update(gomock.Any(), mockServers[0]).Return(nil) provider := mocks.NewMockProvider(controller) provider.EXPECT().Create(gomock.Any(), gomock.Any()).Return(mockInstance, nil) a := allocator{servers: store, provider: provider, metrics: &metrics.NopCollector{}} err := a.Allocate(mockctx) a.wg.Wait() if err != nil { t.Error(err) } if got, want := mockServers[0].State, autoscaler.StateCreated; got != want { t.Errorf("Want server state Created, got %v", got) } } func TestAllocate_ServerCreateError(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() mockctx := context.Background() mockerr := errors.New("mock error") mockServers := []*autoscaler.Server{ {State: autoscaler.StatePending}, } store := mocks.NewMockServerStore(controller) store.EXPECT().ListState(mockctx, autoscaler.StatePending).Return(mockServers, nil) store.EXPECT().Update(mockctx, mockServers[0]).Return(nil) store.EXPECT().Update(gomock.Any(), mockServers[0]).Return(nil) provider := mocks.NewMockProvider(controller) provider.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil, mockerr) a := allocator{servers: store, provider: provider, metrics: &metrics.NopCollector{}} a.Allocate(mockctx) a.wg.Wait() if got, want := mockServers[0].State, autoscaler.StateError; got != want { t.Errorf("Want server state Error, got %v", got) } } func TestAllocate_ServerListError(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() mockctx := context.Background() mockerr := errors.New("mock error") store := mocks.NewMockServerStore(controller) store.EXPECT().ListState(mockctx, autoscaler.StatePending).Return(nil, mockerr) a := allocator{servers: store} if got, want := a.Allocate(mockctx), mockerr; got != want { t.Errorf("Want error getting server list") } } func TestAllocate_ServerUpdateError(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() mockctx := context.Background() mockerr := errors.New("mock error") mockServers := []*autoscaler.Server{ {State: autoscaler.StatePending}, } store := mocks.NewMockServerStore(controller) store.EXPECT().ListState(mockctx, autoscaler.StatePending).Return(mockServers, nil) store.EXPECT().Update(mockctx, mockServers[0]).Return(mockerr) a := allocator{servers: store, metrics: &metrics.NopCollector{}} if got, want := a.Allocate(mockctx), mockerr; got != want { t.Errorf("Want error updating server") } if got, want := mockServers[0].State, autoscaler.StateCreating; got != want { t.Errorf("Want server state Staging, got %v", got) } } ================================================ FILE: engine/calc.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package engine import "math" // helper function returns the absolute value of x. func abs(x int) int { if x < 0 { x = x * -1 } return x } // helper function returns the larger of x or y. func max(x, y int) int { if x > y { return x } return y } // helper function calculates the different between the existing // server count and required server count to handle queue volume. func serverDiff(pending, available, concurrency int) int { return int( math.Ceil( float64(pending-available) / float64(concurrency), ), ) } // helper function adjusts the number of servers to provision // to ensure it does not exceed the max server count. func serverCeil(count, additions, ceiling int) int { if count+additions >= ceiling { additions = ceiling - count } return additions } // helper function adjusts the number of servers to provision // to ensure the minimum server count is maintained. func serverFloor(count, deletions, floor int) int { if deletions == 0 { return 0 } if floor > count-deletions { deletions = count - floor } return max(deletions, 0) } ================================================ FILE: engine/calc_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package engine import "testing" func TestAbs(t *testing.T) { tests := []struct { x, want int }{ {0, 0}, {1, 1}, {-1, 1}, } for _, test := range tests { if got, want := abs(test.x), test.want; got != want { t.Errorf("Want abs value %d, got %d", want, got) } } } func TestMax(t *testing.T) { tests := []struct { x, y, want int }{ {0, 1, 1}, {0, 0, 0}, {1, 1, 1}, {-1, 0, 0}, {-1, 1, 1}, } for _, test := range tests { if got, want := max(test.x, test.y), test.want; got != want { t.Errorf("Want max value %d, got %d", want, got) } } } func TestServerDiff(t *testing.T) { tests := []struct { pending, // count pending builds available, // count available capacity concurrency, // per-server concurrency want int }{ // use 2 of 2 existing { pending: 2, available: 2, concurrency: 2, want: 0, }, // use 1 of 2 existing { pending: 1, available: 2, concurrency: 2, want: 0, }, // want 1 server { pending: 4, available: 2, concurrency: 2, want: 1, }, // want 2 servers { pending: 4, available: 2, concurrency: 1, want: 2, }, // want 2 servers (round-up) { pending: 5, available: 2, concurrency: 2, want: 2, }, // // the following test cases check for instances when // we have exceess server capacity and want to remove // server instances. // // want 0 servers removed, at capacity { pending: 1, available: 1, concurrency: 2, want: 0, }, // want 0 servers removed, at capacity (server partially used) { pending: 1, available: 2, concurrency: 2, want: 0, }, // want 1 server removed, pending builds, but excess capacity { pending: 2, available: 4, concurrency: 2, want: -1, }, // want 2 servers removed (round down) { pending: 0, available: 5, concurrency: 2, want: -2, }, // want 10 servers removed { pending: 4, available: 24, concurrency: 2, want: -10, }, } for _, test := range tests { diff := serverDiff( test.pending, test.available, test.concurrency, ) if got, want := diff, test.want; got != want { t.Errorf("Got server diff %d, want %d", got, want) } } } func TestSeverCeil(t *testing.T) { tests := []struct { curr, // count of servers running diff, // count of servers to add ceil, // max number of servers want int }{ // add 0 servers { curr: 2, diff: 0, ceil: 2, want: 0, }, // add 0 servers, handle 0 current count { curr: 0, diff: 0, ceil: 1, want: 0, }, // add 1 server { curr: 2, diff: 1, ceil: 4, want: 1, }, // add 1 server, handle 0 current count { curr: 0, diff: 2, ceil: 1, want: 1, }, // add 2 servers { curr: 2, diff: 2, ceil: 4, want: 2, }, // add 2 servers, adjust to ceil { curr: 2, diff: 4, ceil: 4, want: 2, }, // add 4 servers, adjust to ceil { curr: 0, diff: 10, ceil: 4, want: 4, }, } for _, test := range tests { diff := serverCeil( test.curr, test.diff, test.ceil, ) if got, want := diff, test.want; got != want { t.Errorf("Got server diff %d, want %d", got, want) } } } func TestSeverFloor(t *testing.T) { tests := []struct { curr, // count of servers running diff, // count of servers to remove floor, // min number of servers want int }{ // remove 0 servers { curr: 2, diff: 0, floor: 2, want: 0, }, // remove 1 server { curr: 4, diff: 1, floor: 2, want: 1, }, // remove 2 servers { curr: 4, diff: 2, floor: 2, want: 2, }, // remove 2 servers, adjust to floor { curr: 4, diff: 3, floor: 2, want: 2, }, // remove 0 servers, adjust to floor { curr: 2, diff: 1, floor: 2, want: 0, }, // should not remove non-existent servers { curr: 0, diff: 4, floor: 2, want: 0, }, { curr: 1, diff: 4, floor: 2, want: 0, }, } for _, test := range tests { diff := serverFloor( test.curr, test.diff, test.floor, ) if got, want := diff, test.want; got != want { t.Errorf("Got server diff %d, want %d", got, want) } } } ================================================ FILE: engine/certs/cert.go ================================================ // Copyright Docker.IO, Inc. All rights reserved. // https://github.com/docker/machine package certs import ( "bytes" "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "math/big" "time" ) const ( // default key size. size = 2048 // default organization name for certificates. organization = "drone.autoscaler.generated" ) // Certificate stores a certificate and private key. type Certificate struct { Cert []byte Key []byte } // GenerateCert generates a certificate for the host address. func GenerateCert(host string, ca *Certificate) (*Certificate, error) { template, err := newCertificate(organization) if err != nil { return nil, err } template.DNSNames = append(template.DNSNames, host) tlsCert, err := tls.X509KeyPair(ca.Cert, ca.Key) if err != nil { return nil, err } priv, err := rsa.GenerateKey(rand.Reader, size) if err != nil { return nil, err } x509Cert, err := x509.ParseCertificate(tlsCert.Certificate[0]) if err != nil { return nil, err } derBytes, err := x509.CreateCertificate( rand.Reader, template, x509Cert, &priv.PublicKey, tlsCert.PrivateKey) if err != nil { return nil, err } certOut := new(bytes.Buffer) pem.Encode(certOut, &pem.Block{ Type: "CERTIFICATE", Bytes: derBytes, }) keyOut := new(bytes.Buffer) pem.Encode(keyOut, &pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv), }) return &Certificate{ Cert: certOut.Bytes(), Key: keyOut.Bytes(), }, nil } // GenerateCA generates a CA certificate. func GenerateCA() (*Certificate, error) { template, err := newCertificate(organization) if err != nil { return nil, err } template.IsCA = true template.KeyUsage |= x509.KeyUsageCertSign template.KeyUsage |= x509.KeyUsageKeyEncipherment template.KeyUsage |= x509.KeyUsageKeyAgreement priv, err := rsa.GenerateKey(rand.Reader, size) if err != nil { return nil, err } derBytes, err := x509.CreateCertificate( rand.Reader, template, template, &priv.PublicKey, priv) if err != nil { return nil, err } certOut := new(bytes.Buffer) pem.Encode(certOut, &pem.Block{ Type: "CERTIFICATE", Bytes: derBytes, }) keyOut := new(bytes.Buffer) pem.Encode(keyOut, &pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv), }) return &Certificate{ Cert: certOut.Bytes(), Key: keyOut.Bytes(), }, nil } func newCertificate(org string) (*x509.Certificate, error) { now := time.Now() // need to set notBefore slightly in the past to account for time // skew in the VMs otherwise the certs sometimes are not yet valid notBefore := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute()-5, 0, 0, time.Local) notAfter := notBefore.Add(time.Hour * 24 * 1080) serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { return nil, err } return &x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ Organization: []string{org}, }, NotBefore: notBefore, NotAfter: notAfter, KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageKeyAgreement, BasicConstraintsValid: true, }, nil } ================================================ FILE: engine/certs/cert_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package certs import ( "testing" ) func TestGenerate(t *testing.T) { ca, err := GenerateCA() if err != nil { t.Error(err) } _, err = GenerateCert("company.com", ca) if err != nil { t.Error(err) } } ================================================ FILE: engine/collect.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package engine import ( "context" "sync" "time" "github.com/docker/docker/api/types/container" "github.com/drone/autoscaler" "github.com/drone/autoscaler/logger" ) type collector struct { wg sync.WaitGroup timeout time.Duration servers autoscaler.ServerStore provider autoscaler.Provider client clientFunc } func (c *collector) Collect(ctx context.Context) error { logger := logger.FromContext(ctx) servers, err := c.servers.ListState(ctx, autoscaler.StateShutdown) if err != nil { return err } for _, server := range servers { server.State = autoscaler.StateStopping err = c.servers.Update(ctx, server) if err != nil { logger.WithError(err). WithField("server", server.Name). WithField("state", "stopping"). Errorln("failed to update server state") return err } c.wg.Add(1) go func(server *autoscaler.Server) { c.collect(ctx, server) c.wg.Done() }(server) } return nil } func (c *collector) collect(ctx context.Context, server *autoscaler.Server) error { logger := logger.FromContext(ctx) logger.WithField("server", server.Name). Debugln("destroying server") defer func() { if err := recover(); err != nil { logger.WithField("error", err). WithField("server", server.Name). Errorln("unexpected panic") } }() // if the server was never created there is nothing // to terminate, so we can just set the agent state // to term if server.ID == "" { logger.WithField("server", server.Name). Debugln("server never provisioned. nothing to stop") server.Stopped = time.Now().Unix() server.State = autoscaler.StateStopped err := c.servers.Update(ctx, server) if err != nil { logger.WithError(err). WithField("server", server.Name). Errorln("cannot update server state") } else { logger.WithField("server", server.Name). Debugln("updated server state to stopped") } return err } // first we need to gracefully shutdown the runner so // that in-progress pipelines can complete. They will // have up to 60 minutes to complete before being // force-killed. if server.Address != "" { client, closer, err := c.client(server) if closer != nil { defer closer.Close() } if err != nil { return err } logger.WithField("server", server.Name). Debugln("stopping the agent") ctxStop, cancel := context.WithTimeout(ctx, c.timeout) defer cancel() // 1 minute offset between docker stop timeout and // the context timeout. timeout := c.timeout - time.Minute timeoutSeconds := int(timeout.Seconds()) err = client.ContainerStop(ctxStop, "agent", container.StopOptions{Timeout: &timeoutSeconds}) if err != nil { logger.WithError(err). WithField("server", server.Name). Errorln("cannot stop the agent") } else { logger.WithField("server", server.Name). Debugln("stopped the agent") } } // next we need to terminate the remote instance (e.g. in aws). // It is possible the server was terminated out-of-band in which // case there is nothing to terminate. in := &autoscaler.Instance{ ID: server.ID, Provider: server.Provider, Name: server.Name, Address: server.Address, Region: server.Region, Image: server.Image, Size: server.Size, } ctx, cancel := context.WithTimeout(ctx, time.Hour) defer cancel() err := c.provider.Destroy(ctx, in) if err == autoscaler.ErrInstanceNotFound { logger. WithField("state", "error"). WithField("server", server.Name). Infoln("server no longer exists. nothing to destroy") server.Stopped = time.Now().Unix() server.State = autoscaler.StateStopped } else if err != nil { logger.WithField("server", server.Name). Errorln("failed to destroy server") server.Error = err.Error() server.State = autoscaler.StateError } else { logger.WithField("server", server.Name). Debugln("destroyed server") server.Stopped = time.Now().Unix() server.State = autoscaler.StateStopped } err = c.servers.Update(ctx, server) if err != nil { logger.WithError(err). WithField("server", server.Name). Errorln("failed to update server state") return err } return nil } ================================================ FILE: engine/collect_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package engine import ( "context" "errors" "io" "testing" "github.com/drone/autoscaler" "github.com/drone/autoscaler/mocks" docker "github.com/docker/docker/client" "github.com/golang/mock/gomock" ) func TestCollect(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() mockctx := context.Background() mockServers := []*autoscaler.Server{ { ID: "i-1234567890abcdef0", Address: "1.2.3.4", State: autoscaler.StateShutdown, }, } client := mocks.NewMockAPIClient(controller) client.EXPECT().ContainerStop(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) store := mocks.NewMockServerStore(controller) store.EXPECT().ListState(mockctx, autoscaler.StateShutdown).Return(mockServers, nil) store.EXPECT().Update(mockctx, mockServers[0]).Return(nil) store.EXPECT().Update(gomock.Any(), mockServers[0]).Return(nil) provider := mocks.NewMockProvider(controller) provider.EXPECT().Destroy(gomock.Any(), gomock.Any()).Return(nil) c := collector{ servers: store, provider: provider, client: func(*autoscaler.Server) (docker.APIClient, io.Closer, error) { return client, nil, nil }, } err := c.Collect(mockctx) c.wg.Wait() if err != nil { t.Error(err) } if got, want := mockServers[0].State, autoscaler.StateStopped; got != want { t.Errorf("Want server state Stopped, got %v", got) } } func TestCollect_DockerStopError(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() mockerr := errors.New("oh no") mockctx := context.Background() mockServers := []*autoscaler.Server{ { ID: "i-1234567890abcdef0", Address: "1.2.3.4", State: autoscaler.StateShutdown, }, } client := mocks.NewMockAPIClient(controller) client.EXPECT().ContainerStop(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockerr) store := mocks.NewMockServerStore(controller) store.EXPECT().ListState(mockctx, autoscaler.StateShutdown).Return(mockServers, nil) store.EXPECT().Update(mockctx, mockServers[0]).Return(nil) store.EXPECT().Update(gomock.Any(), mockServers[0]).Return(nil) provider := mocks.NewMockProvider(controller) provider.EXPECT().Destroy(gomock.Any(), gomock.Any()).Return(nil) c := collector{ servers: store, provider: provider, client: func(*autoscaler.Server) (docker.APIClient, io.Closer, error) { return client, nil, nil }, } err := c.Collect(mockctx) c.wg.Wait() if err != nil { t.Error(err) } if got, want := mockServers[0].State, autoscaler.StateStopped; got != want { t.Errorf("Want server state Stopped, got %v", got) } } func TestCollect_ServerDestroyError(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() mockctx := context.Background() mockerr := errors.New("mock error") mockServers := []*autoscaler.Server{ { ID: "i-1234567890abcdef0", Name: "agent-807jVFwj", Address: "1.2.3.4", State: autoscaler.StateShutdown, }, } client := mocks.NewMockAPIClient(controller) client.EXPECT().ContainerStop(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) store := mocks.NewMockServerStore(controller) store.EXPECT().ListState(mockctx, autoscaler.StateShutdown).Return(mockServers, nil) store.EXPECT().Update(mockctx, mockServers[0]).Return(nil) store.EXPECT().Update(gomock.Any(), mockServers[0]).Return(nil) provider := mocks.NewMockProvider(controller) provider.EXPECT().Destroy(gomock.Any(), gomock.Any()).Return(mockerr) c := collector{ servers: store, provider: provider, client: func(*autoscaler.Server) (docker.APIClient, io.Closer, error) { return client, nil, nil }, } c.Collect(mockctx) c.wg.Wait() if got, want := mockServers[0].State, autoscaler.StateError; got != want { t.Errorf("Want server state Error, got %v", got) } } func TestCollect_ServerListError(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() mockctx := context.Background() mockerr := errors.New("mock error") store := mocks.NewMockServerStore(controller) store.EXPECT().ListState(mockctx, autoscaler.StateShutdown).Return(nil, mockerr) c := collector{servers: store} if got, want := c.Collect(mockctx), mockerr; got != want { t.Errorf("Want error getting server list") } } func TestCollect_ServerUpdateError(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() mockctx := context.Background() mockerr := errors.New("mock error") mockServers := []*autoscaler.Server{ { ID: "i-1234567890abcdef0", Address: "1.2.3.4", State: autoscaler.StateShutdown, }, } store := mocks.NewMockServerStore(controller) store.EXPECT().ListState(mockctx, autoscaler.StateShutdown).Return(mockServers, nil) store.EXPECT().Update(mockctx, mockServers[0]).Return(mockerr) c := collector{servers: store} if got, want := c.Collect(mockctx), mockerr; got != want { t.Errorf("Want error updating server") } if got, want := mockServers[0].State, autoscaler.StateStopping; got != want { t.Errorf("Want server state Stopping, got %v", got) } } func TestCollect_ServerNeverProvisioned(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() mockctx := context.Background() mockServer := &autoscaler.Server{ ID: "", State: autoscaler.StateShutdown, } store := mocks.NewMockServerStore(controller) store.EXPECT().Update(gomock.Any(), mockServer).Return(nil).Times(1) c := collector{servers: store} if err := c.collect(mockctx, mockServer); err != nil { t.Error(err) } if got, want := mockServer.State, autoscaler.StateStopped; got != want { t.Errorf("Want server state Stopping, got %v", got) } } ================================================ FILE: engine/docker.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package engine import ( "crypto/tls" "crypto/x509" "fmt" "io" "net/http" docker "github.com/docker/docker/client" "github.com/drone/autoscaler" ) // clientFunc defines a builder funciton used to build and return // the docker client from a Server. This is primarily used for // mock unit testing. type clientFunc func(*autoscaler.Server) (docker.APIClient, io.Closer, error) // newDockerClient returns a new Docker client configured for the // Server host and certificate chain. func newDockerClient(server *autoscaler.Server) (docker.APIClient, io.Closer, error) { tlsCert, err := tls.X509KeyPair(server.TLSCert, server.TLSKey) if err != nil { return nil, nil, err } tlsConfig := &tls.Config{ ServerName: server.Name, Certificates: []tls.Certificate{tlsCert}, } tlsConfig.RootCAs = x509.NewCertPool() tlsConfig.RootCAs.AppendCertsFromPEM(server.CACert) client := &http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsConfig, }, } dockerClient, err := docker.NewClientWithOpts( docker.WithAPIVersionNegotiation(), docker.WithHTTPClient(client), docker.WithHost(fmt.Sprintf("https://%s:2376", server.Address)), ) return dockerClient, dockerClient, err } ================================================ FILE: engine/engine.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package engine import ( "context" "sync" "time" "github.com/drone/autoscaler" "github.com/drone/autoscaler/config" "github.com/drone/autoscaler/logger" "github.com/drone/autoscaler/metrics" "github.com/drone/drone-go/drone" ) // defines the interval at which terminated instances are // purged from the database. const purge = time.Hour * 24 type engine struct { mu sync.Mutex allocator *allocator collector *collector installer *installer pinger *pinger planner *planner reaper *reaper metrics metrics.Collector interval time.Duration paused bool } // New returns a new autoscale Engine. func New( client drone.Client, config config.Config, servers autoscaler.ServerStore, provider autoscaler.Provider, metrics metrics.Collector, ) autoscaler.Engine { return &engine{ paused: false, interval: config.Interval, allocator: &allocator{ servers: servers, provider: provider, metrics: metrics, }, collector: &collector{ timeout: config.Timeout.Stop, servers: servers, provider: provider, client: newDockerClient, }, installer: &installer{ metrics: metrics, servers: servers, os: config.Agent.OS, arch: config.Agent.Arch, image: config.Agent.Image, secret: config.Agent.Token, envs: config.Agent.Environ, volumes: config.Agent.Volumes, ports: config.Agent.Ports, labels: config.Agent.Labels, proto: config.Server.Proto, host: config.Server.Host, client: newDockerClient, runner: config.Runner, checkInterval: config.Check.Interval, checkDeadline: config.Check.Deadline, gcEnabled: config.GC.Enabled, gcDebug: config.GC.Debug, gcImage: config.GC.Image, gcIgnore: config.GC.Images, gcInterval: config.GC.Interval, gcCache: config.GC.Cache, watchtowerEnabled: config.Watchtower.Enabled, watchtowerImage: config.Watchtower.Image, watchtowerStopSignal: config.Watchtower.Signal, watchtowerSignalEnabled: config.Watchtower.SignalEnabled, watchtowerTimeout: config.Watchtower.Timeout, watchtowerInterval: config.Watchtower.Interval, }, pinger: &pinger{ servers: servers, client: newDockerClient, enabled: config.Pinger.Enabled, interval: config.Pinger.Interval, }, planner: &planner{ client: client, servers: servers, os: config.Agent.OS, arch: config.Agent.Arch, version: config.Agent.Version, kernel: config.Agent.Kernel, buffer: config.CapacityBuffer, ttu: config.Pool.MinAge, min: config.Pool.Min, max: config.Pool.Max, cap: config.Agent.Concurrency, labels: config.Agent.Labels, namePrefix: config.Agent.NamePrefix, }, reaper: &reaper{ servers: servers, provider: provider, interval: config.Reaper.Interval, enabled: config.Reaper.Enabled, }, } } // Pause paueses the scaler. func (e *engine) Pause() { e.mu.Lock() e.paused = true e.mu.Unlock() } // Paused returns true if scaling is paused. func (e *engine) Paused() bool { e.mu.Lock() defer e.mu.Unlock() return e.paused } // Resume resumes the scaler. func (e *engine) Resume() { e.mu.Lock() e.paused = false e.mu.Unlock() } func (e *engine) Start(ctx context.Context) { e.reset(ctx) var wg sync.WaitGroup wg.Add(7) go func() { e.allocate(ctx) wg.Done() }() go func() { e.install(ctx) wg.Done() }() go func() { e.collect(ctx) wg.Done() }() go func() { e.plan(ctx) wg.Done() }() go func() { e.purge(ctx) wg.Done() }() go func() { e.reap(ctx) wg.Done() }() go func() { e.ping(ctx) wg.Done() }() wg.Wait() } // runs the allocation process. func (e *engine) allocate(ctx context.Context) { const interval = time.Second * 10 for { select { case <-ctx.Done(): return case <-time.After(interval): e.allocator.Allocate(ctx) } } } // runs the installation process. func (e *engine) install(ctx context.Context) { const interval = time.Second * 10 for { select { case <-ctx.Done(): return case <-time.After(interval): e.installer.Install(ctx) } } } // runs the collection process. func (e *engine) collect(ctx context.Context) { const interval = time.Second * 10 for { select { case <-ctx.Done(): return case <-time.After(interval): e.collector.Collect(ctx) } } } // runs the planning process. func (e *engine) plan(ctx context.Context) { for { select { case <-ctx.Done(): return case <-time.After(e.interval): if !e.Paused() { e.planner.Plan(ctx) } } } } // runs the ping process. func (e *engine) ping(ctx context.Context) { // by default, run the pinger every 10m. for { select { case <-ctx.Done(): return case <-time.After(e.pinger.interval): e.pinger.Ping(ctx) } } } // runs the purge process. func (e *engine) purge(ctx context.Context) { const interval = time.Hour * 24 const retain = time.Hour * 24 * -1 logger := logger.FromContext(ctx) for { select { case <-ctx.Done(): return case <-time.After(interval): logger.WithField("ttl", retain.String()). Debugln("clear stopped servers from database") e.planner.servers.Purge(ctx, time.Now().Add(retain).Unix()) } } } // runs the reaper process. func (e *engine) reap(ctx context.Context) { // by default, the reaper is run hourly since in general this // should happen infrequently. for { select { case <-ctx.Done(): return case <-time.After(e.reaper.interval): e.reaper.Reap(ctx) } } } func (e *engine) reset(ctx context.Context) { // handle the situation where the autoscaler is stopped or // restarted during instance setup or teardown. If this happens // reset the instance state to resume. servers, _ := e.allocator.servers.List(ctx) for _, s := range servers { switch s.State { case autoscaler.StateStaging: log := logger.FromContext(ctx). WithField("instance", s.Name). WithField("address", s.Address). WithField("from-state", "staging"). WithField("to-state", "created") log.Infoln("reset instance state") s.State = autoscaler.StateCreated if err := e.allocator.servers.Update(ctx, s); err != nil { log.WithError(err). Error("failed to reset instance state") } case autoscaler.StateStopping: log := logger.FromContext(ctx). WithField("instance", s.Name). WithField("address", s.Address). WithField("from-state", "stopping"). WithField("to-state", "shutdown") log.Infoln("reset instance state") s.State = autoscaler.StateShutdown if err := e.allocator.servers.Update(ctx, s); err != nil { log.WithError(err). Errorln("failed to reset instance state") } } } } ================================================ FILE: engine/install.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package engine import ( "context" "fmt" "io" "io/ioutil" "regexp" "strings" "sync" "time" "github.com/drone/autoscaler" "github.com/drone/autoscaler/config" "github.com/drone/autoscaler/logger" "github.com/drone/autoscaler/metrics" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/mount" docker "github.com/docker/docker/client" "github.com/docker/go-connections/nat" ) type installer struct { wg sync.WaitGroup os string arch string image string secret string volumes []string ports []string host string proto string envs []string keepaliveTime time.Duration keepaliveTimeout time.Duration runner config.Runner labels map[string]string checkInterval time.Duration checkDeadline time.Duration gcEnabled bool gcDebug bool gcImage string gcIgnore []string gcInterval time.Duration gcCache string watchtowerEnabled bool watchtowerImage string watchtowerStopSignal string watchtowerSignalEnabled bool watchtowerInterval int watchtowerTimeout time.Duration servers autoscaler.ServerStore metrics metrics.Collector client clientFunc } func (i *installer) Install(ctx context.Context) error { logger := logger.FromContext(ctx) servers, err := i.servers.ListState(ctx, autoscaler.StateCreated) if err != nil { return err } for _, server := range servers { server.State = autoscaler.StateStaging err = i.servers.Update(ctx, server) if err != nil { logger.WithError(err). WithField("server", server.Name). WithField("state", "staging"). Errorln("failed to update server state") return err } i.wg.Add(1) go func(server *autoscaler.Server) { i.install(ctx, server) i.wg.Done() }(server) } return nil } func (i *installer) install(ctx context.Context, instance *autoscaler.Server) error { logger := logger.FromContext(ctx). WithField("ip", instance.Address). WithField("name", instance.Name) client, closer, err := i.client(instance) if closer != nil { defer closer.Close() } if err != nil { i.metrics.IncrServerInitError() logger.WithError(err). Errorln("cannot create docker client") return i.errorUpdate(ctx, instance, err) } logger.WithField("name", instance.Name). Debugln("check docker connectivity") timeout, cancel := context.WithTimeout(ctx, i.checkDeadline) defer cancel() start := time.Now() interval := time.Duration(0) poller: for { select { case <-timeout.Done(): i.metrics.IncrServerInitError() logger.WithField("name", instance.Name). Debugln("connection timeout") return i.errorUpdate(ctx, instance, timeout.Err()) case <-time.After(interval): interval = i.checkInterval logger.WithField("name", instance.Name). Debugln("connecting to docker") _, err := client.ContainerList(ctx, container.ListOptions{}) if err != nil { logger. WithField("error", err.Error()). WithField("name", instance.Name). Debugf("cannot connect, retry in %v", interval) continue } break poller } } // track elapsed time to establish a connection i.metrics.TrackServerInitTime(start) logger.WithField("image", i.image). Debugln("pull docker image") start = time.Now() rc, err := client.ImagePull(ctx, i.image, image.PullOptions{}) if err != nil { i.metrics.IncrServerSetupError() logger.WithError(err). WithField("image", i.image). Errorln("cannot pull docker image") return i.errorUpdate(ctx, instance, err) } io.Copy(ioutil.Discard, rc) rc.Close() logger.WithField("image", i.image). Debugln("create agent container") envs := append(i.envs, fmt.Sprintf("DRONE_RPC_HOST=%s", i.host), fmt.Sprintf("DRONE_RPC_PROTO=%s", i.proto), fmt.Sprintf("DRONE_RPC_SERVER=%s://%s", i.proto, i.host), fmt.Sprintf("DRONE_RPC_SECRET=%s", i.secret), fmt.Sprintf("DRONE_RUNNER_CAPACITY=%v", instance.Capacity), fmt.Sprintf("DRONE_RUNNER_NAME=%s", instance.Name), ) if s := i.runner.Volumes; s != "" { envs = append(envs, fmt.Sprintf("DRONE_RUNNER_VOLUMES=%s", s)) } if s := i.runner.Devices; s != "" { envs = append(envs, fmt.Sprintf("DRONE_RUNNER_DEVICES=%s", s)) } if s := i.runner.EnvFile; s != "" { envs = append(envs, fmt.Sprintf("DRONE_RUNNER_ENV_FILE=%s", s)) } if s := i.runner.Privileged; s != "" { envs = append(envs, fmt.Sprintf("DRONE_RUNNER_PRIVILEGED_IMAGES=%s", s)) } if len(i.labels) > 0 { var stringLabels []string for key, val := range i.labels { stringLabels = append(stringLabels, fmt.Sprintf("%s:%s", key, val)) } envs = append(envs, fmt.Sprintf("DRONE_RUNNER_LABELS=%s", strings.Join(stringLabels, ",")), ) } var mounts []mount.Mount volumes := i.volumes switch i.os { case "windows": mounts = append(mounts, mount.Mount{ Source: `\\.\pipe\docker_engine`, Target: `\\.\pipe\docker_engine`, Type: mount.TypeNamedPipe, }) default: volumes = append(volumes, "/var/run/docker.sock:/var/run/docker.sock", ) // if memory serves me correctly, we need to explicitly // set this to nil to ensure the json representation // of this value is null. but I could be wrong in which // case this can be removed. ‾\_(ツ)_/‾ mounts = nil } exposedPorts, portBindings, err := nat.ParsePortSpecs(i.ports) if err != nil { i.metrics.IncrServerInitError() logger.WithError(err).Errorln("could not create port binding") return i.errorUpdate(ctx, instance, err) } res, err := client.ContainerCreate(ctx, &container.Config{ Image: i.image, AttachStdout: true, AttachStderr: true, Env: envs, Volumes: toVol(volumes), ExposedPorts: exposedPorts, Labels: map[string]string{ "com.centurylinklabs.watchtower.enable": fmt.Sprint(i.watchtowerSignalEnabled), "com.centurylinklabs.watchtower.stop-signal": i.watchtowerStopSignal, "io.drone.agent.name": instance.Name, "io.drone.agent.zone": instance.Region, "io.drone.agent.size": instance.Size, "io.drone.agent.instance": instance.ID, "io.drone.agent.capacity": fmt.Sprint(instance.Capacity), }, }, &container.HostConfig{ Binds: volumes, Mounts: mounts, PortBindings: portBindings, RestartPolicy: container.RestartPolicy{ Name: "always", }, }, nil, nil, "agent") if err != nil { i.metrics.IncrServerSetupError() logger.WithField("image", i.image). Errorln("cannot create agent container") return i.errorUpdate(ctx, instance, err) } logger.WithField("image", i.image). Debugln("start the agent container") err = client.ContainerStart(ctx, res.ID, container.StartOptions{}) if err != nil { i.metrics.IncrServerSetupError() logger.WithField("image", i.image). Debugln("cannot start the agent container") return i.errorUpdate(ctx, instance, err) } logger.WithField("image", i.image). Debugln("agent container started") if i.gcEnabled { logger.WithField("image", i.image). Debugln("setup the garbage collector") err = i.setupGarbageCollector(ctx, client) if err != nil { logger.WithError(err). WithField("image", i.image). Warnln("cannot setup the garbage collector") } } if i.watchtowerEnabled { logger.WithField("image", i.image). Debugln("setup watchtower") err = i.setupWatchtower(ctx, client) if err != nil { logger.WithError(err). WithField("image", i.image). Warnln("cannot setup watchtwoer") } } // track elapsed time to install software. i.metrics.TrackServerSetupTime(start) instance.State = autoscaler.StateRunning err = i.servers.Update(ctx, instance) if err != nil { i.metrics.IncrServerSetupError() logger.WithError(err). WithField("server", instance.Name). WithField("state", "running"). Errorln("failed to update server state") return err } return nil } func (i *installer) setupWatchtower(ctx context.Context, client docker.APIClient) error { vols := []string{"/var/run/docker.sock:/var/run/docker.sock"} res, err := client.ContainerCreate(ctx, &container.Config{ Image: i.watchtowerImage, AttachStdout: true, AttachStderr: true, Volumes: toVol(vols), Env: []string{ fmt.Sprintf("WATCHTOWER_POLL_INTERVAL=%d", i.watchtowerInterval), fmt.Sprintf("WATCHTOWER_TIMEOUT=%s", i.watchtowerTimeout), fmt.Sprintf("WATCHTOWER_CLEANUP=true"), fmt.Sprintf("WATCHTOWER_LABEL_ENABLE=true"), }, }, &container.HostConfig{ Binds: vols, RestartPolicy: container.RestartPolicy{ Name: "always", }, }, nil, nil, "watchtower") if err != nil { return err } return client.ContainerStart(ctx, res.ID, container.StartOptions{}) } func (i *installer) setupGarbageCollector(ctx context.Context, client docker.APIClient) error { logger := logger.FromContext(ctx) vols := []string{"/var/run/docker.sock:/var/run/docker.sock"} envs := []string{ fmt.Sprintf("GC_CACHE=%s", i.gcCache), fmt.Sprintf("GC_DEBUG=%v", i.gcDebug), fmt.Sprintf("GC_INTERVAL=%s", i.gcInterval), } if len(i.gcIgnore) > 0 { envs = append(envs, fmt.Sprintf("GC_IGNORE=%s", strings.Join(i.gcIgnore, ",")), ) } logger.WithField("image", i.gcImage). Debugln("pull gc image") rc, err := client.ImagePull(ctx, i.gcImage, image.PullOptions{}) if err != nil { logger.WithError(err). WithField("image", i.gcImage). Errorln("cannot pull gc image") return err } io.Copy(ioutil.Discard, rc) rc.Close() res, err := client.ContainerCreate(ctx, &container.Config{ Image: i.gcImage, AttachStdout: true, AttachStderr: true, Volumes: toVol(vols), Env: envs, Labels: map[string]string{ "com.centurylinklabs.watchtower.enable": fmt.Sprint(i.watchtowerSignalEnabled), }, }, &container.HostConfig{ Binds: vols, RestartPolicy: container.RestartPolicy{ Name: "always", }, }, nil, nil, "drone-gc") if err != nil { return err } return client.ContainerStart(ctx, res.ID, container.StartOptions{}) } func (i *installer) errorUpdate(ctx context.Context, server *autoscaler.Server, err error) error { if err != nil { server.State = autoscaler.StateError server.Error = err.Error() xerr := i.servers.Update(ctx, server) if xerr != nil { logger.FromContext(ctx). WithError(xerr). WithField("server", server.Name). WithField("state", "error"). Errorln("failed to update server state") } } return err } // helper function that converts a slice of volume paths to a set of // unique volume names. func toVol(paths []string) map[string]struct{} { set := map[string]struct{}{} for _, path := range paths { parts, err := splitVolumeParts(path) if err != nil { continue } if len(parts) < 2 { continue } set[parts[1]] = struct{}{} } return set } // helper function that split volume path func splitVolumeParts(volumeParts string) ([]string, error) { pattern := `^((?:[\w]\:)?[^\:]*)\:((?:[\w]\:)?[^\:]*)(?:\:([rwom]*))?` r, err := regexp.Compile(pattern) if err != nil { return []string{}, err } if r.MatchString(volumeParts) { results := r.FindStringSubmatch(volumeParts)[1:] cleanResults := []string{} for _, item := range results { if item != "" { cleanResults = append(cleanResults, item) } } return cleanResults, nil } else { return strings.Split(volumeParts, ":"), nil } } ================================================ FILE: engine/install_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package engine import ( "reflect" "testing" ) func TestSplitVolumeParts(t *testing.T) { testdata := []struct { from string to []string success bool }{ { from: `Z::Z::rw`, to: []string{`Z:`, `Z:`, `rw`}, success: true, }, { from: `Z:\:Z:\:rw`, to: []string{`Z:\`, `Z:\`, `rw`}, success: true, }, { from: `Z:\git\refs:Z:\git\refs:rw`, to: []string{`Z:\git\refs`, `Z:\git\refs`, `rw`}, success: true, }, { from: `Z:\git\refs:Z:\git\refs`, to: []string{`Z:\git\refs`, `Z:\git\refs`}, success: true, }, { from: `Z:/:Z:/:rw`, to: []string{`Z:/`, `Z:/`, `rw`}, success: true, }, { from: `Z:/git/refs:Z:/git/refs:rw`, to: []string{`Z:/git/refs`, `Z:/git/refs`, `rw`}, success: true, }, { from: `Z:/git/refs:Z:/git/refs`, to: []string{`Z:/git/refs`, `Z:/git/refs`}, success: true, }, { from: `/test:/test`, to: []string{`/test`, `/test`}, success: true, }, { from: `test:/test`, to: []string{`test`, `/test`}, success: true, }, { from: `test:test`, to: []string{`test`, `test`}, success: true, }, } for _, test := range testdata { results, err := splitVolumeParts(test.from) if test.success == (err != nil) { } else { if reflect.DeepEqual(results, test.to) != test.success { t.Errorf("Expect %q matches %q is %v", test.from, results, test.to) } } } } ================================================ FILE: engine/pinger.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package engine import ( "context" "sync" "time" "github.com/drone/autoscaler" "github.com/drone/autoscaler/logger" ) type pinger struct { wg sync.WaitGroup servers autoscaler.ServerStore client clientFunc interval time.Duration enabled bool } func (p *pinger) Ping(ctx context.Context) error { // this is a feature flag that can be used to enable // experimental pinging and detection of zombie instances. if !p.enabled { return nil } servers, err := p.servers.ListState(ctx, autoscaler.StateRunning) if err != nil { return err } for _, server := range servers { p.wg.Add(1) go func(server *autoscaler.Server) { p.ping(ctx, server) p.wg.Done() }(server) } return nil } func (p *pinger) ping(ctx context.Context, server *autoscaler.Server) error { logger := logger.FromContext(ctx). WithField("ip", server.Address). WithField("name", server.Name) client, closer, err := p.client(server) if closer != nil { defer closer.Close() } if err != nil { logger.WithError(err). Errorln("cannot create docker client") return nil } // the system will attempt to ping the server a maximum of // five times, with a 1 minute timeout for each ping. If the // server cannot be reached, it will be placed in an error // state. for i := 0; i < 5; i++ { logger.Debugln("pinging the server") timeout, cancel := context.WithTimeout(ctx, time.Minute) _, err := client.Ping(timeout) cancel() // If the global context is in an error state we // should assume this is because the program is // being gracefully terminated. This could cause // false positive ping errors, so we ignore and // exit the routine. if ctx.Err() != nil { return nil } if err == nil { logger.WithField("state", "healthy"). Debugln("server ping successful") return nil } else { logger.WithError(err). Warnln("server ping unsuccessful") } } server, err = p.servers.Find(ctx, server.Name) if err != nil { // if the server no longer exists in the database // it is possible it was mutated by another goroutine. return err } if server.State != autoscaler.StateRunning { // if the server was mutated by another goroutine // we should exit without making any changes. return nil } logger.WithField("state", "unhealthy"). Debugln("failed to reach server") server.Error = "Failed to ping the server" server.Stopped = time.Now().Unix() server.State = autoscaler.StateError err = p.servers.Update(ctx, server) if err != nil { logger.WithError(err). WithField("server", server.Name). WithField("state", "error"). Errorln("failed to update server state") return err } return nil } ================================================ FILE: engine/pinger_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package engine ================================================ FILE: engine/planner.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package engine import ( "context" "sort" "time" "github.com/drone/autoscaler" "github.com/drone/autoscaler/logger" "github.com/drone/drone-go/drone" "github.com/dchest/uniuri" ) // a planner is responsible for capacity planning. It will assess // current build volume and plan the creation or termination of // server resources accordingly. type planner struct { os string arch string version string kernel string namePrefix string min int // min number of servers max int // max number of servers to allocate cap int // capacity per-server buffer int // buffer capacity to have warm and ready ttu time.Duration // minimum server age labels map[string]string client drone.Client servers autoscaler.ServerStore } func (p *planner) Plan(ctx context.Context) error { // generate a unique identifier for the current // execution cycle for tracing and grouping logs. cycle := uniuri.New() log := logger.FromContext(ctx).WithField("id", cycle) log.Debugln("calculate unfinished jobs") pending, running, err := p.count(ctx) if err != nil { log.WithError(err). Errorln("cannot calculate unfinished jobs") return err } log.Debugln("calculate server capacity") capacity, servers, err := p.capacity(ctx) if err != nil { log.WithError(err). Errorln("cannot calculate server capacity") return err } log. WithField("min-pool", p.min). WithField("max-pool", p.max). WithField("server-buffer", p.buffer). WithField("server-capacity", capacity). WithField("server-count", servers). WithField("pending-builds", pending). WithField("running-builds", running). Debugln("check capacity") defer func() { log.Debugln("check capacity complete") }() ctx = logger.WithContext(ctx, log) free := max(capacity-running-p.buffer, 0) diff := serverDiff(pending, free, p.cap) // if the server differential to handle the build volume // is positive, we can reduce server capacity. if diff < 0 { return p.mark(ctx, // we should adjust the desired capacity to ensure // we maintain the minimum required server count. serverFloor(servers, abs(diff), p.min), ) } // if the server differential to handle the build volume // is positive, we need to allocate more server capacity. if diff > 0 { return p.alloc(ctx, // we should adjust the desired capacity to ensure // it does not exceed the max server count. serverCeil(servers, diff, p.max), ) } log.Debugln("no capacity changes required") return nil } // helper function allocates n new server instances. func (p *planner) alloc(ctx context.Context, n int) error { logger := logger.FromContext(ctx) logger.Debugf("allocate %d servers", n) namePrefix := p.namePrefix if namePrefix == "" { namePrefix = "agent-" } for i := 0; i < n; i++ { server := &autoscaler.Server{ Name: p.namePrefix + uniuri.NewLen(8), State: autoscaler.StatePending, Secret: uniuri.New(), Capacity: p.cap, } err := p.servers.Create(ctx, server) if err != nil { logger.WithError(err). Errorln("cannot create server") return err } } return nil } // helper function marks instances for termination. func (p *planner) mark(ctx context.Context, n int) error { logger := logger.FromContext(ctx) logger.Debugf("terminate %d servers", n) if n == 0 { return nil } servers, err := p.servers.ListState(ctx, autoscaler.StateRunning) if err != nil { logger.WithError(err). Errorln("cannot fetch server list") return err } sort.Sort(sort.Reverse(byCreated(servers))) // Abort marking servers for termination if the total // number of running servers, minus the total number // of servers to terminate, falls below the minimum // number of servers (including the buffer). if len(servers)-n < p.min { logger.WithField("servers-to-terminate", n). WithField("servers-running", len(servers)). WithField("min-pool", p.min). Debugf("abort terminating instances to ensure minimum capacity met") return nil } busy, err := p.listBusy(ctx) if err != nil { logger.WithError(err). Errorln("cannot ascertain busy server list") return err } var idle []*autoscaler.Server for _, server := range servers { // skip busy servers if _, ok := busy[server.Name]; ok { logger.WithField("server", server.Name). Debugln("server is busy") continue } // skip servers less than minage if time.Now().Before(time.Unix(server.Created, 0).Add(p.ttu)) { logger. WithField("server", server.Name). WithField("age", timeDiff(time.Now(), time.Unix(server.Created, 0))). WithField("min-age", p.ttu). Debugln("server min-age not reached") continue } idle = append(idle, server) logger.WithField("server", server.Name). Debugln("server is idle") } // if there are no idle servers, there are no servers // to retire, we can exit. if len(idle) == 0 { logger.Debugln("no idle servers to shutdown") } if len(idle) > n { idle = idle[:n] } for _, server := range idle { server.State = autoscaler.StateShutdown err := p.servers.Update(ctx, server) if err != nil { logger.WithError(err). WithField("server", server.Name). WithField("state", "shutdown"). Errorln("cannot update server state") } } return nil } // helper function returns the number of pending and // running builds in the remote Drone installation. func (p *planner) count(ctx context.Context) (pending, running int, err error) { stages, err := p.client.Queue() if err != nil { return pending, running, err } for _, stage := range stages { if p.match(stage) == false { continue } switch stage.Status { case drone.StatusPending: pending++ case drone.StatusRunning: running++ } } return } // helper function returns our current capacity. func (p *planner) capacity(ctx context.Context) (capacity, count int, err error) { servers, err := p.servers.List(ctx) if err != nil { return capacity, count, err } for _, server := range servers { switch server.State { case autoscaler.StateStopped: // ignore state default: count++ capacity += server.Capacity } } return } // helper function returns a list of busy servers. func (p *planner) listBusy(ctx context.Context) (map[string]struct{}, error) { busy := map[string]struct{}{} stages, err := p.client.Queue() if err != nil { return busy, err } for _, stage := range stages { if p.match(stage) == false { continue } if stage.Status == drone.StatusRunning || stage.Status == drone.StatusPending { busy[stage.Machine] = struct{}{} } } return busy, nil } // helper function returns true if the os, arch, variant // and kernel match the stage. func (p *planner) match(stage *drone.Stage) bool { labelMatch := true if len(p.labels) > 0 || len(stage.Labels) > 0 { labelMatch = checkLabels(p.labels, stage.Labels) } return stage.OS == p.os && stage.Arch == p.arch && stage.Variant == p.version && stage.Kernel == p.kernel && labelMatch } func checkLabels(a, b map[string]string) bool { if len(a) != len(b) { return false } for k, v := range a { if w, ok := b[k]; !ok || v != w { return false } } return true } func timeDiff(t time.Time, start time.Time) time.Duration { var d time.Duration if t.After(start) { d = t.Sub(start) } return d } ================================================ FILE: engine/planner_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package engine import ( "context" "testing" "time" "github.com/drone/autoscaler" "github.com/drone/autoscaler/config" "github.com/drone/autoscaler/mocks" "github.com/drone/drone-go/drone" "github.com/golang/mock/gomock" ) // This test verifies that if the server capacity is // >= the pending count, and the server capacity is // <= the pool minimum size, no actions are taken. func TestPlan_Noop(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() servers := []*autoscaler.Server{ {Name: "server1", Capacity: 2, State: autoscaler.StateRunning}, {Name: "server2", Capacity: 2, State: autoscaler.StateRunning}, } store := mocks.NewMockServerStore(controller) store.EXPECT().List(gomock.Any()).Return(servers, nil) client := mocks.NewMockClient(controller) client.EXPECT().Queue().Return([]*drone.Stage{ {Status: drone.StatusRunning}, {Status: drone.StatusPending}, {Status: drone.StatusPending}, }, nil) p := planner{ cap: 2, min: 2, max: 10, client: client, servers: store, } err := p.Plan(context.TODO()) if err != nil { t.Error(err) } } // This test verifies that if that no servers are // destroyed if there is excess capacity and the // the server count <= the min pool size. func TestPlan_MinBufferCapacity(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() // x2 capacity servers := []*autoscaler.Server{ {Name: "server1", Capacity: 1, State: autoscaler.StateRunning}, {Name: "server2", Capacity: 1, State: autoscaler.StateRunning}, } // x0 running builds // x0 pending builds builds := []*drone.Stage{} store := mocks.NewMockServerStore(controller) store.EXPECT().List(gomock.Any()).Return(servers, nil) client := mocks.NewMockClient(controller) client.EXPECT().Queue().Return(builds, nil) p := planner{ cap: 2, buffer: 1, min: 2, max: 4, client: client, servers: store, } err := p.Plan(context.TODO()) if err != nil { t.Error(err) } } // This test verifies that if the server capacity minus buffer is // less than the pending count, and the server capacity is // >= the pool maximum, no actions are taken. func TestPlan_MaxBufferCapacity(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() // x4 capacity servers := []*autoscaler.Server{ {Name: "server1", Capacity: 1, State: autoscaler.StateRunning}, {Name: "server2", Capacity: 1, State: autoscaler.StateRunning}, {Name: "server3", Capacity: 1, State: autoscaler.StateRunning}, {Name: "server4", Capacity: 1, State: autoscaler.StateRunning}, } // x3 running builds // x1 pending builds builds := []*drone.Stage{ {Status: drone.StatusRunning}, {Status: drone.StatusRunning}, {Status: drone.StatusRunning}, {Status: drone.StatusPending}, } store := mocks.NewMockServerStore(controller) store.EXPECT().List(gomock.Any()).Return(servers, nil) client := mocks.NewMockClient(controller) client.EXPECT().Queue().Return(builds, nil) p := planner{ cap: 1, buffer: 2, min: 2, max: 4, client: client, servers: store, } err := p.Plan(context.TODO()) if err != nil { t.Error(err) } } // This test verifies that if the server capacity minus buffer is // less than the pending count, and the server capacity is // < the pool maximum, additional servers are provisioned. func TestPlan_MoreBufferCapacity(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() // x4 capacity servers := []*autoscaler.Server{ {Name: "server1", Capacity: 2, State: autoscaler.StateRunning}, {Name: "server2", Capacity: 2, State: autoscaler.StateRunning}, } // x2 running builds // x1 pending builds builds := []*drone.Stage{ {Status: drone.StatusRunning}, {Status: drone.StatusRunning}, {Status: drone.StatusPending}, } store := mocks.NewMockServerStore(controller) store.EXPECT().List(gomock.Any()).Return(servers, nil) store.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil) client := mocks.NewMockClient(controller) client.EXPECT().Queue().Return(builds, nil) p := planner{ cap: 2, buffer: 2, min: 2, max: 4, client: client, servers: store, } err := p.Plan(context.TODO()) if err != nil { t.Error(err) } } // This test verifies that if the server capacity is // < than the pending count, and the server capacity is // >= the pool maximum, no actions are taken. func TestPlan_MaxCapacity(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() // x4 capacity servers := []*autoscaler.Server{ {Name: "server1", Capacity: 1, State: autoscaler.StateRunning}, {Name: "server2", Capacity: 1, State: autoscaler.StateRunning}, {Name: "server3", Capacity: 1, State: autoscaler.StateRunning}, {Name: "server4", Capacity: 1, State: autoscaler.StateRunning}, } // x4 running builds // x3 pending builds builds := []*drone.Stage{ {Status: drone.StatusRunning}, {Status: drone.StatusRunning}, {Status: drone.StatusRunning}, {Status: drone.StatusRunning}, {Status: drone.StatusPending}, {Status: drone.StatusPending}, {Status: drone.StatusPending}, } store := mocks.NewMockServerStore(controller) store.EXPECT().List(gomock.Any()).Return(servers, nil) client := mocks.NewMockClient(controller) client.EXPECT().Queue().Return(builds, nil) config := config.Config{} config.Pool.Min = 2 config.Pool.Max = 4 config.Agent.Concurrency = 2 p := planner{ cap: 2, min: 2, max: 4, client: client, servers: store, } err := p.Plan(context.TODO()) if err != nil { t.Error(err) } } // This test verifies that if the server capacity is // less than the pending count, and the server capacity is // < the pool maximum, additional servers are provisioned. func TestPlan_MoreCapacity(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() // x2 capacity servers := []*autoscaler.Server{ {Name: "server1", Capacity: 1, State: autoscaler.StateRunning}, {Name: "server2", Capacity: 1, State: autoscaler.StateRunning}, } // x2 running builds // x3 pending builds builds := []*drone.Stage{ {Status: drone.StatusRunning}, {Status: drone.StatusRunning}, {Status: drone.StatusPending}, {Status: drone.StatusPending}, {Status: drone.StatusPending}, {Status: drone.StatusPending}, // ignore, would exceed max pool size {Status: drone.StatusPending}, // ignore, would exceed max pool size {Status: drone.StatusPending}, // ignore, would exceed max pool size {Status: drone.StatusPending}, // ignore, would exceed max pool size {Status: drone.StatusPending}, // ignore, would exceed max pool size {Status: drone.StatusPending}, // ignore, would exceed max pool size } store := mocks.NewMockServerStore(controller) store.EXPECT().List(gomock.Any()).Return(servers, nil) store.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil) store.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil) client := mocks.NewMockClient(controller) client.EXPECT().Queue().Return(builds, nil) p := planner{ cap: 2, min: 2, max: 4, client: client, servers: store, } err := p.Plan(context.TODO()) if err != nil { t.Error(err) } } // This test verifies that if that no servers are // destroyed if there is excess capacity and the // the server count <= the min pool size. func TestPlan_MinPool(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() // x2 capacity servers := []*autoscaler.Server{ {Name: "server1", Capacity: 1, State: autoscaler.StateRunning}, {Name: "server2", Capacity: 1, State: autoscaler.StateRunning}, } // x0 running builds // x0 pending builds builds := []*drone.Stage{} store := mocks.NewMockServerStore(controller) store.EXPECT().List(gomock.Any()).Return(servers, nil) client := mocks.NewMockClient(controller) client.EXPECT().Queue().Return(builds, nil) p := planner{ cap: 2, min: 2, max: 4, client: client, servers: store, } err := p.Plan(context.TODO()) if err != nil { t.Error(err) } } // This test verifies that no servers are // destroyed if no idle servers exist. func TestPlan_NoIdle(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() // x2 capacity servers := []*autoscaler.Server{ {Name: "server1", Capacity: 2, State: autoscaler.StateRunning}, {Name: "server2", Capacity: 2, State: autoscaler.StateRunning}, } // x2 running builds // x0 pending builds builds := []*drone.Stage{ {Status: drone.StatusRunning, Machine: "server1"}, {Status: drone.StatusRunning, Machine: "server2"}, } store := mocks.NewMockServerStore(controller) store.EXPECT().List(gomock.Any()).Return(servers, nil) store.EXPECT().ListState(gomock.Any(), autoscaler.StateRunning).Return(servers, nil) client := mocks.NewMockClient(controller) client.EXPECT().Queue().Return(builds, nil) client.EXPECT().Queue().Return(builds, nil) p := planner{ cap: 2, min: 1, max: 4, client: client, servers: store, } err := p.Plan(context.Background()) if err != nil { t.Error(err) } } // This test verifies that idle servers are not // garbage collected until the min-age is reached. func TestScale_MinAge(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() // x2 capacity servers := []*autoscaler.Server{ {Name: "server1", Capacity: 1, State: autoscaler.StateRunning, Created: time.Now().Unix()}, {Name: "server2", Capacity: 1, State: autoscaler.StateRunning, Created: time.Now().Unix()}, } // x0 running builds // x0 pending builds builds := []*drone.Stage{} store := mocks.NewMockServerStore(controller) store.EXPECT().List(gomock.Any()).Return(servers, nil) store.EXPECT().ListState(gomock.Any(), autoscaler.StateRunning).Return(servers, nil) client := mocks.NewMockClient(controller) client.EXPECT().Queue().Return(builds, nil) client.EXPECT().Queue().Return(builds, nil) p := planner{ cap: 2, min: 1, max: 4, ttu: time.Hour, client: client, servers: store, } err := p.Plan(context.TODO()) if err != nil { t.Error(err) } } func TestPlan_ShutdownIdle(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() // x3 capacity servers := []*autoscaler.Server{ {Name: "server1", Capacity: 2, Created: 1, State: autoscaler.StateRunning}, {Name: "server2", Capacity: 2, Created: 2, State: autoscaler.StateRunning}, {Name: "server3", Capacity: 2, Created: 3, State: autoscaler.StateRunning}, } // x0 running builds // x0 pending builds builds := []*drone.Stage{} store := mocks.NewMockServerStore(controller) store.EXPECT().List(gomock.Any()).Return(servers, nil) store.EXPECT().ListState(gomock.Any(), autoscaler.StateRunning).Return(servers, nil) store.EXPECT().Update(gomock.Any(), servers[2]).Return(nil) store.EXPECT().Update(gomock.Any(), servers[1]).Return(nil) client := mocks.NewMockClient(controller) client.EXPECT().Queue().Return(builds, nil) client.EXPECT().Queue().Return(builds, nil) p := planner{ cap: 2, min: 1, max: 4, client: client, servers: store, } err := p.Plan(context.TODO()) if err != nil { t.Error(err) } } func TestPlan_ExcludePendingWhenTerminating(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() servers := []*autoscaler.Server{ // x3 capacity {Name: "server1", Capacity: 2, Created: 1, State: autoscaler.StateRunning}, {Name: "server2", Capacity: 2, Created: 2, State: autoscaler.StateRunning}, {Name: "server3", Capacity: 2, Created: 3, State: autoscaler.StateRunning}, // x3 pending / staging / starting {Name: "server4", Capacity: 2, Created: 4, State: autoscaler.StateCreating}, {Name: "server5", Capacity: 2, Created: 5, State: autoscaler.StateCreated}, {Name: "server6", Capacity: 2, Created: 6, State: autoscaler.StateStaging}, } // x0 running builds // x0 pending builds builds := []*drone.Stage{} store := mocks.NewMockServerStore(controller) store.EXPECT().List(gomock.Any()).Return(servers, nil) store.EXPECT().ListState(gomock.Any(), autoscaler.StateRunning).Return(servers[:3], nil) client := mocks.NewMockClient(controller) client.EXPECT().Queue().Return(builds, nil) p := planner{ cap: 2, min: 3, max: 10, client: client, servers: store, } err := p.Plan(context.TODO()) if err != nil { t.Error(err) } } func TestListBusy(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() client := mocks.NewMockClient(controller) client.EXPECT().Queue().Return([]*drone.Stage{ {Status: drone.StatusPending, Machine: "machine1"}, {Status: drone.StatusRunning, Machine: "machine2"}, }, nil) p := planner{ client: client, } busy, err := p.listBusy(context.TODO()) if err != nil { t.Error(err) } if got, want := len(busy), 2; got != want { t.Errorf("Want busy server count %d, got %d", want, got) } if _, ok := busy["machine1"]; !ok { t.Errorf("Expected server not in busy list") } if _, ok := busy["machine2"]; !ok { t.Errorf("Expected server not in busy list") } } func TestListBusyWithPendingAndRunning(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() client := mocks.NewMockClient(controller) client.EXPECT().Queue().Return([]*drone.Stage{ {Status: drone.StatusPending, Machine: "machine1"}, {Status: drone.StatusRunning, Machine: "machine2"}, {Status: drone.StatusPending, Machine: "machine3"}, }, nil) p := planner{ client: client, } busy, err := p.listBusy(context.TODO()) if err != nil { t.Error(err) } if got, want := len(busy), 3; got != want { t.Errorf("Want busy server count %d, got %d", want, got) } if _, ok := busy["machine1"]; !ok { t.Errorf("Machine1 not found in the busy server list") } if _, ok := busy["machine2"]; !ok { t.Errorf("Machine2 not found in the busy server list") } if _, ok := busy["machine3"]; !ok { t.Errorf("Machine3 not found in the busy server list") } } func TestCapacity(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() servers := []*autoscaler.Server{ {Name: "server1", Capacity: 4}, {Name: "server2", Capacity: 3}, {Name: "server3", Capacity: 2}, {Name: "server4", Capacity: 1}, } store := mocks.NewMockServerStore(controller) store.EXPECT().List(gomock.Any()).Return(servers, nil) p := planner{ servers: store, } capacity, count, err := p.capacity(context.TODO()) if err != nil { t.Error(err) return } if got, want := capacity, 10; got != want { t.Errorf("Want capacity count %d, got %d", want, got) } if got, want := count, 4; got != want { t.Errorf("Want server count %d, got %d", want, got) } } func TestCount(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() client := mocks.NewMockClient(controller) client.EXPECT().Queue().Return([]*drone.Stage{ {Status: drone.StatusPending}, {Status: drone.StatusPending}, {Status: drone.StatusPending}, {Status: drone.StatusRunning}, {Status: drone.StatusRunning}, }, nil) p := planner{ client: client, } pending, running, err := p.count(context.TODO()) if err != nil { t.Error(err) return } if got, want := pending, 3; got != want { t.Errorf("Want pending count %d, got %d", want, got) } if got, want := running, 2; got != want { t.Errorf("Want running count %d, got %d", want, got) } } func TestMatch(t *testing.T) { tests := []struct { match bool os string arch string version string kernel string labels map[string]string stage *drone.Stage }{ { match: true, os: "linux", arch: "amd64", stage: &drone.Stage{ OS: "linux", Arch: "amd64", }, }, { match: false, os: "linux", arch: "amd64", stage: &drone.Stage{ OS: "linux", Arch: "arm", }, }, { match: false, os: "linux", arch: "amd64", stage: &drone.Stage{ OS: "linux", Arch: "amd64", Labels: map[string]string{ "region": "us-west-2", }, }, }, { match: false, os: "linux", arch: "amd64", labels: map[string]string{ "region": "us-west-2", }, stage: &drone.Stage{ OS: "linux", Arch: "amd64", }, }, { match: true, os: "linux", arch: "amd64", labels: map[string]string{ "region": "us-west-2", }, stage: &drone.Stage{ OS: "linux", Arch: "amd64", Labels: map[string]string{ "region": "us-west-2", }, }, }, { match: true, os: "linux", arch: "amd64", labels: map[string]string{ "region": "us-west-2", "mem": "high", }, stage: &drone.Stage{ OS: "linux", Arch: "amd64", Labels: map[string]string{ "region": "us-west-2", "mem": "high", }, }, }, { match: false, os: "linux", arch: "amd64", labels: map[string]string{ "region": "us-east-2", }, stage: &drone.Stage{ OS: "linux", Arch: "amd64", Labels: map[string]string{ "region": "us-west-2", }, }, }, { match: false, os: "linux", arch: "amd64", labels: map[string]string{ "region": "us-east-2", "mem": "high", }, stage: &drone.Stage{ OS: "linux", Arch: "amd64", Labels: map[string]string{ "region": "us-west-2", }, }, }, } for _, test := range tests { p := &planner{ os: test.os, arch: test.arch, version: test.version, kernel: test.kernel, labels: test.labels, } if p.match(test.stage) != test.match { t.Fail() return } } } ================================================ FILE: engine/reaper.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package engine import ( "context" "sync" "time" "github.com/drone/autoscaler" "github.com/drone/autoscaler/logger" ) // // The reaper looks for and removes errored instances. The // collector, on the other hand, is responsible for garbage // collecting running instances that are no longer required. // // Note that I am open to using a more descriptive name if // anyone has a better suggestion. // type reaper struct { wg sync.WaitGroup servers autoscaler.ServerStore provider autoscaler.Provider interval time.Duration enabled bool } func (r *reaper) Reap(ctx context.Context) error { // this is a feature flag that can be used to enable // experimental reaping of errored instances. if !r.enabled { return nil } servers, err := r.servers.ListState(ctx, autoscaler.StateError) if err != nil { return err } for _, server := range servers { r.wg.Add(1) go func(server *autoscaler.Server) { r.reap(ctx, server) r.wg.Done() }(server) } return nil } func (r *reaper) reap(ctx context.Context, server *autoscaler.Server) error { logger := logger.FromContext(ctx) logger. WithField("state", "error"). WithField("server", server.Name). Debugln("inspecting failed server") // if the server ID is an empty string it indicates // the server was never provisioned, but still has an // entry in the database. In this case, we can simply // delete the database entry if server.ID == "" { logger. WithField("state", "error"). WithField("server", server.Name). Infoln("server never provisioned. nothing to destroy") } else { logger. WithField("state", "error"). WithField("server", server.Name). Infoln("destroy provisioned server") in := &autoscaler.Instance{ ID: server.ID, Provider: server.Provider, Name: server.Name, Address: server.Address, Region: server.Region, Image: server.Image, Size: server.Size, } err := r.provider.Destroy(ctx, in) // TODO implement ErrInstanceNotFound in Google driver // TODO implement ErrInstanceNotFound in Hetzner driver // TODO implement ErrInstanceNotFound in Packet driver if err == autoscaler.ErrInstanceNotFound { logger. WithField("state", "error"). WithField("server", server.Name). Infoln("server no longer exists. nothing to destroy") // this accounts for the fact that the server can be // manually terminated outside of the autoscaler. In // this case the reaper continues and updates the // server state to stopped (below) } else if err != nil { logger.WithError(err). WithField("state", "error"). WithField("server", server.Name). Errorln("cannot destroy server") return err } } server.Stopped = time.Now().Unix() server.State = autoscaler.StateStopped err := r.servers.Update(ctx, server) if err != nil { logger.WithError(err). WithField("server", server.Name). WithField("state", "stopped"). Errorln("failed to update server state") return err } return nil } ================================================ FILE: engine/reaper_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package engine ================================================ FILE: engine/sort.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package engine import "github.com/drone/autoscaler" // byCreated sorts the server list by created date. type byCreated []*autoscaler.Server func (a byCreated) Len() int { return len(a) } func (a byCreated) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a byCreated) Less(i, j int) bool { return a[i].Created < a[j].Created } ================================================ FILE: engine/sort_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package engine import ( "sort" "testing" "github.com/drone/autoscaler" ) func TestSortByCreated(t *testing.T) { servers := []*autoscaler.Server{ {Created: 4, Name: "fourth"}, {Created: 2, Name: "second"}, {Created: 3, Name: "third"}, {Created: 5, Name: "fifth"}, {Created: 1, Name: "first"}, } sort.Sort(byCreated(servers)) for i, server := range servers { if server.Created != int64(i+1) { t.Errorf("Invalid sort order %d for %q", i, server.Name) } } } ================================================ FILE: engine.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package autoscaler import "context" // An Engine is responsible for running the scaling // alogirthm to provision and shutdown instances according // to build volume. type Engine interface { // Start starts the Engine. The context can be used // to cancel a running engine. Start(context.Context) // Pause pauses the Engine. Pause() // Paused returns true if th Engine is paused. Paused() bool // Resume resumes the Engine if paused. Resume() } ================================================ FILE: go.mod ================================================ module github.com/drone/autoscaler go 1.22.4 replace ( go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp => go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 go.opentelemetry.io/otel => go.opentelemetry.io/otel v1.32.0 go.opentelemetry.io/otel/metric => go.opentelemetry.io/otel/metric v1.32.0 go.opentelemetry.io/otel/trace => go.opentelemetry.io/otel/trace v1.32.0 ) require ( github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d github.com/Microsoft/go-winio v0.5.1 // indirect github.com/avast/retry-go v3.0.0+incompatible github.com/aws/aws-sdk-go v1.44.205 github.com/beorn7/perks v1.0.1 // indirect github.com/bluele/slack v0.0.0-20171128075526-307046097ee9 github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 github.com/digitalocean/godo v1.1.1 github.com/docker/docker v28.0.2+incompatible github.com/docker/go-connections v0.4.0 github.com/drone/drone-go v1.0.5-0.20190504210458-4d6116b897ba github.com/drone/envconfig v1.4.1 github.com/drone/funcmap v0.0.0-20220929084810-72602997d16f github.com/drone/signal v0.0.0-20170915013802-ac5d07ef1315 github.com/dustin/go-humanize v1.0.0 github.com/go-chi/chi v3.3.2+incompatible github.com/go-sql-driver/mysql v1.3.0 github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.6.0 github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 // indirect github.com/google/uuid v1.3.0 // indirect github.com/gophercloud/gophercloud v0.0.0-20181014043407-c8947f7d1c51 github.com/h2non/gock v1.2.0 github.com/hetznercloud/hcloud-go v1.4.0 github.com/jmoiron/sqlx v0.0.0-20180228184624-cf35089a1979 github.com/joho/godotenv v1.2.0 github.com/kr/pretty v0.2.1 github.com/lib/pq v1.10.4 github.com/mattn/go-sqlite3 v1.14.16 github.com/packethost/packngo v0.1.0 github.com/prometheus/client_golang v1.14.0 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.3 github.com/sirupsen/logrus v1.9.3 golang.org/x/crypto v0.14.0 golang.org/x/oauth2 v0.10.0 golang.org/x/sync v0.3.0 golang.org/x/time v0.1.0 google.golang.org/api v0.126.0 ) require ( cloud.google.com/go/compute v1.21.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/docker/go-units v0.4.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect github.com/googleapis/gax-go/v2 v2.11.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/tent/http-link-go v0.0.0-20130702225549-ac974c61c2f9 // indirect ) require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/containerd/log v0.1.0 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/s2a-go v0.1.4 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 // indirect github.com/morikuni/aec v1.0.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0 // indirect go.opentelemetry.io/otel v1.32.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0 // indirect go.opentelemetry.io/otel/metric v1.32.0 // indirect go.opentelemetry.io/otel/sdk v1.3.0 // indirect go.opentelemetry.io/otel/trace v1.32.0 // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect google.golang.org/grpc v1.58.3 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gotest.tools/v3 v3.0.3 // indirect launchpad.net/gocheck v0.0.0-20140225173054-000000000087 // indirect sigs.k8s.io/yaml v1.2.0 // indirect ) ================================================ FILE: go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/compute v1.21.0 h1:JNBsyXVoOoNJtTQcnEY5uYpZIbeCTYIeDe0Xh1bySMk= cloud.google.com/go/compute v1.21.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d h1:j6oB/WPCigdOkxtuPl1VSIiLpy7Mdsu6phQffbF19Ng= github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d/go.mod h1:3cARGAK9CfW3HoxCy1a0G4TKrdiKke8ftOMEOHyySYs= github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e/go.mod h1:Xa6lInWHNQnuWoF0YPSsx+INFA9qk7/7pTjwb3PInkY= github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/aws/aws-sdk-go v1.44.205 h1:q23NJXgLPIuBMn4zaluWWz57HPP5z7Ut8ZtK1D3N9bs= github.com/aws/aws-sdk-go v1.44.205/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 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/bluele/slack v0.0.0-20171128075526-307046097ee9 h1:bnBrnv6TsEcPq7HIJykjAGl8jw/J1FZMYYceCQfLgEI= github.com/bluele/slack v0.0.0-20171128075526-307046097ee9/go.mod h1:W679Ri2W93VLD8cVpEY/zLH1ow4zhJcCyjzrKxfM3QM= github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo= github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 h1:74lLNRzvsdIlkTgfDSMuaPjBr4cf6k7pwQQANm/yLKU= github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= github.com/digitalocean/godo v1.1.1 h1:v0A7yF3xmKLjjdJGIeBbINfMufcrrRhqZsxuVQMoT+U= github.com/digitalocean/godo v1.1.1/go.mod h1:h6faOIcZ8lWIwNQ+DN7b3CgX4Kwby5T+nbpNqkUIozU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dnaeon/go-vcr v1.0.1 h1:r8L/HqC0Hje5AXMu1ooW8oyQyOFv4GxqpL0nRP7SLLY= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/docker/docker v28.0.2+incompatible h1:9BILleFwug5FSSqWBgVevgL3ewDJfWWWyZVqlDMttE8= github.com/docker/docker v28.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/drone/drone-go v1.0.5-0.20190504210458-4d6116b897ba h1:GKiT4UPBligLXJAP1zRllHvTUygAAlgS3t9LM9aasp0= github.com/drone/drone-go v1.0.5-0.20190504210458-4d6116b897ba/go.mod h1:GxyeGClYohaKNYJv/ZpsmVHtMJ7WhoT+uDaJNcDIrk4= github.com/drone/envconfig v1.4.1 h1:QROCzj78Z/4kWt0+79aO9bodvmQ44jTdQJQ8uFFV920= github.com/drone/envconfig v1.4.1/go.mod h1:jECq3U/qeRJS8nuiZbcNCeXikzUMCpNfRBY1DfU6WtA= github.com/drone/funcmap v0.0.0-20220929084810-72602997d16f h1:/jEs7lulqVO2u1+XI5rW4oFwIIusxuDOVKD9PAzlW2E= github.com/drone/funcmap v0.0.0-20220929084810-72602997d16f/go.mod h1:nDRkX7PHq+p39AD5/usv3KZMerxZTYU/9rfLS5IDspU= github.com/drone/signal v0.0.0-20170915013802-ac5d07ef1315 h1:pNSCIqkfTtVWwSHCOzCdEODGIdVh399LUj3wWiB4J+8= github.com/drone/signal v0.0.0-20170915013802-ac5d07ef1315/go.mod h1:S8t92eFT0g4WUgEc/LxG+LCuiskpMNsG0ajAMGnyZpc= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-chi/chi v3.3.2+incompatible h1:uQNcQN3NsV1j4ANsPh42P4ew4t6rnRbJb8frvpp31qQ= github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.3.0 h1:pgwjLi/dvffoP9aabwkT3AKpXQM93QARkjFhDDqC1UE= github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 h1:zLTLjkaOFEFIOxY5BWLFLwh+cL8vOBW4XJ2aqLE/Tf0= github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4= github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= github.com/gophercloud/gophercloud v0.0.0-20181014043407-c8947f7d1c51 h1:boFYpbhy0scteX2LHVBgOwPfEMaisiv6ShNqvmi16+I= github.com/gophercloud/gophercloud v0.0.0-20181014043407-c8947f7d1c51/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hetznercloud/hcloud-go v1.4.0 h1:TXLSp+Ft0jBqbL9CVfENSc+ynjkF+e0xoRko9CVpX9Y= github.com/hetznercloud/hcloud-go v1.4.0/go.mod h1:g5pff0YNAZywQaivY/CmhUYFVp7oP0nu3MiODC2W4Hw= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmoiron/sqlx v0.0.0-20180228184624-cf35089a1979 h1:2Xvj9kCxHDj//km9z+jV09L1ATggC+0pDMjqqAfyWcY= github.com/jmoiron/sqlx v0.0.0-20180228184624-cf35089a1979/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU= github.com/joho/godotenv v1.2.0 h1:vGTvz69FzUFp+X4/bAkb0j5BoLC+9bpqTWY8mjhA9pc= github.com/joho/godotenv v1.2.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 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/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 h1:yH0SvLzcbZxcJXho2yh7CqdENGMQe73Cw3woZBpPli0= github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/packethost/packngo v0.1.0 h1:G/5zumXb2fbPm5MAM3y8MmugE66Ehpio5qx0IhdhTPc= github.com/packethost/packngo v0.1.0/go.mod h1:otzZQXgoO96RTzDB/Hycg0qZcXZsWJGJRSXbmEIJ+4M= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.3 h1:4o0hpXUFqgMR1NnGbX7SeHFODlkHOZqoeVkABmnM0P8= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.3/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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/tent/http-link-go v0.0.0-20130702225549-ac974c61c2f9 h1:/Bsw4C+DEdqPjt8vAqaC9LAqpAQnaCQQqmolqq3S1T4= github.com/tent/http-link-go v0.0.0-20130702225549-ac974c61c2f9/go.mod h1:RHkNRtSLfOK7qBTHaeSX1D6BNpI3qw7NTxsmNr4RvN8= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0 h1:R/OBkMoGgfy2fLhs2QhkCI1w4HLEQX92GCcJB6SSdNk= go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0 h1:giGm8w67Ja7amYNfYMdme7xSp2pIxThWopw8+QP51Yk= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0/go.mod h1:hO1KLR7jcKaDDKDkvI9dP/FIhpmna5lkqPUQdEjFAM8= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0 h1:Ydage/P0fRrSPpZeCVxzjqGcI6iVmG2xb43+IR8cjqM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0/go.mod h1:QNX1aly8ehqqX1LEa6YniTU7VY9I6R3X/oPxhGdTceE= go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= go.opentelemetry.io/otel/sdk v1.3.0 h1:3278edCoH89MEJ0Ky8WQXVmDQv3FX4ZJ3Pp+9fJreAI= go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs= go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.11.0 h1:cLDgIBTf4lLOlztkhzAEdQsJ4Lj+i5Wc9k6Nn0K1VyU= go.opentelemetry.io/proto/otlp v0.11.0/go.mod h1:QpEjXPrNQzrFDZgoTo49dgHR9RYRSrg3NAKnUGl9YpQ= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.126.0 h1:q4GJq+cAdMAC7XP7njvQ4tvohGLiSlytuL4BQxbIZ+o= google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g= google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98/go.mod h1:S7mY02OqCJTD0E1OiQy1F72PWFB4bZJ87cAtLPYgDR0= google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw= google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= launchpad.net/gocheck v0.0.0-20140225173054-000000000087 h1:Izowp2XBH6Ya6rv+hqbceQyw/gSGoXfH/UPoTGduL54= launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= ================================================ FILE: licenses/Polyform-Free-Trial.md ================================================ # Polyform Free Trial License 1.0.0 ## Acceptance In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses. ## Copyright License The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license), and you may not distribute copies of the software. ## Changes and New Works License The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose. ## Patent License The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software. ## Fair Use You may have "fair use" rights for the software under the law. These terms do not limit them. ## Free Trial Use to evaluate whether the software suits a particular application for less than 32 consecutive calendar days, on behalf of you or your company, is use for a permitted purpose. ## No Other Rights These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses. ## Patent Defense If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company. ## Violations If you violate any of these terms, or do anything with the software not covered by your licenses, all your licenses end immediately. ## No Liability ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.*** ## Definitions The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms. **You** refers to the individual or entity agreeing to these terms. **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect. **Your licenses** are all the licenses granted to you for the software under these terms. **Use** means anything you do with the software requiring one of your licenses. ================================================ FILE: licenses/Polyform-Small-Business.md ================================================ # Polyform Small Business License 1.0.0 ## Acceptance In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses. ## Copyright License The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license). ## Distribution License The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license). ## Notices You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example: > Required Notice: Copyright Yoyodyne, Inc. (http://example.com) ## Changes and New Works License The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose. ## Patent License The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software. ## Fair Use You may have "fair use" rights for the software under the law. These terms do not limit them. ## Small Business Use of the software for the benefit of your company is use for a permitted purpose if your company has fewer than 100 total individuals working as employees and independent contractors, and less than 1,000,000 USD (2019) total revenue in the prior tax year. Adjust this revenue threshold for inflation according to the United States Bureau of Labor Statistics' consumer price index for all urban consumers, U.S. city average, for all items, not seasonally adjusted, with 1982–1984=100 reference base. ## No Other Rights These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses. ## Patent Defense If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company. ## Violations The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately. ## No Liability ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.*** ## Definitions The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms. **You** refers to the individual or entity agreeing to these terms. **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect. **Your licenses** are all the licenses granted to you for the software under these terms. **Use** means anything you do with the software requiring one of your licenses. ================================================ FILE: logger/context.go ================================================ // Copyright 2019 Drone.IO Inc. All rights reserved. // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package logger import ( "context" "net/http" ) type loggerKey struct{} // WithContext returns a new context with the provided logger. // Use in combination with logger.WithField for great effect. func WithContext(ctx context.Context, logger Logger) context.Context { return context.WithValue(ctx, loggerKey{}, logger) } // FromContext retrieves the current logger from the context. func FromContext(ctx context.Context) Logger { logger := ctx.Value(loggerKey{}) if logger == nil { return Default } return logger.(Logger) } // FromRequest retrieves the current logger from the request. If no // logger is available, the default logger is returned. func FromRequest(r *http.Request) Logger { return FromContext(r.Context()) } ================================================ FILE: logger/context_test.go ================================================ // Copyright 2019 Drone.IO Inc. All rights reserved. // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package logger import ( "context" "net/http" "testing" ) func TestContext(t *testing.T) { entry := Discard() ctx := WithContext(context.Background(), entry) got := FromContext(ctx) if got != entry { t.Errorf("Expected Logger from context") } } func TestEmptyContext(t *testing.T) { got := FromContext(context.Background()) if got == nil { t.Errorf("Expected Logger from context") } if _, ok := got.(*discard); !ok { t.Errorf("Expected discard Logger from context") } } func TestRequest(t *testing.T) { entry := Discard() ctx := WithContext(context.Background(), entry) req := new(http.Request) req = req.WithContext(ctx) got := FromRequest(req) if got != entry { t.Errorf("Expected Logger from http.Request") } } ================================================ FILE: logger/history/history.go ================================================ // Copyright 2019 Drone.IO Inc. All rights reserved. // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package history // Package history implements a logrus hook that provides access // to log recent log activity. import ( "sync" "github.com/sirupsen/logrus" ) // default log entry limit. const defaultLimit = 250 // Level is the log level. type Level string // log levels. const ( LevelError = Level("error") LevelWarn = Level("warn") LevelInfo = Level("info") LevelDebug = Level("debug") LevelTrace = Level("trace") ) // Entry provides a log entry. type Entry struct { Level Level Message string Data map[string]interface{} Unix int64 } // Hook is a logrus hook that track the log history. type Hook struct { sync.RWMutex limit int entries []*Entry } // New returns a new history hook. func New() *Hook { return NewLimit(defaultLimit) } // NewLimit returns a new history hook with a custom // history limit. func NewLimit(limit int) *Hook { return &Hook{limit: limit} } // Fire receives the log entry. func (h *Hook) Fire(e *logrus.Entry) error { // ignore http request logs if _, ok := e.Data["user-agent"]; ok { return nil } h.Lock() if len(h.entries) >= h.limit { h.entries = h.entries[1:] } h.entries = append(h.entries, &Entry{ Level: convertLevel(e.Level), Data: convertFields(e.Data), Unix: e.Time.Unix(), Message: e.Message, }) h.Unlock() return nil } // Levels returns the supported log levels. func (h *Hook) Levels() []logrus.Level { return logrus.AllLevels } // Entries returns a list of all entries. func (h *Hook) Entries() []*Entry { h.RLock() defer h.RUnlock() entries := make([]*Entry, len(h.entries)) for i, entry := range h.entries { entries[i] = copyEntry(entry) } return entries } // Filter returns a list of all entries for which the filter // function returns true. func (h *Hook) Filter(filter func(*Entry) bool) []*Entry { h.RLock() defer h.RUnlock() var entries []*Entry for _, entry := range h.entries { if filter(entry) { entries = append(entries, copyEntry(entry)) } } return entries } // helper funtion copies an entry for threadsafe access. func copyEntry(src *Entry) *Entry { dst := new(Entry) *dst = *src dst.Data = map[string]interface{}{} for k, v := range src.Data { dst.Data[k] = v } return dst } // helper function converts a logrus.Level to the local type. func convertLevel(level logrus.Level) Level { switch level { case logrus.PanicLevel: return LevelError case logrus.FatalLevel: return LevelError case logrus.ErrorLevel: return LevelError case logrus.WarnLevel: return LevelWarn case logrus.DebugLevel: return LevelDebug case logrus.TraceLevel: return LevelTrace default: return LevelInfo } } // helper fucntion copies logrus.Fields to a basic map. func convertFields(src logrus.Fields) map[string]interface{} { dst := map[string]interface{}{} for k, v := range src { dst[k] = v } return dst } ================================================ FILE: logger/history/history_test.go ================================================ // Copyright 2019 Drone.IO Inc. All rights reserved. // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package history import ( "testing" "time" "github.com/google/go-cmp/cmp" "github.com/sirupsen/logrus" ) func TestLevels(t *testing.T) { hook := New() if diff := cmp.Diff(hook.Levels(), logrus.AllLevels); diff != "" { t.Errorf("Hook should return all levels") t.Log(diff) } } func TestConvertLevels(t *testing.T) { tests := []struct { before logrus.Level after Level }{ {logrus.PanicLevel, LevelError}, {logrus.FatalLevel, LevelError}, {logrus.ErrorLevel, LevelError}, {logrus.WarnLevel, LevelWarn}, {logrus.InfoLevel, LevelInfo}, {logrus.DebugLevel, LevelDebug}, {logrus.TraceLevel, LevelTrace}, } if len(tests) != len(logrus.AllLevels) { t.Errorf("missing unit tests for all logrus levels") } for _, test := range tests { if got, want := convertLevel(test.before), test.after; got != want { t.Errorf("Want entry level %v, got %v", want, got) } } } func TestLimit(t *testing.T) { hook := NewLimit(4) hook.Fire(&logrus.Entry{}) hook.Fire(&logrus.Entry{}) hook.Fire(&logrus.Entry{}) hook.Fire(&logrus.Entry{}) hook.Fire(&logrus.Entry{}) if got, want := len(hook.entries), 4; got != want { t.Errorf("Expect entries pruned to %d, got %d", want, got) } } func TestHistory(t *testing.T) { hook := New() now := time.Now() hook.Fire(&logrus.Entry{ Level: logrus.DebugLevel, Message: "foo", Data: logrus.Fields{"foo": "bar"}, Time: now, }) hook.Fire(&logrus.Entry{ Level: logrus.InfoLevel, Message: "bar", Data: logrus.Fields{"baz": "qux"}, Time: now, }) if len(hook.entries) != 2 { t.Errorf("Expected 2 hooks added to history") } entries := hook.Entries() if len(entries) != 2 { t.Errorf("Expected 2 hooks returned") } if entries[0] == hook.entries[0] { t.Errorf("Expect copy of entries, got a reference") } if entries[1] == hook.entries[1] { t.Errorf("Expect copy of entries, got a reference") } expect := []*Entry{ { Level: LevelDebug, Message: "foo", Data: logrus.Fields{"foo": "bar"}, Unix: now.Unix(), }, { Level: LevelInfo, Message: "bar", Data: logrus.Fields{"baz": "qux"}, Unix: now.Unix(), }, } if diff := cmp.Diff(entries, expect); diff != "" { t.Errorf("Entries should return an exact copy of all entries") t.Log(diff) } } func TestFilter(t *testing.T) { hook := New() now := time.Now() hook.Fire(&logrus.Entry{ Level: logrus.DebugLevel, Message: "foo", Data: logrus.Fields{"foo": "bar"}, Time: now, }) hook.Fire(&logrus.Entry{ Level: logrus.InfoLevel, Message: "bar", Data: logrus.Fields{"baz": "qux"}, Time: now, }) expect := []*Entry{ { Level: LevelDebug, Message: "foo", Data: logrus.Fields{"foo": "bar"}, Unix: now.Unix(), }, } entries := hook.Filter(func(entry *Entry) bool { return entry.Data["foo"] == "bar" }) if diff := cmp.Diff(entries, expect); diff != "" { t.Errorf("Entries should return an exact copy of all entries") t.Log(diff) } } ================================================ FILE: logger/logger.go ================================================ // Copyright 2019 Drone.IO Inc. All rights reserved. // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. // Package logger defines interfaces that logger drivers // implement to log messages. package logger // A Logger represents an active logging object that generates // lines of output to an io.Writer. type Logger interface { Debug(args ...interface{}) Debugf(format string, args ...interface{}) Debugln(args ...interface{}) Error(args ...interface{}) Errorf(format string, args ...interface{}) Errorln(args ...interface{}) Info(args ...interface{}) Infof(format string, args ...interface{}) Infoln(args ...interface{}) Trace(args ...interface{}) Tracef(format string, args ...interface{}) Traceln(args ...interface{}) Warn(args ...interface{}) Warnf(format string, args ...interface{}) Warnln(args ...interface{}) WithError(error) Logger WithField(string, interface{}) Logger } // Default returns the default logger. var Default = Discard() // Discard returns a no-op logger func Discard() Logger { return &discard{} } type discard struct{} func (*discard) Debug(args ...interface{}) {} func (*discard) Debugf(format string, args ...interface{}) {} func (*discard) Debugln(args ...interface{}) {} func (*discard) Error(args ...interface{}) {} func (*discard) Errorf(format string, args ...interface{}) {} func (*discard) Errorln(args ...interface{}) {} func (*discard) Info(args ...interface{}) {} func (*discard) Infof(format string, args ...interface{}) {} func (*discard) Infoln(args ...interface{}) {} func (*discard) Trace(args ...interface{}) {} func (*discard) Tracef(format string, args ...interface{}) {} func (*discard) Traceln(args ...interface{}) {} func (*discard) Warn(args ...interface{}) {} func (*discard) Warnf(format string, args ...interface{}) {} func (*discard) Warnln(args ...interface{}) {} func (d *discard) WithError(error) Logger { return d } func (d *discard) WithField(string, interface{}) Logger { return d } ================================================ FILE: logger/logger_test.go ================================================ // Copyright 2019 Drone.IO Inc. All rights reserved. // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package logger import "testing" func TestWithError(t *testing.T) { d := &discard{} if d.WithError(nil) != d { t.Errorf("Expect WithError to return base logger") } } func TestWithField(t *testing.T) { d := &discard{} if d.WithField("hello", "world") != d { t.Errorf("Expect WithField to return base logger") } } ================================================ FILE: logger/logrus.go ================================================ // Copyright 2019 Drone.IO Inc. All rights reserved. // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package logger import "github.com/sirupsen/logrus" // Logrus returns a Logger that wraps a logrus.Entry. func Logrus(entry *logrus.Entry) Logger { return &wrapLogrus{entry} } type wrapLogrus struct { *logrus.Entry } func (w *wrapLogrus) WithError(err error) Logger { return &wrapLogrus{w.Entry.WithError(err)} } func (w *wrapLogrus) WithField(key string, value interface{}) Logger { return &wrapLogrus{w.Entry.WithField(key, value)} } ================================================ FILE: logger/logrus_test.go ================================================ // Copyright 2019 Drone.IO Inc. All rights reserved. // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package logger import ( "testing" "github.com/sirupsen/logrus" ) func TestLogrus(t *testing.T) { logger := Logrus( logrus.NewEntry( logrus.StandardLogger(), ), ) if _, ok := logger.(*wrapLogrus); !ok { t.Errorf("Expect wrapped logrus") } if _, ok := logger.WithError(nil).(*wrapLogrus); !ok { t.Errorf("Expect WithError wraps logrus") } if _, ok := logger.WithField("foo", "bar").(*wrapLogrus); !ok { t.Errorf("Expect WithField logrus") } } ================================================ FILE: logger/request/request.go ================================================ // Copyright 2019 Drone.IO Inc. All rights reserved. // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package request import ( "net/http" "time" "github.com/drone/autoscaler/logger" "github.com/go-chi/chi/middleware" "github.com/sirupsen/logrus" ) // Logger provides logrus middleware. func Logger(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { start := time.Now() rw := middleware.NewWrapResponseWriter(w, r.ProtoMajor) fields := logrus.Fields{ "method": r.Method, "request": r.RequestURI, "remote": r.RemoteAddr, "referer": r.Referer(), "user-agent": r.UserAgent(), } log := logrus.WithFields(fields) ctx := r.Context() ctx = logger.WithContext(ctx, logger.Logrus(log)) next.ServeHTTP(rw, r) fields["status"] = rw.Status() fields["duration"] = time.Since(start) if id := r.Context().Value(middleware.RequestIDKey); id != nil { fields["request-id"] = id } log.WithFields(fields).Debugln("request completed") } return http.HandlerFunc(fn) } ================================================ FILE: metrics/metrics.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package metrics import ( "context" "time" "github.com/prometheus/client_golang/prometheus" ) var noContext = context.Background() // Collector defines a metrics collector. type Collector interface { // TrackServerCreateTime registers the elapsed time it takes // to provision a server instance. TrackServerCreateTime(start time.Time) // TrackServerInitTime registers the elapsed time it takes // for a server instance to initialize and begin accepting // network connections. TrackServerInitTime(start time.Time) // TrackServerSetupTime registers the elapsed time it takes // to install software (i.e. docker, runners) on the server. TrackServerSetupTime(start time.Time) // IncrServerCreateError keeps a count of errors encountered // when provisioning servers. IncrServerCreateError() // IncrServerInitError keeps a count of errors encountered // when initializing and establishing networking connections // with servers. IncrServerInitError() // IncrServerSetupError keeps a count of errors encountered // when installing software on servers. IncrServerSetupError() } // Prometheus is a Prometheus metrics collector. type Prometheus struct { trackServerCreateTime prometheus.Histogram trackServerInitTime prometheus.Histogram trackServerSetupTime prometheus.Histogram countServerCreateErr prometheus.Counter countServerInitErr prometheus.Counter countServerSetupErr prometheus.Counter } // New returns a new Prometheus metrics provider. func New() *Prometheus { p := new(Prometheus) p.trackServerCreateTime = prometheus.NewHistogram(prometheus.HistogramOpts{ Name: "drone_server_create_time_seconds", Help: "Elapsed time creating a server.", Buckets: []float64{60, 150, 300, 600, 900, 1200}, }) p.trackServerInitTime = prometheus.NewHistogram(prometheus.HistogramOpts{ Name: "drone_server_boot_time_seconds", Help: "Elapsed time initializing a server.", Buckets: []float64{60, 150, 300, 600, 900, 1200}, }) p.trackServerSetupTime = prometheus.NewHistogram(prometheus.HistogramOpts{ Name: "drone_server_install_time_seconds", Help: "Elapsed time installing software on a server.", Buckets: []float64{60, 150, 300, 600, 900, 1200}, }) p.countServerCreateErr = prometheus.NewCounter(prometheus.CounterOpts{ Name: "drone_server_create_errors_total", Help: "Total number of errors initializing a server.", }) p.countServerInitErr = prometheus.NewCounter(prometheus.CounterOpts{ Name: "drone_server_boot_errors_total", Help: "Total number of errors initializing a server.", }) p.countServerSetupErr = prometheus.NewCounter(prometheus.CounterOpts{ Name: "drone_server_install_errors_total", Help: "Total number of errors installing software on a server.", }) prometheus.MustRegister(p.trackServerCreateTime) prometheus.MustRegister(p.trackServerInitTime) prometheus.MustRegister(p.trackServerSetupTime) prometheus.MustRegister(p.countServerCreateErr) prometheus.MustRegister(p.countServerInitErr) prometheus.MustRegister(p.countServerSetupErr) return p } // TrackServerCreateTime registers the elapsed time it takes // to provision a server instance. func (m *Prometheus) TrackServerCreateTime(start time.Time) { m.trackServerCreateTime.Observe( time.Now().Sub(start).Round(time.Second).Seconds(), ) } // TrackServerInitTime registers the elapsed time it takes // for a server instance to initialize and begin accepting // network connections. func (m *Prometheus) TrackServerInitTime(start time.Time) { m.trackServerInitTime.Observe( time.Now().Sub(start).Round(time.Second).Seconds(), ) } // TrackServerSetupTime registers the elapsed time it takes // to install software (i.e. docker, runners) on the server. func (m *Prometheus) TrackServerSetupTime(start time.Time) { m.trackServerSetupTime.Observe( time.Now().Sub(start).Round(time.Second).Seconds(), ) } // IncrServerCreateError keeps a count of errors encountered // when provisioning servers. func (m *Prometheus) IncrServerCreateError() { m.countServerCreateErr.Inc() } // IncrServerInitError keeps a count of errors encountered // when initializing and establishing networking connections // with servers. func (m *Prometheus) IncrServerInitError() { m.countServerInitErr.Inc() } // IncrServerSetupError keeps a count of errors encountered // when installing software on servers. func (m *Prometheus) IncrServerSetupError() { m.countServerSetupErr.Inc() } // NopCollector provides a no-op metrics collector. type NopCollector struct{} // TrackServerCreateTime registers the elapsed time it takes // to provision a server instance. func (*NopCollector) TrackServerCreateTime(start time.Time) {} // TrackServerInitTime registers the elapsed time it takes // for a server instance to initialize and begin accepting // network connections. func (*NopCollector) TrackServerInitTime(start time.Time) {} // TrackServerSetupTime registers the elapsed time it takes // to install software (i.e. docker, runners) on the server. func (*NopCollector) TrackServerSetupTime(start time.Time) {} // IncrServerCreateError keeps a count of errors encountered // when provisioning servers. func (*NopCollector) IncrServerCreateError() {} // IncrServerInitError keeps a count of errors encountered // when initializing and establishing networking connections // with servers. func (*NopCollector) IncrServerInitError() {} // IncrServerSetupError keeps a count of errors encountered // when installing software on servers. func (*NopCollector) IncrServerSetupError() {} ================================================ FILE: metrics/server_capacity.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package metrics import ( "github.com/drone/autoscaler" "github.com/prometheus/client_golang/prometheus" ) // ServerCapacity provides metrics for server capacity count. func ServerCapacity(store autoscaler.ServerStore) autoscaler.ServerStore { prometheus.MustRegister( prometheus.NewGaugeFunc(prometheus.GaugeOpts{ Name: "drone_server_capacity", Help: "Total capacity of active servers.", }, func() float64 { var capacity int servers, _ := store.ListState(noContext, autoscaler.StateRunning) for _, server := range servers { capacity += server.Capacity } return float64(capacity) }), ) return store } ================================================ FILE: metrics/server_capacity_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package metrics import ( "testing" "time" "github.com/drone/autoscaler" "github.com/drone/autoscaler/mocks" "github.com/golang/mock/gomock" "github.com/prometheus/client_golang/prometheus" ) func TestServerCapacity(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() // restore the default prometheus registerer // when the unit test is complete. snapshot := prometheus.DefaultRegisterer defer func() { prometheus.DefaultRegisterer = snapshot }() // creates a blank registry registry := prometheus.NewRegistry() prometheus.DefaultRegisterer = registry // x2 server count // x3 server capacity servers := []*autoscaler.Server{ {Name: "server1", Capacity: 1, Created: time.Now().Unix()}, {Name: "server2", Capacity: 2, Created: time.Now().Unix()}, } store := mocks.NewMockServerStore(controller) store.EXPECT().ListState(gomock.Any(), autoscaler.StateRunning).Return(servers, nil) ServerCapacity(store) metrics, err := registry.Gather() if err != nil { t.Error(err) return } if want, got := len(metrics), 1; want != got { t.Errorf("Expect registered metric") return } metric := metrics[0] if want, got := metric.GetName(), "drone_server_capacity"; want != got { t.Errorf("Expect metric name %s, got %s", want, got) } if want, got := metric.Metric[0].Gauge.GetValue(), float64(3); want != got { t.Errorf("Expect metric value %f, got %f", want, got) } } ================================================ FILE: metrics/server_count.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package metrics import ( "github.com/drone/autoscaler" "github.com/prometheus/client_golang/prometheus" ) // ServerCount provides metrics for server counts. func ServerCount(store autoscaler.ServerStore) autoscaler.ServerStore { prometheus.MustRegister( prometheus.NewGaugeFunc(prometheus.GaugeOpts{ Name: "drone_server_count", Help: "Total number of active servers.", }, func() float64 { servers, _ := store.ListState(noContext, autoscaler.StateRunning) return float64(len(servers)) }), ) return store } ================================================ FILE: metrics/server_count_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package metrics import ( "testing" "time" "github.com/drone/autoscaler" "github.com/drone/autoscaler/mocks" "github.com/golang/mock/gomock" "github.com/prometheus/client_golang/prometheus" ) func TestServerCount(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() // restore the default prometheus registerer // when the unit test is complete. snapshot := prometheus.DefaultRegisterer defer func() { prometheus.DefaultRegisterer = snapshot }() // creates a blank registry registry := prometheus.NewRegistry() prometheus.DefaultRegisterer = registry // x2 server count servers := []*autoscaler.Server{ {Name: "server1", Capacity: 1, Created: time.Now().Unix()}, {Name: "server2", Capacity: 1, Created: time.Now().Unix()}, } store := mocks.NewMockServerStore(controller) store.EXPECT().ListState(gomock.Any(), autoscaler.StateRunning).Return(servers, nil).AnyTimes() ServerCount(store) metrics, err := registry.Gather() if err != nil { t.Error(err) return } if want, got := len(metrics), 1; want != got { t.Errorf("Expect registered metric") return } metric := metrics[0] if want, got := metric.GetName(), "drone_server_count"; want != got { t.Errorf("Expect metric name %s, got %s", want, got) } if want, got := metric.Metric[0].Gauge.GetValue(), float64(len(servers)); want != got { t.Errorf("Expect metric value %f, got %f", want, got) } } ================================================ FILE: metrics/server_create.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package metrics import ( "context" "github.com/drone/autoscaler" "github.com/prometheus/client_golang/prometheus" ) // ServerCreate provides metrics for servers created. func ServerCreate(provider autoscaler.Provider) autoscaler.Provider { counter := prometheus.NewCounter(prometheus.CounterOpts{ Name: "drone_servers_created", Help: "Total number of servers created.", }) errors := prometheus.NewCounter(prometheus.CounterOpts{ Name: "drone_servers_created_err", Help: "Total number of server creation errors.", }) prometheus.MustRegister(counter) prometheus.MustRegister(errors) return &providerWrapCreate{ Provider: provider, created: counter, errors: errors, } } // instruments the Provider to count server create events. type providerWrapCreate struct { autoscaler.Provider created prometheus.Counter errors prometheus.Counter } func (p *providerWrapCreate) Create(ctx context.Context, opts autoscaler.InstanceCreateOpts) (*autoscaler.Instance, error) { instance, err := p.Provider.Create(ctx, opts) if err == nil { p.created.Add(1) } else { p.errors.Add(1) } return instance, err } ================================================ FILE: metrics/server_create_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package metrics import ( "errors" "testing" "github.com/drone/autoscaler" "github.com/drone/autoscaler/mocks" "github.com/golang/mock/gomock" "github.com/prometheus/client_golang/prometheus" ) func TestServerCreate(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() // restore the default prometheus registerer // when the unit test is complete. snapshot := prometheus.DefaultRegisterer defer func() { prometheus.DefaultRegisterer = snapshot }() // creates a blank registry registry := prometheus.NewRegistry() prometheus.DefaultRegisterer = registry opts := autoscaler.InstanceCreateOpts{Name: "server1"} instance := &autoscaler.Instance{} provider := mocks.NewMockProvider(controller) provider.EXPECT().Create(gomock.Any(), opts).Times(3).Return(instance, nil) provider.EXPECT().Create(gomock.Any(), opts).Return(nil, errors.New("error")) providerInst := ServerCreate(provider) for i := 0; i < 3; i++ { res, err := providerInst.Create(noContext, opts) if err != nil { t.Error(err) } if res != instance { t.Errorf("Expect instance returned") } } _, err := providerInst.Create(noContext, opts) if err == nil { t.Errorf("Expect error returned from provider") } metrics, err := registry.Gather() if err != nil { t.Error(err) return } if want, got := len(metrics), 2; want != got { t.Errorf("Expect registered metric") return } if got, want := metrics[0].GetName(), "drone_servers_created"; want != got { t.Errorf("Expect metric name %s, got %s", want, got) } if got, want := metrics[0].Metric[0].Counter.GetValue(), float64(3); want != got { t.Errorf("Expect metric value %f, got %f", want, got) } if got, want := metrics[1].GetName(), "drone_servers_created_err"; want != got { t.Errorf("Expect metric name %s, got %s", want, got) } if got, want := metrics[1].Metric[0].Counter.GetValue(), float64(1); want != got { t.Errorf("Expect metric value %f, got %f", want, got) } } ================================================ FILE: metrics/server_delete.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package metrics import ( "context" "github.com/drone/autoscaler" "github.com/prometheus/client_golang/prometheus" ) // ServerDelete provides metrics for servers deleted. func ServerDelete(provider autoscaler.Provider) autoscaler.Provider { created := prometheus.NewCounter(prometheus.CounterOpts{ Name: "drone_servers_deleted", Help: "Total number of servers deleted.", }) errors := prometheus.NewCounter(prometheus.CounterOpts{ Name: "drone_servers_deleted_err", Help: "Total number of server deletion errors.", }) prometheus.MustRegister(created) prometheus.MustRegister(errors) return &providerWrapDestroy{ Provider: provider, created: created, errors: errors, } } // instruments the Provider to count server destroy events. type providerWrapDestroy struct { autoscaler.Provider created prometheus.Counter errors prometheus.Counter } func (p *providerWrapDestroy) Destroy(ctx context.Context, instance *autoscaler.Instance) error { err := p.Provider.Destroy(ctx, instance) if err == nil { p.created.Add(1) } else { p.errors.Add(1) } return err } ================================================ FILE: metrics/server_delete_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package metrics import ( "errors" "testing" "github.com/drone/autoscaler" "github.com/drone/autoscaler/mocks" "github.com/golang/mock/gomock" "github.com/prometheus/client_golang/prometheus" ) func TestServerDelete(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() // restore the default prometheus registerer // when the unit test is complete. snapshot := prometheus.DefaultRegisterer defer func() { prometheus.DefaultRegisterer = snapshot }() // creates a blank registry registry := prometheus.NewRegistry() prometheus.DefaultRegisterer = registry instance := &autoscaler.Instance{Name: "server1"} provider := mocks.NewMockProvider(controller) provider.EXPECT().Destroy(noContext, instance).Times(3).Return(nil) provider.EXPECT().Destroy(noContext, instance).Return(errors.New("error")) providerInst := ServerDelete(provider) for i := 0; i < 3; i++ { err := providerInst.Destroy(noContext, instance) if err != nil { t.Error(err) } } err := providerInst.Destroy(noContext, instance) if err == nil { t.Errorf("Expect error returned from provider") } metrics, err := registry.Gather() if err != nil { t.Error(err) return } if want, got := len(metrics), 2; want != got { t.Errorf("Expect registered metric") return } if got, want := metrics[0].GetName(), "drone_servers_deleted"; want != got { t.Errorf("Expect metric name %s, got %s", want, got) } if got, want := metrics[0].Metric[0].Counter.GetValue(), float64(3); want != got { t.Errorf("Expect metric value %f, got %f", want, got) } if got, want := metrics[1].GetName(), "drone_servers_deleted_err"; want != got { t.Errorf("Expect metric name %s, got %s", want, got) } if got, want := metrics[1].Metric[0].Counter.GetValue(), float64(1); want != got { t.Errorf("Expect metric value %f, got %f", want, got) } } ================================================ FILE: mocks/mock_docker.go ================================================ // Code generated by MockGen. DO NOT EDIT. // Source: github.com/docker/docker/client (interfaces: APIClient) // Package mocks is a generated GoMock package. package mocks import ( context "context" io "io" net "net" http "net/http" reflect "reflect" types "github.com/docker/docker/api/types" checkpoint "github.com/docker/docker/api/types/checkpoint" common "github.com/docker/docker/api/types/common" container "github.com/docker/docker/api/types/container" events "github.com/docker/docker/api/types/events" filters "github.com/docker/docker/api/types/filters" image "github.com/docker/docker/api/types/image" network "github.com/docker/docker/api/types/network" registry "github.com/docker/docker/api/types/registry" swarm "github.com/docker/docker/api/types/swarm" system "github.com/docker/docker/api/types/system" volume "github.com/docker/docker/api/types/volume" client "github.com/docker/docker/client" gomock "github.com/golang/mock/gomock" v1 "github.com/opencontainers/image-spec/specs-go/v1" ) // MockAPIClient is a mock of APIClient interface. type MockAPIClient struct { ctrl *gomock.Controller recorder *MockAPIClientMockRecorder } // MockAPIClientMockRecorder is the mock recorder for MockAPIClient. type MockAPIClientMockRecorder struct { mock *MockAPIClient } // NewMockAPIClient creates a new mock instance. func NewMockAPIClient(ctrl *gomock.Controller) *MockAPIClient { mock := &MockAPIClient{ctrl: ctrl} mock.recorder = &MockAPIClientMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockAPIClient) EXPECT() *MockAPIClientMockRecorder { return m.recorder } // BuildCachePrune mocks base method. func (m *MockAPIClient) BuildCachePrune(arg0 context.Context, arg1 types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "BuildCachePrune", arg0, arg1) ret0, _ := ret[0].(*types.BuildCachePruneReport) ret1, _ := ret[1].(error) return ret0, ret1 } // BuildCachePrune indicates an expected call of BuildCachePrune. func (mr *MockAPIClientMockRecorder) BuildCachePrune(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildCachePrune", reflect.TypeOf((*MockAPIClient)(nil).BuildCachePrune), arg0, arg1) } // BuildCancel mocks base method. func (m *MockAPIClient) BuildCancel(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "BuildCancel", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // BuildCancel indicates an expected call of BuildCancel. func (mr *MockAPIClientMockRecorder) BuildCancel(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildCancel", reflect.TypeOf((*MockAPIClient)(nil).BuildCancel), arg0, arg1) } // CheckpointCreate mocks base method. func (m *MockAPIClient) CheckpointCreate(arg0 context.Context, arg1 string, arg2 checkpoint.CreateOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CheckpointCreate", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // CheckpointCreate indicates an expected call of CheckpointCreate. func (mr *MockAPIClientMockRecorder) CheckpointCreate(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckpointCreate", reflect.TypeOf((*MockAPIClient)(nil).CheckpointCreate), arg0, arg1, arg2) } // CheckpointDelete mocks base method. func (m *MockAPIClient) CheckpointDelete(arg0 context.Context, arg1 string, arg2 checkpoint.DeleteOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CheckpointDelete", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // CheckpointDelete indicates an expected call of CheckpointDelete. func (mr *MockAPIClientMockRecorder) CheckpointDelete(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckpointDelete", reflect.TypeOf((*MockAPIClient)(nil).CheckpointDelete), arg0, arg1, arg2) } // CheckpointList mocks base method. func (m *MockAPIClient) CheckpointList(arg0 context.Context, arg1 string, arg2 checkpoint.ListOptions) ([]checkpoint.Summary, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CheckpointList", arg0, arg1, arg2) ret0, _ := ret[0].([]checkpoint.Summary) ret1, _ := ret[1].(error) return ret0, ret1 } // CheckpointList indicates an expected call of CheckpointList. func (mr *MockAPIClientMockRecorder) CheckpointList(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckpointList", reflect.TypeOf((*MockAPIClient)(nil).CheckpointList), arg0, arg1, arg2) } // ClientVersion mocks base method. func (m *MockAPIClient) ClientVersion() string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ClientVersion") ret0, _ := ret[0].(string) return ret0 } // ClientVersion indicates an expected call of ClientVersion. func (mr *MockAPIClientMockRecorder) ClientVersion() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientVersion", reflect.TypeOf((*MockAPIClient)(nil).ClientVersion)) } // Close mocks base method. func (m *MockAPIClient) Close() error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Close") ret0, _ := ret[0].(error) return ret0 } // Close indicates an expected call of Close. func (mr *MockAPIClientMockRecorder) Close() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockAPIClient)(nil).Close)) } // ConfigCreate mocks base method. func (m *MockAPIClient) ConfigCreate(arg0 context.Context, arg1 swarm.ConfigSpec) (types.ConfigCreateResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ConfigCreate", arg0, arg1) ret0, _ := ret[0].(types.ConfigCreateResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // ConfigCreate indicates an expected call of ConfigCreate. func (mr *MockAPIClientMockRecorder) ConfigCreate(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigCreate", reflect.TypeOf((*MockAPIClient)(nil).ConfigCreate), arg0, arg1) } // ConfigInspectWithRaw mocks base method. func (m *MockAPIClient) ConfigInspectWithRaw(arg0 context.Context, arg1 string) (swarm.Config, []byte, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ConfigInspectWithRaw", arg0, arg1) ret0, _ := ret[0].(swarm.Config) ret1, _ := ret[1].([]byte) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } // ConfigInspectWithRaw indicates an expected call of ConfigInspectWithRaw. func (mr *MockAPIClientMockRecorder) ConfigInspectWithRaw(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigInspectWithRaw", reflect.TypeOf((*MockAPIClient)(nil).ConfigInspectWithRaw), arg0, arg1) } // ConfigList mocks base method. func (m *MockAPIClient) ConfigList(arg0 context.Context, arg1 types.ConfigListOptions) ([]swarm.Config, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ConfigList", arg0, arg1) ret0, _ := ret[0].([]swarm.Config) ret1, _ := ret[1].(error) return ret0, ret1 } // ConfigList indicates an expected call of ConfigList. func (mr *MockAPIClientMockRecorder) ConfigList(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigList", reflect.TypeOf((*MockAPIClient)(nil).ConfigList), arg0, arg1) } // ConfigRemove mocks base method. func (m *MockAPIClient) ConfigRemove(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ConfigRemove", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // ConfigRemove indicates an expected call of ConfigRemove. func (mr *MockAPIClientMockRecorder) ConfigRemove(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigRemove", reflect.TypeOf((*MockAPIClient)(nil).ConfigRemove), arg0, arg1) } // ConfigUpdate mocks base method. func (m *MockAPIClient) ConfigUpdate(arg0 context.Context, arg1 string, arg2 swarm.Version, arg3 swarm.ConfigSpec) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ConfigUpdate", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) return ret0 } // ConfigUpdate indicates an expected call of ConfigUpdate. func (mr *MockAPIClientMockRecorder) ConfigUpdate(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigUpdate", reflect.TypeOf((*MockAPIClient)(nil).ConfigUpdate), arg0, arg1, arg2, arg3) } // ContainerAttach mocks base method. func (m *MockAPIClient) ContainerAttach(arg0 context.Context, arg1 string, arg2 container.AttachOptions) (types.HijackedResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerAttach", arg0, arg1, arg2) ret0, _ := ret[0].(types.HijackedResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // ContainerAttach indicates an expected call of ContainerAttach. func (mr *MockAPIClientMockRecorder) ContainerAttach(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerAttach", reflect.TypeOf((*MockAPIClient)(nil).ContainerAttach), arg0, arg1, arg2) } // ContainerCommit mocks base method. func (m *MockAPIClient) ContainerCommit(arg0 context.Context, arg1 string, arg2 container.CommitOptions) (common.IDResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerCommit", arg0, arg1, arg2) ret0, _ := ret[0].(common.IDResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // ContainerCommit indicates an expected call of ContainerCommit. func (mr *MockAPIClientMockRecorder) ContainerCommit(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerCommit", reflect.TypeOf((*MockAPIClient)(nil).ContainerCommit), arg0, arg1, arg2) } // ContainerCreate mocks base method. func (m *MockAPIClient) ContainerCreate(arg0 context.Context, arg1 *container.Config, arg2 *container.HostConfig, arg3 *network.NetworkingConfig, arg4 *v1.Platform, arg5 string) (container.CreateResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerCreate", arg0, arg1, arg2, arg3, arg4, arg5) ret0, _ := ret[0].(container.CreateResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // ContainerCreate indicates an expected call of ContainerCreate. func (mr *MockAPIClientMockRecorder) ContainerCreate(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerCreate", reflect.TypeOf((*MockAPIClient)(nil).ContainerCreate), arg0, arg1, arg2, arg3, arg4, arg5) } // ContainerDiff mocks base method. func (m *MockAPIClient) ContainerDiff(arg0 context.Context, arg1 string) ([]container.FilesystemChange, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerDiff", arg0, arg1) ret0, _ := ret[0].([]container.FilesystemChange) ret1, _ := ret[1].(error) return ret0, ret1 } // ContainerDiff indicates an expected call of ContainerDiff. func (mr *MockAPIClientMockRecorder) ContainerDiff(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerDiff", reflect.TypeOf((*MockAPIClient)(nil).ContainerDiff), arg0, arg1) } // ContainerExecAttach mocks base method. func (m *MockAPIClient) ContainerExecAttach(arg0 context.Context, arg1 string, arg2 container.ExecStartOptions) (types.HijackedResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerExecAttach", arg0, arg1, arg2) ret0, _ := ret[0].(types.HijackedResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // ContainerExecAttach indicates an expected call of ContainerExecAttach. func (mr *MockAPIClientMockRecorder) ContainerExecAttach(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerExecAttach", reflect.TypeOf((*MockAPIClient)(nil).ContainerExecAttach), arg0, arg1, arg2) } // ContainerExecCreate mocks base method. func (m *MockAPIClient) ContainerExecCreate(arg0 context.Context, arg1 string, arg2 container.ExecOptions) (common.IDResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerExecCreate", arg0, arg1, arg2) ret0, _ := ret[0].(common.IDResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // ContainerExecCreate indicates an expected call of ContainerExecCreate. func (mr *MockAPIClientMockRecorder) ContainerExecCreate(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerExecCreate", reflect.TypeOf((*MockAPIClient)(nil).ContainerExecCreate), arg0, arg1, arg2) } // ContainerExecInspect mocks base method. func (m *MockAPIClient) ContainerExecInspect(arg0 context.Context, arg1 string) (container.ExecInspect, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerExecInspect", arg0, arg1) ret0, _ := ret[0].(container.ExecInspect) ret1, _ := ret[1].(error) return ret0, ret1 } // ContainerExecInspect indicates an expected call of ContainerExecInspect. func (mr *MockAPIClientMockRecorder) ContainerExecInspect(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerExecInspect", reflect.TypeOf((*MockAPIClient)(nil).ContainerExecInspect), arg0, arg1) } // ContainerExecResize mocks base method. func (m *MockAPIClient) ContainerExecResize(arg0 context.Context, arg1 string, arg2 container.ResizeOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerExecResize", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // ContainerExecResize indicates an expected call of ContainerExecResize. func (mr *MockAPIClientMockRecorder) ContainerExecResize(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerExecResize", reflect.TypeOf((*MockAPIClient)(nil).ContainerExecResize), arg0, arg1, arg2) } // ContainerExecStart mocks base method. func (m *MockAPIClient) ContainerExecStart(arg0 context.Context, arg1 string, arg2 container.ExecStartOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerExecStart", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // ContainerExecStart indicates an expected call of ContainerExecStart. func (mr *MockAPIClientMockRecorder) ContainerExecStart(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerExecStart", reflect.TypeOf((*MockAPIClient)(nil).ContainerExecStart), arg0, arg1, arg2) } // ContainerExport mocks base method. func (m *MockAPIClient) ContainerExport(arg0 context.Context, arg1 string) (io.ReadCloser, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerExport", arg0, arg1) ret0, _ := ret[0].(io.ReadCloser) ret1, _ := ret[1].(error) return ret0, ret1 } // ContainerExport indicates an expected call of ContainerExport. func (mr *MockAPIClientMockRecorder) ContainerExport(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerExport", reflect.TypeOf((*MockAPIClient)(nil).ContainerExport), arg0, arg1) } // ContainerInspect mocks base method. func (m *MockAPIClient) ContainerInspect(arg0 context.Context, arg1 string) (container.InspectResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerInspect", arg0, arg1) ret0, _ := ret[0].(container.InspectResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // ContainerInspect indicates an expected call of ContainerInspect. func (mr *MockAPIClientMockRecorder) ContainerInspect(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerInspect", reflect.TypeOf((*MockAPIClient)(nil).ContainerInspect), arg0, arg1) } // ContainerInspectWithRaw mocks base method. func (m *MockAPIClient) ContainerInspectWithRaw(arg0 context.Context, arg1 string, arg2 bool) (container.InspectResponse, []byte, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerInspectWithRaw", arg0, arg1, arg2) ret0, _ := ret[0].(container.InspectResponse) ret1, _ := ret[1].([]byte) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } // ContainerInspectWithRaw indicates an expected call of ContainerInspectWithRaw. func (mr *MockAPIClientMockRecorder) ContainerInspectWithRaw(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerInspectWithRaw", reflect.TypeOf((*MockAPIClient)(nil).ContainerInspectWithRaw), arg0, arg1, arg2) } // ContainerKill mocks base method. func (m *MockAPIClient) ContainerKill(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerKill", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // ContainerKill indicates an expected call of ContainerKill. func (mr *MockAPIClientMockRecorder) ContainerKill(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerKill", reflect.TypeOf((*MockAPIClient)(nil).ContainerKill), arg0, arg1, arg2) } // ContainerList mocks base method. func (m *MockAPIClient) ContainerList(arg0 context.Context, arg1 container.ListOptions) ([]container.Summary, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerList", arg0, arg1) ret0, _ := ret[0].([]container.Summary) ret1, _ := ret[1].(error) return ret0, ret1 } // ContainerList indicates an expected call of ContainerList. func (mr *MockAPIClientMockRecorder) ContainerList(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerList", reflect.TypeOf((*MockAPIClient)(nil).ContainerList), arg0, arg1) } // ContainerLogs mocks base method. func (m *MockAPIClient) ContainerLogs(arg0 context.Context, arg1 string, arg2 container.LogsOptions) (io.ReadCloser, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerLogs", arg0, arg1, arg2) ret0, _ := ret[0].(io.ReadCloser) ret1, _ := ret[1].(error) return ret0, ret1 } // ContainerLogs indicates an expected call of ContainerLogs. func (mr *MockAPIClientMockRecorder) ContainerLogs(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerLogs", reflect.TypeOf((*MockAPIClient)(nil).ContainerLogs), arg0, arg1, arg2) } // ContainerPause mocks base method. func (m *MockAPIClient) ContainerPause(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerPause", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // ContainerPause indicates an expected call of ContainerPause. func (mr *MockAPIClientMockRecorder) ContainerPause(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerPause", reflect.TypeOf((*MockAPIClient)(nil).ContainerPause), arg0, arg1) } // ContainerRemove mocks base method. func (m *MockAPIClient) ContainerRemove(arg0 context.Context, arg1 string, arg2 container.RemoveOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerRemove", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // ContainerRemove indicates an expected call of ContainerRemove. func (mr *MockAPIClientMockRecorder) ContainerRemove(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerRemove", reflect.TypeOf((*MockAPIClient)(nil).ContainerRemove), arg0, arg1, arg2) } // ContainerRename mocks base method. func (m *MockAPIClient) ContainerRename(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerRename", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // ContainerRename indicates an expected call of ContainerRename. func (mr *MockAPIClientMockRecorder) ContainerRename(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerRename", reflect.TypeOf((*MockAPIClient)(nil).ContainerRename), arg0, arg1, arg2) } // ContainerResize mocks base method. func (m *MockAPIClient) ContainerResize(arg0 context.Context, arg1 string, arg2 container.ResizeOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerResize", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // ContainerResize indicates an expected call of ContainerResize. func (mr *MockAPIClientMockRecorder) ContainerResize(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerResize", reflect.TypeOf((*MockAPIClient)(nil).ContainerResize), arg0, arg1, arg2) } // ContainerRestart mocks base method. func (m *MockAPIClient) ContainerRestart(arg0 context.Context, arg1 string, arg2 container.StopOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerRestart", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // ContainerRestart indicates an expected call of ContainerRestart. func (mr *MockAPIClientMockRecorder) ContainerRestart(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerRestart", reflect.TypeOf((*MockAPIClient)(nil).ContainerRestart), arg0, arg1, arg2) } // ContainerStart mocks base method. func (m *MockAPIClient) ContainerStart(arg0 context.Context, arg1 string, arg2 container.StartOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerStart", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // ContainerStart indicates an expected call of ContainerStart. func (mr *MockAPIClientMockRecorder) ContainerStart(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerStart", reflect.TypeOf((*MockAPIClient)(nil).ContainerStart), arg0, arg1, arg2) } // ContainerStatPath mocks base method. func (m *MockAPIClient) ContainerStatPath(arg0 context.Context, arg1, arg2 string) (container.PathStat, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerStatPath", arg0, arg1, arg2) ret0, _ := ret[0].(container.PathStat) ret1, _ := ret[1].(error) return ret0, ret1 } // ContainerStatPath indicates an expected call of ContainerStatPath. func (mr *MockAPIClientMockRecorder) ContainerStatPath(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerStatPath", reflect.TypeOf((*MockAPIClient)(nil).ContainerStatPath), arg0, arg1, arg2) } // ContainerStats mocks base method. func (m *MockAPIClient) ContainerStats(arg0 context.Context, arg1 string, arg2 bool) (container.StatsResponseReader, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerStats", arg0, arg1, arg2) ret0, _ := ret[0].(container.StatsResponseReader) ret1, _ := ret[1].(error) return ret0, ret1 } // ContainerStats indicates an expected call of ContainerStats. func (mr *MockAPIClientMockRecorder) ContainerStats(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerStats", reflect.TypeOf((*MockAPIClient)(nil).ContainerStats), arg0, arg1, arg2) } // ContainerStatsOneShot mocks base method. func (m *MockAPIClient) ContainerStatsOneShot(arg0 context.Context, arg1 string) (container.StatsResponseReader, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerStatsOneShot", arg0, arg1) ret0, _ := ret[0].(container.StatsResponseReader) ret1, _ := ret[1].(error) return ret0, ret1 } // ContainerStatsOneShot indicates an expected call of ContainerStatsOneShot. func (mr *MockAPIClientMockRecorder) ContainerStatsOneShot(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerStatsOneShot", reflect.TypeOf((*MockAPIClient)(nil).ContainerStatsOneShot), arg0, arg1) } // ContainerStop mocks base method. func (m *MockAPIClient) ContainerStop(arg0 context.Context, arg1 string, arg2 container.StopOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerStop", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // ContainerStop indicates an expected call of ContainerStop. func (mr *MockAPIClientMockRecorder) ContainerStop(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerStop", reflect.TypeOf((*MockAPIClient)(nil).ContainerStop), arg0, arg1, arg2) } // ContainerTop mocks base method. func (m *MockAPIClient) ContainerTop(arg0 context.Context, arg1 string, arg2 []string) (container.TopResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerTop", arg0, arg1, arg2) ret0, _ := ret[0].(container.TopResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // ContainerTop indicates an expected call of ContainerTop. func (mr *MockAPIClientMockRecorder) ContainerTop(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerTop", reflect.TypeOf((*MockAPIClient)(nil).ContainerTop), arg0, arg1, arg2) } // ContainerUnpause mocks base method. func (m *MockAPIClient) ContainerUnpause(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerUnpause", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // ContainerUnpause indicates an expected call of ContainerUnpause. func (mr *MockAPIClientMockRecorder) ContainerUnpause(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerUnpause", reflect.TypeOf((*MockAPIClient)(nil).ContainerUnpause), arg0, arg1) } // ContainerUpdate mocks base method. func (m *MockAPIClient) ContainerUpdate(arg0 context.Context, arg1 string, arg2 container.UpdateConfig) (container.UpdateResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerUpdate", arg0, arg1, arg2) ret0, _ := ret[0].(container.UpdateResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // ContainerUpdate indicates an expected call of ContainerUpdate. func (mr *MockAPIClientMockRecorder) ContainerUpdate(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerUpdate", reflect.TypeOf((*MockAPIClient)(nil).ContainerUpdate), arg0, arg1, arg2) } // ContainerWait mocks base method. func (m *MockAPIClient) ContainerWait(arg0 context.Context, arg1 string, arg2 container.WaitCondition) (<-chan container.WaitResponse, <-chan error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerWait", arg0, arg1, arg2) ret0, _ := ret[0].(<-chan container.WaitResponse) ret1, _ := ret[1].(<-chan error) return ret0, ret1 } // ContainerWait indicates an expected call of ContainerWait. func (mr *MockAPIClientMockRecorder) ContainerWait(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainerWait", reflect.TypeOf((*MockAPIClient)(nil).ContainerWait), arg0, arg1, arg2) } // ContainersPrune mocks base method. func (m *MockAPIClient) ContainersPrune(arg0 context.Context, arg1 filters.Args) (container.PruneReport, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainersPrune", arg0, arg1) ret0, _ := ret[0].(container.PruneReport) ret1, _ := ret[1].(error) return ret0, ret1 } // ContainersPrune indicates an expected call of ContainersPrune. func (mr *MockAPIClientMockRecorder) ContainersPrune(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContainersPrune", reflect.TypeOf((*MockAPIClient)(nil).ContainersPrune), arg0, arg1) } // CopyFromContainer mocks base method. func (m *MockAPIClient) CopyFromContainer(arg0 context.Context, arg1, arg2 string) (io.ReadCloser, container.PathStat, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CopyFromContainer", arg0, arg1, arg2) ret0, _ := ret[0].(io.ReadCloser) ret1, _ := ret[1].(container.PathStat) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } // CopyFromContainer indicates an expected call of CopyFromContainer. func (mr *MockAPIClientMockRecorder) CopyFromContainer(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CopyFromContainer", reflect.TypeOf((*MockAPIClient)(nil).CopyFromContainer), arg0, arg1, arg2) } // CopyToContainer mocks base method. func (m *MockAPIClient) CopyToContainer(arg0 context.Context, arg1, arg2 string, arg3 io.Reader, arg4 container.CopyToContainerOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CopyToContainer", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(error) return ret0 } // CopyToContainer indicates an expected call of CopyToContainer. func (mr *MockAPIClientMockRecorder) CopyToContainer(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CopyToContainer", reflect.TypeOf((*MockAPIClient)(nil).CopyToContainer), arg0, arg1, arg2, arg3, arg4) } // DaemonHost mocks base method. func (m *MockAPIClient) DaemonHost() string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DaemonHost") ret0, _ := ret[0].(string) return ret0 } // DaemonHost indicates an expected call of DaemonHost. func (mr *MockAPIClientMockRecorder) DaemonHost() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DaemonHost", reflect.TypeOf((*MockAPIClient)(nil).DaemonHost)) } // DialHijack mocks base method. func (m *MockAPIClient) DialHijack(arg0 context.Context, arg1, arg2 string, arg3 map[string][]string) (net.Conn, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DialHijack", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(net.Conn) ret1, _ := ret[1].(error) return ret0, ret1 } // DialHijack indicates an expected call of DialHijack. func (mr *MockAPIClientMockRecorder) DialHijack(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DialHijack", reflect.TypeOf((*MockAPIClient)(nil).DialHijack), arg0, arg1, arg2, arg3) } // Dialer mocks base method. func (m *MockAPIClient) Dialer() func(context.Context) (net.Conn, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Dialer") ret0, _ := ret[0].(func(context.Context) (net.Conn, error)) return ret0 } // Dialer indicates an expected call of Dialer. func (mr *MockAPIClientMockRecorder) Dialer() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Dialer", reflect.TypeOf((*MockAPIClient)(nil).Dialer)) } // DiskUsage mocks base method. func (m *MockAPIClient) DiskUsage(arg0 context.Context, arg1 types.DiskUsageOptions) (types.DiskUsage, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DiskUsage", arg0, arg1) ret0, _ := ret[0].(types.DiskUsage) ret1, _ := ret[1].(error) return ret0, ret1 } // DiskUsage indicates an expected call of DiskUsage. func (mr *MockAPIClientMockRecorder) DiskUsage(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DiskUsage", reflect.TypeOf((*MockAPIClient)(nil).DiskUsage), arg0, arg1) } // DistributionInspect mocks base method. func (m *MockAPIClient) DistributionInspect(arg0 context.Context, arg1, arg2 string) (registry.DistributionInspect, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DistributionInspect", arg0, arg1, arg2) ret0, _ := ret[0].(registry.DistributionInspect) ret1, _ := ret[1].(error) return ret0, ret1 } // DistributionInspect indicates an expected call of DistributionInspect. func (mr *MockAPIClientMockRecorder) DistributionInspect(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DistributionInspect", reflect.TypeOf((*MockAPIClient)(nil).DistributionInspect), arg0, arg1, arg2) } // Events mocks base method. func (m *MockAPIClient) Events(arg0 context.Context, arg1 events.ListOptions) (<-chan events.Message, <-chan error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Events", arg0, arg1) ret0, _ := ret[0].(<-chan events.Message) ret1, _ := ret[1].(<-chan error) return ret0, ret1 } // Events indicates an expected call of Events. func (mr *MockAPIClientMockRecorder) Events(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Events", reflect.TypeOf((*MockAPIClient)(nil).Events), arg0, arg1) } // HTTPClient mocks base method. func (m *MockAPIClient) HTTPClient() *http.Client { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "HTTPClient") ret0, _ := ret[0].(*http.Client) return ret0 } // HTTPClient indicates an expected call of HTTPClient. func (mr *MockAPIClientMockRecorder) HTTPClient() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HTTPClient", reflect.TypeOf((*MockAPIClient)(nil).HTTPClient)) } // ImageBuild mocks base method. func (m *MockAPIClient) ImageBuild(arg0 context.Context, arg1 io.Reader, arg2 types.ImageBuildOptions) (types.ImageBuildResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ImageBuild", arg0, arg1, arg2) ret0, _ := ret[0].(types.ImageBuildResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // ImageBuild indicates an expected call of ImageBuild. func (mr *MockAPIClientMockRecorder) ImageBuild(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageBuild", reflect.TypeOf((*MockAPIClient)(nil).ImageBuild), arg0, arg1, arg2) } // ImageCreate mocks base method. func (m *MockAPIClient) ImageCreate(arg0 context.Context, arg1 string, arg2 image.CreateOptions) (io.ReadCloser, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ImageCreate", arg0, arg1, arg2) ret0, _ := ret[0].(io.ReadCloser) ret1, _ := ret[1].(error) return ret0, ret1 } // ImageCreate indicates an expected call of ImageCreate. func (mr *MockAPIClientMockRecorder) ImageCreate(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageCreate", reflect.TypeOf((*MockAPIClient)(nil).ImageCreate), arg0, arg1, arg2) } // ImageHistory mocks base method. func (m *MockAPIClient) ImageHistory(arg0 context.Context, arg1 string, arg2 ...client.ImageHistoryOption) ([]image.HistoryResponseItem, error) { m.ctrl.T.Helper() varargs := []interface{}{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "ImageHistory", varargs...) ret0, _ := ret[0].([]image.HistoryResponseItem) ret1, _ := ret[1].(error) return ret0, ret1 } // ImageHistory indicates an expected call of ImageHistory. func (mr *MockAPIClientMockRecorder) ImageHistory(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() varargs := append([]interface{}{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageHistory", reflect.TypeOf((*MockAPIClient)(nil).ImageHistory), varargs...) } // ImageImport mocks base method. func (m *MockAPIClient) ImageImport(arg0 context.Context, arg1 image.ImportSource, arg2 string, arg3 image.ImportOptions) (io.ReadCloser, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ImageImport", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(io.ReadCloser) ret1, _ := ret[1].(error) return ret0, ret1 } // ImageImport indicates an expected call of ImageImport. func (mr *MockAPIClientMockRecorder) ImageImport(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageImport", reflect.TypeOf((*MockAPIClient)(nil).ImageImport), arg0, arg1, arg2, arg3) } // ImageInspect mocks base method. func (m *MockAPIClient) ImageInspect(arg0 context.Context, arg1 string, arg2 ...client.ImageInspectOption) (image.InspectResponse, error) { m.ctrl.T.Helper() varargs := []interface{}{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "ImageInspect", varargs...) ret0, _ := ret[0].(image.InspectResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // ImageInspect indicates an expected call of ImageInspect. func (mr *MockAPIClientMockRecorder) ImageInspect(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() varargs := append([]interface{}{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageInspect", reflect.TypeOf((*MockAPIClient)(nil).ImageInspect), varargs...) } // ImageInspectWithRaw mocks base method. func (m *MockAPIClient) ImageInspectWithRaw(arg0 context.Context, arg1 string) (image.InspectResponse, []byte, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ImageInspectWithRaw", arg0, arg1) ret0, _ := ret[0].(image.InspectResponse) ret1, _ := ret[1].([]byte) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } // ImageInspectWithRaw indicates an expected call of ImageInspectWithRaw. func (mr *MockAPIClientMockRecorder) ImageInspectWithRaw(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageInspectWithRaw", reflect.TypeOf((*MockAPIClient)(nil).ImageInspectWithRaw), arg0, arg1) } // ImageList mocks base method. func (m *MockAPIClient) ImageList(arg0 context.Context, arg1 image.ListOptions) ([]image.Summary, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ImageList", arg0, arg1) ret0, _ := ret[0].([]image.Summary) ret1, _ := ret[1].(error) return ret0, ret1 } // ImageList indicates an expected call of ImageList. func (mr *MockAPIClientMockRecorder) ImageList(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageList", reflect.TypeOf((*MockAPIClient)(nil).ImageList), arg0, arg1) } // ImageLoad mocks base method. func (m *MockAPIClient) ImageLoad(arg0 context.Context, arg1 io.Reader, arg2 ...client.ImageLoadOption) (image.LoadResponse, error) { m.ctrl.T.Helper() varargs := []interface{}{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "ImageLoad", varargs...) ret0, _ := ret[0].(image.LoadResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // ImageLoad indicates an expected call of ImageLoad. func (mr *MockAPIClientMockRecorder) ImageLoad(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() varargs := append([]interface{}{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageLoad", reflect.TypeOf((*MockAPIClient)(nil).ImageLoad), varargs...) } // ImagePull mocks base method. func (m *MockAPIClient) ImagePull(arg0 context.Context, arg1 string, arg2 image.PullOptions) (io.ReadCloser, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ImagePull", arg0, arg1, arg2) ret0, _ := ret[0].(io.ReadCloser) ret1, _ := ret[1].(error) return ret0, ret1 } // ImagePull indicates an expected call of ImagePull. func (mr *MockAPIClientMockRecorder) ImagePull(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImagePull", reflect.TypeOf((*MockAPIClient)(nil).ImagePull), arg0, arg1, arg2) } // ImagePush mocks base method. func (m *MockAPIClient) ImagePush(arg0 context.Context, arg1 string, arg2 image.PushOptions) (io.ReadCloser, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ImagePush", arg0, arg1, arg2) ret0, _ := ret[0].(io.ReadCloser) ret1, _ := ret[1].(error) return ret0, ret1 } // ImagePush indicates an expected call of ImagePush. func (mr *MockAPIClientMockRecorder) ImagePush(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImagePush", reflect.TypeOf((*MockAPIClient)(nil).ImagePush), arg0, arg1, arg2) } // ImageRemove mocks base method. func (m *MockAPIClient) ImageRemove(arg0 context.Context, arg1 string, arg2 image.RemoveOptions) ([]image.DeleteResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ImageRemove", arg0, arg1, arg2) ret0, _ := ret[0].([]image.DeleteResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // ImageRemove indicates an expected call of ImageRemove. func (mr *MockAPIClientMockRecorder) ImageRemove(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageRemove", reflect.TypeOf((*MockAPIClient)(nil).ImageRemove), arg0, arg1, arg2) } // ImageSave mocks base method. func (m *MockAPIClient) ImageSave(arg0 context.Context, arg1 []string, arg2 ...client.ImageSaveOption) (io.ReadCloser, error) { m.ctrl.T.Helper() varargs := []interface{}{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "ImageSave", varargs...) ret0, _ := ret[0].(io.ReadCloser) ret1, _ := ret[1].(error) return ret0, ret1 } // ImageSave indicates an expected call of ImageSave. func (mr *MockAPIClientMockRecorder) ImageSave(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() varargs := append([]interface{}{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageSave", reflect.TypeOf((*MockAPIClient)(nil).ImageSave), varargs...) } // ImageSearch mocks base method. func (m *MockAPIClient) ImageSearch(arg0 context.Context, arg1 string, arg2 registry.SearchOptions) ([]registry.SearchResult, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ImageSearch", arg0, arg1, arg2) ret0, _ := ret[0].([]registry.SearchResult) ret1, _ := ret[1].(error) return ret0, ret1 } // ImageSearch indicates an expected call of ImageSearch. func (mr *MockAPIClientMockRecorder) ImageSearch(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageSearch", reflect.TypeOf((*MockAPIClient)(nil).ImageSearch), arg0, arg1, arg2) } // ImageTag mocks base method. func (m *MockAPIClient) ImageTag(arg0 context.Context, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ImageTag", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // ImageTag indicates an expected call of ImageTag. func (mr *MockAPIClientMockRecorder) ImageTag(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageTag", reflect.TypeOf((*MockAPIClient)(nil).ImageTag), arg0, arg1, arg2) } // ImagesPrune mocks base method. func (m *MockAPIClient) ImagesPrune(arg0 context.Context, arg1 filters.Args) (image.PruneReport, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ImagesPrune", arg0, arg1) ret0, _ := ret[0].(image.PruneReport) ret1, _ := ret[1].(error) return ret0, ret1 } // ImagesPrune indicates an expected call of ImagesPrune. func (mr *MockAPIClientMockRecorder) ImagesPrune(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImagesPrune", reflect.TypeOf((*MockAPIClient)(nil).ImagesPrune), arg0, arg1) } // Info mocks base method. func (m *MockAPIClient) Info(arg0 context.Context) (system.Info, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Info", arg0) ret0, _ := ret[0].(system.Info) ret1, _ := ret[1].(error) return ret0, ret1 } // Info indicates an expected call of Info. func (mr *MockAPIClientMockRecorder) Info(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockAPIClient)(nil).Info), arg0) } // NegotiateAPIVersion mocks base method. func (m *MockAPIClient) NegotiateAPIVersion(arg0 context.Context) { m.ctrl.T.Helper() m.ctrl.Call(m, "NegotiateAPIVersion", arg0) } // NegotiateAPIVersion indicates an expected call of NegotiateAPIVersion. func (mr *MockAPIClientMockRecorder) NegotiateAPIVersion(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NegotiateAPIVersion", reflect.TypeOf((*MockAPIClient)(nil).NegotiateAPIVersion), arg0) } // NegotiateAPIVersionPing mocks base method. func (m *MockAPIClient) NegotiateAPIVersionPing(arg0 types.Ping) { m.ctrl.T.Helper() m.ctrl.Call(m, "NegotiateAPIVersionPing", arg0) } // NegotiateAPIVersionPing indicates an expected call of NegotiateAPIVersionPing. func (mr *MockAPIClientMockRecorder) NegotiateAPIVersionPing(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NegotiateAPIVersionPing", reflect.TypeOf((*MockAPIClient)(nil).NegotiateAPIVersionPing), arg0) } // NetworkConnect mocks base method. func (m *MockAPIClient) NetworkConnect(arg0 context.Context, arg1, arg2 string, arg3 *network.EndpointSettings) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NetworkConnect", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) return ret0 } // NetworkConnect indicates an expected call of NetworkConnect. func (mr *MockAPIClientMockRecorder) NetworkConnect(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NetworkConnect", reflect.TypeOf((*MockAPIClient)(nil).NetworkConnect), arg0, arg1, arg2, arg3) } // NetworkCreate mocks base method. func (m *MockAPIClient) NetworkCreate(arg0 context.Context, arg1 string, arg2 network.CreateOptions) (network.CreateResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NetworkCreate", arg0, arg1, arg2) ret0, _ := ret[0].(network.CreateResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // NetworkCreate indicates an expected call of NetworkCreate. func (mr *MockAPIClientMockRecorder) NetworkCreate(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NetworkCreate", reflect.TypeOf((*MockAPIClient)(nil).NetworkCreate), arg0, arg1, arg2) } // NetworkDisconnect mocks base method. func (m *MockAPIClient) NetworkDisconnect(arg0 context.Context, arg1, arg2 string, arg3 bool) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NetworkDisconnect", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) return ret0 } // NetworkDisconnect indicates an expected call of NetworkDisconnect. func (mr *MockAPIClientMockRecorder) NetworkDisconnect(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NetworkDisconnect", reflect.TypeOf((*MockAPIClient)(nil).NetworkDisconnect), arg0, arg1, arg2, arg3) } // NetworkInspect mocks base method. func (m *MockAPIClient) NetworkInspect(arg0 context.Context, arg1 string, arg2 network.InspectOptions) (network.Inspect, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NetworkInspect", arg0, arg1, arg2) ret0, _ := ret[0].(network.Inspect) ret1, _ := ret[1].(error) return ret0, ret1 } // NetworkInspect indicates an expected call of NetworkInspect. func (mr *MockAPIClientMockRecorder) NetworkInspect(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NetworkInspect", reflect.TypeOf((*MockAPIClient)(nil).NetworkInspect), arg0, arg1, arg2) } // NetworkInspectWithRaw mocks base method. func (m *MockAPIClient) NetworkInspectWithRaw(arg0 context.Context, arg1 string, arg2 network.InspectOptions) (network.Inspect, []byte, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NetworkInspectWithRaw", arg0, arg1, arg2) ret0, _ := ret[0].(network.Inspect) ret1, _ := ret[1].([]byte) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } // NetworkInspectWithRaw indicates an expected call of NetworkInspectWithRaw. func (mr *MockAPIClientMockRecorder) NetworkInspectWithRaw(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NetworkInspectWithRaw", reflect.TypeOf((*MockAPIClient)(nil).NetworkInspectWithRaw), arg0, arg1, arg2) } // NetworkList mocks base method. func (m *MockAPIClient) NetworkList(arg0 context.Context, arg1 network.ListOptions) ([]network.Inspect, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NetworkList", arg0, arg1) ret0, _ := ret[0].([]network.Inspect) ret1, _ := ret[1].(error) return ret0, ret1 } // NetworkList indicates an expected call of NetworkList. func (mr *MockAPIClientMockRecorder) NetworkList(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NetworkList", reflect.TypeOf((*MockAPIClient)(nil).NetworkList), arg0, arg1) } // NetworkRemove mocks base method. func (m *MockAPIClient) NetworkRemove(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NetworkRemove", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // NetworkRemove indicates an expected call of NetworkRemove. func (mr *MockAPIClientMockRecorder) NetworkRemove(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NetworkRemove", reflect.TypeOf((*MockAPIClient)(nil).NetworkRemove), arg0, arg1) } // NetworksPrune mocks base method. func (m *MockAPIClient) NetworksPrune(arg0 context.Context, arg1 filters.Args) (network.PruneReport, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NetworksPrune", arg0, arg1) ret0, _ := ret[0].(network.PruneReport) ret1, _ := ret[1].(error) return ret0, ret1 } // NetworksPrune indicates an expected call of NetworksPrune. func (mr *MockAPIClientMockRecorder) NetworksPrune(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NetworksPrune", reflect.TypeOf((*MockAPIClient)(nil).NetworksPrune), arg0, arg1) } // NodeInspectWithRaw mocks base method. func (m *MockAPIClient) NodeInspectWithRaw(arg0 context.Context, arg1 string) (swarm.Node, []byte, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NodeInspectWithRaw", arg0, arg1) ret0, _ := ret[0].(swarm.Node) ret1, _ := ret[1].([]byte) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } // NodeInspectWithRaw indicates an expected call of NodeInspectWithRaw. func (mr *MockAPIClientMockRecorder) NodeInspectWithRaw(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeInspectWithRaw", reflect.TypeOf((*MockAPIClient)(nil).NodeInspectWithRaw), arg0, arg1) } // NodeList mocks base method. func (m *MockAPIClient) NodeList(arg0 context.Context, arg1 types.NodeListOptions) ([]swarm.Node, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NodeList", arg0, arg1) ret0, _ := ret[0].([]swarm.Node) ret1, _ := ret[1].(error) return ret0, ret1 } // NodeList indicates an expected call of NodeList. func (mr *MockAPIClientMockRecorder) NodeList(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeList", reflect.TypeOf((*MockAPIClient)(nil).NodeList), arg0, arg1) } // NodeRemove mocks base method. func (m *MockAPIClient) NodeRemove(arg0 context.Context, arg1 string, arg2 types.NodeRemoveOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NodeRemove", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // NodeRemove indicates an expected call of NodeRemove. func (mr *MockAPIClientMockRecorder) NodeRemove(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeRemove", reflect.TypeOf((*MockAPIClient)(nil).NodeRemove), arg0, arg1, arg2) } // NodeUpdate mocks base method. func (m *MockAPIClient) NodeUpdate(arg0 context.Context, arg1 string, arg2 swarm.Version, arg3 swarm.NodeSpec) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NodeUpdate", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) return ret0 } // NodeUpdate indicates an expected call of NodeUpdate. func (mr *MockAPIClientMockRecorder) NodeUpdate(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeUpdate", reflect.TypeOf((*MockAPIClient)(nil).NodeUpdate), arg0, arg1, arg2, arg3) } // Ping mocks base method. func (m *MockAPIClient) Ping(arg0 context.Context) (types.Ping, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Ping", arg0) ret0, _ := ret[0].(types.Ping) ret1, _ := ret[1].(error) return ret0, ret1 } // Ping indicates an expected call of Ping. func (mr *MockAPIClientMockRecorder) Ping(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockAPIClient)(nil).Ping), arg0) } // PluginCreate mocks base method. func (m *MockAPIClient) PluginCreate(arg0 context.Context, arg1 io.Reader, arg2 types.PluginCreateOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "PluginCreate", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // PluginCreate indicates an expected call of PluginCreate. func (mr *MockAPIClientMockRecorder) PluginCreate(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginCreate", reflect.TypeOf((*MockAPIClient)(nil).PluginCreate), arg0, arg1, arg2) } // PluginDisable mocks base method. func (m *MockAPIClient) PluginDisable(arg0 context.Context, arg1 string, arg2 types.PluginDisableOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "PluginDisable", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // PluginDisable indicates an expected call of PluginDisable. func (mr *MockAPIClientMockRecorder) PluginDisable(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginDisable", reflect.TypeOf((*MockAPIClient)(nil).PluginDisable), arg0, arg1, arg2) } // PluginEnable mocks base method. func (m *MockAPIClient) PluginEnable(arg0 context.Context, arg1 string, arg2 types.PluginEnableOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "PluginEnable", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // PluginEnable indicates an expected call of PluginEnable. func (mr *MockAPIClientMockRecorder) PluginEnable(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginEnable", reflect.TypeOf((*MockAPIClient)(nil).PluginEnable), arg0, arg1, arg2) } // PluginInspectWithRaw mocks base method. func (m *MockAPIClient) PluginInspectWithRaw(arg0 context.Context, arg1 string) (*types.Plugin, []byte, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "PluginInspectWithRaw", arg0, arg1) ret0, _ := ret[0].(*types.Plugin) ret1, _ := ret[1].([]byte) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } // PluginInspectWithRaw indicates an expected call of PluginInspectWithRaw. func (mr *MockAPIClientMockRecorder) PluginInspectWithRaw(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginInspectWithRaw", reflect.TypeOf((*MockAPIClient)(nil).PluginInspectWithRaw), arg0, arg1) } // PluginInstall mocks base method. func (m *MockAPIClient) PluginInstall(arg0 context.Context, arg1 string, arg2 types.PluginInstallOptions) (io.ReadCloser, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "PluginInstall", arg0, arg1, arg2) ret0, _ := ret[0].(io.ReadCloser) ret1, _ := ret[1].(error) return ret0, ret1 } // PluginInstall indicates an expected call of PluginInstall. func (mr *MockAPIClientMockRecorder) PluginInstall(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginInstall", reflect.TypeOf((*MockAPIClient)(nil).PluginInstall), arg0, arg1, arg2) } // PluginList mocks base method. func (m *MockAPIClient) PluginList(arg0 context.Context, arg1 filters.Args) (types.PluginsListResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "PluginList", arg0, arg1) ret0, _ := ret[0].(types.PluginsListResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // PluginList indicates an expected call of PluginList. func (mr *MockAPIClientMockRecorder) PluginList(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginList", reflect.TypeOf((*MockAPIClient)(nil).PluginList), arg0, arg1) } // PluginPush mocks base method. func (m *MockAPIClient) PluginPush(arg0 context.Context, arg1, arg2 string) (io.ReadCloser, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "PluginPush", arg0, arg1, arg2) ret0, _ := ret[0].(io.ReadCloser) ret1, _ := ret[1].(error) return ret0, ret1 } // PluginPush indicates an expected call of PluginPush. func (mr *MockAPIClientMockRecorder) PluginPush(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginPush", reflect.TypeOf((*MockAPIClient)(nil).PluginPush), arg0, arg1, arg2) } // PluginRemove mocks base method. func (m *MockAPIClient) PluginRemove(arg0 context.Context, arg1 string, arg2 types.PluginRemoveOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "PluginRemove", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // PluginRemove indicates an expected call of PluginRemove. func (mr *MockAPIClientMockRecorder) PluginRemove(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginRemove", reflect.TypeOf((*MockAPIClient)(nil).PluginRemove), arg0, arg1, arg2) } // PluginSet mocks base method. func (m *MockAPIClient) PluginSet(arg0 context.Context, arg1 string, arg2 []string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "PluginSet", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // PluginSet indicates an expected call of PluginSet. func (mr *MockAPIClientMockRecorder) PluginSet(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginSet", reflect.TypeOf((*MockAPIClient)(nil).PluginSet), arg0, arg1, arg2) } // PluginUpgrade mocks base method. func (m *MockAPIClient) PluginUpgrade(arg0 context.Context, arg1 string, arg2 types.PluginInstallOptions) (io.ReadCloser, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "PluginUpgrade", arg0, arg1, arg2) ret0, _ := ret[0].(io.ReadCloser) ret1, _ := ret[1].(error) return ret0, ret1 } // PluginUpgrade indicates an expected call of PluginUpgrade. func (mr *MockAPIClientMockRecorder) PluginUpgrade(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PluginUpgrade", reflect.TypeOf((*MockAPIClient)(nil).PluginUpgrade), arg0, arg1, arg2) } // RegistryLogin mocks base method. func (m *MockAPIClient) RegistryLogin(arg0 context.Context, arg1 registry.AuthConfig) (registry.AuthenticateOKBody, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RegistryLogin", arg0, arg1) ret0, _ := ret[0].(registry.AuthenticateOKBody) ret1, _ := ret[1].(error) return ret0, ret1 } // RegistryLogin indicates an expected call of RegistryLogin. func (mr *MockAPIClientMockRecorder) RegistryLogin(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegistryLogin", reflect.TypeOf((*MockAPIClient)(nil).RegistryLogin), arg0, arg1) } // SecretCreate mocks base method. func (m *MockAPIClient) SecretCreate(arg0 context.Context, arg1 swarm.SecretSpec) (types.SecretCreateResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SecretCreate", arg0, arg1) ret0, _ := ret[0].(types.SecretCreateResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // SecretCreate indicates an expected call of SecretCreate. func (mr *MockAPIClientMockRecorder) SecretCreate(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SecretCreate", reflect.TypeOf((*MockAPIClient)(nil).SecretCreate), arg0, arg1) } // SecretInspectWithRaw mocks base method. func (m *MockAPIClient) SecretInspectWithRaw(arg0 context.Context, arg1 string) (swarm.Secret, []byte, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SecretInspectWithRaw", arg0, arg1) ret0, _ := ret[0].(swarm.Secret) ret1, _ := ret[1].([]byte) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } // SecretInspectWithRaw indicates an expected call of SecretInspectWithRaw. func (mr *MockAPIClientMockRecorder) SecretInspectWithRaw(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SecretInspectWithRaw", reflect.TypeOf((*MockAPIClient)(nil).SecretInspectWithRaw), arg0, arg1) } // SecretList mocks base method. func (m *MockAPIClient) SecretList(arg0 context.Context, arg1 types.SecretListOptions) ([]swarm.Secret, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SecretList", arg0, arg1) ret0, _ := ret[0].([]swarm.Secret) ret1, _ := ret[1].(error) return ret0, ret1 } // SecretList indicates an expected call of SecretList. func (mr *MockAPIClientMockRecorder) SecretList(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SecretList", reflect.TypeOf((*MockAPIClient)(nil).SecretList), arg0, arg1) } // SecretRemove mocks base method. func (m *MockAPIClient) SecretRemove(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SecretRemove", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // SecretRemove indicates an expected call of SecretRemove. func (mr *MockAPIClientMockRecorder) SecretRemove(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SecretRemove", reflect.TypeOf((*MockAPIClient)(nil).SecretRemove), arg0, arg1) } // SecretUpdate mocks base method. func (m *MockAPIClient) SecretUpdate(arg0 context.Context, arg1 string, arg2 swarm.Version, arg3 swarm.SecretSpec) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SecretUpdate", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) return ret0 } // SecretUpdate indicates an expected call of SecretUpdate. func (mr *MockAPIClientMockRecorder) SecretUpdate(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SecretUpdate", reflect.TypeOf((*MockAPIClient)(nil).SecretUpdate), arg0, arg1, arg2, arg3) } // ServerVersion mocks base method. func (m *MockAPIClient) ServerVersion(arg0 context.Context) (types.Version, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ServerVersion", arg0) ret0, _ := ret[0].(types.Version) ret1, _ := ret[1].(error) return ret0, ret1 } // ServerVersion indicates an expected call of ServerVersion. func (mr *MockAPIClientMockRecorder) ServerVersion(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServerVersion", reflect.TypeOf((*MockAPIClient)(nil).ServerVersion), arg0) } // ServiceCreate mocks base method. func (m *MockAPIClient) ServiceCreate(arg0 context.Context, arg1 swarm.ServiceSpec, arg2 types.ServiceCreateOptions) (swarm.ServiceCreateResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ServiceCreate", arg0, arg1, arg2) ret0, _ := ret[0].(swarm.ServiceCreateResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // ServiceCreate indicates an expected call of ServiceCreate. func (mr *MockAPIClientMockRecorder) ServiceCreate(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServiceCreate", reflect.TypeOf((*MockAPIClient)(nil).ServiceCreate), arg0, arg1, arg2) } // ServiceInspectWithRaw mocks base method. func (m *MockAPIClient) ServiceInspectWithRaw(arg0 context.Context, arg1 string, arg2 types.ServiceInspectOptions) (swarm.Service, []byte, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ServiceInspectWithRaw", arg0, arg1, arg2) ret0, _ := ret[0].(swarm.Service) ret1, _ := ret[1].([]byte) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } // ServiceInspectWithRaw indicates an expected call of ServiceInspectWithRaw. func (mr *MockAPIClientMockRecorder) ServiceInspectWithRaw(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServiceInspectWithRaw", reflect.TypeOf((*MockAPIClient)(nil).ServiceInspectWithRaw), arg0, arg1, arg2) } // ServiceList mocks base method. func (m *MockAPIClient) ServiceList(arg0 context.Context, arg1 types.ServiceListOptions) ([]swarm.Service, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ServiceList", arg0, arg1) ret0, _ := ret[0].([]swarm.Service) ret1, _ := ret[1].(error) return ret0, ret1 } // ServiceList indicates an expected call of ServiceList. func (mr *MockAPIClientMockRecorder) ServiceList(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServiceList", reflect.TypeOf((*MockAPIClient)(nil).ServiceList), arg0, arg1) } // ServiceLogs mocks base method. func (m *MockAPIClient) ServiceLogs(arg0 context.Context, arg1 string, arg2 container.LogsOptions) (io.ReadCloser, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ServiceLogs", arg0, arg1, arg2) ret0, _ := ret[0].(io.ReadCloser) ret1, _ := ret[1].(error) return ret0, ret1 } // ServiceLogs indicates an expected call of ServiceLogs. func (mr *MockAPIClientMockRecorder) ServiceLogs(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServiceLogs", reflect.TypeOf((*MockAPIClient)(nil).ServiceLogs), arg0, arg1, arg2) } // ServiceRemove mocks base method. func (m *MockAPIClient) ServiceRemove(arg0 context.Context, arg1 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ServiceRemove", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // ServiceRemove indicates an expected call of ServiceRemove. func (mr *MockAPIClientMockRecorder) ServiceRemove(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServiceRemove", reflect.TypeOf((*MockAPIClient)(nil).ServiceRemove), arg0, arg1) } // ServiceUpdate mocks base method. func (m *MockAPIClient) ServiceUpdate(arg0 context.Context, arg1 string, arg2 swarm.Version, arg3 swarm.ServiceSpec, arg4 types.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ServiceUpdate", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(swarm.ServiceUpdateResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // ServiceUpdate indicates an expected call of ServiceUpdate. func (mr *MockAPIClientMockRecorder) ServiceUpdate(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServiceUpdate", reflect.TypeOf((*MockAPIClient)(nil).ServiceUpdate), arg0, arg1, arg2, arg3, arg4) } // SwarmGetUnlockKey mocks base method. func (m *MockAPIClient) SwarmGetUnlockKey(arg0 context.Context) (types.SwarmUnlockKeyResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SwarmGetUnlockKey", arg0) ret0, _ := ret[0].(types.SwarmUnlockKeyResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // SwarmGetUnlockKey indicates an expected call of SwarmGetUnlockKey. func (mr *MockAPIClientMockRecorder) SwarmGetUnlockKey(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SwarmGetUnlockKey", reflect.TypeOf((*MockAPIClient)(nil).SwarmGetUnlockKey), arg0) } // SwarmInit mocks base method. func (m *MockAPIClient) SwarmInit(arg0 context.Context, arg1 swarm.InitRequest) (string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SwarmInit", arg0, arg1) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } // SwarmInit indicates an expected call of SwarmInit. func (mr *MockAPIClientMockRecorder) SwarmInit(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SwarmInit", reflect.TypeOf((*MockAPIClient)(nil).SwarmInit), arg0, arg1) } // SwarmInspect mocks base method. func (m *MockAPIClient) SwarmInspect(arg0 context.Context) (swarm.Swarm, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SwarmInspect", arg0) ret0, _ := ret[0].(swarm.Swarm) ret1, _ := ret[1].(error) return ret0, ret1 } // SwarmInspect indicates an expected call of SwarmInspect. func (mr *MockAPIClientMockRecorder) SwarmInspect(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SwarmInspect", reflect.TypeOf((*MockAPIClient)(nil).SwarmInspect), arg0) } // SwarmJoin mocks base method. func (m *MockAPIClient) SwarmJoin(arg0 context.Context, arg1 swarm.JoinRequest) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SwarmJoin", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // SwarmJoin indicates an expected call of SwarmJoin. func (mr *MockAPIClientMockRecorder) SwarmJoin(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SwarmJoin", reflect.TypeOf((*MockAPIClient)(nil).SwarmJoin), arg0, arg1) } // SwarmLeave mocks base method. func (m *MockAPIClient) SwarmLeave(arg0 context.Context, arg1 bool) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SwarmLeave", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // SwarmLeave indicates an expected call of SwarmLeave. func (mr *MockAPIClientMockRecorder) SwarmLeave(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SwarmLeave", reflect.TypeOf((*MockAPIClient)(nil).SwarmLeave), arg0, arg1) } // SwarmUnlock mocks base method. func (m *MockAPIClient) SwarmUnlock(arg0 context.Context, arg1 swarm.UnlockRequest) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SwarmUnlock", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // SwarmUnlock indicates an expected call of SwarmUnlock. func (mr *MockAPIClientMockRecorder) SwarmUnlock(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SwarmUnlock", reflect.TypeOf((*MockAPIClient)(nil).SwarmUnlock), arg0, arg1) } // SwarmUpdate mocks base method. func (m *MockAPIClient) SwarmUpdate(arg0 context.Context, arg1 swarm.Version, arg2 swarm.Spec, arg3 swarm.UpdateFlags) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SwarmUpdate", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) return ret0 } // SwarmUpdate indicates an expected call of SwarmUpdate. func (mr *MockAPIClientMockRecorder) SwarmUpdate(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SwarmUpdate", reflect.TypeOf((*MockAPIClient)(nil).SwarmUpdate), arg0, arg1, arg2, arg3) } // TaskInspectWithRaw mocks base method. func (m *MockAPIClient) TaskInspectWithRaw(arg0 context.Context, arg1 string) (swarm.Task, []byte, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "TaskInspectWithRaw", arg0, arg1) ret0, _ := ret[0].(swarm.Task) ret1, _ := ret[1].([]byte) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } // TaskInspectWithRaw indicates an expected call of TaskInspectWithRaw. func (mr *MockAPIClientMockRecorder) TaskInspectWithRaw(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TaskInspectWithRaw", reflect.TypeOf((*MockAPIClient)(nil).TaskInspectWithRaw), arg0, arg1) } // TaskList mocks base method. func (m *MockAPIClient) TaskList(arg0 context.Context, arg1 types.TaskListOptions) ([]swarm.Task, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "TaskList", arg0, arg1) ret0, _ := ret[0].([]swarm.Task) ret1, _ := ret[1].(error) return ret0, ret1 } // TaskList indicates an expected call of TaskList. func (mr *MockAPIClientMockRecorder) TaskList(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TaskList", reflect.TypeOf((*MockAPIClient)(nil).TaskList), arg0, arg1) } // TaskLogs mocks base method. func (m *MockAPIClient) TaskLogs(arg0 context.Context, arg1 string, arg2 container.LogsOptions) (io.ReadCloser, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "TaskLogs", arg0, arg1, arg2) ret0, _ := ret[0].(io.ReadCloser) ret1, _ := ret[1].(error) return ret0, ret1 } // TaskLogs indicates an expected call of TaskLogs. func (mr *MockAPIClientMockRecorder) TaskLogs(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TaskLogs", reflect.TypeOf((*MockAPIClient)(nil).TaskLogs), arg0, arg1, arg2) } // VolumeCreate mocks base method. func (m *MockAPIClient) VolumeCreate(arg0 context.Context, arg1 volume.CreateOptions) (volume.Volume, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "VolumeCreate", arg0, arg1) ret0, _ := ret[0].(volume.Volume) ret1, _ := ret[1].(error) return ret0, ret1 } // VolumeCreate indicates an expected call of VolumeCreate. func (mr *MockAPIClientMockRecorder) VolumeCreate(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeCreate", reflect.TypeOf((*MockAPIClient)(nil).VolumeCreate), arg0, arg1) } // VolumeInspect mocks base method. func (m *MockAPIClient) VolumeInspect(arg0 context.Context, arg1 string) (volume.Volume, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "VolumeInspect", arg0, arg1) ret0, _ := ret[0].(volume.Volume) ret1, _ := ret[1].(error) return ret0, ret1 } // VolumeInspect indicates an expected call of VolumeInspect. func (mr *MockAPIClientMockRecorder) VolumeInspect(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeInspect", reflect.TypeOf((*MockAPIClient)(nil).VolumeInspect), arg0, arg1) } // VolumeInspectWithRaw mocks base method. func (m *MockAPIClient) VolumeInspectWithRaw(arg0 context.Context, arg1 string) (volume.Volume, []byte, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "VolumeInspectWithRaw", arg0, arg1) ret0, _ := ret[0].(volume.Volume) ret1, _ := ret[1].([]byte) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } // VolumeInspectWithRaw indicates an expected call of VolumeInspectWithRaw. func (mr *MockAPIClientMockRecorder) VolumeInspectWithRaw(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeInspectWithRaw", reflect.TypeOf((*MockAPIClient)(nil).VolumeInspectWithRaw), arg0, arg1) } // VolumeList mocks base method. func (m *MockAPIClient) VolumeList(arg0 context.Context, arg1 volume.ListOptions) (volume.ListResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "VolumeList", arg0, arg1) ret0, _ := ret[0].(volume.ListResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // VolumeList indicates an expected call of VolumeList. func (mr *MockAPIClientMockRecorder) VolumeList(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeList", reflect.TypeOf((*MockAPIClient)(nil).VolumeList), arg0, arg1) } // VolumeRemove mocks base method. func (m *MockAPIClient) VolumeRemove(arg0 context.Context, arg1 string, arg2 bool) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "VolumeRemove", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // VolumeRemove indicates an expected call of VolumeRemove. func (mr *MockAPIClientMockRecorder) VolumeRemove(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeRemove", reflect.TypeOf((*MockAPIClient)(nil).VolumeRemove), arg0, arg1, arg2) } // VolumeUpdate mocks base method. func (m *MockAPIClient) VolumeUpdate(arg0 context.Context, arg1 string, arg2 swarm.Version, arg3 volume.UpdateOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "VolumeUpdate", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) return ret0 } // VolumeUpdate indicates an expected call of VolumeUpdate. func (mr *MockAPIClientMockRecorder) VolumeUpdate(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumeUpdate", reflect.TypeOf((*MockAPIClient)(nil).VolumeUpdate), arg0, arg1, arg2, arg3) } // VolumesPrune mocks base method. func (m *MockAPIClient) VolumesPrune(arg0 context.Context, arg1 filters.Args) (volume.PruneReport, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "VolumesPrune", arg0, arg1) ret0, _ := ret[0].(volume.PruneReport) ret1, _ := ret[1].(error) return ret0, ret1 } // VolumesPrune indicates an expected call of VolumesPrune. func (mr *MockAPIClientMockRecorder) VolumesPrune(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VolumesPrune", reflect.TypeOf((*MockAPIClient)(nil).VolumesPrune), arg0, arg1) } ================================================ FILE: mocks/mock_drone.go ================================================ // Code generated by MockGen. DO NOT EDIT. // Source: github.com/drone/drone-go/drone (interfaces: Client) // Package mocks is a generated GoMock package. package mocks import ( http "net/http" reflect "reflect" drone "github.com/drone/drone-go/drone" gomock "github.com/golang/mock/gomock" ) // MockClient is a mock of Client interface. type MockClient struct { ctrl *gomock.Controller recorder *MockClientMockRecorder } // MockClientMockRecorder is the mock recorder for MockClient. type MockClientMockRecorder struct { mock *MockClient } // NewMockClient creates a new mock instance. func NewMockClient(ctrl *gomock.Controller) *MockClient { mock := &MockClient{ctrl: ctrl} mock.recorder = &MockClientMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockClient) EXPECT() *MockClientMockRecorder { return m.recorder } // Approve mocks base method. func (m *MockClient) Approve(arg0, arg1 string, arg2, arg3 int) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Approve", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) return ret0 } // Approve indicates an expected call of Approve. func (mr *MockClientMockRecorder) Approve(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Approve", reflect.TypeOf((*MockClient)(nil).Approve), arg0, arg1, arg2, arg3) } // AutoscalePause mocks base method. func (m *MockClient) AutoscalePause() error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AutoscalePause") ret0, _ := ret[0].(error) return ret0 } // AutoscalePause indicates an expected call of AutoscalePause. func (mr *MockClientMockRecorder) AutoscalePause() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AutoscalePause", reflect.TypeOf((*MockClient)(nil).AutoscalePause)) } // AutoscaleResume mocks base method. func (m *MockClient) AutoscaleResume() error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AutoscaleResume") ret0, _ := ret[0].(error) return ret0 } // AutoscaleResume indicates an expected call of AutoscaleResume. func (mr *MockClientMockRecorder) AutoscaleResume() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AutoscaleResume", reflect.TypeOf((*MockClient)(nil).AutoscaleResume)) } // AutoscaleVersion mocks base method. func (m *MockClient) AutoscaleVersion() (*drone.Version, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AutoscaleVersion") ret0, _ := ret[0].(*drone.Version) ret1, _ := ret[1].(error) return ret0, ret1 } // AutoscaleVersion indicates an expected call of AutoscaleVersion. func (mr *MockClientMockRecorder) AutoscaleVersion() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AutoscaleVersion", reflect.TypeOf((*MockClient)(nil).AutoscaleVersion)) } // Build mocks base method. func (m *MockClient) Build(arg0, arg1 string, arg2 int) (*drone.Build, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Build", arg0, arg1, arg2) ret0, _ := ret[0].(*drone.Build) ret1, _ := ret[1].(error) return ret0, ret1 } // Build indicates an expected call of Build. func (mr *MockClientMockRecorder) Build(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Build", reflect.TypeOf((*MockClient)(nil).Build), arg0, arg1, arg2) } // BuildCancel mocks base method. func (m *MockClient) BuildCancel(arg0, arg1 string, arg2 int) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "BuildCancel", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // BuildCancel indicates an expected call of BuildCancel. func (mr *MockClientMockRecorder) BuildCancel(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildCancel", reflect.TypeOf((*MockClient)(nil).BuildCancel), arg0, arg1, arg2) } // BuildLast mocks base method. func (m *MockClient) BuildLast(arg0, arg1, arg2 string) (*drone.Build, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "BuildLast", arg0, arg1, arg2) ret0, _ := ret[0].(*drone.Build) ret1, _ := ret[1].(error) return ret0, ret1 } // BuildLast indicates an expected call of BuildLast. func (mr *MockClientMockRecorder) BuildLast(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildLast", reflect.TypeOf((*MockClient)(nil).BuildLast), arg0, arg1, arg2) } // BuildList mocks base method. func (m *MockClient) BuildList(arg0, arg1 string, arg2 drone.ListOptions) ([]*drone.Build, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "BuildList", arg0, arg1, arg2) ret0, _ := ret[0].([]*drone.Build) ret1, _ := ret[1].(error) return ret0, ret1 } // BuildList indicates an expected call of BuildList. func (mr *MockClientMockRecorder) BuildList(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildList", reflect.TypeOf((*MockClient)(nil).BuildList), arg0, arg1, arg2) } // BuildPurge mocks base method. func (m *MockClient) BuildPurge(arg0, arg1 string, arg2 int) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "BuildPurge", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // BuildPurge indicates an expected call of BuildPurge. func (mr *MockClientMockRecorder) BuildPurge(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildPurge", reflect.TypeOf((*MockClient)(nil).BuildPurge), arg0, arg1, arg2) } // BuildRestart mocks base method. func (m *MockClient) BuildRestart(arg0, arg1 string, arg2 int, arg3 map[string]string) (*drone.Build, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "BuildRestart", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(*drone.Build) ret1, _ := ret[1].(error) return ret0, ret1 } // BuildRestart indicates an expected call of BuildRestart. func (mr *MockClientMockRecorder) BuildRestart(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildRestart", reflect.TypeOf((*MockClient)(nil).BuildRestart), arg0, arg1, arg2, arg3) } // Cron mocks base method. func (m *MockClient) Cron(arg0, arg1, arg2 string) (*drone.Cron, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Cron", arg0, arg1, arg2) ret0, _ := ret[0].(*drone.Cron) ret1, _ := ret[1].(error) return ret0, ret1 } // Cron indicates an expected call of Cron. func (mr *MockClientMockRecorder) Cron(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cron", reflect.TypeOf((*MockClient)(nil).Cron), arg0, arg1, arg2) } // CronCreate mocks base method. func (m *MockClient) CronCreate(arg0, arg1 string, arg2 *drone.Cron) (*drone.Cron, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CronCreate", arg0, arg1, arg2) ret0, _ := ret[0].(*drone.Cron) ret1, _ := ret[1].(error) return ret0, ret1 } // CronCreate indicates an expected call of CronCreate. func (mr *MockClientMockRecorder) CronCreate(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CronCreate", reflect.TypeOf((*MockClient)(nil).CronCreate), arg0, arg1, arg2) } // CronDelete mocks base method. func (m *MockClient) CronDelete(arg0, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CronDelete", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // CronDelete indicates an expected call of CronDelete. func (mr *MockClientMockRecorder) CronDelete(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CronDelete", reflect.TypeOf((*MockClient)(nil).CronDelete), arg0, arg1, arg2) } // CronList mocks base method. func (m *MockClient) CronList(arg0, arg1 string) ([]*drone.Cron, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CronList", arg0, arg1) ret0, _ := ret[0].([]*drone.Cron) ret1, _ := ret[1].(error) return ret0, ret1 } // CronList indicates an expected call of CronList. func (mr *MockClientMockRecorder) CronList(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CronList", reflect.TypeOf((*MockClient)(nil).CronList), arg0, arg1) } // CronUpdate mocks base method. func (m *MockClient) CronUpdate(arg0, arg1, arg2 string, arg3 *drone.CronPatch) (*drone.Cron, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CronUpdate", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(*drone.Cron) ret1, _ := ret[1].(error) return ret0, ret1 } // CronUpdate indicates an expected call of CronUpdate. func (mr *MockClientMockRecorder) CronUpdate(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CronUpdate", reflect.TypeOf((*MockClient)(nil).CronUpdate), arg0, arg1, arg2, arg3) } // Decline mocks base method. func (m *MockClient) Decline(arg0, arg1 string, arg2, arg3 int) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Decline", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) return ret0 } // Decline indicates an expected call of Decline. func (mr *MockClientMockRecorder) Decline(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Decline", reflect.TypeOf((*MockClient)(nil).Decline), arg0, arg1, arg2, arg3) } // Encrypt mocks base method. func (m *MockClient) Encrypt(arg0, arg1 string, arg2 *drone.Secret) (string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Encrypt", arg0, arg1, arg2) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } // Encrypt indicates an expected call of Encrypt. func (mr *MockClientMockRecorder) Encrypt(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Encrypt", reflect.TypeOf((*MockClient)(nil).Encrypt), arg0, arg1, arg2) } // Logs mocks base method. func (m *MockClient) Logs(arg0, arg1 string, arg2, arg3, arg4 int) ([]*drone.Line, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Logs", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].([]*drone.Line) ret1, _ := ret[1].(error) return ret0, ret1 } // Logs indicates an expected call of Logs. func (mr *MockClientMockRecorder) Logs(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logs", reflect.TypeOf((*MockClient)(nil).Logs), arg0, arg1, arg2, arg3, arg4) } // LogsPurge mocks base method. func (m *MockClient) LogsPurge(arg0, arg1 string, arg2, arg3, arg4 int) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "LogsPurge", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(error) return ret0 } // LogsPurge indicates an expected call of LogsPurge. func (mr *MockClientMockRecorder) LogsPurge(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogsPurge", reflect.TypeOf((*MockClient)(nil).LogsPurge), arg0, arg1, arg2, arg3, arg4) } // Node mocks base method. func (m *MockClient) Node(arg0 string) (*drone.Node, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Node", arg0) ret0, _ := ret[0].(*drone.Node) ret1, _ := ret[1].(error) return ret0, ret1 } // Node indicates an expected call of Node. func (mr *MockClientMockRecorder) Node(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Node", reflect.TypeOf((*MockClient)(nil).Node), arg0) } // NodeCreate mocks base method. func (m *MockClient) NodeCreate(arg0 *drone.Node) (*drone.Node, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NodeCreate", arg0) ret0, _ := ret[0].(*drone.Node) ret1, _ := ret[1].(error) return ret0, ret1 } // NodeCreate indicates an expected call of NodeCreate. func (mr *MockClientMockRecorder) NodeCreate(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeCreate", reflect.TypeOf((*MockClient)(nil).NodeCreate), arg0) } // NodeDelete mocks base method. func (m *MockClient) NodeDelete(arg0 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NodeDelete", arg0) ret0, _ := ret[0].(error) return ret0 } // NodeDelete indicates an expected call of NodeDelete. func (mr *MockClientMockRecorder) NodeDelete(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeDelete", reflect.TypeOf((*MockClient)(nil).NodeDelete), arg0) } // NodeList mocks base method. func (m *MockClient) NodeList() ([]*drone.Node, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NodeList") ret0, _ := ret[0].([]*drone.Node) ret1, _ := ret[1].(error) return ret0, ret1 } // NodeList indicates an expected call of NodeList. func (mr *MockClientMockRecorder) NodeList() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeList", reflect.TypeOf((*MockClient)(nil).NodeList)) } // NodeUpdate mocks base method. func (m *MockClient) NodeUpdate(arg0 string, arg1 *drone.NodePatch) (*drone.Node, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NodeUpdate", arg0, arg1) ret0, _ := ret[0].(*drone.Node) ret1, _ := ret[1].(error) return ret0, ret1 } // NodeUpdate indicates an expected call of NodeUpdate. func (mr *MockClientMockRecorder) NodeUpdate(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeUpdate", reflect.TypeOf((*MockClient)(nil).NodeUpdate), arg0, arg1) } // OrgSecret mocks base method. func (m *MockClient) OrgSecret(arg0, arg1 string) (*drone.Secret, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "OrgSecret", arg0, arg1) ret0, _ := ret[0].(*drone.Secret) ret1, _ := ret[1].(error) return ret0, ret1 } // OrgSecret indicates an expected call of OrgSecret. func (mr *MockClientMockRecorder) OrgSecret(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrgSecret", reflect.TypeOf((*MockClient)(nil).OrgSecret), arg0, arg1) } // OrgSecretCreate mocks base method. func (m *MockClient) OrgSecretCreate(arg0 string, arg1 *drone.Secret) (*drone.Secret, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "OrgSecretCreate", arg0, arg1) ret0, _ := ret[0].(*drone.Secret) ret1, _ := ret[1].(error) return ret0, ret1 } // OrgSecretCreate indicates an expected call of OrgSecretCreate. func (mr *MockClientMockRecorder) OrgSecretCreate(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrgSecretCreate", reflect.TypeOf((*MockClient)(nil).OrgSecretCreate), arg0, arg1) } // OrgSecretDelete mocks base method. func (m *MockClient) OrgSecretDelete(arg0, arg1 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "OrgSecretDelete", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // OrgSecretDelete indicates an expected call of OrgSecretDelete. func (mr *MockClientMockRecorder) OrgSecretDelete(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrgSecretDelete", reflect.TypeOf((*MockClient)(nil).OrgSecretDelete), arg0, arg1) } // OrgSecretList mocks base method. func (m *MockClient) OrgSecretList(arg0 string) ([]*drone.Secret, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "OrgSecretList", arg0) ret0, _ := ret[0].([]*drone.Secret) ret1, _ := ret[1].(error) return ret0, ret1 } // OrgSecretList indicates an expected call of OrgSecretList. func (mr *MockClientMockRecorder) OrgSecretList(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrgSecretList", reflect.TypeOf((*MockClient)(nil).OrgSecretList), arg0) } // OrgSecretListAll mocks base method. func (m *MockClient) OrgSecretListAll() ([]*drone.Secret, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "OrgSecretListAll") ret0, _ := ret[0].([]*drone.Secret) ret1, _ := ret[1].(error) return ret0, ret1 } // OrgSecretListAll indicates an expected call of OrgSecretListAll. func (mr *MockClientMockRecorder) OrgSecretListAll() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrgSecretListAll", reflect.TypeOf((*MockClient)(nil).OrgSecretListAll)) } // OrgSecretUpdate mocks base method. func (m *MockClient) OrgSecretUpdate(arg0 string, arg1 *drone.Secret) (*drone.Secret, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "OrgSecretUpdate", arg0, arg1) ret0, _ := ret[0].(*drone.Secret) ret1, _ := ret[1].(error) return ret0, ret1 } // OrgSecretUpdate indicates an expected call of OrgSecretUpdate. func (mr *MockClientMockRecorder) OrgSecretUpdate(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrgSecretUpdate", reflect.TypeOf((*MockClient)(nil).OrgSecretUpdate), arg0, arg1) } // Promote mocks base method. func (m *MockClient) Promote(arg0, arg1 string, arg2 int, arg3 string, arg4 map[string]string) (*drone.Build, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Promote", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(*drone.Build) ret1, _ := ret[1].(error) return ret0, ret1 } // Promote indicates an expected call of Promote. func (mr *MockClientMockRecorder) Promote(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Promote", reflect.TypeOf((*MockClient)(nil).Promote), arg0, arg1, arg2, arg3, arg4) } // Queue mocks base method. func (m *MockClient) Queue() ([]*drone.Stage, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Queue") ret0, _ := ret[0].([]*drone.Stage) ret1, _ := ret[1].(error) return ret0, ret1 } // Queue indicates an expected call of Queue. func (mr *MockClientMockRecorder) Queue() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Queue", reflect.TypeOf((*MockClient)(nil).Queue)) } // QueuePause mocks base method. func (m *MockClient) QueuePause() error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "QueuePause") ret0, _ := ret[0].(error) return ret0 } // QueuePause indicates an expected call of QueuePause. func (mr *MockClientMockRecorder) QueuePause() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueuePause", reflect.TypeOf((*MockClient)(nil).QueuePause)) } // QueueResume mocks base method. func (m *MockClient) QueueResume() error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "QueueResume") ret0, _ := ret[0].(error) return ret0 } // QueueResume indicates an expected call of QueueResume. func (mr *MockClientMockRecorder) QueueResume() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueueResume", reflect.TypeOf((*MockClient)(nil).QueueResume)) } // Repo mocks base method. func (m *MockClient) Repo(arg0, arg1 string) (*drone.Repo, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Repo", arg0, arg1) ret0, _ := ret[0].(*drone.Repo) ret1, _ := ret[1].(error) return ret0, ret1 } // Repo indicates an expected call of Repo. func (mr *MockClientMockRecorder) Repo(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Repo", reflect.TypeOf((*MockClient)(nil).Repo), arg0, arg1) } // RepoChown mocks base method. func (m *MockClient) RepoChown(arg0, arg1 string) (*drone.Repo, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RepoChown", arg0, arg1) ret0, _ := ret[0].(*drone.Repo) ret1, _ := ret[1].(error) return ret0, ret1 } // RepoChown indicates an expected call of RepoChown. func (mr *MockClientMockRecorder) RepoChown(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RepoChown", reflect.TypeOf((*MockClient)(nil).RepoChown), arg0, arg1) } // RepoDelete mocks base method. func (m *MockClient) RepoDelete(arg0, arg1 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RepoDelete", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // RepoDelete indicates an expected call of RepoDelete. func (mr *MockClientMockRecorder) RepoDelete(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RepoDelete", reflect.TypeOf((*MockClient)(nil).RepoDelete), arg0, arg1) } // RepoDisable mocks base method. func (m *MockClient) RepoDisable(arg0, arg1 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RepoDisable", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // RepoDisable indicates an expected call of RepoDisable. func (mr *MockClientMockRecorder) RepoDisable(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RepoDisable", reflect.TypeOf((*MockClient)(nil).RepoDisable), arg0, arg1) } // RepoEnable mocks base method. func (m *MockClient) RepoEnable(arg0, arg1 string) (*drone.Repo, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RepoEnable", arg0, arg1) ret0, _ := ret[0].(*drone.Repo) ret1, _ := ret[1].(error) return ret0, ret1 } // RepoEnable indicates an expected call of RepoEnable. func (mr *MockClientMockRecorder) RepoEnable(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RepoEnable", reflect.TypeOf((*MockClient)(nil).RepoEnable), arg0, arg1) } // RepoList mocks base method. func (m *MockClient) RepoList() ([]*drone.Repo, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RepoList") ret0, _ := ret[0].([]*drone.Repo) ret1, _ := ret[1].(error) return ret0, ret1 } // RepoList indicates an expected call of RepoList. func (mr *MockClientMockRecorder) RepoList() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RepoList", reflect.TypeOf((*MockClient)(nil).RepoList)) } // RepoListSync mocks base method. func (m *MockClient) RepoListSync() ([]*drone.Repo, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RepoListSync") ret0, _ := ret[0].([]*drone.Repo) ret1, _ := ret[1].(error) return ret0, ret1 } // RepoListSync indicates an expected call of RepoListSync. func (mr *MockClientMockRecorder) RepoListSync() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RepoListSync", reflect.TypeOf((*MockClient)(nil).RepoListSync)) } // RepoRepair mocks base method. func (m *MockClient) RepoRepair(arg0, arg1 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RepoRepair", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // RepoRepair indicates an expected call of RepoRepair. func (mr *MockClientMockRecorder) RepoRepair(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RepoRepair", reflect.TypeOf((*MockClient)(nil).RepoRepair), arg0, arg1) } // RepoUpdate mocks base method. func (m *MockClient) RepoUpdate(arg0, arg1 string, arg2 *drone.RepoPatch) (*drone.Repo, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RepoUpdate", arg0, arg1, arg2) ret0, _ := ret[0].(*drone.Repo) ret1, _ := ret[1].(error) return ret0, ret1 } // RepoUpdate indicates an expected call of RepoUpdate. func (mr *MockClientMockRecorder) RepoUpdate(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RepoUpdate", reflect.TypeOf((*MockClient)(nil).RepoUpdate), arg0, arg1, arg2) } // Rollback mocks base method. func (m *MockClient) Rollback(arg0, arg1 string, arg2 int, arg3 string, arg4 map[string]string) (*drone.Build, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Rollback", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(*drone.Build) ret1, _ := ret[1].(error) return ret0, ret1 } // Rollback indicates an expected call of Rollback. func (mr *MockClientMockRecorder) Rollback(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rollback", reflect.TypeOf((*MockClient)(nil).Rollback), arg0, arg1, arg2, arg3, arg4) } // Secret mocks base method. func (m *MockClient) Secret(arg0, arg1, arg2 string) (*drone.Secret, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Secret", arg0, arg1, arg2) ret0, _ := ret[0].(*drone.Secret) ret1, _ := ret[1].(error) return ret0, ret1 } // Secret indicates an expected call of Secret. func (mr *MockClientMockRecorder) Secret(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Secret", reflect.TypeOf((*MockClient)(nil).Secret), arg0, arg1, arg2) } // SecretCreate mocks base method. func (m *MockClient) SecretCreate(arg0, arg1 string, arg2 *drone.Secret) (*drone.Secret, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SecretCreate", arg0, arg1, arg2) ret0, _ := ret[0].(*drone.Secret) ret1, _ := ret[1].(error) return ret0, ret1 } // SecretCreate indicates an expected call of SecretCreate. func (mr *MockClientMockRecorder) SecretCreate(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SecretCreate", reflect.TypeOf((*MockClient)(nil).SecretCreate), arg0, arg1, arg2) } // SecretDelete mocks base method. func (m *MockClient) SecretDelete(arg0, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SecretDelete", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // SecretDelete indicates an expected call of SecretDelete. func (mr *MockClientMockRecorder) SecretDelete(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SecretDelete", reflect.TypeOf((*MockClient)(nil).SecretDelete), arg0, arg1, arg2) } // SecretList mocks base method. func (m *MockClient) SecretList(arg0, arg1 string) ([]*drone.Secret, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SecretList", arg0, arg1) ret0, _ := ret[0].([]*drone.Secret) ret1, _ := ret[1].(error) return ret0, ret1 } // SecretList indicates an expected call of SecretList. func (mr *MockClientMockRecorder) SecretList(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SecretList", reflect.TypeOf((*MockClient)(nil).SecretList), arg0, arg1) } // SecretUpdate mocks base method. func (m *MockClient) SecretUpdate(arg0, arg1 string, arg2 *drone.Secret) (*drone.Secret, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SecretUpdate", arg0, arg1, arg2) ret0, _ := ret[0].(*drone.Secret) ret1, _ := ret[1].(error) return ret0, ret1 } // SecretUpdate indicates an expected call of SecretUpdate. func (mr *MockClientMockRecorder) SecretUpdate(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SecretUpdate", reflect.TypeOf((*MockClient)(nil).SecretUpdate), arg0, arg1, arg2) } // Self mocks base method. func (m *MockClient) Self() (*drone.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Self") ret0, _ := ret[0].(*drone.User) ret1, _ := ret[1].(error) return ret0, ret1 } // Self indicates an expected call of Self. func (mr *MockClientMockRecorder) Self() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Self", reflect.TypeOf((*MockClient)(nil).Self)) } // Server mocks base method. func (m *MockClient) Server(arg0 string) (*drone.Server, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Server", arg0) ret0, _ := ret[0].(*drone.Server) ret1, _ := ret[1].(error) return ret0, ret1 } // Server indicates an expected call of Server. func (mr *MockClientMockRecorder) Server(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Server", reflect.TypeOf((*MockClient)(nil).Server), arg0) } // ServerCreate mocks base method. func (m *MockClient) ServerCreate() (*drone.Server, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ServerCreate") ret0, _ := ret[0].(*drone.Server) ret1, _ := ret[1].(error) return ret0, ret1 } // ServerCreate indicates an expected call of ServerCreate. func (mr *MockClientMockRecorder) ServerCreate() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServerCreate", reflect.TypeOf((*MockClient)(nil).ServerCreate)) } // ServerDelete mocks base method. func (m *MockClient) ServerDelete(arg0 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ServerDelete", arg0) ret0, _ := ret[0].(error) return ret0 } // ServerDelete indicates an expected call of ServerDelete. func (mr *MockClientMockRecorder) ServerDelete(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServerDelete", reflect.TypeOf((*MockClient)(nil).ServerDelete), arg0) } // ServerList mocks base method. func (m *MockClient) ServerList() ([]*drone.Server, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ServerList") ret0, _ := ret[0].([]*drone.Server) ret1, _ := ret[1].(error) return ret0, ret1 } // ServerList indicates an expected call of ServerList. func (mr *MockClientMockRecorder) ServerList() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServerList", reflect.TypeOf((*MockClient)(nil).ServerList)) } // SetAddress mocks base method. func (m *MockClient) SetAddress(arg0 string) { m.ctrl.T.Helper() m.ctrl.Call(m, "SetAddress", arg0) } // SetAddress indicates an expected call of SetAddress. func (mr *MockClientMockRecorder) SetAddress(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAddress", reflect.TypeOf((*MockClient)(nil).SetAddress), arg0) } // SetClient mocks base method. func (m *MockClient) SetClient(arg0 *http.Client) { m.ctrl.T.Helper() m.ctrl.Call(m, "SetClient", arg0) } // SetClient indicates an expected call of SetClient. func (mr *MockClientMockRecorder) SetClient(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetClient", reflect.TypeOf((*MockClient)(nil).SetClient), arg0) } // Sign mocks base method. func (m *MockClient) Sign(arg0, arg1, arg2 string) (string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Sign", arg0, arg1, arg2) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } // Sign indicates an expected call of Sign. func (mr *MockClientMockRecorder) Sign(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sign", reflect.TypeOf((*MockClient)(nil).Sign), arg0, arg1, arg2) } // User mocks base method. func (m *MockClient) User(arg0 string) (*drone.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "User", arg0) ret0, _ := ret[0].(*drone.User) ret1, _ := ret[1].(error) return ret0, ret1 } // User indicates an expected call of User. func (mr *MockClientMockRecorder) User(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "User", reflect.TypeOf((*MockClient)(nil).User), arg0) } // UserCreate mocks base method. func (m *MockClient) UserCreate(arg0 *drone.User) (*drone.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UserCreate", arg0) ret0, _ := ret[0].(*drone.User) ret1, _ := ret[1].(error) return ret0, ret1 } // UserCreate indicates an expected call of UserCreate. func (mr *MockClientMockRecorder) UserCreate(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserCreate", reflect.TypeOf((*MockClient)(nil).UserCreate), arg0) } // UserDelete mocks base method. func (m *MockClient) UserDelete(arg0 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UserDelete", arg0) ret0, _ := ret[0].(error) return ret0 } // UserDelete indicates an expected call of UserDelete. func (mr *MockClientMockRecorder) UserDelete(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserDelete", reflect.TypeOf((*MockClient)(nil).UserDelete), arg0) } // UserList mocks base method. func (m *MockClient) UserList() ([]*drone.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UserList") ret0, _ := ret[0].([]*drone.User) ret1, _ := ret[1].(error) return ret0, ret1 } // UserList indicates an expected call of UserList. func (mr *MockClientMockRecorder) UserList() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserList", reflect.TypeOf((*MockClient)(nil).UserList)) } // UserUpdate mocks base method. func (m *MockClient) UserUpdate(arg0 string, arg1 *drone.UserPatch) (*drone.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UserUpdate", arg0, arg1) ret0, _ := ret[0].(*drone.User) ret1, _ := ret[1].(error) return ret0, ret1 } // UserUpdate indicates an expected call of UserUpdate. func (mr *MockClientMockRecorder) UserUpdate(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserUpdate", reflect.TypeOf((*MockClient)(nil).UserUpdate), arg0, arg1) } // Verify mocks base method. func (m *MockClient) Verify(arg0, arg1, arg2 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Verify", arg0, arg1, arg2) ret0, _ := ret[0].(error) return ret0 } // Verify indicates an expected call of Verify. func (mr *MockClientMockRecorder) Verify(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockClient)(nil).Verify), arg0, arg1, arg2) } ================================================ FILE: mocks/mock_engine.go ================================================ // Code generated by MockGen. DO NOT EDIT. // Source: github.com/drone/autoscaler (interfaces: Engine) // Package mocks is a generated GoMock package. package mocks import ( context "context" reflect "reflect" gomock "github.com/golang/mock/gomock" ) // MockEngine is a mock of Engine interface. type MockEngine struct { ctrl *gomock.Controller recorder *MockEngineMockRecorder } // MockEngineMockRecorder is the mock recorder for MockEngine. type MockEngineMockRecorder struct { mock *MockEngine } // NewMockEngine creates a new mock instance. func NewMockEngine(ctrl *gomock.Controller) *MockEngine { mock := &MockEngine{ctrl: ctrl} mock.recorder = &MockEngineMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockEngine) EXPECT() *MockEngineMockRecorder { return m.recorder } // Pause mocks base method. func (m *MockEngine) Pause() { m.ctrl.T.Helper() m.ctrl.Call(m, "Pause") } // Pause indicates an expected call of Pause. func (mr *MockEngineMockRecorder) Pause() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Pause", reflect.TypeOf((*MockEngine)(nil).Pause)) } // Paused mocks base method. func (m *MockEngine) Paused() bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Paused") ret0, _ := ret[0].(bool) return ret0 } // Paused indicates an expected call of Paused. func (mr *MockEngineMockRecorder) Paused() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Paused", reflect.TypeOf((*MockEngine)(nil).Paused)) } // Resume mocks base method. func (m *MockEngine) Resume() { m.ctrl.T.Helper() m.ctrl.Call(m, "Resume") } // Resume indicates an expected call of Resume. func (mr *MockEngineMockRecorder) Resume() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resume", reflect.TypeOf((*MockEngine)(nil).Resume)) } // Start mocks base method. func (m *MockEngine) Start(arg0 context.Context) { m.ctrl.T.Helper() m.ctrl.Call(m, "Start", arg0) } // Start indicates an expected call of Start. func (mr *MockEngineMockRecorder) Start(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockEngine)(nil).Start), arg0) } ================================================ FILE: mocks/mock_metrics.go ================================================ // Code generated by MockGen. DO NOT EDIT. // Source: github.com/drone/autoscaler/metrics (interfaces: Collector) // Package mocks is a generated GoMock package. package mocks import ( reflect "reflect" time "time" gomock "github.com/golang/mock/gomock" ) // MockCollector is a mock of Collector interface. type MockCollector struct { ctrl *gomock.Controller recorder *MockCollectorMockRecorder } // MockCollectorMockRecorder is the mock recorder for MockCollector. type MockCollectorMockRecorder struct { mock *MockCollector } // NewMockCollector creates a new mock instance. func NewMockCollector(ctrl *gomock.Controller) *MockCollector { mock := &MockCollector{ctrl: ctrl} mock.recorder = &MockCollectorMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockCollector) EXPECT() *MockCollectorMockRecorder { return m.recorder } // IncrServerCreateError mocks base method. func (m *MockCollector) IncrServerCreateError() { m.ctrl.T.Helper() m.ctrl.Call(m, "IncrServerCreateError") } // IncrServerCreateError indicates an expected call of IncrServerCreateError. func (mr *MockCollectorMockRecorder) IncrServerCreateError() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrServerCreateError", reflect.TypeOf((*MockCollector)(nil).IncrServerCreateError)) } // IncrServerInitError mocks base method. func (m *MockCollector) IncrServerInitError() { m.ctrl.T.Helper() m.ctrl.Call(m, "IncrServerInitError") } // IncrServerInitError indicates an expected call of IncrServerInitError. func (mr *MockCollectorMockRecorder) IncrServerInitError() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrServerInitError", reflect.TypeOf((*MockCollector)(nil).IncrServerInitError)) } // IncrServerSetupError mocks base method. func (m *MockCollector) IncrServerSetupError() { m.ctrl.T.Helper() m.ctrl.Call(m, "IncrServerSetupError") } // IncrServerSetupError indicates an expected call of IncrServerSetupError. func (mr *MockCollectorMockRecorder) IncrServerSetupError() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrServerSetupError", reflect.TypeOf((*MockCollector)(nil).IncrServerSetupError)) } // TrackServerCreateTime mocks base method. func (m *MockCollector) TrackServerCreateTime(arg0 time.Time) { m.ctrl.T.Helper() m.ctrl.Call(m, "TrackServerCreateTime", arg0) } // TrackServerCreateTime indicates an expected call of TrackServerCreateTime. func (mr *MockCollectorMockRecorder) TrackServerCreateTime(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TrackServerCreateTime", reflect.TypeOf((*MockCollector)(nil).TrackServerCreateTime), arg0) } // TrackServerInitTime mocks base method. func (m *MockCollector) TrackServerInitTime(arg0 time.Time) { m.ctrl.T.Helper() m.ctrl.Call(m, "TrackServerInitTime", arg0) } // TrackServerInitTime indicates an expected call of TrackServerInitTime. func (mr *MockCollectorMockRecorder) TrackServerInitTime(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TrackServerInitTime", reflect.TypeOf((*MockCollector)(nil).TrackServerInitTime), arg0) } // TrackServerSetupTime mocks base method. func (m *MockCollector) TrackServerSetupTime(arg0 time.Time) { m.ctrl.T.Helper() m.ctrl.Call(m, "TrackServerSetupTime", arg0) } // TrackServerSetupTime indicates an expected call of TrackServerSetupTime. func (mr *MockCollectorMockRecorder) TrackServerSetupTime(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TrackServerSetupTime", reflect.TypeOf((*MockCollector)(nil).TrackServerSetupTime), arg0) } ================================================ FILE: mocks/mock_provider.go ================================================ // Code generated by MockGen. DO NOT EDIT. // Source: github.com/drone/autoscaler (interfaces: Provider) // Package mocks is a generated GoMock package. package mocks import ( context "context" reflect "reflect" autoscaler "github.com/drone/autoscaler" gomock "github.com/golang/mock/gomock" ) // MockProvider is a mock of Provider interface. type MockProvider struct { ctrl *gomock.Controller recorder *MockProviderMockRecorder } // MockProviderMockRecorder is the mock recorder for MockProvider. type MockProviderMockRecorder struct { mock *MockProvider } // NewMockProvider creates a new mock instance. func NewMockProvider(ctrl *gomock.Controller) *MockProvider { mock := &MockProvider{ctrl: ctrl} mock.recorder = &MockProviderMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockProvider) EXPECT() *MockProviderMockRecorder { return m.recorder } // Create mocks base method. func (m *MockProvider) Create(arg0 context.Context, arg1 autoscaler.InstanceCreateOpts) (*autoscaler.Instance, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Create", arg0, arg1) ret0, _ := ret[0].(*autoscaler.Instance) ret1, _ := ret[1].(error) return ret0, ret1 } // Create indicates an expected call of Create. func (mr *MockProviderMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockProvider)(nil).Create), arg0, arg1) } // Destroy mocks base method. func (m *MockProvider) Destroy(arg0 context.Context, arg1 *autoscaler.Instance) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Destroy", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // Destroy indicates an expected call of Destroy. func (mr *MockProviderMockRecorder) Destroy(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Destroy", reflect.TypeOf((*MockProvider)(nil).Destroy), arg0, arg1) } ================================================ FILE: mocks/mock_server.go ================================================ // Code generated by MockGen. DO NOT EDIT. // Source: github.com/drone/autoscaler (interfaces: ServerStore) // Package mocks is a generated GoMock package. package mocks import ( context "context" reflect "reflect" autoscaler "github.com/drone/autoscaler" gomock "github.com/golang/mock/gomock" ) // MockServerStore is a mock of ServerStore interface. type MockServerStore struct { ctrl *gomock.Controller recorder *MockServerStoreMockRecorder } // MockServerStoreMockRecorder is the mock recorder for MockServerStore. type MockServerStoreMockRecorder struct { mock *MockServerStore } // NewMockServerStore creates a new mock instance. func NewMockServerStore(ctrl *gomock.Controller) *MockServerStore { mock := &MockServerStore{ctrl: ctrl} mock.recorder = &MockServerStoreMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockServerStore) EXPECT() *MockServerStoreMockRecorder { return m.recorder } // Create mocks base method. func (m *MockServerStore) Create(arg0 context.Context, arg1 *autoscaler.Server) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Create", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // Create indicates an expected call of Create. func (mr *MockServerStoreMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockServerStore)(nil).Create), arg0, arg1) } // Delete mocks base method. func (m *MockServerStore) Delete(arg0 context.Context, arg1 *autoscaler.Server) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Delete", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // Delete indicates an expected call of Delete. func (mr *MockServerStoreMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockServerStore)(nil).Delete), arg0, arg1) } // Find mocks base method. func (m *MockServerStore) Find(arg0 context.Context, arg1 string) (*autoscaler.Server, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Find", arg0, arg1) ret0, _ := ret[0].(*autoscaler.Server) ret1, _ := ret[1].(error) return ret0, ret1 } // Find indicates an expected call of Find. func (mr *MockServerStoreMockRecorder) Find(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockServerStore)(nil).Find), arg0, arg1) } // List mocks base method. func (m *MockServerStore) List(arg0 context.Context) ([]*autoscaler.Server, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List", arg0) ret0, _ := ret[0].([]*autoscaler.Server) ret1, _ := ret[1].(error) return ret0, ret1 } // List indicates an expected call of List. func (mr *MockServerStoreMockRecorder) List(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockServerStore)(nil).List), arg0) } // ListState mocks base method. func (m *MockServerStore) ListState(arg0 context.Context, arg1 autoscaler.ServerState) ([]*autoscaler.Server, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListState", arg0, arg1) ret0, _ := ret[0].([]*autoscaler.Server) ret1, _ := ret[1].(error) return ret0, ret1 } // ListState indicates an expected call of ListState. func (mr *MockServerStoreMockRecorder) ListState(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListState", reflect.TypeOf((*MockServerStore)(nil).ListState), arg0, arg1) } // Purge mocks base method. func (m *MockServerStore) Purge(arg0 context.Context, arg1 int64) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Purge", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // Purge indicates an expected call of Purge. func (mr *MockServerStoreMockRecorder) Purge(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Purge", reflect.TypeOf((*MockServerStore)(nil).Purge), arg0, arg1) } // Update mocks base method. func (m *MockServerStore) Update(arg0 context.Context, arg1 *autoscaler.Server) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Update", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // Update indicates an expected call of Update. func (mr *MockServerStoreMockRecorder) Update(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockServerStore)(nil).Update), arg0, arg1) } ================================================ FILE: mocks/mocks.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package mocks //go:generate mockgen -package=mocks -destination=mock_engine.go github.com/drone/autoscaler Engine //go:generate mockgen -package=mocks -destination=mock_server.go github.com/drone/autoscaler ServerStore //go:generate mockgen -package=mocks -destination=mock_provider.go github.com/drone/autoscaler Provider //go:generate mockgen -package=mocks -destination=mock_metrics.go github.com/drone/autoscaler/metrics Collector //go:generate mockgen -package=mocks -destination=mock_drone.go github.com/drone/drone-go/drone Client //go:generate mockgen -package=mocks -destination=mock_docker.go github.com/docker/docker/client APIClient ================================================ FILE: provider.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package autoscaler import ( "context" "database/sql/driver" "errors" ) // ProviderType specifies the hosting provider. type ProviderType string // Value converts the value to a sql string. func (s ProviderType) Value() (driver.Value, error) { return string(s), nil } // Provider type enumeration. const ( ProviderAmazon = ProviderType("amazon") ProviderAzure = ProviderType("azure") ProviderDigitalOcean = ProviderType("digitalocean") ProviderGoogle = ProviderType("google") ProviderHetznerCloud = ProviderType("hetznercloud") ProviderLinode = ProviderType("linode") ProviderOpenStack = ProviderType("openstack") ProviderPacket = ProviderType("packet") ProviderScaleway = ProviderType("scaleway") ProviderVultr = ProviderType("vultr") ) // ErrInstanceNotFound is returned when the requested // instance does not exist in the cloud provider. var ErrInstanceNotFound = errors.New("Not Found") // A Provider represents a hosting provider, such as // Digital Ocean and is responsible for server management. type Provider interface { // Create creates a new server. Create(context.Context, InstanceCreateOpts) (*Instance, error) // Destroy destroys an existing server. Destroy(context.Context, *Instance) error } // An Instance represents a server instance // (e.g Digital Ocean Droplet). type Instance struct { Provider ProviderType ID string Name string Address string Region string Image string Size string ServiceAccountEmail string Scopes []string } // InstanceCreateOpts define soptional instructions for // creating server instances. type InstanceCreateOpts struct { Name string CAKey []byte CACert []byte TLSKey []byte TLSCert []byte } // InstanceError snapshots an error creating an instance // with server logs. type InstanceError struct { Err error Logs []byte } // Error implements the error interface. func (e *InstanceError) Error() string { return e.Err.Error() } ================================================ FILE: server/auth.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package server import ( "net/http" "strings" "github.com/drone/autoscaler/config" "github.com/drone/autoscaler/logger" "github.com/drone/drone-go/drone" "golang.org/x/oauth2" ) // CheckDrone returns a middleware function that authorizes // the incoming http.Request using the Drone API. func CheckDrone(conf config.Config) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() log := logger.FromContext(ctx) // the user can authenticate with a global authorization // token provied in the Authorization header. token := r.Header.Get("Authorization") token = strings.TrimPrefix(token, "Bearer ") token = strings.TrimSpace(token) if token == "" { log.Debugln("missing authorization header") writeUnauthorized(w, errInvalidToken) return } // creates a new drone client using the bearer token // in the incoming request to authenticate with drone. config := new(oauth2.Config) auther := config.Client( oauth2.NoContext, &oauth2.Token{ AccessToken: token, }, ) server := conf.Server.Proto + "://" + conf.Server.Host client := drone.NewClient(server, auther) // fetch the user account associated with the currently // authenticated bearer token. This user must exist in // drone and must be an administrator. user, err := client.Self() if err != nil { log.WithError(err). Errorln("cannot authenticate user") writeUnauthorized(w, errUnauthorized) return } if !user.Admin { log.WithError(err). WithField("username", user.Login). Errorln("insufficient privileges") writeForbidden(w, errForbidden) return } log = log.WithField("username", user.Login) log.Debugln("user authorized") next.ServeHTTP(w, r.WithContext( logger.WithContext(ctx, log), )) }) } } ================================================ FILE: server/auth_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package server import ( "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/drone/autoscaler/config" "github.com/drone/drone-go/drone" "github.com/h2non/gock" ) func TestAuthorize(t *testing.T) { defer gock.Off() w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/", nil) r.Header.Set("Authorization", "Bearer NTE2M2MwMWRlYToxNGM3MWEyYTIx") user := &drone.User{ Login: "octocat", Admin: true, } c := config.Config{} c.Server.Host = "company.drone.com" c.Server.Proto = "https" gock.New("https://company.drone.com"). Get("/api/user"). MatchHeader("Authorization", "Bearer NTE2M2MwMWRlYToxNGM3MWEyYTIx"). Reply(200). JSON(user) CheckDrone(c)( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusTeapot) }), ).ServeHTTP(w, r) if got, want := w.Code, http.StatusTeapot; got != want { t.Errorf("Want status code %d, got %d", want, got) } } func TestAuthorizeMissingToken(t *testing.T) { defer gock.Off() w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/", nil) c := config.Config{} c.Server.Host = "company.drone.com" c.Server.Proto = "https" CheckDrone(c)( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Errorf("Expect access to handler is restricted") }), ).ServeHTTP(w, r) if got, want := w.Code, 401; got != want { t.Errorf("Want status code %d, got %d", want, got) } errjson := &Error{} json.NewDecoder(w.Body).Decode(errjson) if got, want := errjson.Message, errInvalidToken.Error(); got != want { t.Errorf("Want error message %s, got %s", want, got) } } func TestAuthorizeNotFound(t *testing.T) { defer gock.Off() w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/", nil) r.Header.Set("Authorization", "Bearer NTE2M2MwMWRlYToxNGM3MWEyYTIx") c := config.Config{} c.Server.Host = "company.drone.com" c.Server.Proto = "https" gock.New("https://company.drone.com"). Get("/api/user"). MatchHeader("Authorization", "Bearer NTE2M2MwMWRlYToxNGM3MWEyYTIx"). Reply(404) CheckDrone(c)( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Errorf("Expect access to handler is restricted") }), ).ServeHTTP(w, r) if got, want := w.Code, 401; got != want { t.Errorf("Want status code %d, got %d", want, got) } errjson := &Error{} json.NewDecoder(w.Body).Decode(errjson) if got, want := errjson.Message, errUnauthorized.Error(); got != want { t.Errorf("Want error message %s, got %s", want, got) } } func TestAuthorizeNonAdmin(t *testing.T) { defer gock.Off() w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/", nil) r.Header.Set("Authorization", "Bearer NTE2M2MwMWRlYToxNGM3MWEyYTIx") user := &drone.User{ Login: "octocat", Admin: false, } c := config.Config{} c.Server.Host = "company.drone.com" c.Server.Proto = "https" gock.New("https://company.drone.com"). Get("/api/user"). MatchHeader("Authorization", "Bearer NTE2M2MwMWRlYToxNGM3MWEyYTIx"). Reply(200). JSON(user) CheckDrone(c)( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Errorf("Expect access to handler is restricted") }), ).ServeHTTP(w, r) if got, want := w.Code, 403; got != want { t.Errorf("Want status code %d, got %d", want, got) } errjson := &Error{} json.NewDecoder(w.Body).Decode(errjson) if got, want := errjson.Message, errForbidden.Error(); got != want { t.Errorf("Want error message %s, got %s", want, got) } } ================================================ FILE: server/engine.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package server import ( "net/http" "github.com/drone/autoscaler" ) // HandleEnginePause returns an http.HandlerFunc that pauses // scaling engine. func HandleEnginePause(engine autoscaler.Engine) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { engine.Pause() w.WriteHeader(204) } } // HandleEngineResume returns an http.HandlerFunc that resumes // scaling engine. func HandleEngineResume(engine autoscaler.Engine) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { engine.Resume() w.WriteHeader(204) } } ================================================ FILE: server/engine_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package server import ( "net/http/httptest" "testing" "github.com/drone/autoscaler/mocks" "github.com/golang/mock/gomock" ) func TestHandleEnginePause(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() w := httptest.NewRecorder() r := httptest.NewRequest("POST", "/api/pause", nil) e := mocks.NewMockEngine(controller) e.EXPECT().Pause() HandleEnginePause(e).ServeHTTP(w, r) if got, want := w.Code, 204; want != got { t.Errorf("Want response code %d, got %d", want, got) } } func TestHandleEngineResume(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() w := httptest.NewRecorder() r := httptest.NewRequest("POST", "/api/resume", nil) e := mocks.NewMockEngine(controller) e.EXPECT().Resume() HandleEngineResume(e).ServeHTTP(w, r) if got, want := w.Code, 204; want != got { t.Errorf("Want response code %d, got %d", want, got) } } ================================================ FILE: server/healthz.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package server import ( "io" "net/http" ) // HandleHealthz creates an http.HandlerFunc that returns performs system // healthchecks and returns 500 if the system is in an unhealthy state. func HandleHealthz() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) w.Header().Set("Content-Type", "text/plain") io.WriteString(w, "OK") } } ================================================ FILE: server/healthz_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package server import ( "net/http/httptest" "testing" "github.com/golang/mock/gomock" ) func TestHandleHealthz(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/healthz", nil) HandleHealthz().ServeHTTP(w, r) if got, want := w.Code, 200; want != got { t.Errorf("Want response code %d, got %d", want, got) } } ================================================ FILE: server/metrics.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package server import ( "net/http" "github.com/prometheus/client_golang/prometheus/promhttp" ) // HandleMetrics returns an http.HandlerFunc that writes // metrics to the response body in plain text format. func HandleMetrics(token string) http.HandlerFunc { handler := promhttp.Handler() return func(w http.ResponseWriter, r *http.Request) { // if a bearer token is not configured we should // just server the http request. if token == "" { handler.ServeHTTP(w, r) return } header := r.Header.Get("Authorization") if header == "" { http.Error(w, errInvalidToken.Error(), 401) return } if header != "Bearer "+token { http.Error(w, errInvalidToken.Error(), 401) return } handler.ServeHTTP(w, r) } } ================================================ FILE: server/metrics_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package server import ( "net/http/httptest" "testing" ) func TestHandleMetrics(t *testing.T) { w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/", nil) r.Header.Set("Authorization", "Bearer correct-horse-batter-staple") HandleMetrics("correct-horse-batter-staple").ServeHTTP(w, r) if got, want := w.Code, 200; got != want { t.Errorf("Want status code %d, got %d", want, got) } if got, want := w.HeaderMap.Get("Content-Type"), "text/plain; version=0.0.4; charset=utf-8"; got != want { t.Errorf("Want prometheus header %q, got %q", want, got) } } func TestHandleMetricsUnprotected(t *testing.T) { w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/", nil) HandleMetrics("").ServeHTTP(w, r) if got, want := w.Code, 200; got != want { t.Errorf("Want status code %d, got %d", want, got) } if got, want := w.HeaderMap.Get("Content-Type"), "text/plain; version=0.0.4; charset=utf-8"; got != want { t.Errorf("Want prometheus header %q, got %q", want, got) } } func TestHandleMetricsMissingToken(t *testing.T) { w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/", nil) HandleMetrics("correct-horse-batter-staple").ServeHTTP(w, r) if got, want := w.Code, 401; got != want { t.Errorf("Want status code %d, got %d", want, got) } } func TestHandleMetricsInvalidToken(t *testing.T) { w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/", nil) r.Header.Set("Authorization", "correct-horse-batter-staple") HandleMetrics("correct-horse-batter-staple").ServeHTTP(w, r) if got, want := w.Code, 401; got != want { t.Errorf("Want status code %d, got %d", want, got) } } ================================================ FILE: server/servers.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package server import ( "net/http" "strconv" "github.com/dchest/uniuri" "github.com/drone/autoscaler" "github.com/drone/autoscaler/config" "github.com/drone/autoscaler/logger" "github.com/go-chi/chi" ) // HandleServerList returns an http.HandlerFunc that writes // the json-encoded server list to the the response body. func HandleServerList(servers autoscaler.ServerStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() list, err := servers.List(ctx) if err != nil { logger.FromContext(ctx). WithError(err). Errorln("cannot get server list") writeError(w, err) return } writeJSON(w, list, 200) } } // HandleServerFind returns an http.HandlerFunc that finds // and writes the json-encoded server to the the response body. func HandleServerFind(servers autoscaler.ServerStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() name := chi.URLParam(r, "name") server, err := servers.Find(ctx, name) if err != nil { logger.FromContext(ctx). WithError(err). WithField("server", name). Errorln("cannot get server") writeNotFound(w, err) return } writeJSON(w, server, 200) } } // HandleServerDelete returns an http.HandlerFunc that destroys // and then deletes the named server. func HandleServerDelete( servers autoscaler.ServerStore, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() name := chi.URLParam(r, "name") force, _ := strconv.ParseBool(r.FormValue("force")) server, err := servers.Find(ctx, name) if err != nil { logger.FromContext(ctx). WithError(err). WithField("server", name). Errorln("cannot get server") writeNotFound(w, err) return } // in some cases the server fails to create and is stuck // in an error state. In this case we force-delete from // the database. if server.State == autoscaler.StateError && (server.ID == "" || force) { logger.FromContext(ctx). WithField("server", server.Name). WithField("state", string(server.State)). WithField("force", force). Infoln("force delete server from database") err = servers.Delete(ctx, server) if err != nil { logger.FromContext(ctx). WithError(err). WithField("server", server.Name). Errorln("cannot delete instance") writeError(w, err) return } w.WriteHeader(204) return } logger.FromContext(ctx). WithField("server", server.Name). WithField("state", string(server.State)). WithField("force", force). Infoln("schedule server shutdown") server.State = autoscaler.StateShutdown err = servers.Update(ctx, server) if err != nil { logger.FromContext(ctx). WithError(err). WithField("server", server.Name). WithField("state", "shutdown"). Errorln("cannot update server") writeError(w, err) return } writeJSON(w, server, 200) } } // HandleServerCreate returns an http.HandlerFunc that creates // and a new server. func HandleServerCreate( servers autoscaler.ServerStore, config config.Config, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() server := &autoscaler.Server{ Name: config.Agent.NamePrefix + uniuri.NewLen(8), State: autoscaler.StatePending, Capacity: config.Agent.Concurrency, } err := servers.Create(ctx, server) if err != nil { logger.FromContext(ctx). WithError(err). Errorln("cannot persist server") writeError(w, err) return } writeJSON(w, server, 200) } } ================================================ FILE: server/servers_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package server import ( "encoding/json" "errors" "net/http/httptest" "reflect" "testing" "github.com/drone/autoscaler" "github.com/drone/autoscaler/config" "github.com/drone/autoscaler/mocks" "github.com/go-chi/chi" "github.com/golang/mock/gomock" "github.com/kr/pretty" ) func TestHandleServerList(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/api/servers", nil) servers := []*autoscaler.Server{ {Name: "server1", Capacity: 1}, {Name: "server2", Capacity: 1}, } store := mocks.NewMockServerStore(controller) store.EXPECT().List(gomock.Any()).Return(servers, nil) HandleServerList(store).ServeHTTP(w, r) if got, want := w.Code, 200; want != got { t.Errorf("Want response code %d, got %d", want, got) } got, want := []*autoscaler.Server{}, servers json.NewDecoder(w.Body).Decode(&got) if !reflect.DeepEqual(got, want) { t.Errorf("response body does match expected result") pretty.Ldiff(t, got, want) } } func TestHandleServerListErr(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/api/servers", nil) err := errors.New("not found") store := mocks.NewMockServerStore(controller) store.EXPECT().List(gomock.Any()).Return(nil, err) HandleServerList(store).ServeHTTP(w, r) if got, want := w.Code, 500; want != got { t.Errorf("Want response code %d, got %d", want, got) } errjson := &Error{} json.NewDecoder(w.Body).Decode(errjson) if got, want := errjson.Message, err.Error(); got != want { t.Errorf("Want error message %s, got %s", want, got) } } func TestHandleServerFind(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/api/servers/server1", nil) server := &autoscaler.Server{Name: "server1", Capacity: 1} store := mocks.NewMockServerStore(controller) store.EXPECT().Find(gomock.Any(), "server1").Return(server, nil) router := chi.NewRouter() router.Get("/api/servers/{name}", HandleServerFind(store)) router.ServeHTTP(w, r) if got, want := w.Code, 200; want != got { t.Errorf("Want response code %d, got %d", want, got) } got, want := &autoscaler.Server{}, server json.NewDecoder(w.Body).Decode(got) if !reflect.DeepEqual(got, want) { t.Errorf("response body does match expected result") pretty.Ldiff(t, got, want) } } func TestHandleServerFindErr(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/api/servers/server1", nil) err := errors.New("not found") store := mocks.NewMockServerStore(controller) store.EXPECT().Find(gomock.Any(), "server1").Return(nil, err) router := chi.NewRouter() router.Get("/api/servers/{name}", HandleServerFind(store)) router.ServeHTTP(w, r) if got, want := w.Code, 404; want != got { t.Errorf("Want response code %d, got %d", want, got) } errjson := &Error{} json.NewDecoder(w.Body).Decode(errjson) if got, want := errjson.Message, err.Error(); got != want { t.Errorf("Want error message %s, got %s", want, got) } } func TestHandleServerCreate(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() w := httptest.NewRecorder() r := httptest.NewRequest("POST", "/api/servers", nil) store := mocks.NewMockServerStore(controller) store.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil) HandleServerCreate(store, config.Config{}).ServeHTTP(w, r) if got, want := w.Code, 200; want != got { t.Errorf("Want response code %d, got %d", want, got) } } func TestHandleServerCreateFailure(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() w := httptest.NewRecorder() r := httptest.NewRequest("POST", "/api/servers", nil) err := errors.New("oops") store := mocks.NewMockServerStore(controller) store.EXPECT().Create(gomock.Any(), gomock.Any()).Return(err) h := HandleServerCreate(store, config.Config{}) h.ServeHTTP(w, r) if got, want := w.Code, 500; want != got { t.Errorf("Want response code %d, got %d", want, got) } errjson := &Error{} json.NewDecoder(w.Body).Decode(errjson) if got, want := errjson.Message, err.Error(); got != want { t.Errorf("Want error message %s, got %s", want, got) } } func TestHandleServerDelete(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() w := httptest.NewRecorder() r := httptest.NewRequest("DELETE", "/api/servers/i-5203422c", nil) server := &autoscaler.Server{ Name: "i-5203422c", Image: "docker-18-04", Region: "nyc1", Size: "s-1vcpu-1gb", } store := mocks.NewMockServerStore(controller) store.EXPECT().Find(gomock.Any(), server.Name).Return(server, nil) store.EXPECT().Update(gomock.Any(), server).Return(nil) router := chi.NewRouter() router.Delete("/api/servers/{name}", HandleServerDelete(store)) router.ServeHTTP(w, r) if got, want := w.Code, 200; want != got { t.Errorf("Want response code %d, got %d", want, got) } if got, want := server.State, autoscaler.StateShutdown; got != want { t.Errorf("Want server state Shutdown, got %s", got) } } func TestHandleServerDeleteNotFound(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() w := httptest.NewRecorder() r := httptest.NewRequest("DELETE", "/api/servers/i-5203422c", nil) err := errors.New("not found") store := mocks.NewMockServerStore(controller) store.EXPECT().Find(gomock.Any(), "i-5203422c").Return(nil, err) router := chi.NewRouter() router.Delete("/api/servers/{name}", HandleServerDelete(store)) router.ServeHTTP(w, r) if got, want := w.Code, 404; want != got { t.Errorf("Want response code %d, got %d", want, got) } errjson := &Error{} json.NewDecoder(w.Body).Decode(errjson) if got, want := errjson.Message, err.Error(); got != want { t.Errorf("Want error message %s, got %s", want, got) } } func TestHandleServerDeleteFailure(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() w := httptest.NewRecorder() r := httptest.NewRequest("DELETE", "/api/servers/i-5203422c", nil) server := &autoscaler.Server{ Name: "i-5203422c", Image: "docker-18-04", Region: "nyc1", Size: "s-1vcpu-1gb", } err := errors.New("bad request") store := mocks.NewMockServerStore(controller) store.EXPECT().Find(gomock.Any(), server.Name).Return(server, nil) store.EXPECT().Update(gomock.Any(), server).Return(err) router := chi.NewRouter() router.Delete("/api/servers/{name}", HandleServerDelete(store)) router.ServeHTTP(w, r) if got, want := w.Code, 500; want != got { t.Errorf("Want response code %d, got %d", want, got) } errjson := &Error{} json.NewDecoder(w.Body).Decode(errjson) if got, want := errjson.Message, err.Error(); got != want { t.Errorf("Want error message %s, got %s", want, got) } } func TestHandleServerDeleteErrorState(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() w := httptest.NewRecorder() r := httptest.NewRequest("DELETE", "/api/servers/i-5203422c", nil) server := &autoscaler.Server{ ID: "", State: autoscaler.StateError, Name: "i-5203422c", Image: "docker-18-04", Region: "nyc1", Size: "s-1vcpu-1gb", } store := mocks.NewMockServerStore(controller) store.EXPECT().Find(gomock.Any(), server.Name).Return(server, nil) store.EXPECT().Delete(gomock.Any(), server).Return(nil) router := chi.NewRouter() router.Delete("/api/servers/{name}", HandleServerDelete(store)) router.ServeHTTP(w, r) if got, want := w.Code, 204; want != got { t.Errorf("Want response code %d, got %d", want, got) } } func TestHandleServerForceDeleteErrorState(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() w := httptest.NewRecorder() r := httptest.NewRequest("DELETE", "/api/servers/i-5203422c?force=true", nil) server := &autoscaler.Server{ ID: "i-5203422c", State: autoscaler.StateError, Name: "i-5203422c", Image: "docker-18-04", Region: "nyc1", Size: "s-1vcpu-1gb", } store := mocks.NewMockServerStore(controller) store.EXPECT().Find(gomock.Any(), server.Name).Return(server, nil) store.EXPECT().Delete(gomock.Any(), server).Return(nil) router := chi.NewRouter() router.Delete("/api/servers/{name}", HandleServerDelete(store)) router.ServeHTTP(w, r) if got, want := w.Code, 204; want != got { t.Errorf("Want response code %d, got %d", want, got) } } ================================================ FILE: server/varz.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package server import ( "net/http" "github.com/drone/autoscaler" ) type varz struct { Paused bool `json:"paused"` } // HandleVarz creates an http.HandlerFunc that returns system // configuration and runtime information. func HandleVarz(engine autoscaler.Engine) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { data := varz{ Paused: engine.Paused(), } writeJSON(w, &data, 200) } } ================================================ FILE: server/varz_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package server import ( "encoding/json" "net/http/httptest" "reflect" "testing" "github.com/drone/autoscaler/mocks" "github.com/go-chi/chi" "github.com/golang/mock/gomock" "github.com/kr/pretty" ) func TestHandleVarz(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() mockVarz := &varz{ Paused: true, } w := httptest.NewRecorder() r := httptest.NewRequest("POST", "/varz", nil) engine := mocks.NewMockEngine(controller) engine.EXPECT().Paused().Return(true) router := chi.NewRouter() router.Post("/varz", HandleVarz(engine)) router.ServeHTTP(w, r) if got, want := w.Code, 200; want != got { t.Errorf("Want response code %d, got %d", want, got) } got, want := &varz{}, mockVarz json.NewDecoder(w.Body).Decode(got) if !reflect.DeepEqual(got, want) { t.Errorf("response body does match expected result") pretty.Ldiff(t, got, want) } } ================================================ FILE: server/version.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package server import ( "net/http" ) // version information, loosely based on // https://github.com/mozilla-services/Dockerflow type versionInfo struct { Source string `json:"source,omitempty"` Version string `json:"version,omitempty"` Commit string `json:"commit,omitempty"` } // HandleVersion creates an http.HandlerFunc that returns the // version number and build details. func HandleVersion(source, version, commit string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { data := versionInfo{ Source: source, Version: version, Commit: commit, } writeJSON(w, &data, 200) } } ================================================ FILE: server/version_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package server import ( "encoding/json" "net/http/httptest" "reflect" "testing" "github.com/golang/mock/gomock" "github.com/kr/pretty" ) func TestHandleVersion(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/version", nil) mockVersion := &versionInfo{ Source: "github.com/octocat/hello-world", Version: "1.0.0", Commit: "ad2aec", } h := HandleVersion(mockVersion.Source, mockVersion.Version, mockVersion.Commit) h.ServeHTTP(w, r) if got, want := w.Code, 200; want != got { t.Errorf("Want response code %d, got %d", want, got) } got, want := &versionInfo{}, mockVersion json.NewDecoder(w.Body).Decode(got) if !reflect.DeepEqual(got, want) { t.Errorf("response body does match expected result") pretty.Ldiff(t, got, want) } } ================================================ FILE: server/web/handler.go ================================================ // Copyright 2019 Drone.IO Inc. All rights reserved. // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. // Package web provides HTTP handlers that expose pipeline // state and status. package web import ( "net/http" "github.com/drone/autoscaler" "github.com/drone/autoscaler/logger/history" ) // HandleServers returns a http.HandlerFunc that displays a // list of activate servers. func HandleServers(servers autoscaler.ServerStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { nocache(w) items, _ := servers.List(r.Context()) filtered := []*autoscaler.Server{} for _, item := range items { if item.State != autoscaler.StateStopped { filtered = append(filtered, item) } } render(w, "index.tmpl", struct { Items []*autoscaler.Server }{filtered}) } } // HandleLogging returns a http.HandlerFunc that displays a // list recent log entries. func HandleLogging(t *history.Hook) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { nocache(w) render(w, "logs.tmpl", struct { Entries []*history.Entry }{t.Entries()}) } } ================================================ FILE: server/web/nocache.go ================================================ // Copyright 2019 Drone.IO Inc. All rights reserved. // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package web import ( "net/http" "time" ) // unix epoch time var epoch = time.Unix(0, 0).Format(time.RFC1123) // http headers to disable caching. var noCacheHeaders = map[string]string{ "Expires": epoch, "Cache-Control": "no-cache, private, max-age=0", "Pragma": "no-cache", "X-Accel-Expires": "0", } // helper function to prevent http response caching. func nocache(w http.ResponseWriter) { for k, v := range noCacheHeaders { w.Header().Set(k, v) } } ================================================ FILE: server/web/nocache_test.go ================================================ // Copyright 2019 Drone.IO Inc. All rights reserved. // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package web ================================================ FILE: server/web/render.go ================================================ // Copyright 2019 Drone.IO Inc. All rights reserved. // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package web import ( "net/http" "github.com/drone/autoscaler/server/web/template" ) // render writes the template to the response body. func render(w http.ResponseWriter, t string, v interface{}) { w.Header().Set("Content-Type", "text/html") template.T.ExecuteTemplate(w, t, v) } ================================================ FILE: server/web/render_test.go ================================================ // Copyright 2019 Drone.IO Inc. All rights reserved. // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package web ================================================ FILE: server/web/static/files/reset.css ================================================ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit; vertical-align: baseline; } /* HTML5 display-role reset for older browsers */ article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { display: block; } body { line-height: 1; } ol, ul { list-style: none; } blockquote, q { quotes: none; } blockquote:before, blockquote:after, q:before, q:after { content: ''; content: none; } table { border-collapse: collapse; border-spacing: 0; } ================================================ FILE: server/web/static/files/style.css ================================================ :root { --font-sans: -apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif; --font-mono: Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace; --font-size-1: 12px; --font-size-2: 14px; --font-size-3: 16px; --font-size-4: 18px; --font-size-5: 20px; --font-size-6: 24px; --font-size-7: 30px; --font-size-8: 38px; --font-size-9: 48px; --spacing-1: 4px; --spacing-2: 8px; --spacing-3: 12px; --spacing-4: 16px; --spacing-5: 24px; --spacing-6: 32px; --spacing-7: 48px; --spacing-8: 64px; --spacing-9: 96px; --height-1: 16px; --height-2: 20px; --height-3: 24px; --height-4: 32px; --height-5: 40px; --height-6: 48px; --height-7: 64px; --height-8: 80px; --height-9: 96px; --box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); --box-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); --box-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); --box-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); --box-shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06); --box-shadow-outline: 0 0 0 3px #F6B9FF; /* --background: #edf2f7; --text-color: #1a202c; --text-color-pimary: #1a202c; --text-color-secondary: #1a202c; --card-text-color-primary: #2D3748; --card-text-color-secondary: #829ab1; --card-background-color: #FFF; --card-border-radius: 8px; --badge-border-radius: 20px; --badge-font-size: 11px; --badge-font-weight: 500; --badge-pending-background: #FFEA7F; --badge-pending-color: #960F18; --badge-running-background: #D0FFED; --badge-running-color: #083F37; --badge-stopped-background: #FFEA7F; --badge-stopped-color: #960F18; --badge-error-background: #FFE0E8; --badge-error-color: #800220; --nav-link-color: #718096; --nav-link-color-active: #E6DFFF; --header-background-color: #4A5468; --header-height: 72px; */ --background: #1A202C; --text-color: #829ab1; --card-text-color-title: #FFFFFF; --card-text-color-primary: #E2E8F0; --card-text-color-secondary: #829ab1; --card-background-color: #2D3748; --card-border-radius: 8px; --card-icon-background: #718096; --badge-border-radius: 8px; --badge-font-size: var(--font-size-1); --badge-font-weight: 500; --badge-pending-background: rgba(255,255,255,0.075); --badge-pending-color: #FFD567; --badge-running-background: rgba(255,255,255,0.075); --badge-running-color: #53FEBE; --badge-stopped-background: rgba(255,255,255,0.075); --badge-stopped-color: #FFE0E8; --badge-error-background: #3c4656; --badge-error-color: #FFE0E8; --nav-link-color: #718096; --nav-link-color-active: #EDF2F7; --header-background-color: #2D3748; --header-height: 72px; } html, body { background: var(--background); color: var(--text-color); width: 100%; height: 100%; font-family: var(--font-sans); font-size: var(--font-size-2); } main { box-sizing: border-box; max-width: 800px; margin: 0 auto; margin-bottom: var(--spacing-6); } main section > header h1 { height: var(--height-5); font-size: var(--font-size-7); font-weight: 400; font-style: normal; font-stretch: normal; line-height: normal; letter-spacing: normal; color: var(--text-color); margin-top: var(--spacing-6); margin-bottom: var(--spacing-3); } body > header { background-color: var(--header-background-color); height: var(--header-height); box-shadow: var(--box-shadow-lg); box-sizing: border-box; padding: 0 var(--spacing-4); display: flex; align-items: center; } header .logo { width: 30px; height: 30px; } .navbar .inline-nav { display: flex; flex: 1 1 auto; justify-content: flex-end; } .navbar .inline-nav li { display: inline-block; margin-left: var(--spacing-4); } .navbar .inline-nav a, .navbar .inline-nav a:active, .navbar .inline-nav a:visited { color: var(--nav-link-color); text-decoration: none; font-size: var(--font-size-3); } .navbar .inline-nav a.active, .navbar .inline-nav a:hover { color: var(--nav-link-color-active); } /* * cards */ .card { padding: var(--spacing-5); color: var(--card-text-color-primary); box-shadow: var(--box-shadow-md); box-sizing: border-box; border-radius: var(--card-border-radius); background-color: var(--card-background-color); margin-bottom: var(--spacing-3); } /* * instance card component */ .instance { display: grid; grid-gap: var(--spacing-1) var(--spacing-2); grid-template-columns: 40px 210px 130px 110px 110px 110px; } .instance:hover { cursor: pointer; } /* .instance:hover { box-shadow: var(--box-shadow-outline); } */ .instance .icon { grid-column: 1; grid-row: 1 / span 2; user-select: none; } .instance .addr { grid-column: 2; grid-row: 1; font-weight: 500; line-height: var(--height-3); color: var(--card-text-color-title); } .instance .id { grid-column: 2; grid-row: 2; color: var(--card-text-color-secondary); font-weight: 300; } .instance .state { grid-column: 3; grid-row: 1 / span 2; line-height: var(--height-3); /* display: flex; align-items: center; */ } .instance .region { grid-column: 4; grid-row: 1; line-height: var(--height-3); } .instance .image { grid-column: 4; grid-row: 2; color: var(--card-text-color-secondary); font-weight: 300; } .instance .size { grid-column: 5; grid-row: 1 / span 2; line-height: var(--height-3); } .instance .time { grid-column: 6; grid-row: 1 / span 2; text-align: right; line-height: var(--height-3); } /* * instance icon server */ .instance .icon-server { width: 22px; height: 22px; } .instance .icon .primary { fill: var(--card-icon-background); } .instance .icon .secondary { fill: var(--card-background-color); } /* * badge components */ .badge { border-radius: var(--badge-border-radius); text-align: center; text-transform: uppercase; font-size: var(--badge-font-size); font-weight: var(--badge-font-weight); display: inline-block; height: var(--height-3); padding-right: var(--spacing-4); padding-left: var(--spacing-4); cursor: default; user-select: none; display: inline-flex; align-content: center; } /** * badge for state */ .state .badge { padding-left: 0px; } .badge-creating, .badge-created, .badge-staging, .badge-starting, .badge-pending { background: var(--badge-pending-background); color: var(--badge-pending-color); } .badge-running { background: var(--badge-running-background); color: var(--badge-running-color); } .badge-stopping, .badge-stopped, .badge-shutdown { background: var(--badge-stopped-background); color: var(--badge-stopped-color); } .state .badge-error { background: var(--badge-error-background); color: var(--badge-error-color); } .badge svg { width: 24px; height: 24px; fill: var(--badge-pending-color); animation: blink 2s linear infinite; } .badge .icon-close-circle .secondary { fill: var(--badge-error-color); } .badge .icon-close-circle .primary { fill: var(--badge-error-background); } .badge-running svg { fill: var(--badge-running-color); } .badge-stopped svg, .badge-shutdown svg, .badge-error svg { animation: none; } /** * badge for trace logging */ .badge-error, .badge-panic { color: #FF9AA2; } .badge-warn { color: var(--badge-pending-color); } .badge-info { color: #00DDFF; } .badge-debug { color: #F564FF; } .badge-trace { color: #B3FFE3; } /** * log entry componenets */ .entry { display: grid; grid-gap: var(--spacing-1) var(--spacing-2); grid-template-columns: 85px 200px 1fr; border-radius: 0px; margin-bottom: 0px; border-bottom: 2px solid var(--background); } .entry:first-child { border-top-left-radius: var(--card-border-radius); border-top-right-radius: var(--card-border-radius); } .entry:last-child { border-bottom-left-radius: var(--card-border-radius); border-bottom-right-radius: var(--card-border-radius); border-bottom: none; } .entry .level { grid-column: 1; grid-row: 1; display: flex; align-items: center; align-content: center; font-size: var(--font-size-2); } .entry .level .badge { display: flex; align-items: center; align-content: center; font-family: var(--font-mono); font-size: var(--font-size-2); } .entry .message { grid-column: 3; grid-row: 1; /* padding: 0px var(--spacing-2); */ border-radius: var(--card-border-radius); font-family: var(--font-mono); font-size: var(--font-size-2); display: flex; align-items: center; align-content: center; } .entry .fields { grid-column: 3; grid-row: 2; /* padding: var(--spacing-1) var(--spacing-2); */ /* border-radius: var(--card-border-radius); */ line-height: var(--height-1); font-family: var(--font-mono); font-size: var(--font-size-2); } .entry .time { grid-column: 2; text-align: left; font-family: var(--font-mono); font-size: var(--font-size-2); display: flex; align-items: center; align-content: center; } .fields span { display: block; color: #718096; } .fields span em:after { content: "="; color: #A0AEC0; margin: 0px 3px; } /** * alerts */ .alert { display: grid; grid-template-columns: 1fr 80px; } .alert span:first-child { display: flex; flex-direction: column; justify-content: flex-end; } .alert h1 { color: var(--card-text-color-primary); font-size: var(--font-size-6); margin-bottom: var(--spacing-3); } .alert p { color: var(--card-text-color-secondary); font-size: var(--font-size-2); } /* * animations */ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(359deg); } } @keyframes wrench { 0% { transform: rotate(-12deg); } 8% { transform: rotate(12deg); } 10% { transform: rotate(24deg); } 18% { transform: rotate(-24deg); } 20% { transform: rotate(-24deg); } 28% { transform: rotate(24deg); } 30% { transform: rotate(24deg); } 38% { transform: rotate(-24deg); } 40% { transform: rotate(-24deg); } 48% { transform: rotate(24deg); } 50% { transform: rotate(24deg); } 58% { transform: rotate(-24deg); } 60% { transform: rotate(-24deg); } 68% { transform: rotate(24deg); } 75%, 100% { transform: rotate(0deg); } } @keyframes blink { 50% { opacity: 0.0; } } ================================================ FILE: server/web/static/files/timeago.js ================================================ !function(s,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((s=s||self).timeago={})}(this,function(s){"use strict";var a=["second","minute","hour","day","week","month","year"];function n(s,n){if(0===n)return["just now","right now"];var e=a[~~(n/2)];return 1=o[t]&&t=o[e]&&e=o[t]&&t=o[e]&&e header h1 { height: var(--height-5); font-size: var(--font-size-7); font-weight: 400; font-style: normal; font-stretch: normal; line-height: normal; letter-spacing: normal; color: var(--text-color); margin-top: var(--spacing-6); margin-bottom: var(--spacing-3); } body > header { background-color: var(--header-background-color); height: var(--header-height); box-shadow: var(--box-shadow-lg); box-sizing: border-box; padding: 0 var(--spacing-4); display: flex; align-items: center; } header .logo { width: 30px; height: 30px; } .navbar .inline-nav { display: flex; flex: 1 1 auto; justify-content: flex-end; } .navbar .inline-nav li { display: inline-block; margin-left: var(--spacing-4); } .navbar .inline-nav a, .navbar .inline-nav a:active, .navbar .inline-nav a:visited { color: var(--nav-link-color); text-decoration: none; font-size: var(--font-size-3); } .navbar .inline-nav a.active, .navbar .inline-nav a:hover { color: var(--nav-link-color-active); } /* * cards */ .card { padding: var(--spacing-5); color: var(--card-text-color-primary); box-shadow: var(--box-shadow-md); box-sizing: border-box; border-radius: var(--card-border-radius); background-color: var(--card-background-color); margin-bottom: var(--spacing-3); } /* * instance card component */ .instance { display: grid; grid-gap: var(--spacing-1) var(--spacing-2); grid-template-columns: 40px 210px 130px 110px 110px 110px; } .instance:hover { cursor: pointer; } /* .instance:hover { box-shadow: var(--box-shadow-outline); } */ .instance .icon { grid-column: 1; grid-row: 1 / span 2; user-select: none; } .instance .addr { grid-column: 2; grid-row: 1; font-weight: 500; line-height: var(--height-3); color: var(--card-text-color-title); } .instance .id { grid-column: 2; grid-row: 2; color: var(--card-text-color-secondary); font-weight: 300; } .instance .state { grid-column: 3; grid-row: 1 / span 2; line-height: var(--height-3); /* display: flex; align-items: center; */ } .instance .region { grid-column: 4; grid-row: 1; line-height: var(--height-3); } .instance .image { grid-column: 4; grid-row: 2; color: var(--card-text-color-secondary); font-weight: 300; } .instance .size { grid-column: 5; grid-row: 1 / span 2; line-height: var(--height-3); } .instance .time { grid-column: 6; grid-row: 1 / span 2; text-align: right; line-height: var(--height-3); } /* * instance icon server */ .instance .icon-server { width: 22px; height: 22px; } .instance .icon .primary { fill: var(--card-icon-background); } .instance .icon .secondary { fill: var(--card-background-color); } /* * badge components */ .badge { border-radius: var(--badge-border-radius); text-align: center; text-transform: uppercase; font-size: var(--badge-font-size); font-weight: var(--badge-font-weight); display: inline-block; height: var(--height-3); padding-right: var(--spacing-4); padding-left: var(--spacing-4); cursor: default; user-select: none; display: inline-flex; align-content: center; } /** * badge for state */ .state .badge { padding-left: 0px; } .badge-creating, .badge-created, .badge-staging, .badge-starting, .badge-pending { background: var(--badge-pending-background); color: var(--badge-pending-color); } .badge-running { background: var(--badge-running-background); color: var(--badge-running-color); } .badge-stopping, .badge-stopped, .badge-shutdown { background: var(--badge-stopped-background); color: var(--badge-stopped-color); } .state .badge-error { background: var(--badge-error-background); color: var(--badge-error-color); } .badge svg { width: 24px; height: 24px; fill: var(--badge-pending-color); animation: blink 2s linear infinite; } .badge .icon-close-circle .secondary { fill: var(--badge-error-color); } .badge .icon-close-circle .primary { fill: var(--badge-error-background); } .badge-running svg { fill: var(--badge-running-color); } .badge-stopped svg, .badge-shutdown svg, .badge-error svg { animation: none; } /** * badge for trace logging */ .badge-error, .badge-panic { color: #FF9AA2; } .badge-warn { color: var(--badge-pending-color); } .badge-info { color: #00DDFF; } .badge-debug { color: #F564FF; } .badge-trace { color: #B3FFE3; } /** * log entry componenets */ .entry { display: grid; grid-gap: var(--spacing-1) var(--spacing-2); grid-template-columns: 85px 200px 1fr; border-radius: 0px; margin-bottom: 0px; border-bottom: 2px solid var(--background); } .entry:first-child { border-top-left-radius: var(--card-border-radius); border-top-right-radius: var(--card-border-radius); } .entry:last-child { border-bottom-left-radius: var(--card-border-radius); border-bottom-right-radius: var(--card-border-radius); border-bottom: none; } .entry .level { grid-column: 1; grid-row: 1; display: flex; align-items: center; align-content: center; font-size: var(--font-size-2); } .entry .level .badge { display: flex; align-items: center; align-content: center; font-family: var(--font-mono); font-size: var(--font-size-2); } .entry .message { grid-column: 3; grid-row: 1; /* padding: 0px var(--spacing-2); */ border-radius: var(--card-border-radius); font-family: var(--font-mono); font-size: var(--font-size-2); display: flex; align-items: center; align-content: center; } .entry .fields { grid-column: 3; grid-row: 2; /* padding: var(--spacing-1) var(--spacing-2); */ /* border-radius: var(--card-border-radius); */ line-height: var(--height-1); font-family: var(--font-mono); font-size: var(--font-size-2); } .entry .time { grid-column: 2; text-align: left; font-family: var(--font-mono); font-size: var(--font-size-2); display: flex; align-items: center; align-content: center; } .fields span { display: block; color: #718096; } .fields span em:after { content: "="; color: #A0AEC0; margin: 0px 3px; } /** * alerts */ .alert { display: grid; grid-template-columns: 1fr 80px; } .alert span:first-child { display: flex; flex-direction: column; justify-content: flex-end; } .alert h1 { color: var(--card-text-color-primary); font-size: var(--font-size-6); margin-bottom: var(--spacing-3); } .alert p { color: var(--card-text-color-secondary); font-size: var(--font-size-2); } /* * animations */ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(359deg); } } @keyframes wrench { 0% { transform: rotate(-12deg); } 8% { transform: rotate(12deg); } 10% { transform: rotate(24deg); } 18% { transform: rotate(-24deg); } 20% { transform: rotate(-24deg); } 28% { transform: rotate(24deg); } 30% { transform: rotate(24deg); } 38% { transform: rotate(-24deg); } 40% { transform: rotate(-24deg); } 48% { transform: rotate(24deg); } 50% { transform: rotate(24deg); } 58% { transform: rotate(-24deg); } 60% { transform: rotate(-24deg); } 68% { transform: rotate(24deg); } 75%, 100% { transform: rotate(0deg); } } @keyframes blink { 50% { opacity: 0.0; } } `) ================================================ FILE: server/web/template/files/index.tmpl ================================================ Dashboard

Servers

{{ if not .Items }}

There are no active servers

The system will not provision instances when the queue is empty.

{{ else }} {{ range .Items }}
{{ .Name }}
{{ if .Address }}{{ .Address }}{{ else }}0.0.0.0{{ end }}
{{ if eq .State "error" }} {{ else }} {{ end }} {{ .State }}
{{ .Region }}
{{ .Image }}
{{ .Size }}
{{ end }} {{ end }}
================================================ FILE: server/web/template/files/logs.tmpl ================================================ Dashboard

Recent Logs

{{ range .Entries }}
{{ .Level }}
{{ .Message }}
{{ range $key, $val := .Data }} {{ $key }}{{ $val }} {{ end }}
{{ timestamp .Unix }}
{{ end }}
================================================ FILE: server/web/template/server.go ================================================ // Copyright 2019 Drone.IO Inc. All rights reserved. // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. // +build ignore package main import ( "encoding/json" "html/template" "io/ioutil" "log" "net/http" "path/filepath" "time" ) func main() { addr := ":3333" // serve templates with dummy data http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { path := r.FormValue("data") if path == "" { http.Error(w, "missing data parameter", 500) return } tmpl := r.FormValue("template") if path == "" { http.Error(w, "missing template parameter", 500) return } // read the json data from file. rawjson, err := ioutil.ReadFile(filepath.Join("testdata", path)) if err != nil { http.Error(w, "cannot open json file", 500) return } // unmarshal the json data data := map[string]interface{}{} err = json.Unmarshal(rawjson, &data) if err != nil { http.Error(w, err.Error(), 500) return } // load the templates T := template.New("_").Funcs(funcMap) matches, _ := filepath.Glob("files/*.tmpl") for _, match := range matches { raw, _ := ioutil.ReadFile(match) base := filepath.Base(match) T = template.Must( T.New(base).Parse(string(raw)), ) } // render the template w.Header().Set("Content-Type", "text/html") err = T.ExecuteTemplate(w, tmpl, data) if err != nil { log.Println(err) } }) // serve static content. http.Handle("/static/", http.StripPrefix("/static/", http.FileServer( http.Dir("../static/files"), ), ), ) log.Printf("listening at %s", addr) log.Fatalln(http.ListenAndServe(addr, nil)) } // mirros the func map in template.go var funcMap = map[string]interface{}{ "substr": func(v string, i int) string { return v[0:i] }, "timestamp": func(v float64) string { return time.Unix(int64(v), 0).UTC().Format("2006-01-02T15:04:05Z") }, } ================================================ FILE: server/web/template/template.go ================================================ // Copyright 2019 Drone.IO Inc. All rights reserved. // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package template import "time" //go:generate togo tmpl -func funcMap -format html // mirros the func map in template.go var funcMap = map[string]interface{}{ "timestamp": func(v int64) string { return time.Unix(v, 0).UTC().Format("2006-01-02T15:04:05Z") }, } ================================================ FILE: server/web/template/template_gen.go ================================================ package template import "html/template" // list of embedded template files. var files = []struct { name string data string }{ { name: "index.tmpl", data: index, }, { name: "logs.tmpl", data: logs, }, } // T exposes the embedded templates. var T *template.Template func init() { T = template.New("_").Funcs(funcMap) for _, file := range files { T = template.Must( T.New(file.name).Parse(file.data), ) } } // // embedded template files. // // files/index.tmpl var index = ` Dashboard

Servers

{{ if not .Items }}

There are no active servers

The system will not provision instances when the queue is empty.

{{ else }} {{ range .Items }}
{{ .Name }}
{{ if .Address }}{{ .Address }}{{ else }}0.0.0.0{{ end }}
{{ if eq .State "error" }} {{ else }} {{ end }} {{ .State }}
{{ .Region }}
{{ .Image }}
{{ .Size }}
{{ end }} {{ end }}
` // files/logs.tmpl var logs = ` Dashboard

Recent Logs

{{ range .Entries }}
{{ .Level }}
{{ .Message }}
{{ range $key, $val := .Data }} {{ $key }}{{ $val }} {{ end }}
{{ timestamp .Unix }}
{{ end }}
` ================================================ FILE: server/web/template/testdata/logs.json ================================================ { "Entries": [ { "Level": "trace", "Message": "this is a test trace message", "Data": { "foo": "bar", "baz": "boo" }, "Unix": 1563058875 }, { "Level": "debug", "Message": "this is a test debug message", "Data": { "foo": "bar", "baz": "boo" }, "Unix": 1563058875 }, { "Level": "info", "Message": "this is an info trace message", "Data": { "foo": "bar", "baz": "boo" }, "Unix": 1563058975 }, { "Level": "warn", "Message": "this is a test warning message", "Data": { "foo": "bar", "baz": "boo" }, "Unix": 1563058977 }, { "Level": "error", "Message": "this is a test error message", "Data": { "foo": "bar", "baz": "boo" }, "Unix": 1563059000 } ] } ================================================ FILE: server/web/template/testdata/logs_empty.json ================================================ { "Entries": [] } ================================================ FILE: server/web/template/testdata/servers.json ================================================ { "Items": [ { "ID": "agent-123456789", "Name": "i-5203422c", "Provider": "amazon", "State": "starting", "Address": "54.194.252.215", "Capacity": 2, "Error": "", "Image": "ami-0070c5311b7677678", "Size": "t3.medium", "Region": "us-east-1", "Platform": "linux/amd64", "Created": 1573575703, "Updated": 1573575719, "Started": 1573575703, "Stopped": 1573575719 }, { "ID": "agent-123456789", "Name": "i-5203422c", "Provider": "amazon", "State": "running", "Address": "54.194.252.215", "Capacity": 2, "Error": "", "Image": "ami-0070c5311b7677678", "Size": "t3.medium", "Region": "us-east-1", "Platform": "linux/amd64", "Created": 1573575703, "Updated": 1573575719, "Started": 1573575703, "Stopped": 1573575719 }, { "ID": "agent-123456789", "Name": "i-5203422c", "Provider": "amazon", "State": "running", "Address": "54.194.252.215", "Capacity": 2, "Error": "", "Image": "ami-0070c5311b7677678", "Size": "t3.medium", "Region": "us-east-1", "Platform": "linux/amd64", "Created": 1573575703, "Updated": 1573575719, "Started": 1573575703, "Stopped": 1573575719 }, { "ID": "agent-123456789", "Name": "i-5203422c", "Provider": "amazon", "State": "running", "Address": "54.194.252.215", "Capacity": 2, "Error": "", "Image": "ami-0070c5311b7677678", "Size": "t3.medium", "Region": "us-east-1", "Platform": "linux/amd64", "Created": 1573600782, "Updated": 1573575719, "Started": 1573575703, "Stopped": 1573575719 }, { "ID": "agent-123456789", "Name": "i-5203422c", "Provider": "amazon", "State": "error", "Address": "54.194.252.215", "Capacity": 2, "Error": "", "Image": "ami-0070c5311b7677678", "Size": "t3.medium", "Region": "us-east-1", "Platform": "linux/amd64", "Created": 1573600782, "Updated": 1573575719, "Started": 1573575703, "Stopped": 1573575719 } ] } ================================================ FILE: server/web/template/testdata/servers_empty.json ================================================ { "Items": [] } ================================================ FILE: server/writer.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package server import ( "encoding/json" "errors" "net/http" "os" "strconv" ) // indent the json-encoded API responses var indent bool func init() { indent, _ = strconv.ParseBool( os.Getenv("HTTP_JSON_INDENT"), ) } var ( // errInvalidToken is returned when the api request token is invalid. errInvalidToken = errors.New("Invalid or missing token") // errUnauthorized is returned when the user is not authorized. errUnauthorized = errors.New("Unauthorized") // errForbidden is returned when user access is forbidden. errForbidden = errors.New("Forbidden") // errNotFound is returned when a resource is not found. errNotFound = errors.New("Not Found") ) // Error represents a json-encoded API error. type Error struct { Message string `json:"message"` } // writeErrorCode writes the json-encoded error message to the response. func writeErrorCode(w http.ResponseWriter, err error, status int) { writeJSON(w, &Error{Message: err.Error()}, status) } // writeError writes the json-encoded error message to the response // with a 500 internal server error. func writeError(w http.ResponseWriter, err error) { writeErrorCode(w, err, 500) } // writeNotFound writes the json-encoded error message to the response // with a 404 not found status code. func writeNotFound(w http.ResponseWriter, err error) { writeErrorCode(w, err, 404) } // writeUnauthorized writes the json-encoded error message to the response // with a 401 unauthorized status code. func writeUnauthorized(w http.ResponseWriter, err error) { writeErrorCode(w, err, 401) } // writeForbidden writes the json-encoded error message to the response // with a 403 forbidden status code. func writeForbidden(w http.ResponseWriter, err error) { writeErrorCode(w, err, 403) } // writeBadRequest writes the json-encoded error message to the response // with a 400 bad request status code. func writeBadRequest(w http.ResponseWriter, err error) { writeErrorCode(w, err, 400) } // writeJSON writes the json-encoded error message to the response // with a 400 bad request status code. func writeJSON(w http.ResponseWriter, v interface{}, status int) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("X-Content-Type-Options", "nosniff") w.WriteHeader(status) enc := json.NewEncoder(w) if indent { enc.SetIndent("", " ") } enc.Encode(v) } ================================================ FILE: server/writer_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package server import ( "encoding/json" "errors" "net/http" "net/http/httptest" "testing" ) func TestWriteError(t *testing.T) { w := httptest.NewRecorder() err := errors.New("pc load letter") writeError(w, err) if got, want := w.Code, 500; want != got { t.Errorf("Want response code %d, got %d", want, got) } errjson := &Error{} json.NewDecoder(w.Body).Decode(errjson) if got, want := errjson.Message, err.Error(); got != want { t.Errorf("Want error message %s, got %s", want, got) } } func TestWriteErrorCode(t *testing.T) { w := httptest.NewRecorder() err := errors.New("pc load letter") writeErrorCode(w, err, 418) if got, want := w.Code, 418; want != got { t.Errorf("Want response code %d, got %d", want, got) } errjson := &Error{} json.NewDecoder(w.Body).Decode(errjson) if got, want := errjson.Message, err.Error(); got != want { t.Errorf("Want error message %s, got %s", want, got) } } func TestWriteNotFound(t *testing.T) { w := httptest.NewRecorder() err := errors.New("pc load letter") writeNotFound(w, err) if got, want := w.Code, 404; want != got { t.Errorf("Want response code %d, got %d", want, got) } errjson := &Error{} json.NewDecoder(w.Body).Decode(errjson) if got, want := errjson.Message, err.Error(); got != want { t.Errorf("Want error message %s, got %s", want, got) } } func TestWriteUnauthorized(t *testing.T) { w := httptest.NewRecorder() err := errors.New("pc load letter") writeUnauthorized(w, err) if got, want := w.Code, 401; want != got { t.Errorf("Want response code %d, got %d", want, got) } errjson := &Error{} json.NewDecoder(w.Body).Decode(errjson) if got, want := errjson.Message, err.Error(); got != want { t.Errorf("Want error message %s, got %s", want, got) } } func TestWriteForbidden(t *testing.T) { w := httptest.NewRecorder() err := errors.New("pc load letter") writeForbidden(w, err) if got, want := w.Code, 403; want != got { t.Errorf("Want response code %d, got %d", want, got) } errjson := &Error{} json.NewDecoder(w.Body).Decode(errjson) if got, want := errjson.Message, err.Error(); got != want { t.Errorf("Want error message %s, got %s", want, got) } } func TestWriteBadRequest(t *testing.T) { w := httptest.NewRecorder() err := errors.New("pc load letter") writeBadRequest(w, err) if got, want := w.Code, 400; want != got { t.Errorf("Want response code %d, got %d", want, got) } errjson := &Error{} json.NewDecoder(w.Body).Decode(errjson) if got, want := errjson.Message, err.Error(); got != want { t.Errorf("Want error message %s, got %s", want, got) } } func TestWriteJSON(t *testing.T) { // without indent { w := httptest.NewRecorder() writeJSON(w, map[string]string{"hello": "world"}, http.StatusTeapot) if got, want := w.Body.String(), "{\"hello\":\"world\"}\n"; got != want { t.Errorf("Want JSON body %q, got %q", want, got) } if got, want := w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8"; got != want { t.Errorf("Want Content-Type %q, got %q", want, got) } if got, want := w.Code, http.StatusTeapot; got != want { t.Errorf("Want status code %d, got %d", want, got) } } // with indent { indent = true defer func() { indent = false }() w := httptest.NewRecorder() writeJSON(w, map[string]string{"hello": "world"}, http.StatusTeapot) if got, want := w.Body.String(), "{\n \"hello\": \"world\"\n}\n"; got != want { t.Errorf("Want JSON body %q, got %q", want, got) } } } ================================================ FILE: server.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package autoscaler import ( "context" "database/sql/driver" "errors" ) // ServerState specifies the server state. type ServerState string // Value converts the value to a sql string. func (s ServerState) Value() (driver.Value, error) { return string(s), nil } // ServerState type enumeration. const ( StatePending = ServerState("pending") StateCreating = ServerState("creating") StateCreated = ServerState("created") StateStaging = ServerState("staging") // starting StateRunning = ServerState("running") StateShutdown = ServerState("shutdown") StateStopping = ServerState("stopping") StateStopped = ServerState("stopped") StateError = ServerState("error") ) // ErrServerNotFound is returned when the requested server // does not exist in the store. var ErrServerNotFound = errors.New("Not Found") // A ServerStore persists server information. type ServerStore interface { // Find a server by unique name. Find(context.Context, string) (*Server, error) // List returns all registered servers List(context.Context) ([]*Server, error) // ListState returns all servers with the given state. ListState(context.Context, ServerState) ([]*Server, error) // Create the server record in the store. Create(context.Context, *Server) error // Update the server record in the store. Update(context.Context, *Server) error // Delete the server record from the store. Delete(context.Context, *Server) error // Purge old server records from the store. Purge(context.Context, int64) error } // Server stores the server details. type Server struct { ID string `db:"server_id" json:"id"` Provider ProviderType `db:"server_provider" json:"provider"` State ServerState `db:"server_state" json:"state"` Name string `db:"server_name" json:"name"` Image string `db:"server_image" json:"image"` Region string `db:"server_region" json:"region"` Size string `db:"server_size" json:"size"` Platform string `db:"server_platform" json:"platform"` Address string `db:"server_address" json:"address"` Capacity int `db:"server_capacity" json:"capacity"` Secret string `db:"server_secret" json:"secret"` Error string `db:"server_error" json:"error"` CAKey []byte `db:"server_ca_key" json:"ca_key"` CACert []byte `db:"server_ca_cert" json:"ca_cert"` TLSKey []byte `db:"server_tls_key" json:"tls_key"` TLSCert []byte `db:"server_tls_cert" json:"tls_cert"` Created int64 `db:"server_created" json:"created"` Updated int64 `db:"server_updated" json:"updated"` Started int64 `db:"server_started" json:"started"` Stopped int64 `db:"server_stopped" json:"stopped"` } ================================================ FILE: slack/slack.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package slack import ( "context" "fmt" "strings" "time" "github.com/drone/autoscaler" "github.com/drone/autoscaler/config" "github.com/bluele/slack" "github.com/dustin/go-humanize" ) // New returns a new provider that is instrumented to send // Slack notifications when server instances are provisioned // or terminated. func New(config config.Config, base autoscaler.ServerStore) autoscaler.ServerStore { return ¬ifier{ ServerStore: base, client: slack.NewWebHook(config.Slack.Webhook), create: config.Slack.Create, destroy: config.Slack.Destroy, error: config.Slack.Error, } } type notifier struct { autoscaler.ServerStore client *slack.WebHook channel string create bool destroy bool error bool } func (n *notifier) Update(ctx context.Context, server *autoscaler.Server) error { err := n.ServerStore.Update(ctx, server) switch { case server.State == autoscaler.StateRunning && n.create: n.notifyCreate(server) case server.State == autoscaler.StateStopped && n.destroy: n.notifyDestroy(server) case server.State == autoscaler.StateError && n.error: n.notifyError(server) } return err } func (n *notifier) notifyCreate(server *autoscaler.Server) error { opts := &slack.WebHookPostPayload{ Text: fmt.Sprintf("Provisioned server instance %s", server.Name), Attachments: []*slack.Attachment{ { Color: "#00BFA5", Fields: []*slack.AttachmentField{ { Title: "Name", Value: server.Name, Short: false, }, { Title: "Size", Value: server.Size, Short: false, }, { Title: "Region", Value: server.Region, Short: false, }, }, }, }, } return n.client.PostMessage(opts) } func (n *notifier) notifyDestroy(server *autoscaler.Server) error { opts := &slack.WebHookPostPayload{ Text: fmt.Sprintf("Terminated server instance %s", server.Name), Attachments: []*slack.Attachment{ { Color: "#CFD8DC", Fields: []*slack.AttachmentField{ { Title: "Name", Value: server.Name, Short: false, }, { Title: "Size", Value: server.Size, Short: false, }, { Title: "Region", Value: server.Region, Short: false, }, { Title: "Uptime", Value: humanizeTime(server.Created), Short: false, }, }, }, }, } return n.client.PostMessage(opts) } func (n *notifier) notifyError(server *autoscaler.Server) error { opts := &slack.WebHookPostPayload{ Text: fmt.Sprintf("Problem with server instance %s", server.Name), Attachments: []*slack.Attachment{ { Color: "#F44336", Fields: []*slack.AttachmentField{ { Title: "Name", Value: server.Name, Short: false, }, { Title: "Error", Value: server.Error, Short: false, }, }, }, }, } return n.client.PostMessage(opts) } func humanizeTime(unix int64) string { d := time.Unix(unix, 0) s := humanize.RelTime(d, time.Now(), "", "") return strings.TrimSpace(s) } ================================================ FILE: slack/slack_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package slack import ( "context" "os" "testing" "time" "github.com/drone/autoscaler" "github.com/drone/autoscaler/config" "github.com/drone/autoscaler/mocks" "github.com/bluele/slack" "github.com/golang/mock/gomock" "github.com/h2non/gock" ) var noContext = context.TODO() func TestHumanizeTime(t *testing.T) { unix := time.Now().Add(time.Minute * 60 * -1).Unix() text := humanizeTime(unix) if got, want := text, "1 hour"; got != want { t.Errorf("Want humanized time %s, got %s", want, got) } } func TestUpdateRunning(t *testing.T) { defer gock.Off() controller := gomock.NewController(t) defer controller.Finish() server := &autoscaler.Server{ Name: "this-is-a-test-message", Region: "nyc1", Size: "s-1vcpu-1gb", State: autoscaler.StateRunning, } gock.New("https://hooks.slack.com"). Post("/services/XXX/YYY/ZZZ"). JSON(createPayload). Reply(200) conf := config.Config{} conf.Slack.Webhook = "https://hooks.slack.com/services/XXX/YYY/ZZZ" conf.Slack.Create = true store := mocks.NewMockServerStore(controller) store.EXPECT().Update(gomock.Any(), server).Return(nil) slack := New(conf, store) err := slack.Update(noContext, server) if err != nil { t.Error(err) } if !gock.IsDone() { t.Errorf("Pending mocks not executed") } } func TestUpdateStopped(t *testing.T) { defer gock.Off() controller := gomock.NewController(t) defer controller.Finish() server := &autoscaler.Server{ Name: "this-is-a-test-message", Region: "nyc1", Size: "s-1vcpu-1gb", State: autoscaler.StateStopped, } gock.New("https://hooks.slack.com"). Post("/services/XXX/YYY/ZZZ"). Reply(200) conf := config.Config{} conf.Slack.Webhook = "https://hooks.slack.com/services/XXX/YYY/ZZZ" conf.Slack.Destroy = true store := mocks.NewMockServerStore(controller) store.EXPECT().Update(gomock.Any(), server).Return(nil) slack := New(conf, store) err := slack.Update(noContext, server) if err != nil { t.Error(err) return } if !gock.IsDone() { t.Errorf("Pending mocks not executed") } } func TestUpdateError(t *testing.T) { defer gock.Off() controller := gomock.NewController(t) defer controller.Finish() server := &autoscaler.Server{ Name: "this-is-a-test-message", Region: "nyc1", Size: "s-1vcpu-1gb", Error: "pc load letter", State: autoscaler.StateError, } gock.New("https://hooks.slack.com"). Post("/services/XXX/YYY/ZZZ"). JSON(errorPayload). Reply(200) conf := config.Config{} conf.Slack.Webhook = "https://hooks.slack.com/services/XXX/YYY/ZZZ" conf.Slack.Error = true store := mocks.NewMockServerStore(controller) store.EXPECT().Update(gomock.Any(), server).Return(nil) slack := New(conf, store) err := slack.Update(noContext, server) if err != nil { t.Error(err) } if !gock.IsDone() { t.Errorf("Pending mocks not executed") } } var createPayload = slack.WebHookPostPayload{ Text: "Provisioned server instance this-is-a-test-message", Attachments: []*slack.Attachment{ { Color: "#00BFA5", Fields: []*slack.AttachmentField{ { Title: "Name", Value: "this-is-a-test-message", }, { Title: "Size", Value: "s-1vcpu-1gb", }, { Title: "Region", Value: "nyc1", }, }, }, }, } var errorPayload = slack.WebHookPostPayload{ Text: "Problem with server instance this-is-a-test-message", Attachments: []*slack.Attachment{ { Color: "#F44336", Fields: []*slack.AttachmentField{ { Title: "Name", Value: "this-is-a-test-message", }, { Title: "Error", Value: "pc load letter", }, }, }, }, } // This is an integration test that will send a real // message to a Slack channel using a webhook defined // in the TEST_SLACK_WEBHOOK environment variable. func TestIntegration(t *testing.T) { webhook := os.Getenv("TEST_SLACK_WEBHOOK") if webhook == "" { t.Skipf("Skip Slack integration test. No webhook provided.") return } controller := gomock.NewController(t) defer controller.Finish() server := &autoscaler.Server{ Name: "i-123789331", Address: "1.2.3.4", Region: "nyc1", Size: "s-1vcpu-1gb", Capacity: 2, State: autoscaler.StateRunning, } conf := config.Config{} conf.Slack.Webhook = webhook store := mocks.NewMockServerStore(controller) store.EXPECT().Update(gomock.Any(), server).Return(nil) slack := New(conf, store) err := slack.Update(noContext, server) if err != nil { t.Error(err) } } ================================================ FILE: store/db.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package store import ( "context" "database/sql" "time" ddl "github.com/drone/autoscaler/store/migrate" "github.com/jmoiron/sqlx" ) var noContext = context.Background() // Connect to a database and verify with a ping. func Connect(driver, datasource string, maxconn int, maxlifetime time.Duration) (*sqlx.DB, error) { db, err := sql.Open(driver, datasource) if err != nil { return nil, err } switch driver { case "postgres": db.SetMaxIdleConns(maxconn) db.SetConnMaxLifetime(maxlifetime) case "mysql": db.SetMaxIdleConns(0) case "sqlite3": db.SetMaxOpenConns(1) } dbx := sqlx.NewDb(db, driver) if err := pingDatabase(dbx); err != nil { return nil, err } if err := setupDatabase(dbx); err != nil { return nil, err } return dbx, nil } // Must is a helper function that wraps a call to Connect // and panics if the error is non-nil. func Must(db *sqlx.DB, err error) *sqlx.DB { if err != nil { panic(err) } return db } // helper function to ping the database with backoff to ensure // a connection can be established before we proceed with the // database setup and migration. func pingDatabase(db *sqlx.DB) (err error) { for i := 0; i < 30; i++ { err = db.Ping() if err == nil { return } time.Sleep(time.Second) } return } // helper function to setup the databsae by performing automated // database migration steps. func setupDatabase(db *sqlx.DB) error { return ddl.Migrate(db) } ================================================ FILE: store/db_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package store import ( "os" "sync" "github.com/jmoiron/sqlx" _ "github.com/go-sql-driver/mysql" _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" ) // connect opens a new test database connection. func connect() (*sqlx.DB, error) { var ( driver = "sqlite3" config = ":memory:" ) if os.Getenv("DATABASE_DRIVER") != "" { driver = os.Getenv("DATABASE_DRIVER") config = os.Getenv("DATABASE_CONFIG") } return Connect(driver, config, 0, 0) } // locker returns a new text locker. func locker() sync.Locker { driver := "sqlite3" if os.Getenv("DATABASE_DRIVER") != "" { driver = os.Getenv("DATABASE_DRIVER") } return NewLocker(driver) } ================================================ FILE: store/lock.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package store import "sync" // NewLocker returns a new database mutex. If the driver // is mysql or postgres a noop is returned. func NewLocker(driver string) sync.Locker { switch driver { case "sqlite3": return new(sync.Mutex) default: return new(noopLocker) } } type noopLocker struct{} func (*noopLocker) Lock() {} func (*noopLocker) Unlock() {} ================================================ FILE: store/migrate/migrate.go ================================================ package ddl import ( "github.com/drone/autoscaler/store/migrate/mysql" "github.com/drone/autoscaler/store/migrate/postgres" "github.com/drone/autoscaler/store/migrate/sqlite" "github.com/jmoiron/sqlx" ) // Migrate performs the database migration. func Migrate(db *sqlx.DB) error { switch db.DriverName() { case "postgres": return postgres.Migrate(db.DB) case "mysql": return mysql.Migrate(db.DB) default: return sqlite.Migrate(db.DB) } } ================================================ FILE: store/migrate/mysql/ddl.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package mysql //go:generate togo ddl -package mysql -dialect mysql ================================================ FILE: store/migrate/mysql/ddl_gen.go ================================================ package mysql import ( "database/sql" ) var migrations = []struct { name string stmt string }{ { name: "create-table-servers", stmt: createTableServers, }, { name: "create-index-server-id", stmt: createIndexServerId, }, { name: "create-index-server-state", stmt: createIndexServerState, }, } // Migrate performs the database migration. If the migration fails // and error is returned. func Migrate(db *sql.DB) error { if err := createTable(db); err != nil { return err } completed, err := selectCompleted(db) if err != nil && err != sql.ErrNoRows { return err } for _, migration := range migrations { if _, ok := completed[migration.name]; ok { continue } if _, err := db.Exec(migration.stmt); err != nil { return err } if err := insertMigration(db, migration.name); err != nil { return err } } return nil } func createTable(db *sql.DB) error { _, err := db.Exec(migrationTableCreate) return err } func insertMigration(db *sql.DB, name string) error { _, err := db.Exec(migrationInsert, name) return err } func selectCompleted(db *sql.DB) (map[string]struct{}, error) { migrations := map[string]struct{}{} rows, err := db.Query(migrationSelect) if err != nil { return nil, err } defer rows.Close() for rows.Next() { var name string if err := rows.Scan(&name); err != nil { return nil, err } migrations[name] = struct{}{} } return migrations, nil } // // migration table ddl and sql // var migrationTableCreate = ` CREATE TABLE IF NOT EXISTS migrations ( name VARCHAR(255) ,UNIQUE(name) ) ` var migrationInsert = ` INSERT INTO migrations (name) VALUES (?) ` var migrationSelect = ` SELECT name FROM migrations ` // // 001_create_table_servers.sql // var createTableServers = ` CREATE TABLE servers ( server_name VARCHAR(50) PRIMARY KEY ,server_id VARCHAR(250) ,server_provider VARCHAR(50) ,server_state VARCHAR(50) ,server_image VARCHAR(250) ,server_region VARCHAR(50) ,server_size VARCHAR(50) ,server_platform VARCHAR(50) ,server_address VARCHAR(250) ,server_capacity INTEGER ,server_secret VARCHAR(50) ,server_error BLOB ,server_ca_key BLOB ,server_ca_cert BLOB ,server_tls_key BLOB ,server_tls_cert BLOB ,server_created INTEGER ,server_updated INTEGER ,server_started INTEGER ,server_stopped INTEGER ); ` var createIndexServerId = ` CREATE INDEX ix_servers_id ON servers (server_id); ` var createIndexServerState = ` CREATE INDEX ix_servers_state ON servers (server_state); ` ================================================ FILE: store/migrate/mysql/files/001_create_table_servers.sql ================================================ -- name: create-table-servers CREATE TABLE servers ( server_name VARCHAR(50) PRIMARY KEY ,server_id VARCHAR(250) ,server_provider VARCHAR(50) ,server_state VARCHAR(50) ,server_image VARCHAR(250) ,server_region VARCHAR(50) ,server_size VARCHAR(50) ,server_platform VARCHAR(50) ,server_address VARCHAR(250) ,server_capacity INTEGER ,server_secret VARCHAR(50) ,server_error BLOB ,server_ca_key BLOB ,server_ca_cert BLOB ,server_tls_key BLOB ,server_tls_cert BLOB ,server_created INTEGER ,server_updated INTEGER ,server_started INTEGER ,server_stopped INTEGER ); -- name: create-index-server-id CREATE INDEX ix_servers_id ON servers (server_id); -- name: create-index-server-state CREATE INDEX ix_servers_state ON servers (server_state); ================================================ FILE: store/migrate/postgres/ddl.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package postgres //go:generate togo ddl -package postgres -dialect postgres ================================================ FILE: store/migrate/postgres/ddl_gen.go ================================================ package postgres import ( "database/sql" ) var migrations = []struct { name string stmt string }{ { name: "create-table-servers", stmt: createTableServers, }, { name: "create-index-server-id", stmt: createIndexServerId, }, { name: "create-index-server-state", stmt: createIndexServerState, }, } // Migrate performs the database migration. If the migration fails // and error is returned. func Migrate(db *sql.DB) error { if err := createTable(db); err != nil { return err } completed, err := selectCompleted(db) if err != nil && err != sql.ErrNoRows { return err } for _, migration := range migrations { if _, ok := completed[migration.name]; ok { continue } if _, err := db.Exec(migration.stmt); err != nil { return err } if err := insertMigration(db, migration.name); err != nil { return err } } return nil } func createTable(db *sql.DB) error { _, err := db.Exec(migrationTableCreate) return err } func insertMigration(db *sql.DB, name string) error { _, err := db.Exec(migrationInsert, name) return err } func selectCompleted(db *sql.DB) (map[string]struct{}, error) { migrations := map[string]struct{}{} rows, err := db.Query(migrationSelect) if err != nil { return nil, err } defer rows.Close() for rows.Next() { var name string if err := rows.Scan(&name); err != nil { return nil, err } migrations[name] = struct{}{} } return migrations, nil } // // migration table ddl and sql // var migrationTableCreate = ` CREATE TABLE IF NOT EXISTS migrations ( name VARCHAR(255) ,UNIQUE(name) ) ` var migrationInsert = ` INSERT INTO migrations (name) VALUES ($1) ` var migrationSelect = ` SELECT name FROM migrations ` // // 001_create_table_servers.sql // var createTableServers = ` CREATE TABLE servers ( server_name VARCHAR(50) PRIMARY KEY ,server_id VARCHAR(250) ,server_provider VARCHAR(50) ,server_state VARCHAR(50) ,server_image VARCHAR(250) ,server_region VARCHAR(50) ,server_size VARCHAR(50) ,server_platform VARCHAR(50) ,server_address VARCHAR(250) ,server_capacity INTEGER ,server_secret VARCHAR(50) ,server_error TEXT ,server_ca_key TEXT ,server_ca_cert TEXT ,server_tls_key TEXT ,server_tls_cert TEXT ,server_created INTEGER ,server_updated INTEGER ,server_started INTEGER ,server_stopped INTEGER ); ` var createIndexServerId = ` CREATE INDEX ix_servers_id ON servers (server_id); ` var createIndexServerState = ` CREATE INDEX ix_servers_state ON servers (server_state); ` ================================================ FILE: store/migrate/postgres/files/001_create_table_servers.sql ================================================ -- name: create-table-servers CREATE TABLE servers ( server_name VARCHAR(50) PRIMARY KEY ,server_id VARCHAR(250) ,server_provider VARCHAR(50) ,server_state VARCHAR(50) ,server_image VARCHAR(250) ,server_region VARCHAR(50) ,server_size VARCHAR(50) ,server_platform VARCHAR(50) ,server_address VARCHAR(250) ,server_capacity INTEGER ,server_secret VARCHAR(50) ,server_error TEXT ,server_ca_key TEXT ,server_ca_cert TEXT ,server_tls_key TEXT ,server_tls_cert TEXT ,server_created INTEGER ,server_updated INTEGER ,server_started INTEGER ,server_stopped INTEGER ); -- name: create-index-server-id CREATE INDEX ix_servers_id ON servers (server_id); -- name: create-index-server-state CREATE INDEX ix_servers_state ON servers (server_state); ================================================ FILE: store/migrate/sqlite/ddl.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package sqlite //go:generate togo ddl -package sqlite -dialect sqlite3 ================================================ FILE: store/migrate/sqlite/ddl_gen.go ================================================ package sqlite import ( "database/sql" ) var migrations = []struct { name string stmt string }{ { name: "create-table-servers", stmt: createTableServers, }, { name: "create-index-server-id", stmt: createIndexServerId, }, { name: "create-index-server-state", stmt: createIndexServerState, }, } // Migrate performs the database migration. If the migration fails // and error is returned. func Migrate(db *sql.DB) error { if err := createTable(db); err != nil { return err } completed, err := selectCompleted(db) if err != nil && err != sql.ErrNoRows { return err } for _, migration := range migrations { if _, ok := completed[migration.name]; ok { continue } if _, err := db.Exec(migration.stmt); err != nil { return err } if err := insertMigration(db, migration.name); err != nil { return err } } return nil } func createTable(db *sql.DB) error { _, err := db.Exec(migrationTableCreate) return err } func insertMigration(db *sql.DB, name string) error { _, err := db.Exec(migrationInsert, name) return err } func selectCompleted(db *sql.DB) (map[string]struct{}, error) { migrations := map[string]struct{}{} rows, err := db.Query(migrationSelect) if err != nil { return nil, err } defer rows.Close() for rows.Next() { var name string if err := rows.Scan(&name); err != nil { return nil, err } migrations[name] = struct{}{} } return migrations, nil } // // migration table ddl and sql // var migrationTableCreate = ` CREATE TABLE IF NOT EXISTS migrations ( name VARCHAR(255) ,UNIQUE(name) ) ` var migrationInsert = ` INSERT INTO migrations (name) VALUES (?) ` var migrationSelect = ` SELECT name FROM migrations ` // // 001_create_table_servers.sql // var createTableServers = ` CREATE TABLE IF NOT EXISTS servers ( server_name TEXT PRIMARY KEY ,server_id TEXT ,server_provider TEXT ,server_state TEXT ,server_image TEXT ,server_region TEXT ,server_size TEXT ,server_platform TEXT ,server_address TEXT ,server_capacity INTEGER ,server_secret TEXT ,server_error TEXT ,server_ca_key TEXT ,server_ca_cert TEXT ,server_tls_key TEXT ,server_tls_cert TEXT ,server_created INTEGER ,server_updated INTEGER ,server_started INTEGER ,server_stopped INTEGER ); ` var createIndexServerId = ` CREATE INDEX IF NOT EXISTS ix_servers_id ON servers (server_id); ` var createIndexServerState = ` CREATE INDEX IF NOT EXISTS ix_servers_state ON servers (server_state); ` ================================================ FILE: store/migrate/sqlite/files/001_create_table_servers.sql ================================================ -- name: create-table-servers CREATE TABLE IF NOT EXISTS servers ( server_name TEXT PRIMARY KEY ,server_id TEXT ,server_provider TEXT ,server_state TEXT ,server_image TEXT ,server_region TEXT ,server_size TEXT ,server_platform TEXT ,server_address TEXT ,server_capacity INTEGER ,server_secret TEXT ,server_error TEXT ,server_ca_key TEXT ,server_ca_cert TEXT ,server_tls_key TEXT ,server_tls_cert TEXT ,server_created INTEGER ,server_updated INTEGER ,server_started INTEGER ,server_stopped INTEGER ); -- name: create-index-server-id CREATE INDEX IF NOT EXISTS ix_servers_id ON servers (server_id); -- name: create-index-server-state CREATE INDEX IF NOT EXISTS ix_servers_state ON servers (server_state); ================================================ FILE: store/servers.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package store import ( "context" "database/sql" "sync" "time" "github.com/drone/autoscaler" "github.com/avast/retry-go" "github.com/jmoiron/sqlx" ) // NewServerStore returns a new server store. func NewServerStore(db *sqlx.DB, mu sync.Locker) autoscaler.ServerStore { return &serverStore{mu, db} } type serverStore struct { mu sync.Locker db *sqlx.DB } func (s *serverStore) Find(_ context.Context, name string) (*autoscaler.Server, error) { s.mu.Lock() defer s.mu.Unlock() dest := &autoscaler.Server{Name: name} stmt, args, err := s.db.BindNamed(serverFindStmt, dest) if err != nil { return nil, err } err = s.db.GetContext(noContext, dest, stmt, args...) return dest, err } func (s *serverStore) List(_ context.Context) ([]*autoscaler.Server, error) { s.mu.Lock() defer s.mu.Unlock() dest := []*autoscaler.Server{} err := s.db.SelectContext(noContext, &dest, serverListStmt) return dest, err } func (s *serverStore) ListState(_ context.Context, state autoscaler.ServerState) ([]*autoscaler.Server, error) { s.mu.Lock() defer s.mu.Unlock() dest := []*autoscaler.Server{} stmt, args, err := s.db.BindNamed(serverListStateStmt, map[string]interface{}{"server_state": state}) if err != nil { return nil, err } err = s.db.SelectContext(noContext, &dest, stmt, args...) if err == sql.ErrNoRows { return dest, nil } return dest, err } func (s *serverStore) Create(_ context.Context, server *autoscaler.Server) error { return retry.Do( func() error { if err := s.create(server); isConnReset(err) { return err } else { return retry.Unrecoverable(err) } }, retry.Attempts(5), retry.MaxDelay(time.Second*5), retry.LastErrorOnly(true), ) } func (s *serverStore) create(server *autoscaler.Server) error { s.mu.Lock() defer s.mu.Unlock() server.Created = time.Now().Unix() server.Updated = time.Now().Unix() stmt, args, err := s.db.BindNamed(serverInsertStmt, server) if err != nil { return err } _, err = s.db.ExecContext(noContext, stmt, args...) return err } func (s *serverStore) Update(_ context.Context, server *autoscaler.Server) error { return retry.Do( func() error { if err := s.update(server); isConnReset(err) { return err } else { return retry.Unrecoverable(err) } }, retry.Attempts(5), retry.MaxDelay(time.Second*5), retry.LastErrorOnly(true), ) } func (s *serverStore) update(server *autoscaler.Server) error { s.mu.Lock() defer s.mu.Unlock() server.Updated = time.Now().Unix() stmt, args, err := s.db.BindNamed(serverUpdateStmt, server) if err != nil { return err } _, err = s.db.ExecContext(noContext, stmt, args...) return err } func (s *serverStore) Delete(_ context.Context, server *autoscaler.Server) error { s.mu.Lock() defer s.mu.Unlock() stmt, args, err := s.db.BindNamed(serverDeleteStmt, server) if err != nil { return err } _, err = s.db.ExecContext(noContext, stmt, args...) return err } func (s *serverStore) Purge(_ context.Context, before int64) error { s.mu.Lock() defer s.mu.Unlock() stmt, args, err := s.db.BindNamed(serverPurgeStmt, &autoscaler.Server{Stopped: before}) if err != nil { return err } _, err = s.db.ExecContext(noContext, stmt, args...) return err } const serverFindStmt = ` SELECT server_name ,server_id ,server_provider ,server_state ,server_image ,server_region ,server_size ,server_platform ,server_address ,server_capacity ,server_secret ,server_error ,server_ca_key ,server_ca_cert ,server_tls_key ,server_tls_cert ,server_created ,server_updated ,server_started ,server_stopped FROM servers WHERE server_name=:server_name ` const serverListStmt = ` SELECT server_name ,server_id ,server_provider ,server_state ,server_image ,server_region ,server_size ,server_platform ,server_address ,server_capacity ,server_secret ,server_error ,server_ca_key ,server_ca_cert ,server_tls_key ,server_tls_cert ,server_created ,server_updated ,server_started ,server_stopped FROM servers ORDER BY server_created ASC ` const serverListStateStmt = ` SELECT server_name ,server_id ,server_provider ,server_state ,server_image ,server_region ,server_size ,server_platform ,server_address ,server_capacity ,server_secret ,server_error ,server_ca_key ,server_ca_cert ,server_tls_key ,server_tls_cert ,server_created ,server_updated ,server_started ,server_stopped FROM servers WHERE server_state=:server_state ORDER BY server_created ASC ` const serverInsertStmt = ` INSERT INTO servers ( server_name ,server_id ,server_provider ,server_state ,server_image ,server_region ,server_size ,server_platform ,server_address ,server_capacity ,server_secret ,server_error ,server_ca_key ,server_ca_cert ,server_tls_key ,server_tls_cert ,server_created ,server_updated ,server_started ,server_stopped ) VALUES ( :server_name ,:server_id ,:server_provider ,:server_state ,:server_image ,:server_region ,:server_size ,:server_platform ,:server_address ,:server_capacity ,:server_secret ,:server_error ,:server_ca_key ,:server_ca_cert ,:server_tls_key ,:server_tls_cert ,:server_created ,:server_updated ,:server_started ,:server_stopped ) ` const serverUpdateStmt = ` UPDATE servers SET server_id=:server_id ,server_provider=:server_provider ,server_state=:server_state ,server_image=:server_image ,server_region=:server_region ,server_size=:server_size ,server_platform=:server_platform ,server_address=:server_address ,server_capacity=:server_capacity ,server_secret=:server_secret ,server_error=:server_error ,server_ca_key=:server_ca_key ,server_ca_cert=:server_ca_cert ,server_tls_key=:server_tls_key ,server_tls_cert=:server_tls_cert ,server_updated=:server_updated ,server_started=:server_started ,server_stopped=:server_stopped WHERE server_name=:server_name ` const serverDeleteStmt = ` DELETE FROM servers WHERE server_name=:server_name ` const serverPurgeStmt = ` DELETE FROM servers WHERE server_state = 'stopped' AND server_stopped < :server_stopped ` ================================================ FILE: store/servers_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package store import ( "context" "database/sql" "testing" "time" "github.com/drone/autoscaler" ) func TestServer(t *testing.T) { conn, err := connect() if err != nil { t.Error(err) return } defer conn.Close() mu := locker() store := NewServerStore(conn, mu).(*serverStore) t.Run("Create", testServerCreate(store)) t.Run("Find", testServerFind(store)) t.Run("List", testServerList(store)) t.Run("ListState", testServerListState(store)) t.Run("Update", testServerUpdate(store)) t.Run("Delete", testServerDelete(store)) t.Run("Purge", testServerPurge(store)) } func testServerCreate(store *serverStore) func(t *testing.T) { return func(t *testing.T) { server := &autoscaler.Server{ Provider: autoscaler.ProviderGoogle, State: autoscaler.StateRunning, Name: "i-5203422c", Address: "54.194.252.215", Capacity: 2, Created: time.Now().Unix(), Updated: time.Now().Unix(), } err := store.Create(context.TODO(), server) if err != nil { t.Error(err) } } } func testServerFind(store *serverStore) func(t *testing.T) { return func(t *testing.T) { server, err := store.Find(context.TODO(), "i-5203422c") if err != nil { t.Error(err) } else { t.Run("Fields", testServer(server)) } } } func testServerList(store *serverStore) func(t *testing.T) { return func(t *testing.T) { servers, err := store.List(context.TODO()) if err != nil { t.Error(err) return } if got, want := len(servers), 1; got != want { t.Errorf("Want server count %d, got %d", want, got) } else { t.Run("Fields", testServer(servers[0])) } } } func testServerListState(store *serverStore) func(t *testing.T) { return func(t *testing.T) { // seed the database with two servers with shutdown state. // to confirm we can list servers by state. These will be // used in a subsequent purge test. store.Create(context.TODO(), &autoscaler.Server{ Provider: autoscaler.ProviderGoogle, State: autoscaler.StateStopped, Name: "agent-123456789", }) store.Create(context.TODO(), &autoscaler.Server{ Provider: autoscaler.ProviderGoogle, State: autoscaler.StateStopped, Name: "agent-987654321", }) servers, err := store.ListState(context.TODO(), autoscaler.StateStopped) if err != nil { t.Error(err) return } if got, want := len(servers), 2; got != want { t.Errorf("Want server count %d, got %d", want, got) } } } func testServerUpdate(store *serverStore) func(t *testing.T) { return func(t *testing.T) { server := &autoscaler.Server{ Provider: autoscaler.ProviderGoogle, Name: "i-5203422c", Address: "54.194.252.215", Capacity: 2, Created: time.Now().Unix(), Updated: time.Now().Unix(), } err := store.Update(context.TODO(), server) if err != nil { t.Error(err) return } updated, err := store.Find(context.TODO(), server.Name) if err != nil { t.Error(err) return } if got, want := updated.Capacity, server.Capacity; got != want { t.Errorf("Want updated capacity %d, got %d", want, got) } } } func testServerDelete(store *serverStore) func(t *testing.T) { return func(t *testing.T) { _, err := store.Find(context.TODO(), "i-5203422c") if err != nil { t.Error(err) return } err = store.Delete(context.TODO(), &autoscaler.Server{Name: "i-5203422c"}) if err != nil { t.Error(err) return } _, err = store.Find(context.TODO(), "i-5203422c") if got, want := err, sql.ErrNoRows; got != want { t.Errorf("Want ErrNoRows, got %s", got) } } } func testServerPurge(store *serverStore) func(t *testing.T) { return func(t *testing.T) { // this test attempts to purge the database of all // servers with a state of stopped. The database was // seeded with stopped servers in testServerListState. before, _ := store.List(context.TODO()) if got, want := len(before), 2; got != want { t.Errorf("Want %d servers, got %d", want, got) return } err := store.Purge(context.TODO(), time.Now().Unix()+1) if err != nil { t.Error(err) return } after, err := store.List(context.TODO()) if err != nil { t.Error(err) return } if got, want := len(after), 0; got != want { t.Errorf("Want 0 remaining servers, got %d", got) } } } func testServer(server *autoscaler.Server) func(t *testing.T) { return func(t *testing.T) { if got, want := server.Name, "i-5203422c"; got != want { t.Errorf("Want server Name %q, got %q", want, got) } if got, want := server.State, autoscaler.StateRunning; got != want { t.Errorf("Want server State %v, got %v", want, got) } if got, want := server.Address, "54.194.252.215"; got != want { t.Errorf("Want server Address %q, got %q", want, got) } if got, want := server.Capacity, 2; got != want { t.Errorf("Want server Capacity %d, got %d", want, got) } if got, want := server.Provider, autoscaler.ProviderGoogle; got != want { t.Errorf("Want server Provider %v, got %v", want, got) } } } ================================================ FILE: store/util.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package store import "strings" // helper function returns true if the error message // indicates the connection has been reset. func isConnReset(err error) bool { if err == nil { return false } return strings.Contains(err.Error(), "connection reset by peer") } ================================================ FILE: store/util_test.go ================================================ // Copyright 2018 Drone.IO Inc // Use of this source code is governed by the Polyform License // that can be found in the LICENSE file. package store import ( "database/sql" "errors" "testing" ) func TestConnectionReset(t *testing.T) { if isConnReset(nil) { t.Errorf("Expect nil error returns false") } if isConnReset(sql.ErrNoRows) { t.Errorf("Expect ErrNoRows returns false") } if !isConnReset(errors.New("read: connection reset by peer")) { t.Errorf("Expect connection reset by peer return true") } } // connect: connection timed out