[
  {
    "path": ".drone.sh",
    "content": "#!/bin/sh\n\nset -e\nset -x\n\nCOMMIT=\"-X main.commit=${DRONE_COMMIT_SHA}\"\nVERSION=\"-X main.version=${DRONE_TAG=latest}\"\n\ngo build \\\n    -ldflags \"-extldflags \\\"-static\\\" $COMMIT $VERSION\"   \\\n\t-o release/linux/amd64/drone-autoscaler \\\n\tgithub.com/drone/autoscaler/cmd/drone-autoscaler\n"
  },
  {
    "path": ".drone.yml",
    "content": "---\nkind: pipeline\nname: default\ntype: vm\n\npool:\n  use: ubuntu\n\nplatform:\n  os: linux\n  arch: amd64\n\nsteps:\n- name: test\n  pull: default\n  image: golang\n  volumes:\n  - name: deps\n    path: /go\n  commands:\n  - go get\n  - go test -v -cover ./...\n\n- name: test_postgres\n  pull: default\n  image: golang\n  volumes:\n  - name: deps\n    path: /go\n  commands:\n  - cd store\n  - go test -v\n  environment:\n    DATABASE_CONFIG: host=postgres user=postgres password=password dbname=test sslmode=disable\n    DATABASE_DRIVER: postgres\n\n- name: test_mysql\n  pull: default\n  image: golang\n  volumes:\n  - name: deps\n    path: /go\n  commands:\n  - cd store\n  - go test -v\n  environment:\n    DATABASE_CONFIG: \"root:password@tcp(mysql:3306)/test?parseTime=true\"\n    DATABASE_DRIVER: mysql\n\n- name: build\n  pull: default\n  image: golang\n  volumes:\n  - name: deps\n    path: /go\n  commands:\n  - sh .drone.sh\n\n- name: publish\n  pull: default\n  image: plugins/docker\n  settings:\n    auto_tag: true\n    repo: drone/autoscaler\n    password:\n      from_secret: docker_password\n    username:\n      from_secret: docker_username\n  when:\n    event:\n    - push\n    - tag\n\nvolumes:\n- name: deps\n  temp: {}\n\nservices:\n- name: postgres\n  pull: default\n  image: postgres:9\n  environment:\n    POSTGRES_DB: test\n    POSTGRES_PASSWORD: password\n\n- name: mysql\n  pull: default\n  image: mysql:5\n  environment:\n    MYSQL_DATABASE: test\n    MYSQL_ROOT_PASSWORD: password\n\n...\n"
  },
  {
    "path": ".github/issue_template.md",
    "content": ""
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "<!-- IMPORTANT NOTICE\n\nBy submitting a pull request, you acknowledge that your contribution\nwill be under the terms of the BSD-3-Clause license.\n\n    https://opensource.org/licenses/BSD-3-Clause\n\n-->"
  },
  {
    "path": ".gitignore",
    "content": "./drone-autoscaler\nNOTES.md\nrelease\nvendor\n*.sqlite\n*.sqlite3\n*.bak\n*.out\n*.db\n*.env\n"
  },
  {
    "path": "BUILDING",
    "content": "1. Install go 1.11 or later\n2. Install dependencies:\n\n    go get\n\n3. Compile and test:\n\n    go install ./...\n    go test ./...\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [1.7.5]\n### Fixed\n- ignore unset environment variables when configuring runners, by [@bradrydzewski](https://github.com/bradrydzewski). [d26b8e41](https://github.com/drone/autoscaler/commit/6db28505572d90df9a271404440789043c7b378b).\n\n## [1.7.4]\n### Fixed\n- support colon in map values sourced from environment variables, by [@UnAfraid](https://github.com/kelseyhightower/envconfig/pull/185)\n\n## [1.7.3]\n### Added\n- parameter to configure env file for remote runners. [#74](https://github.com/drone/autoscaler/pull/74).\n\n### Fixed\n- the content-type should be set before the status is written to the http response. [89780e6](https://github.com/drone/autoscaler/commit/89780e6b9585e8116249524fe6fe6dffe3904fd6).\n- pending instance count is excluded from determining available capacity when reducing pool size. [d26b8e41](https://github.com/drone/autoscaler/commit/d26b8e41fd178595fd00d739d1d3b27f2a870314).\n- custom scopes not passed to google cloud configuration. [#79](https://github.com/drone/autoscaler/pull/79).\n\n### Changed\n- docker.NewClient was deprecated; migrate to docker.NewClientWithOpts. [#72](https://github.com/drone/autoscaler/pull/72).\n\n## [1.7.2]\n### Fixed\n- 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).\n\n## [1.7.2]\n### Added\n- support for aws fallback instance types, by [bradrydzewski](https://github.com/bradrydzewski). [d524689b].(https://github.com/drone/autoscaler/commit/d524689bbd1ed73ef8ee77cb3e0c5e6e6f786158).\n\n## [1.7.1]\n### Added\n- support for google compute private ip, by [swjclarke](https://github.com/swjclarke).\n- support for google compute service accounts, by [ademariag](https://github.com/ademariag).\n\n### Fixed\n- google compute instance scopes being ignored, by [ademariag](https://github.com/ademariag).\n\n## [1.7.0]\n### Added\n- parameter to configure docker stop timeout duration.\n- parameter to configure aws volume iops, by [ttousai](https://github.com/ttousai).\n- parameter to configure gcp scopes, by [imranismail](https://github.com/imranismail).\n- metrics to track server boot errors\n- metrics to track server boot time\n- metrics to track server installation errors\n- metrics to track server installation time\n- metrics to track server creation errors\n- metrics to track server creation time\n\n### Fixed\n- do not run docker stop if the instance was not created.\n- do not run docker stop if the instance was not assigned an IP.\n\n## [1.6.1]\n### Added\n- support for instance not found errors in gcp, by [frebib](https://github.com/frebib).\n\n### Fixed\n- resume instance removal when autoscaler unexpectedly restarted, by [@bradrydzewski](https://github.com/bradrydzewski).\n\n## [1.6.0]\n### Changed\n- Use logrus for logging instead of zerolog, by [@bradrydzewski](https://github.com/bradrydzewski).\n\n### Added\n- Read only user interface to visualize servers and logs, by [@bradrydzewski](https://github.com/bradrydzewski). \n- Support for configuring subnetworks with GCP, by [@nsigarora](https://github.com/nsigarora).\n- Support for handling  ErrInstanceNotFound with Hetzner, by [@tboerger](https://github.com/tboerger).\n\n## [1.5.0]\n### Changed\n- Use the new Docker runner image and deprecate the agent, by [@bradrydzewski](https://github.com/bradrydzewski).\n- Enable Digital Ocean private IP addresses, by [@barrypeng6](https://github.com/barrypeng6).\n\n## [1.4.3]\n### Fixed\n- Expired context preventing database updates, by [@bradrydzewski](https://github.com/bradrydzewski).\n\n## [1.4.2]\n### Added\n- Log errors updating the instance state, by [@bradrydzewski](https://github.com/bradrydzewski).\n- Add mutex to database operations for sqlite, by [@bradrydzewski](https://github.com/bradrydzewski).\n\n## [1.4.1] - 2019-10-10\n### Fixed\n- Support for arm machines on Scaleway, by [@tboerger](https://github.com/tboerger).\n\n## [1.4.0] - 2019-09-23\n### Added\n- Ability to configure the reaper internal, by [@msaizar](https://github.com/msaizar).\n- Ability to configure the install check deadline, by [@bradrydzewski](https://github.com/bradrydzewski).\n- Ability to configure the install check interval, by [@bradrydzewski](https://github.com/bradrydzewski).\n\n## [1.3.0] - 2019-09-11\n### Added\n\n- Added support for Scaleway, by [@frebib](https://github.com/frebib). [#45](https://github.com/drone/autoscaler/pull/45).\n\n### Fixed\n\n- Fixed issue where non-existing instance could not be destroyed, by [@jlesage](https://github.com/jlesage). [#50](https://github.com/drone/autoscaler/pull/50).\n- Added timeout when attempting to ping the instance, by [@bradrydzewski](https://github.com/bradrydzewski).\n\n## [1.2.2] - 2019-08-29\n### Added\n\n- Support for loading runner environment variables from file, by [@bradrydzewski](https://github.com/bradrydzewski).\n- Basic support for configuring windows agents, by [@bradrydzewski](https://github.com/bradrydzewski).\n\n### Fixed\n\n- Pull garbage collector image before creating the container, by [@msaizar](https://github.com/msaizar).\n- Handle nil pointer caused by empty or missing interface in AWS driver, by [@bradrydzewski](https://github.com/bradrydzewski).\n\n## [1.2.1] - 2019-08-14\n### Added\n\n- Added postgres driver, by [@mmuehlberger](https://github.com/mmuehlberger).\n- Support for capacity buffer, by [@jones2026](https://github.com/jones2026). [#39](https://github.com/drone/autoscaler/pull/39).\n\n### Fixed\n\n- Close docker client after server ping, by [@msaizar](https://github.com/msaizar), [#42](https://github.com/drone/autoscaler/pull/42).\n\n## [1.2.0] - 2019-07-29\n### Added\n\n- Support for agent label assignment and matching, by [@logikone](https://github.com/logikone).\n- Allow Hetzner to choose datacenter when none specified, by [@tboerger](https://github.com/tboerger).\n\n### Fixed\n\n- Upgraded zerolog to fix duplicate keys in json output, by [@krtx](https://github.com/krtx).\n\n## [1.1.0] - 2019-05-29\n### Added\n\n- Create AWS instances with Name tag set to agent unique id, from [@bradrydzewski](https://github.com/bradrydzewski).\n- Handle AWS instance not found errors, from [@andy-trimble](https://github.com/andy-trimble).\n- Remove hard-coded DNS servers from the default Docker configuration, from [jones2026](https://github.com/jones2026).\n\n## [1.0.0] - 2019-05-06\n### Added\n\n- Optional support for watchtower from [@bradrydzewski](https://github.com/bradrydzewski).\n- Optional support for drone/gc from [@bradrydzewski](https://github.com/bradrydzewski). \n- Update the default agent image to 1.0 stable, from [@bradrydzewski](https://github.com/bradrydzewski).\n- Configure agent environment variables from [@bradrydzewski](https://github.com/bradrydzewski).\n- Configure agent host volume mounts from [@patrickjahns](https://github.com/patrickjahns).\n- Update Digital Ocean default image from [@jlesage](https://github.com/jlesage).\n- Fix problems using custom Digital Ocean image from [@jlesage](https://github.com/jlesage).\n"
  },
  {
    "path": "COPYRIGHT",
    "content": "Copyright 2018 Drone.IO Inc\nUse of this software is governed by the Polyform License\nthat can be found in the LICENSE file."
  },
  {
    "path": "Dockerfile",
    "content": "FROM alpine:3.20 as alpine\nRUN apk add -U --no-cache ca-certificates\n\nFROM alpine:3.20\nEXPOSE 8080 80 443\nVOLUME /data\n\nENV GODEBUG netdns=go\nENV XDG_CACHE_HOME /data\nENV DRONE_DATABASE_DRIVER sqlite3\nENV DRONE_DATABASE_DATASOURCE /data/database.sqlite?cache=shared&mode=rwc&_busy_timeout=9999999\n\nCOPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/\n\nADD release/linux/amd64/drone-autoscaler /bin/\nENTRYPOINT [\"/bin/drone-autoscaler\"]\n"
  },
  {
    "path": "LICENSE.md",
    "content": "[Polyform-Small-Business-1.0.0](https://polyformproject.org/licenses/small-business/1.0.0) OR\n[Polyform-Free-Trial-1.0.0](https://polyformproject.org/licenses/free-trial/1.0.0)"
  },
  {
    "path": "README.md",
    "content": "[![Build Status](https://cloud.drone.io/api/badges/drone/autoscaler/status.svg)](https://cloud.drone.io/drone/autoscaler)\n\nDrone 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.\n\nDocumentation:<br/>\nhttps://autoscale.drone.io\n\nTechnical Support:<br/>\nhttps://discourse.drone.io\n\nIssue Tracker and Roadmap:<br/>\nhttps://trello.com/b/ttae5E5o/drone\n"
  },
  {
    "path": "cmd/drone-autoscaler/main.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/config\"\n\t\"github.com/drone/autoscaler/drivers/amazon\"\n\t\"github.com/drone/autoscaler/drivers/digitalocean\"\n\t\"github.com/drone/autoscaler/drivers/google\"\n\t\"github.com/drone/autoscaler/drivers/hetznercloud\"\n\t\"github.com/drone/autoscaler/drivers/openstack\"\n\t\"github.com/drone/autoscaler/drivers/packet\"\n\t\"github.com/drone/autoscaler/drivers/scaleway\"\n\t\"github.com/drone/autoscaler/engine\"\n\t\"github.com/drone/autoscaler/logger\"\n\t\"github.com/drone/autoscaler/logger/history\"\n\t\"github.com/drone/autoscaler/logger/request\"\n\t\"github.com/drone/autoscaler/metrics\"\n\t\"github.com/drone/autoscaler/server\"\n\t\"github.com/drone/autoscaler/server/web\"\n\t\"github.com/drone/autoscaler/server/web/static\"\n\t\"github.com/drone/autoscaler/slack\"\n\t\"github.com/drone/autoscaler/store\"\n\t\"github.com/drone/drone-go/drone\"\n\t\"github.com/drone/signal\"\n\n\t\"github.com/99designs/basicauth-go\"\n\t\"github.com/go-chi/chi\"\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/crypto/acme/autocert\"\n\t\"golang.org/x/oauth2\"\n\t\"golang.org/x/sync/errgroup\"\n\n\t_ \"github.com/go-sql-driver/mysql\"\n\t_ \"github.com/joho/godotenv/autoload\"\n\t_ \"github.com/lib/pq\"\n\t_ \"github.com/mattn/go-sqlite3\"\n)\n\nvar (\n\tsource  = \"https://github.com/drone/autoscaler.git\"\n\tversion string\n\tcommit  string\n)\n\nfunc main() {\n\tconf := config.MustLoad()\n\tsetupLogging(conf)\n\n\tprovider, err := setupProvider(conf)\n\tif err != nil {\n\t\tlogrus.WithError(err).\n\t\t\tFatalln(\"Invalid or missing hosting provider\")\n\t}\n\n\t// instruments the provider with prometheus metrics.\n\tprovider = metrics.ServerCreate(provider)\n\tprovider = metrics.ServerDelete(provider)\n\n\tdb, err := store.Connect(\n\t\tconf.Database.Driver,\n\t\tconf.Database.Datasource,\n\t\tconf.Database.MaxIdle,\n\t\tconf.Database.MaxLifetime,\n\t)\n\tif err != nil {\n\t\tlogrus.WithError(err).\n\t\t\tFatalln(\"Cannot establish database connection\")\n\t}\n\n\tmu := store.NewLocker(conf.Database.Driver)\n\tservers := store.NewServerStore(db, mu)\n\n\t// instruments the provider with slack notifications\n\t// instance creation and termination events.\n\tif conf.Slack.Webhook != \"\" {\n\t\tservers = slack.New(conf, servers)\n\t}\n\tservers = metrics.ServerCount(servers)\n\tdefer db.Close()\n\n\tclient := setupClient(conf)\n\n\tenginex := engine.New(\n\t\tclient,\n\t\tconf,\n\t\tservers,\n\t\tprovider,\n\t\tmetrics.New(),\n\t)\n\n\t//\n\t// Setup the router\n\t//\n\n\tr := chi.NewRouter()\n\tr.Use(request.Logger)\n\n\t// middleware to require basic authentication.\n\tauth := basicauth.New(conf.UI.Realm, map[string][]string{\n\t\tconf.UI.Username: {conf.UI.Password},\n\t})\n\n\tr.Route(conf.HTTP.Root, func(root chi.Router) {\n\t\t// handler to serve static assets for the dashboard.\n\t\tfs := http.FileServer(static.New())\n\n\t\troot.Handle(\"/\", http.RedirectHandler(\"/ui\", http.StatusSeeOther))\n\t\troot.Get(\"/metrics\", server.HandleMetrics(conf.Prometheus.AuthToken))\n\t\troot.Get(\"/version\", server.HandleVersion(source, version, commit))\n\t\troot.Get(\"/healthz\", server.HandleHealthz())\n\t\troot.Get(\"/varz\", server.HandleVarz(enginex))\n\t\troot.Handle(\"/static/*\", http.StripPrefix(\"/static/\", fs))\n\n\t\tif conf.UI.Password != \"\" {\n\t\t\t// register the history handler\n\t\t\thistory := history.New()\n\t\t\tlogrus.AddHook(history)\n\n\t\t\troot.Route(\"/ui\", func(ui chi.Router) {\n\t\t\t\tui.Use(auth)\n\t\t\t\tui.Get(\"/\", web.HandleServers(servers))\n\t\t\t\tui.Get(\"/logs\", web.HandleLogging(history))\n\t\t\t})\n\t\t}\n\t\troot.Route(\"/api\", func(api chi.Router) {\n\t\t\tapi.Use(server.CheckDrone(conf))\n\n\t\t\tapi.Post(\"/pause\", server.HandleEnginePause(enginex))\n\t\t\tapi.Post(\"/resume\", server.HandleEngineResume(enginex))\n\t\t\tapi.Get(\"/servers\", server.HandleServerList(servers))\n\t\t\tapi.Post(\"/servers\", server.HandleServerCreate(servers, conf))\n\t\t\tapi.Get(\"/servers/{name}\", server.HandleServerFind(servers))\n\t\t\tapi.Delete(\"/servers/{name}\", server.HandleServerDelete(servers))\n\t\t})\n\t})\n\n\t//\n\t// starts the web server.\n\t//\n\n\tsrv := &http.Server{\n\t\tHandler: r,\n\t}\n\n\tctx := context.Background()\n\tctx = signal.WithContextFunc(ctx, func() {\n\t\tlogrus.Println(\"Program terminating, interrupt received\")\n\t\tsrv.Shutdown(ctx)\n\t})\n\n\tvar g errgroup.Group\n\tg.Go(func() error {\n\t\tif conf.TLS.Autocert {\n\t\t\treturn srv.Serve(\n\t\t\t\tautocert.NewListener(conf.HTTP.Host),\n\t\t\t)\n\t\t} else if conf.TLS.Cert != \"\" {\n\t\t\treturn srv.ListenAndServeTLS(\n\t\t\t\tconf.TLS.Cert,\n\t\t\t\tconf.TLS.Key,\n\t\t\t)\n\t\t}\n\t\tsrv.Addr = conf.HTTP.Port\n\n\t\tlogrus.WithField(\"addr\", conf.HTTP.Port).\n\t\t\tInfoln(\"starting the server\")\n\n\t\treturn srv.ListenAndServe()\n\t})\n\n\t//\n\t// starts the auto-scaler routine.\n\t//\n\n\tg.Go(func() error {\n\t\tenginex.Start(ctx)\n\t\treturn nil\n\t})\n\n\tif err := g.Wait(); err != nil {\n\t\t// terminate with non-zero exit code on error\n\t\tlogrus.WithError(err).Fatalln(\"Program terminated\")\n\t} else {\n\t\tlogrus.Println(\"Program terminated\")\n\t}\n}\n\n// helper funciton configures the logging.\nfunc setupLogging(c config.Config) {\n\tlogger.Default = logger.Logrus(\n\t\tlogrus.NewEntry(\n\t\t\tlogrus.StandardLogger(),\n\t\t),\n\t)\n\tif c.Logs.Debug {\n\t\tlogrus.SetLevel(logrus.DebugLevel)\n\t}\n\tif c.Logs.Trace {\n\t\tlogrus.SetLevel(logrus.TraceLevel)\n\t}\n\tif c.Logs.Pretty == false {\n\t\tlogrus.SetFormatter(&logrus.JSONFormatter{})\n\t}\n}\n\n// helper function configures the drone client.\nfunc setupClient(c config.Config) drone.Client {\n\tconfig := new(oauth2.Config)\n\tauther := config.Client(\n\t\toauth2.NoContext,\n\t\t&oauth2.Token{\n\t\t\tAccessToken: c.Server.Token,\n\t\t},\n\t)\n\turi := new(url.URL)\n\turi.Scheme = c.Server.Proto\n\turi.Host = c.Server.Host\n\treturn drone.NewClient(uri.String(), auther)\n}\n\n// helper function configures the hosting provider.\nfunc setupProvider(c config.Config) (autoscaler.Provider, error) {\n\tswitch {\n\tcase c.Google.Project != \"\":\n\t\treturn google.New(\n\t\t\tgoogle.WithDiskSize(c.Google.DiskSize),\n\t\t\tgoogle.WithDiskType(c.Google.DiskType),\n\t\t\tgoogle.WithMachineImage(c.Google.MachineImage),\n\t\t\tgoogle.WithMachineType(c.Google.MachineType),\n\t\t\tgoogle.WithLabels(c.Google.Labels),\n\t\t\tgoogle.WithNetwork(c.Google.Network),\n\t\t\tgoogle.WithSubnetwork(c.Google.Subnetwork),\n\t\t\tgoogle.WithStackType(c.Google.StackType),\n\t\t\tgoogle.WithPrivateIP(c.Google.PrivateIP),\n\t\t\tgoogle.WithServiceAccountEmail(c.Google.ServiceAccountEmail),\n\t\t\tgoogle.WithProject(c.Google.Project),\n\t\t\tgoogle.WithTags(c.Google.Tags...),\n\t\t\tgoogle.WithScopes(c.Google.Scopes...),\n\t\t\tgoogle.WithUserData(c.Google.UserData),\n\t\t\tgoogle.WithUserDataFile(c.Google.UserDataFile),\n\t\t\tgoogle.WithZones(c.Google.Zone...),\n\t\t\tgoogle.WithUserDataKey(c.Google.UserDataKey),\n\t\t\tgoogle.WithRateLimit(c.Google.RateLimit),\n\t\t)\n\tcase c.DigitalOcean.Token != \"\":\n\t\treturn digitalocean.New(\n\t\t\tdigitalocean.WithSSHKey(c.DigitalOcean.SSHKey),\n\t\t\tdigitalocean.WithImage(c.DigitalOcean.Image),\n\t\t\tdigitalocean.WithRegion(c.DigitalOcean.Region),\n\t\t\tdigitalocean.WithSize(c.DigitalOcean.Size),\n\t\t\tdigitalocean.WithFirewall(c.DigitalOcean.Firewall),\n\t\t\tdigitalocean.WithUserDataFile(c.DigitalOcean.UserDataFile),\n\t\t\tdigitalocean.WithUserData(c.DigitalOcean.UserData),\n\t\t\tdigitalocean.WithToken(c.DigitalOcean.Token),\n\t\t\tdigitalocean.WithPrivateIP(c.DigitalOcean.PrivateIP),\n\t\t\tdigitalocean.WithTags(c.DigitalOcean.Tags...),\n\t\t), nil\n\tcase c.Scaleway.AccessKey != \"\":\n\t\treturn scaleway.New(\n\t\t\tscaleway.WithAccessKey(c.Scaleway.AccessKey),\n\t\t\tscaleway.WithSecretKey(c.Scaleway.SecretKey),\n\t\t\tscaleway.WithOrganisationID(c.Scaleway.OrganisationID),\n\t\t\tscaleway.WithZone(c.Scaleway.Zone),\n\t\t\tscaleway.WithSize(c.Scaleway.Size),\n\t\t\tscaleway.WithImage(c.Scaleway.Image),\n\t\t\tscaleway.WithDynamicIP(c.Scaleway.DynamicIP),\n\t\t\tscaleway.WithTags(c.Scaleway.Tags...),\n\t\t\tscaleway.WithUserData(c.Scaleway.UserData),\n\t\t\tscaleway.WithUserDataFile(c.Scaleway.UserDataFile),\n\t\t)\n\tcase c.HetznerCloud.Token != \"\":\n\t\treturn hetznercloud.New(\n\t\t\thetznercloud.WithDatacenter(c.HetznerCloud.Datacenter),\n\t\t\thetznercloud.WithImage(c.HetznerCloud.Image),\n\t\t\thetznercloud.WithUserDataFile(c.HetznerCloud.UserDataFile),\n\t\t\thetznercloud.WithUserData(c.HetznerCloud.UserData),\n\t\t\thetznercloud.WithServerType(c.HetznerCloud.Type),\n\t\t\thetznercloud.WithSSHKey(c.HetznerCloud.SSHKey),\n\t\t\thetznercloud.WithToken(c.HetznerCloud.Token),\n\t\t), nil\n\tcase c.Packet.APIKey != \"\":\n\t\treturn packet.New(\n\t\t\tpacket.WithAPIKey(c.Packet.APIKey),\n\t\t\tpacket.WithFacility(c.Packet.Facility),\n\t\t\tpacket.WithProject(c.Packet.ProjectID),\n\t\t\tpacket.WithPlan(c.Packet.Plan),\n\t\t\tpacket.WithOS(c.Packet.OS),\n\t\t\tpacket.WithSSHKey(c.Packet.SSHKey),\n\t\t\tpacket.WithUserData(c.Packet.UserData),\n\t\t\tpacket.WithUserDataFile(c.Packet.UserDataFile),\n\t\t\tpacket.WithHostname(c.Packet.Hostname),\n\t\t\tpacket.WithTags(c.Packet.Tags...),\n\t\t), nil\n\tcase os.Getenv(\"AWS_ACCESS_KEY_ID\") != \"\" || os.Getenv(\"AWS_IAM\") != \"\":\n\t\treturn amazon.New(\n\t\t\tamazon.WithDeviceName(c.Amazon.DeviceName),\n\t\t\tamazon.WithImage(c.Amazon.Image),\n\t\t\tamazon.WithRegion(c.Amazon.Region),\n\t\t\tamazon.WithRetries(c.Amazon.Retries),\n\t\t\tamazon.WithPrivateIP(c.Amazon.PrivateIP),\n\t\t\tamazon.WithSSHKey(c.Amazon.SSHKey),\n\t\t\tamazon.WithSecurityGroup(c.Amazon.SecurityGroup...),\n\t\t\tamazon.WithSize(c.Amazon.Instance),\n\t\t\tamazon.WithSizeAlt(c.Amazon.InstanceAlt),\n\t\t\tamazon.WithSubnets(append([]string{c.Amazon.SubnetID}, c.Amazon.SubnetIDsAlt...)),\n\t\t\tamazon.WithTags(c.Amazon.Tags),\n\t\t\tamazon.WithUserData(c.Amazon.UserData),\n\t\t\tamazon.WithUserDataFile(c.Amazon.UserDataFile),\n\t\t\tamazon.WithVolumeSize(c.Amazon.VolumeSize),\n\t\t\tamazon.WithVolumeType(c.Amazon.VolumeType),\n\t\t\tamazon.WithVolumeIops(c.Amazon.VolumeIops),\n\t\t\tamazon.WithVolumeThroughput(c.Amazon.VolumeThroughput),\n\t\t\tamazon.WithIamProfileArn(c.Amazon.IamProfileArn),\n\t\t\tamazon.WithMarketType(c.Amazon.MarketType),\n\t\t\tamazon.WithInstanceMetadataTokens(c.Amazon.IMDSTokens),\n\t\t), nil\n\tcase os.Getenv(\"OS_USERNAME\") != \"\":\n\t\treturn openstack.New(\n\t\t\topenstack.WithImage(c.OpenStack.Image),\n\t\t\topenstack.WithRegion(c.OpenStack.Region),\n\t\t\topenstack.WithFlavor(c.OpenStack.Flavor),\n\t\t\topenstack.WithNetwork(c.OpenStack.Network),\n\t\t\topenstack.WithFloatingIpPool(c.OpenStack.Pool),\n\t\t\topenstack.WithSSHKey(c.OpenStack.SSHKey),\n\t\t\topenstack.WithSecurityGroup(c.OpenStack.SecurityGroup...),\n\t\t\topenstack.WithMetadata(c.OpenStack.Metadata),\n\t\t\topenstack.WithUserData(c.OpenStack.UserData),\n\t\t\topenstack.WithUserDataFile(c.OpenStack.UserDataFile),\n\t\t)\n\tdefault:\n\t\treturn nil, errors.New(\"missing provider configuration\")\n\t}\n}\n"
  },
  {
    "path": "config/config.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage config\n\nimport \"time\"\n\n// TODO change DRONE_HTTP_PORT to DRONE_HTTP_BIND\n\ntype (\n\t// Config stores the configuration settings.\n\tConfig struct {\n\t\tInterval       time.Duration `default:\"5m\"`\n\t\tCapacityBuffer int           `default:\"0\" split_words:\"true\"`\n\n\t\tTimeout struct {\n\t\t\tStop time.Duration `envconfig:\"DRONE_TIMEOUT_STOP\" default:\"1h\"`\n\t\t}\n\n\t\tSlack struct {\n\t\t\tWebhook string\n\t\t\tCreate  bool `default:\"true\"`\n\t\t\tDestroy bool `default:\"true\"`\n\t\t\tError   bool `default:\"true\"`\n\t\t}\n\n\t\tLogs struct {\n\t\t\tDebug  bool `default:\"true\"`\n\t\t\tTrace  bool\n\t\t\tPretty bool\n\t\t}\n\n\t\tPool struct {\n\t\t\tMin    int           `default:\"2\"`\n\t\t\tMax    int           `default:\"4\"`\n\t\t\tMinAge time.Duration `default:\"55m\" split_words:\"true\"`\n\t\t}\n\n\t\tCheck struct {\n\t\t\tInterval time.Duration `envconfig:\"DRONE_INSTALL_CHECK_INTERVAL\" default:\"1m\"`\n\t\t\tDeadline time.Duration `envconfig:\"DRONE_INSTALL_CHECK_DEADLINE\" default:\"30m\"`\n\t\t}\n\n\t\tServer struct {\n\t\t\tHost  string\n\t\t\tProto string\n\t\t\tToken string\n\t\t}\n\n\t\tAgent struct {\n\t\t\tToken       string\n\t\t\tImage       string `default:\"drone/drone-runner-docker:1\"`\n\t\t\tConcurrency int    `default:\"2\"`\n\t\t\tOS          string `default:\"linux\"`\n\t\t\tArch        string `default:\"amd64\"`\n\t\t\tVersion     string\n\t\t\tKernel      string\n\t\t\tEnvironFile string `envconfig:\"DRONE_AGENT_ENV_FILE\"`\n\t\t\tEnviron     []string\n\t\t\tVolumes     []string\n\t\t\tPorts       []string          `envconfig:\"DRONE_AGENT_PUBLISHED_PORTS\"`\n\t\t\tLabels      map[string]string `envconfig:\"DRONE_AGENT_LABELS\"`\n\t\t\tNamePrefix  string            `envconfig:\"DRONE_AGENT_NAME_PREFIX\" default:\"agent-\"`\n\t\t}\n\n\t\tRunner Runner\n\n\t\tGC struct {\n\t\t\tEnabled  bool          `envconfig:\"DRONE_GC_ENABLED\"`\n\t\t\tImage    string        `envconfig:\"DRONE_GC_IMAGE\" default:\"drone/gc\"`\n\t\t\tDebug    bool          `envconfig:\"DRONE_GC_DEBUG\"`\n\t\t\tImages   []string      `envconfig:\"DRONE_GC_IGNORE_IMAGES\"`\n\t\t\tInterval time.Duration `envconfig:\"DRONE_GC_INTERVAL\" default:\"30m\"`\n\t\t\tCache    string        `envconfig:\"DRONE_GC_CACHE\" default:\"10gb\"`\n\t\t}\n\n\t\tReaper struct {\n\t\t\tEnabled  bool          `envconfig:\"DRONE_REAPER_ENABLED\", default:\"false\"`\n\t\t\tInterval time.Duration `envconfig:\"DRONE_REAPER_INTERVAL\" default:\"1h\"`\n\t\t}\n\n\t\tPinger struct {\n\t\t\tEnabled  bool          `envconfig:\"DRONE_PINGER_ENABLED\", default:\"false\"`\n\t\t\tInterval time.Duration `envconfig:\"DRONE_PINGER_INTERVAL\" default:\"10m\"`\n\t\t}\n\n\t\tWatchtower struct {\n\t\t\tEnabled       bool          `envconfig:\"DRONE_WATCHTOWER_ENABLED\"`\n\t\t\tSignalEnabled bool          `envconfig:\"DRONE_WATCHTOWER_SIGNAL_ENABLED\" default:\"true\"`\n\t\t\tSignal        string        `envconfig:\"DRONE_WATCHTOWER_STOP_SIGNAL\" default:\"SIGHUP\"`\n\t\t\tImage         string        `envconfig:\"DRONE_WATCHTOWER_IMAGE\" default:\"webhippie/watchtower\"`\n\t\t\tInterval      int           `envconfig:\"DRONE_WATCHTOWER_INTERVAL\" default:\"300\"`\n\t\t\tTimeout       time.Duration `envconfig:\"DRONE_WATCHTOWER_TIMEOUT\" default:\"120m\"`\n\t\t}\n\n\t\tHTTP struct {\n\t\t\tProto string `envconfig:\"DRONE_HTTP_PROTO\" default:\"http\"`\n\t\t\tHost  string `envconfig:\"DRONE_HTTP_HOST\"`\n\t\t\tPort  string `envconfig:\"DRONE_HTTP_PORT\" default:\":8080\"`\n\t\t\tRoot  string `envconfig:\"DRONE_HTTP_ROOT\" default:\"/\"`\n\t\t}\n\n\t\tUI struct {\n\t\t\tUsername string `envconfig:\"DRONE_UI_USERNAME\"`\n\t\t\tPassword string `envconfig:\"DRONE_UI_PASSWORD\"`\n\t\t\tRealm    string `envconfig:\"DRONE_UI_REALM\" default:\"Autoscaler\"`\n\t\t}\n\n\t\tTLS struct {\n\t\t\tAutocert bool\n\t\t\tCert     string\n\t\t\tKey      string\n\t\t}\n\n\t\tPrometheus struct {\n\t\t\tAuthToken string `split_words:\"true\"`\n\t\t}\n\n\t\tDatabase struct {\n\t\t\tDriver      string        `default:\"sqlite3\"`\n\t\t\tDatasource  string        `default:\"database.sqlite?cache=shared&mode=rwc&_busy_timeout=9999999\"`\n\t\t\tMaxIdle     int           `envconfig:\"DRONE_DATABASE_MAX_IDLE\" default:\"0\"`\n\t\t\tMaxLifetime time.Duration `envconfig:\"DRONE_DATABASE_MAX_LIFETIME\"`\n\t\t}\n\n\t\tAmazon struct {\n\t\t\tDeviceName       string `envconfig:\"DRONE_AMAZON_DEVICE_NAME\"`\n\t\t\tImage            string `envconfig:\"DRONE_AMAZON_IMAGE\"`\n\t\t\tInstance         string `envconfig:\"DRONE_AMAZON_INSTANCE\"`\n\t\t\tInstanceAlt      string `envconfig:\"DRONE_AMAZON_INSTANCE_ALT\"`\n\t\t\tPrivateIP        bool   `split_words:\"true\"`\n\t\t\tRegion           string\n\t\t\tRetries          int\n\t\t\tSSHKey           string\n\t\t\tSubnetID         string   `split_words:\"true\"`\n\t\t\tSubnetIDsAlt     []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\n\t\t\tSecurityGroup    []string `split_words:\"true\"`\n\t\t\tTags             map[string]string\n\t\t\tUserData         string `envconfig:\"DRONE_AMAZON_USERDATA\"`\n\t\t\tUserDataFile     string `envconfig:\"DRONE_AMAZON_USERDATA_FILE\"`\n\t\t\tVolumeSize       int64  `envconfig:\"DRONE_AMAZON_VOLUME_SIZE\"`\n\t\t\tVolumeType       string `envconfig:\"DRONE_AMAZON_VOLUME_TYPE\"`\n\t\t\tVolumeIops       int64  `envconfig:\"DRONE_AMAZON_VOLUME_IOPS\"`\n\t\t\tVolumeThroughput int64  `envconfig:\"DRONE_AMAZON_VOLUME_THROUGHPUT\"`\n\t\t\tIamProfileArn    string `envconfig:\"DRONE_AMAZON_IAM_PROFILE_ARN\"`\n\t\t\tMarketType       string `envconfig:\"DRONE_AMAZON_MARKET_TYPE\"`\n\t\t\tIMDSTokens       string `envconfig:\"DRONE_AMAZON_IMDS_TOKENS\"`\n\t\t}\n\n\t\tDigitalOcean struct {\n\t\t\tToken        string\n\t\t\tImage        string\n\t\t\tRegion       string\n\t\t\tSSHKey       string\n\t\t\tSize         string\n\t\t\tFirewall     string\n\t\t\tTags         []string\n\t\t\tPrivateIP    bool   `split_words:\"true\"`\n\t\t\tUserData     string `envconfig:\"DRONE_DIGITALOCEAN_USERDATA\"`\n\t\t\tUserDataFile string `envconfig:\"DRONE_DIGITALOCEAN_USERDATA_FILE\"`\n\t\t}\n\n\t\tGoogle struct {\n\t\t\tMachineType         string            `envconfig:\"DRONE_GOOGLE_MACHINE_TYPE\"`\n\t\t\tMachineImage        string            `envconfig:\"DRONE_GOOGLE_MACHINE_IMAGE\"`\n\t\t\tNetwork             string            `envconfig:\"DRONE_GOOGLE_NETWORK\"`\n\t\t\tSubnetwork          string            `envconfig:\"DRONE_GOOGLE_SUBNETWORK\"`\n\t\t\tStackType           string            `envconfig:\"DRONE_GOOGLE_STACK_TYPE\"`\n\t\t\tLabels              map[string]string `envconfig:\"DRONE_GOOGLE_LABELS\"`\n\t\t\tScopes              []string          `envconfig:\"DRONE_GOOGLE_SCOPES\"`\n\t\t\tServiceAccountEmail string            `envconfig:\"DRONE_GOOGLE_SERVICE_ACCOUNT_EMAIL\"`\n\t\t\tDiskSize            int64             `envconfig:\"DRONE_GOOGLE_DISK_SIZE\"`\n\t\t\tDiskType            string            `envconfig:\"DRONE_GOOGLE_DISK_TYPE\"`\n\t\t\tProject             string            `envconfig:\"DRONE_GOOGLE_PROJECT\"`\n\t\t\tPrivateIP           bool              `split_words:\"true\"`\n\t\t\tTags                []string          `envconfig:\"DRONE_GOOGLE_TAGS\"`\n\t\t\tUserData            string            `envconfig:\"DRONE_GOOGLE_USERDATA\"`\n\t\t\tUserDataFile        string            `envconfig:\"DRONE_GOOGLE_USERDATA_FILE\"`\n\t\t\tZone                []string          `envconfig:\"DRONE_GOOGLE_ZONE\"`\n\t\t\tUserDataKey         string            `envconfig:\"DRONE_GOOGLE_USERDATA_KEY\" default:\"user-data\"`\n\t\t\tRateLimit           int               `envconfig:\"DRONE_GOOGLE_READ_RATELIMIT\" default:\"25\"`\n\t\t}\n\n\t\tHetznerCloud struct {\n\t\t\tDatacenter   string\n\t\t\tImage        string\n\t\t\tSSHKey       int\n\t\t\tToken        string\n\t\t\tType         string\n\t\t\tUserData     string `envconfig:\"DRONE_HETZNERCLOUD_USERDATA\"`\n\t\t\tUserDataFile string `envconfig:\"DRONE_HETZNERCLOUD_USERDATA_FILE\"`\n\t\t}\n\n\t\tPacket struct {\n\t\t\tAPIKey       string\n\t\t\tFacility     string\n\t\t\tPlan         string\n\t\t\tOS           string\n\t\t\tProjectID    string `split_words:\"true\"`\n\t\t\tTags         []string\n\t\t\tSSHKey       string\n\t\t\tUserData     string `envconfig:\"DRONE_PACKET_USERDATA\"`\n\t\t\tUserDataFile string `envconfig:\"DRONE_PACKET_USERDATA_FILE\"`\n\t\t\tHostname     string\n\t\t}\n\n\t\tOpenStack struct {\n\t\t\tRegion        string `envconfig:\"OS_REGION_NAME\"`\n\t\t\tImage         string\n\t\t\tFlavor        string\n\t\t\tNetwork       string\n\t\t\tPool          string   `envconfig:\"DRONE_OPENSTACK_IP_POOL\"`\n\t\t\tSecurityGroup []string `split_words:\"true\"`\n\t\t\tSSHKey        string\n\t\t\tMetadata      map[string]string\n\t\t\tUserData      string `envconfig:\"DRONE_OPENSTACK_USERDATA\"`\n\t\t\tUserDataFile  string `envconfig:\"DRONE_OPENSTACK_USERDATA_FILE\"`\n\t\t}\n\n\t\tScaleway struct {\n\t\t\tAccessKey      string `split_words:\"true\"`\n\t\t\tSecretKey      string `split_words:\"true\"`\n\t\t\tOrganisationID string `split_words:\"true\"`\n\t\t\tZone           string\n\t\t\tSize           string\n\t\t\tImage          string\n\t\t\tDynamicIP      bool `split_words:\"true\"`\n\t\t\tTags           []string\n\t\t\tUserData       string `envconfig:\"DRONE_SCALEWAY_USERDATA\"`\n\t\t\tUserDataFile   string `envconfig:\"DRONE_SCALEWAY_USERDATA_FILE\"`\n\t\t}\n\t}\n\n\tRunner struct {\n\t\tVolumes    string\n\t\tDevices    string\n\t\tPrivileged string\n\t\tEnvFile    string\n\t}\n)\n"
  },
  {
    "path": "config/load.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage config\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/drone/envconfig\"\n\t\"github.com/joho/godotenv\"\n)\n\n// legacy environment variables. the key is the legacy\n// variable name, and the value is the new variable name.\nvar legacy = map[string]string{\n\t\"DRONE_ENABLE_PINGER\": \"DRONE_PINGER_ENABLED\",\n\t\"DRONE_ENABLE_REAPER\": \"DRONE_REAPER_ENABLED\",\n}\n\nfunc init() {\n\t// loop through legacy environment variable and, if set\n\t// rewrite to the new variable name.\n\tfor k, v := range legacy {\n\t\tif s, ok := os.LookupEnv(k); ok {\n\t\t\tos.Setenv(v, s)\n\t\t}\n\t}\n}\n\n// Load loads the configuration from the environment.\nfunc Load() (Config, error) {\n\tconfig := Config{}\n\tif err := envconfig.Process(\"DRONE\", &config); err != nil {\n\t\treturn config, err\n\t}\n\tif path := config.Agent.EnvironFile; path != \"\" {\n\t\tenvs, _ := godotenv.Read(path)\n\t\tfor k, v := range envs {\n\t\t\tconfig.Agent.Environ = append(\n\t\t\t\tconfig.Agent.Environ,\n\t\t\t\tfmt.Sprintf(\"%s=%s\", k, v),\n\t\t\t)\n\t\t}\n\t}\n\t// If environment variables don't contain `=`, we consider that it's an environment name, we fetch and expose the value\n\tfor i, env := range config.Agent.Environ {\n\t\tif !strings.Contains(env, \"=\") {\n\t\t\tconfig.Agent.Environ[i] = fmt.Sprintf(\"%s=%s\", env, os.Getenv(env))\n\t\t}\n\t}\n\tgodotenv.Load()\n\treturn config, nil\n}\n\n// MustLoad loads the configuration from the environmnet\n// and panics if an error is encountered.\nfunc MustLoad() Config {\n\tconfig, err := Load()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn config\n}\n"
  },
  {
    "path": "config/load_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage config\n\nimport (\n\t\"encoding/json\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/kr/pretty\"\n)\n\nfunc TestDefaults(t *testing.T) {\n\tconf := MustLoad()\n\tif got, want := conf.Logs.Debug, true; got != want {\n\t\tt.Errorf(\"Want default DRONE_LOGS_DEBUG of %v, got %v\", want, got)\n\t}\n\tif got, want := conf.Interval, time.Minute*5; got != want {\n\t\tt.Errorf(\"Want default DRONE_INTERVAL of %s, got %s\", want, got)\n\t}\n\tif got, want := conf.CapacityBuffer, 0; got != want {\n\t\tt.Errorf(\"Want default DRONE_CAPACITY_BUFFER of %d, got %d\", want, got)\n\t}\n\tif got, want := conf.Pool.Max, 4; got != want {\n\t\tt.Errorf(\"Want default DRONE_POOL_MIN of %d, got %d\", want, got)\n\t}\n\tif got, want := conf.Pool.Min, 2; got != want {\n\t\tt.Errorf(\"Want default DRONE_POOL_MAX of %d, got %d\", want, got)\n\t}\n\tif got, want := conf.Pool.MinAge, time.Minute*55; got != want {\n\t\tt.Errorf(\"Want default DRONE_POOL_MIN_AGE of %d, got %d\", want, got)\n\t}\n\n\tif got, want := conf.Check.Interval, time.Minute; got != want {\n\t\tt.Errorf(\"Want default DRONE_INSTALL_CHECK_INTERVAL of %s, got %s\", want, got)\n\t}\n\tif got, want := conf.Check.Deadline, time.Minute*30; got != want {\n\t\tt.Errorf(\"Want default DRONE_INSTALL_CHECK_DEADLINE of %s, got %s\", want, got)\n\t}\n\n\tif got, want := conf.HTTP.Port, \":8080\"; got != want {\n\t\tt.Errorf(\"Want default DRONE_HTTP_PORT of %s, got %s\", want, got)\n\t}\n\tif got, want := conf.HTTP.Root, \"/\"; got != want {\n\t\tt.Errorf(\"Want default DRONE_HTTP_ROOT of %s, got %s\", want, got)\n\t}\n\tif got, want := conf.Database.Driver, \"sqlite3\"; got != want {\n\t\tt.Errorf(\"Want default DRONE_DATABASE_DRIVER of %s, got %s\", want, got)\n\t}\n\tif got, want := conf.Database.Datasource, \"database.sqlite?cache=shared&mode=rwc&_busy_timeout=9999999\"; got != want {\n\t\tt.Errorf(\"Want default DRONE_DATABASE_DATASOURCE of %s, got %s\", want, got)\n\t}\n\tif got, want := conf.Agent.Concurrency, 2; got != want {\n\t\tt.Errorf(\"Want default DRONE_AGENT_CONCURRENCY of %d, got %d\", want, got)\n\t}\n\tif got, want := conf.Agent.Image, \"drone/drone-runner-docker:1\"; got != want {\n\t\tt.Errorf(\"Want default DRONE_AGENT_IMAGE of %s, got %s\", want, got)\n\t}\n}\n\nfunc TestLoad(t *testing.T) {\n\tenviron := map[string]string{\n\t\t\"DRONE_INTERVAL\":                   \"1m\",\n\t\t\"DRONE_SLACK_WEBHOOK\":              \"https://hooks.slack.com/services/XXX/YYY/ZZZ\",\n\t\t\"DRONE_SLACK_CREATE\":               \"false\",\n\t\t\"DRONE_SLACK_DESTROY\":              \"false\",\n\t\t\"DRONE_LOGS_DEBUG\":                 \"true\",\n\t\t\"DRONE_LOGS_COLOR\":                 \"true\",\n\t\t\"DRONE_LOGS_PRETTY\":                \"true\",\n\t\t\"DRONE_CAPACITY_BUFFER\":            \"3\",\n\t\t\"DRONE_POOL_MIN_AGE\":               \"1h\",\n\t\t\"DRONE_POOL_MIN\":                   \"1\",\n\t\t\"DRONE_POOL_MAX\":                   \"5\",\n\t\t\"DRONE_SERVER_HOST\":                \"drone.company.com\",\n\t\t\"DRONE_SERVER_PROTO\":               \"http\",\n\t\t\"DRONE_SERVER_TOKEN\":               \"633eb230f5\",\n\t\t\"DRONE_HTTP_HOST\":                  \"autoscaler.drone.company.com\",\n\t\t\"DRONE_HTTP_PORT\":                  \"633eb230f5\",\n\t\t\"DRONE_HTTP_ROOT\":                  \"/autoscaler\",\n\t\t\"DRONE_AGENT_TOKEN\":                \"f5064039f5\",\n\t\t\"DRONE_AGENT_IMAGE\":                \"drone/drone-runner-docker:latest\",\n\t\t\"DRONE_AGENT_CONCURRENCY\":          \"2\",\n\t\t\"DRONE_AGENT_ARCH\":                 \"arm64\",\n\t\t\"DRONE_TLS_AUTOCERT\":               \"true\",\n\t\t\"DRONE_TLS_CERT\":                   \"/path/to/cert.crt\",\n\t\t\"DRONE_TLS_KEY\":                    \"/path/to/cert.key\",\n\t\t\"DRONE_PROMETHEUS_AUTH_TOKEN\":      \"b359e05e8\",\n\t\t\"DRONE_DATABASE_DRIVER\":            \"mysql\",\n\t\t\"DRONE_DATABASE_DATASOURCE\":        \"user:password@/dbname\",\n\t\t\"DRONE_DIGITALOCEAN_TOKEN\":         \"2573633eb\",\n\t\t\"DRONE_DIGITALOCEAN_IMAGE\":         \"docker-18-04\",\n\t\t\"DRONE_DIGITALOCEAN_REGION\":        \"ncy1\",\n\t\t\"DRONE_DIGITALOCEAN_SSHKEY\":        \"/path/to/ssh/key\",\n\t\t\"DRONE_DIGITALOCEAN_SIZE\":          \"s-1vcpu-1gb\",\n\t\t\"DRONE_DIGITALOCEAN_IPV6\":          \"true\",\n\t\t\"DRONE_DIGITALOCEAN_PRIVATE_IP\":    \"false\",\n\t\t\"DRONE_DIGITALOCEAN_FIREWALL\":      \"\",\n\t\t\"DRONE_DIGITALOCEAN_TAGS\":          \"drone,agent,prod\",\n\t\t\"DRONE_DIGITALOCEAN_USERDATA\":      \"#cloud-init\",\n\t\t\"DRONE_DIGITALOCEAN_USERDATA_FILE\": \"/path/to/cloud/init.yml\",\n\t\t\"DRONE_GOOGLE_ZONE\":                \"us-central1-b,us-central1-a\",\n\t\t\"DRONE_GOOGLE_MACHINE_TYPE\":        \"f1-micro\",\n\t\t\"DRONE_GOOGLE_MACHINE_IMAGE\":       \"ubuntu-1510-wily-v20151114\",\n\t\t\"DRONE_GOOGLE_DISK_TYPE\":           \"pd-standard\",\n\t\t\"DRONE_GOOGLE_NETWORK\":             \"default\",\n\t\t\"DRONE_GOOGLE_SUBNETWORK\":          \"\",\n\t\t\"DRONE_GOOGLE_PRIVATE_IP\":          \"false\",\n\t\t\"DRONE_GOOGLE_PREEMPTIBLE\":         \"true\",\n\t\t\"DRONE_GOOGLE_SCOPES\":              \"devstorage.read_only,pubsub\",\n\t\t\"DRONE_GOOGLE_DISK_SIZE\":           \"10\",\n\t\t\"DRONE_GOOGLE_PROJECT\":             \"project-foo\",\n\t\t\"DRONE_GOOGLE_TAGS\":                \"drone,agent,prod\",\n\t\t\"DRONE_GOOGLE_USERDATA\":            \"#cloud-init\",\n\t\t\"DRONE_GOOGLE_USERDATA_FILE\":       \"/path/to/cloud/init.yml\",\n\t\t\"DRONE_GOOGLE_READ_RATELIMIT\":      \"20\",\n\t\t\"DRONE_AMAZON_IMAGE\":               \"ami-07f84a50d2dec2fa4\",\n\t\t\"DRONE_AMAZON_INSTANCE\":            \"t3.medium\",\n\t\t\"DRONE_AMAZON_PRIVATE_IP\":          \"true\",\n\t\t\"DRONE_AMAZON_RETRIES\":             \"1\",\n\t\t\"DRONE_AMAZON_REGION\":              \"us-east-2\",\n\t\t\"DRONE_AMAZON_SSHKEY\":              \"id_rsa\",\n\t\t\"DRONE_AMAZON_SUBNET_ID\":           \"subnet-0b32177f\",\n\t\t\"DRONE_AMAZON_SUBNET_IDS_ALT\":      \"subnet-abcd,subnet-efgh\",\n\t\t\"DRONE_AMAZON_SECURITY_GROUP\":      \"sg-770eabe1\",\n\t\t\"DRONE_AMAZON_TAGS\":                \"os:linux,arch:amd64\",\n\t\t\"DRONE_AMAZON_USERDATA\":            \"#cloud-init\",\n\t\t\"DRONE_AMAZON_USERDATA_FILE\":       \"/path/to/cloud/init.yml\",\n\t\t\"DRONE_HETZNERCLOUD_TOKEN\":         \"12345678\",\n\t\t\"DRONE_HETZNERCLOUD_IMAGE\":         \"ubuntu-16.04\",\n\t\t\"DRONE_HETZNERCLOUD_DATACENTER\":    \"nbg1-dc3\",\n\t\t\"DRONE_HETZNERCLOUD_SSHKEY\":        \"12345\",\n\t\t\"DRONE_HETZNERCLOUD_TYPE\":          \"cx11\",\n\t\t\"DRONE_HETZNERCLOUD_USERDATA\":      \"#cloud-init\",\n\t\t\"DRONE_HETZNERCLOUD_USERDATA_FILE\": \"/path/to/cloud/init.yml\",\n\t\t\"DRONE_PACKET_APIKEY\":              \"12345678\",\n\t\t\"DRONE_PACKET_FACILITY\":            \"facility\",\n\t\t\"DRONE_PACKET_PROJECT_ID\":          \"project\",\n\t\t\"DRONE_PACKET_PLAN\":                \"plan\",\n\t\t\"DRONE_PACKET_OS\":                  \"ubuntu\",\n\t\t\"DRONE_PACKET_SSHKEY\":              \"id_rsa\",\n\t\t\"DRONE_PACKET_USERDATA\":            \"#cloud-init\",\n\t\t\"DRONE_PACKET_USERDATA_FILE\":       \"/path/to/cloud/init.yml\",\n\t\t\"DRONE_PACKET_HOSTNAME\":            \"agent\",\n\t\t\"DRONE_PACKET_TAGS\":                \"drone,agent,prod\",\n\t\t\"DRONE_OPENSTACK_NETWORK\":          \"my-subnet-1\",\n\t\t\"DRONE_OPENSTACK_IP_POOL\":          \"ext-ips-1\",\n\t\t\"DRONE_OPENSTACK_SSHKEY\":           \"drone-ci\",\n\t\t\"DRONE_OPENSTACK_SECURITY_GROUP\":   \"secgrp-feedface\",\n\t\t\"DRONE_OPENSTACK_FLAVOR\":           \"t1.medium\",\n\t\t\"DRONE_OPENSTACK_IMAGE\":            \"ubuntu-16.04-server-latest\",\n\t\t\"DRONE_OPENSTACK_METADATA\":         \"name:agent,owner:drone-ci\",\n\t\t\"DRONE_OPENSTACK_USERDATA\":         \"#cloud-init\",\n\t\t\"DRONE_OPENSTACK_USERDATA_FILE\":    \"/path/to/cloud/init.yml\",\n\t\t\"OS_REGION_NAME\":                   \"sto-01\",\n\t\t\"DRONE_WATCHTOWER_SIGNAL_ENABLED\":  \"false\",\n\t\t\"DRONE_WATCHTOWER_STOP_SIGNAL\":     \"\",\n\t}\n\n\tdefer func() {\n\t\t// reset the environment.\n\t\tfor k := range environ {\n\t\t\tos.Unsetenv(k)\n\t\t}\n\t}()\n\n\t// set test environment variables\n\tfor k, v := range environ {\n\t\tos.Setenv(k, v)\n\t}\n\n\ta := MustLoad()\n\tb := Config{}\n\terr := json.Unmarshal(jsonConfig, &b)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\n\tif !reflect.DeepEqual(a, b) {\n\t\tt.Errorf(\"configuration mismatch\")\n\t\tpretty.Ldiff(t, a, b)\n\t}\n}\n\nvar jsonConfig = []byte(`{\n  \"Interval\": 60000000000,\n  \"CapacityBuffer\": 3,\n  \"Timeout\": {\n    \"Stop\": 3600000000000\n  },\n  \"Slack\": {\n    \"Webhook\": \"https://hooks.slack.com/services/XXX/YYY/ZZZ\",\n    \"Create\": false,\n    \"Destroy\": false,\n    \"Error\": true\n  },\n  \"Logs\": {\n    \"Color\": true,\n    \"Debug\": true,\n    \"Pretty\": true\n  },\n  \"Pool\": {\n    \"Min\": 1,\n    \"Max\": 5,\n    \"MinAge\": 3600000000000\n  },\n  \"Server\": {\n    \"Host\": \"drone.company.com\",\n    \"Proto\": \"http\",\n    \"Token\": \"633eb230f5\"\n  },\n  \"Agent\": {\n    \"OS\": \"linux\",\n    \"Arch\": \"arm64\",\n    \"Token\": \"f5064039f5\",\n    \"Image\": \"drone/drone-runner-docker:latest\",\n    \"Concurrency\": 2,\n    \"KeepaliveTime\": 360000000000,\n    \"KeepaliveTimeout\": 30000000000,\n    \"NamePrefix\": \"agent-\"\n  },\n  \"HTTP\": {\n    \"Proto\": \"http\",\n    \"Host\": \"autoscaler.drone.company.com\",\n    \"Port\": \"633eb230f5\",\n    \"Root\": \"/autoscaler\"\n  },\n  \"UI\": {\n    \"Realm\": \"Autoscaler\"\n  },\n  \"TLS\": {\n    \"Autocert\": true,\n    \"Cert\": \"/path/to/cert.crt\",\n    \"Key\": \"/path/to/cert.key\"\n  },\n  \"Prometheus\": {\n    \"AuthToken\": \"b359e05e8\"\n  },\n  \"Database\": {\n    \"Driver\": \"mysql\",\n    \"Datasource\": \"user:password@/dbname\"\n  },\n  \"DigitalOcean\": {\n    \"Token\": \"2573633eb\",\n    \"Image\": \"docker-18-04\",\n    \"Region\": \"ncy1\",\n    \"SSHKey\": \"/path/to/ssh/key\",\n    \"Size\": \"s-1vcpu-1gb\",\n    \"Tags\": [\n      \"drone\",\n      \"agent\",\n      \"prod\"\n    ],\n    \"UserData\": \"#cloud-init\",\n    \"UserDataFile\": \"/path/to/cloud/init.yml\"\n  },\n  \"Amazon\": {\n    \"Image\": \"ami-07f84a50d2dec2fa4\",\n    \"Instance\": \"t3.medium\",\n    \"PrivateIP\": true,\n    \"Retries\": 1,\n    \"Region\": \"us-east-2\",\n    \"SSHKey\": \"id_rsa\",\n    \"SubnetID\": \"subnet-0b32177f\",\n    \"SubnetIDsAlt\": [\n\t\t\"subnet-abcd\",\n\t\t\"subnet-efgh\"\n\t],\n    \"SecurityGroup\": [\n      \"sg-770eabe1\"\n    ],\n    \"tags\": {\n      \"os\": \"linux\",\n      \"arch\": \"amd64\"\n    },\n    \"UserData\": \"#cloud-init\",\n    \"UserDataFile\": \"/path/to/cloud/init.yml\"\n  },\n  \"Google\": {\n    \"Zone\": [\"us-central1-b\",\"us-central1-a\"],\n    \"MachineType\": \"f1-micro\",\n    \"MachineImage\": \"ubuntu-1510-wily-v20151114\",\n    \"DiskType\": \"pd-standard\",\n    \"Address\": \"\",\n    \"Network\": \"default\",\n    \"Subnetwork\": \"\",\n    \"Preemptible\": true,\n    \"Scopes\": [\n      \"devstorage.read_only\",\n      \"pubsub\"\n    ],\n    \"DiskSize\": 10,\n    \"Project\": \"project-foo\",\n    \"Tags\": [\n      \"drone\",\n      \"agent\",\n      \"prod\"\n    ],\n    \"UserData\": \"#cloud-init\",\n    \"UserDataFile\": \"/path/to/cloud/init.yml\",\n    \"UserDataKey\": \"user-data\",\n\t\"RateLimit\": 20\n  },\n  \"HetznerCloud\": {\n    \"Token\": \"12345678\",\n    \"Image\": \"ubuntu-16.04\",\n    \"Datacenter\": \"nbg1-dc3\",\n    \"SSHKey\": 12345,\n    \"Type\": \"cx11\",\n    \"UserData\": \"#cloud-init\",\n    \"UserDataFile\": \"/path/to/cloud/init.yml\"\n  },\n  \"Packet\": {\n    \"APIKey\": \"12345678\",\n    \"Facility\": \"facility\",\n    \"ProjectID\": \"project\",\n    \"Plan\": \"plan\",\n    \"OS\": \"ubuntu\",\n    \"SSHKey\": \"id_rsa\",\n    \"UserData\": \"#cloud-init\",\n    \"UserDataFile\": \"/path/to/cloud/init.yml\",\n    \"Hostname\": \"agent\",\n    \"Tags\": [\n      \"drone\",\n      \"agent\",\n      \"prod\"\n    ]\n  },\n  \"OpenStack\": {\n    \"Region\": \"sto-01\",\n    \"Image\": \"ubuntu-16.04-server-latest\",\n    \"Flavor\": \"t1.medium\",\n    \"Network\": \"my-subnet-1\",\n    \"Pool\": \"ext-ips-1\",\n    \"SecurityGroup\": [\n      \"secgrp-feedface\"\n    ],\n    \"SSHKey\": \"drone-ci\",\n    \"Metadata\": {\n      \"name\": \"agent\",\n      \"owner\": \"drone-ci\"\n    },\n    \"UserData\": \"#cloud-init\",\n    \"UserDataFile\": \"/path/to/cloud/init.yml\"\n  },\n  \"Watchtower\": {\n    \"Image\": \"webhippie/watchtower\",\n    \"Interval\": 300,\n    \"Timeout\": 7200000000000\n  },\n  \"GC\": {\n    \"Image\": \"drone/gc\",\n    \"Interval\": 1800000000000,\n    \"Cache\": \"10gb\"\n  },\n  \"Reaper\": {\n    \"Interval\": 3600000000000\n  },\n  \"Pinger\": {\n    \"Interval\": 600000000000\n  },\n  \"Check\": {\n    \"Interval\": 60000000000,\n    \"Deadline\": 1800000000000\n  }\n}`)\n\nfunc TestLoadEnvVariables(t *testing.T) {\n\tf, err := ioutil.TempFile(\"\", \"autoscaler-env-file-test\")\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tf.WriteString(\"ENV_FROM_FILE=FILE_VALUE\")\n\tdefer os.Remove(f.Name())\n\n\tenviron := map[string]string{\n\t\t\"ENV_FROM_HOST\":        \"HOST_VALUE\",\n\t\t\"DRONE_AGENT_ENVIRON\":  `ENV=VALUE,ENV_FROM_HOST`,\n\t\t\"DRONE_AGENT_ENV_FILE\": f.Name(),\n\t}\n\n\tdefer func() {\n\t\t// reset the environment.\n\t\tfor k := range environ {\n\t\t\tos.Unsetenv(k)\n\t\t}\n\t}()\n\n\t// set test environment variables\n\tfor k, v := range environ {\n\t\tos.Setenv(k, v)\n\t}\n\n\ta := MustLoad()\n\twant := []string{\n\t\t\"ENV=VALUE\",\n\t\t\"ENV_FROM_HOST=HOST_VALUE\",\n\t\t\"ENV_FROM_FILE=FILE_VALUE\",\n\t}\n\tif got, want := len(a.Agent.Environ), len(want); got != want {\n\t\tt.Errorf(\"Should have an environment of length %d, got %d\", want, got)\n\t}\n\tfor i := range a.Agent.Environ {\n\t\tif got, wantV := a.Agent.Environ[i], want[i]; got != wantV {\n\t\t\tt.Errorf(\"Wanted environ %s at index %d, got %s\", wantV, i, got)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "drivers/amazon/create.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage amazon\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/logger\"\n\n\t\"github.com/aws/aws-sdk-go/aws\"\n\t\"github.com/aws/aws-sdk-go/service/ec2\"\n)\n\ntype attemptOverrides struct {\n\tattempt int\n\tsize    string\n\tsubnet  string\n}\n\nfunc (p *provider) Create(ctx context.Context, opts autoscaler.InstanceCreateOpts) (*autoscaler.Instance, error) {\n\tattemptOverrides := attemptOverrides{\n\t\tattempt: 1,\n\t\tsize:    p.size,\n\t}\n\n\ttryCreateInAllSubnets := func() (*autoscaler.Instance, error) {\n\t\tvar (\n\t\t\tinstance *autoscaler.Instance\n\t\t\terr      error\n\t\t)\n\t\tfor _, subnet := range p.subnets {\n\t\t\tattemptOverrides.subnet = subnet\n\n\t\t\tinstance, err = p.create(ctx, opts, attemptOverrides)\n\t\t\t// if the instance was provisioned (with or without errors), return the instance.\n\t\t\tif instance != nil {\n\t\t\t\treturn instance, err\n\t\t\t}\n\n\t\t\tattemptOverrides.attempt++\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"failed to create instance in all subnets: %w\", err)\n\t}\n\n\tinstance, err := tryCreateInAllSubnets()\n\n\t// if the instance was provisioned (with or without errors), return the instance.\n\tif instance != nil {\n\t\treturn instance, err\n\t}\n\n\t// if the instance was not provisioned, and fallback\n\t// parameters were provided, retry using the fallback\n\tif p.sizeAlt != \"\" {\n\t\tattemptOverrides.size = p.sizeAlt\n\t\tinstance, err = tryCreateInAllSubnets()\n\t}\n\n\t// if there is no fallback logic do not retry\n\treturn instance, err\n}\n\nfunc (p *provider) create(ctx context.Context, opts autoscaler.InstanceCreateOpts, overrides attemptOverrides) (*autoscaler.Instance, error) {\n\tp.init.Do(func() {\n\t\tp.setup(ctx)\n\t})\n\n\tbuf := new(bytes.Buffer)\n\terr := p.userdata.Execute(buf, &opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := p.getClient()\n\n\tvar iamProfile *ec2.IamInstanceProfileSpecification\n\n\tif p.iamProfileArn != \"\" {\n\t\tiamProfile = &ec2.IamInstanceProfileSpecification{\n\t\t\tArn: &p.iamProfileArn,\n\t\t}\n\t}\n\n\tvar marketOptions *ec2.InstanceMarketOptionsRequest\n\n\tif p.spotInstance == true {\n\t\tmarketOptions = &ec2.InstanceMarketOptionsRequest{\n\t\t\tMarketType: aws.String(\"spot\"),\n\t\t}\n\t}\n\n\ttags := createCopy(p.tags)\n\ttags[\"Name\"] = opts.Name\n\n\tvar metadataOptions *ec2.InstanceMetadataOptionsRequest\n\tif p.imdsTokens != \"\" {\n\t\tmetadataOptions = &ec2.InstanceMetadataOptionsRequest{\n\t\t\tHttpTokens: aws.String(p.imdsTokens),\n\t\t}\n\t}\n\n\tin := &ec2.RunInstancesInput{\n\t\tKeyName:               aws.String(p.key),\n\t\tImageId:               aws.String(p.image),\n\t\tInstanceType:          aws.String(overrides.size),\n\t\tMinCount:              aws.Int64(1),\n\t\tMaxCount:              aws.Int64(1),\n\t\tInstanceMarketOptions: marketOptions,\n\t\tIamInstanceProfile:    iamProfile,\n\t\tUserData:              aws.String(base64.StdEncoding.EncodeToString(buf.Bytes())),\n\t\tMetadataOptions:       metadataOptions,\n\t\tNetworkInterfaces: []*ec2.InstanceNetworkInterfaceSpecification{\n\t\t\t{\n\t\t\t\tAssociatePublicIpAddress: aws.Bool(!p.privateIP),\n\t\t\t\tDeviceIndex:              aws.Int64(0),\n\t\t\t\tSubnetId:                 aws.String(overrides.subnet),\n\t\t\t\tGroups:                   aws.StringSlice(p.groups),\n\t\t\t},\n\t\t},\n\t\tTagSpecifications: []*ec2.TagSpecification{\n\t\t\t{\n\t\t\t\tResourceType: aws.String(\"instance\"),\n\t\t\t\tTags:         convertTags(tags),\n\t\t\t},\n\t\t\t{\n\t\t\t\tResourceType: aws.String(\"volume\"),\n\t\t\t\tTags:         convertTags(tags),\n\t\t\t},\n\t\t},\n\t\tBlockDeviceMappings: []*ec2.BlockDeviceMapping{\n\t\t\t{\n\t\t\t\tDeviceName: aws.String(p.deviceName),\n\t\t\t\tEbs: &ec2.EbsBlockDevice{\n\t\t\t\t\tVolumeSize:          aws.Int64(p.volumeSize),\n\t\t\t\t\tVolumeType:          aws.String(p.volumeType),\n\t\t\t\t\tDeleteOnTermination: aws.Bool(true),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tif p.volumeType == \"io1\" || p.volumeType == \"io2\" || p.volumeType == \"gp3\" {\n\t\tfor _, blockDeviceMapping := range in.BlockDeviceMappings {\n\t\t\tif p.volumeIops > 0 {\n\t\t\t\tblockDeviceMapping.Ebs.Iops = aws.Int64(p.volumeIops)\n\t\t\t}\n\t\t}\n\t}\n\n\tif p.volumeType == \"gp3\" {\n\t\tfor _, blockDeviceMapping := range in.BlockDeviceMappings {\n\t\t\tif p.volumeThroughput > 0 {\n\t\t\t\tblockDeviceMapping.Ebs.Throughput = aws.Int64(p.volumeThroughput)\n\t\t\t}\n\t\t}\n\t}\n\n\tlogger := logger.FromContext(ctx).\n\t\tWithField(\"attempt\", overrides.attempt).\n\t\tWithField(\"size\", overrides.size).\n\t\tWithField(\"subnet\", overrides.subnet).\n\t\tWithField(\"region\", p.region).\n\t\tWithField(\"image\", p.image).\n\t\tWithField(\"name\", opts.Name)\n\tlogger.Debug(\"instance create\")\n\n\tresults, err := client.RunInstances(in)\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tError(\"instance create failed\")\n\t\treturn nil, err\n\t}\n\n\tamazonInstance := results.Instances[0]\n\n\tinstance := &autoscaler.Instance{\n\t\tProvider: autoscaler.ProviderAmazon,\n\t\tID:       *amazonInstance.InstanceId,\n\t\tName:     opts.Name,\n\t\tSize:     *amazonInstance.InstanceType,\n\t\tRegion:   *amazonInstance.Placement.AvailabilityZone,\n\t\tImage:    *amazonInstance.ImageId,\n\t}\n\n\tlogger.WithField(\"name\", instance.Name).\n\t\tInfoln(\"instance create success\")\n\n\t// poll the amazon endpoint for server updates\n\t// and exit when a network address is allocated.\n\tinterval := time.Duration(0)\npoller:\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tlogger.WithField(\"name\", instance.Name).\n\t\t\t\tDebugln(\"instance network deadline exceeded\")\n\n\t\t\treturn instance, ctx.Err()\n\t\tcase <-time.After(interval):\n\t\t\tinterval = time.Minute\n\n\t\t\tlogger.WithField(\"name\", instance.Name).\n\t\t\t\tDebugln(\"check instance network\")\n\n\t\t\tdesc, err := client.DescribeInstances(\n\t\t\t\t&ec2.DescribeInstancesInput{\n\t\t\t\t\tInstanceIds: []*string{\n\t\t\t\t\t\tamazonInstance.InstanceId,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\tlogger.WithError(err).\n\t\t\t\t\tWarnln(\"instance details failed\")\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif len(desc.Reservations) == 0 {\n\t\t\t\tlogger.Warnln(\"empty reservations in details\")\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif len(desc.Reservations[0].Instances) == 0 {\n\t\t\t\tlogger.Warnln(\"empty instances in reservations\")\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tamazonInstance = desc.Reservations[0].Instances[0]\n\n\t\t\tif p.privateIP {\n\t\t\t\tif amazonInstance.PrivateIpAddress != nil {\n\t\t\t\t\tinstance.Address = *amazonInstance.PrivateIpAddress\n\t\t\t\t\tbreak poller\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif amazonInstance.PublicIpAddress != nil {\n\t\t\t\tinstance.Address = *amazonInstance.PublicIpAddress\n\t\t\t\tbreak poller\n\t\t\t}\n\t\t}\n\t}\n\n\tlogger.\n\t\tWithField(\"name\", instance.Name).\n\t\tWithField(\"ip\", instance.Address).\n\t\tDebugln(\"instance network ready\")\n\n\treturn instance, nil\n}\n"
  },
  {
    "path": "drivers/amazon/create_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage amazon\n"
  },
  {
    "path": "drivers/amazon/destroy.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage amazon\n\nimport (\n\t\"context\"\n\t\"os\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/logger\"\n\n\t\"github.com/aws/aws-sdk-go/aws\"\n\t\"github.com/aws/aws-sdk-go/aws/awserr\"\n\t\"github.com/aws/aws-sdk-go/service/ec2\"\n)\n\nfunc (p *provider) Destroy(ctx context.Context, instance *autoscaler.Instance) error {\n\tif os.Getenv(\"DRONE_FLAG_ALTERNATE_DESTROY\") == \"true\" {\n\t\treturn p.destroy2(ctx, instance)\n\t}\n\n\tlogger := logger.FromContext(ctx).\n\t\tWithField(\"id\", instance.ID).\n\t\tWithField(\"ip\", instance.Address).\n\t\tWithField(\"name\", instance.Name).\n\t\tWithField(\"zone\", instance.Region)\n\n\tlogger.Debugln(\"terminate instance\")\n\n\tinput := &ec2.TerminateInstancesInput{\n\t\tInstanceIds: []*string{\n\t\t\taws.String(instance.ID),\n\t\t},\n\t}\n\t_, err := p.getClient().TerminateInstances(input)\n\tif awsErr, ok := err.(awserr.Error); ok {\n\t\tswitch awsErr.Code() {\n\t\tcase ec2.UnsuccessfulInstanceCreditSpecificationErrorCodeInvalidInstanceIdMalformed:\n\t\t\tfallthrough\n\t\tcase ec2.UnsuccessfulInstanceCreditSpecificationErrorCodeInvalidInstanceIdNotFound:\n\t\t\tlogger.Debugln(\"instance does not exist\")\n\t\t\treturn autoscaler.ErrInstanceNotFound\n\t\t}\n\t}\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tErrorln(\"cannot terminate instance\")\n\t\treturn err\n\t}\n\n\tlogger.Debugln(\"terminated\")\n\n\treturn nil\n}\n\nfunc (p *provider) destroy2(ctx context.Context, instance *autoscaler.Instance) error {\n\tlogger := logger.FromContext(ctx).\n\t\tWithField(\"id\", instance.ID).\n\t\tWithField(\"ip\", instance.Address).\n\t\tWithField(\"name\", instance.Name).\n\t\tWithField(\"zone\", instance.Region)\n\n\tlogger.Debugln(\"terminate instance\")\n\n\tinput := &ec2.TerminateInstancesInput{\n\t\tInstanceIds: []*string{\n\t\t\taws.String(instance.ID),\n\t\t},\n\t}\n\t_, err := p.getClient().TerminateInstances(input)\n\tif err == nil {\n\t\tlogger.Debugln(\"terminated\")\n\t\treturn nil\n\t}\n\n\t// if terminate instance returns an error indicating\n\t// the instance no longer exists, return a not found\n\t// error.\n\tif awsErr, ok := err.(awserr.Error); ok {\n\t\tswitch awsErr.Code() {\n\t\tcase ec2.UnsuccessfulInstanceCreditSpecificationErrorCodeInvalidInstanceIdMalformed:\n\t\t\tlogger.Debugln(\"instance does not exist\")\n\t\t\treturn autoscaler.ErrInstanceNotFound\n\t\tcase ec2.UnsuccessfulInstanceCreditSpecificationErrorCodeInvalidInstanceIdNotFound:\n\t\t\tlogger.Debugln(\"instance does not exist\")\n\t\t\treturn autoscaler.ErrInstanceNotFound\n\t\t}\n\t}\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tErrorln(\"cannot terminate instance\")\n\t}\n\n\tlogger.Debugln(\"describe instance\")\n\n\tdescribe := &ec2.DescribeInstancesInput{\n\t\tInstanceIds: []*string{\n\t\t\taws.String(instance.ID),\n\t\t},\n\t}\n\t_, desErr := p.getClient().DescribeInstances(describe)\n\t// if we are able to describe the instance it confirms the\n\t// instance still exists and could not be terminated. Return\n\t// an error so that the instance is flagged as being in an\n\t// error state and requires manual attention.\n\tif desErr == nil {\n\t\tlogger.Errorln(\"describe instance was successful. instance still exists\")\n\t\treturn err\n\t}\n\n\t// if the ware unable to describe the instance because the\n\t// instance no longer exists, we can return a not found error.\n\t// this will result in the instance being deleted from the\n\t// system, since we will have confirmed it no longer exists.\n\tif awsErr, ok := desErr.(awserr.Error); ok {\n\t\tswitch awsErr.Code() {\n\t\tcase ec2.UnsuccessfulInstanceCreditSpecificationErrorCodeInvalidInstanceIdMalformed:\n\t\t\tlogger.Debugln(\"instance does not exist\")\n\t\t\treturn autoscaler.ErrInstanceNotFound\n\t\tcase ec2.UnsuccessfulInstanceCreditSpecificationErrorCodeInvalidInstanceIdNotFound:\n\t\t\tlogger.Debugln(\"instance does not exist\")\n\t\t\treturn autoscaler.ErrInstanceNotFound\n\t\t}\n\t}\n\n\t// otherwise we return the original error returned when\n\t// attempting to delete the instance.\n\treturn err\n}\n"
  },
  {
    "path": "drivers/amazon/destroy_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage amazon\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/drone/autoscaler\"\n\n\t\"github.com/h2non/gock\"\n)\n\nfunc TestDestroy(t *testing.T) {\n\tdefer gock.Off()\n\n\tos.Setenv(\"AWS_ACCESS_KEY_ID\", \"your_access_key_id\")\n\tos.Setenv(\"AWS_SECRET_ACCESS_KEY\", \"your_secret_access_key\")\n\tdefer func() {\n\t\tos.Unsetenv(\"AWS_ACCESS_KEY_ID\")\n\t\tos.Unsetenv(\"AWS_SECRET_ACCESS_KEY\")\n\t}()\n\n\tgock.New(\"https://ec2.us-east-1.amazonaws.com\").\n\t\tPost(\"/\").\n\t\tReply(200)\n\n\tmockContext := context.TODO()\n\tmockInstance := &autoscaler.Instance{\n\t\tID: \"i-1234567890abcdef0\",\n\t}\n\n\tp := New(\n\t\tWithRegion(\"us-east-1\"),\n\t).(*provider)\n\tp.retries = 1\n\n\terr := p.Destroy(mockContext, mockInstance)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc TestDestroyDeleteError(t *testing.T) {\n\tmockContext := context.TODO()\n\tmockInstance := &autoscaler.Instance{\n\t\tID: \"i-1234567890abcdef0\",\n\t}\n\n\tp := New(\n\t\tWithRegion(\"us-east-1\"),\n\t).(*provider)\n\tp.retries = 1\n\n\terr := p.Destroy(mockContext, mockInstance)\n\tif err == nil {\n\t\tt.Errorf(\"Expect error returned from aws\")\n\t}\n}\n\nfunc TestDestroyNotFound(t *testing.T) {\n\tdefer gock.Off()\n\n\tos.Setenv(\"AWS_ACCESS_KEY_ID\", \"your_access_key_id\")\n\tos.Setenv(\"AWS_SECRET_ACCESS_KEY\", \"your_secret_access_key\")\n\tdefer func() {\n\t\tos.Unsetenv(\"AWS_ACCESS_KEY_ID\")\n\t\tos.Unsetenv(\"AWS_SECRET_ACCESS_KEY\")\n\t}()\n\n\tgock.New(\"https://ec2.us-east-1.amazonaws.com\").\n\t\tPost(\"/\").\n\t\tReply(400).\n\t\tBodyString(`<Response><Errors><Error><Code>InvalidInstanceID.NotFound</Code><Message>The instance ID 'i-1a2b3c4d' does not exist</Message></Error></Errors><RequestID>ea966190-f9aa-478e-9ede-example</RequestID></Response>`)\n\n\tmockContext := context.TODO()\n\tmockInstance := &autoscaler.Instance{\n\t\tID: \"i-1234567890abcdef0\",\n\t}\n\n\tp := New(\n\t\tWithRegion(\"us-east-1\"),\n\t).(*provider)\n\tp.retries = 1\n\n\terr := p.Destroy(mockContext, mockInstance)\n\tif err == nil {\n\t\tt.Errorf(\"Expect error returned from aws\")\n\t}\n\tif err != autoscaler.ErrInstanceNotFound {\n\t\tt.Errorf(\"Expect instance not found returned from aws\")\n\t}\n}\n"
  },
  {
    "path": "drivers/amazon/option.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage amazon\n\nimport (\n\t\"io/ioutil\"\n\n\t\"github.com/drone/autoscaler/drivers/internal/userdata\"\n)\n\n// Option configures a Digital Ocean provider option.\ntype Option func(*provider)\n\n// WithDeviceName returns an option to set the device name.\nfunc WithDeviceName(n string) Option {\n\treturn func(p *provider) {\n\t\tp.deviceName = n\n\t}\n}\n\n// WithImage returns an option to set the image.\nfunc WithImage(image string) Option {\n\treturn func(p *provider) {\n\t\tp.image = image\n\t}\n}\n\n// WithPrivateIP returns an option to set the private IP address.\nfunc WithPrivateIP(private bool) Option {\n\treturn func(p *provider) {\n\t\tp.privateIP = private\n\t}\n}\n\n// WithRetries returns an option to set the retry count.\nfunc WithRetries(retries int) Option {\n\treturn func(p *provider) {\n\t\tp.retries = retries\n\t}\n}\n\n// WithRegion returns an option to set the target region.\nfunc WithRegion(region string) Option {\n\treturn func(p *provider) {\n\t\tp.region = region\n\t}\n}\n\n// WithSecurityGroup returns an option to set the instance size.\nfunc WithSecurityGroup(group ...string) Option {\n\treturn func(p *provider) {\n\t\tp.groups = group\n\t}\n}\n\n// WithSize returns an option to set the instance size.\nfunc WithSize(size string) Option {\n\treturn func(p *provider) {\n\t\tp.size = size\n\t}\n}\n\n// WithSizeAlt returns an option to set the alternate instance\n// size. If instance creation fails, the system will attempt to\n// provision a second instance using the alternate size.\nfunc WithSizeAlt(size string) Option {\n\treturn func(p *provider) {\n\t\tp.sizeAlt = size\n\t}\n}\n\n// WithSSHKey returns an option to set the ssh key.\nfunc WithSSHKey(key string) Option {\n\treturn func(p *provider) {\n\t\tp.key = key\n\t}\n}\n\n// WithSubnets returns an option to set the subnet ids.\nfunc WithSubnets(ids []string) Option {\n\treturn func(p *provider) {\n\t\tp.subnets = ids\n\t}\n}\n\n// WithTags returns an option to set the image.\nfunc WithTags(tags map[string]string) Option {\n\treturn func(p *provider) {\n\t\tp.tags = tags\n\t}\n}\n\n// WithUserData returns an option to set the cloud-init\n// template from text.\nfunc WithUserData(text string) Option {\n\treturn func(p *provider) {\n\t\tif text != \"\" {\n\t\t\tp.userdata = userdata.Parse(text)\n\t\t}\n\t}\n}\n\n// WithUserDataFile returns an option to set the cloud-init\n// template from file.\nfunc WithUserDataFile(filepath string) Option {\n\treturn func(p *provider) {\n\t\tif filepath != \"\" {\n\t\t\tb, err := ioutil.ReadFile(filepath)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tp.userdata = userdata.Parse(string(b))\n\t\t}\n\t}\n}\n\n// WithVolumeSize returns an option to set the volume size\n// in gigabytes.\nfunc WithVolumeSize(s int64) Option {\n\treturn func(p *provider) {\n\t\tp.volumeSize = s\n\t}\n}\n\n// WithVolumeType returns an option to set the volume type.\nfunc WithVolumeType(t string) Option {\n\treturn func(p *provider) {\n\t\tp.volumeType = t\n\t}\n}\n\n// WithVolumeIops returns an option to set the volume iops.\nfunc WithVolumeIops(i int64) Option {\n\treturn func(p *provider) {\n\t\tp.volumeIops = i\n\t}\n}\n\n// WithVolumeThroughput returns an option to set the volume throughput.\nfunc WithVolumeThroughput(i int64) Option {\n\treturn func(p *provider) {\n\t\tp.volumeThroughput = i\n\t}\n}\n\n// WithIamProfileArn returns an option to set the iam profile arn.\nfunc WithIamProfileArn(t string) Option {\n\treturn func(p *provider) {\n\t\tp.iamProfileArn = t\n\t}\n}\n\n// WithInstanceMetadataTokens returns an option to set the instance metadata service tokens requiment.\nfunc WithInstanceMetadataTokens(t string) Option {\n\treturn func(p *provider) {\n\t\tp.imdsTokens = t\n\t}\n}\n\n// WithMarketType returns an option to set the instance market type.\nfunc WithMarketType(t string) Option {\n\treturn func(p *provider) {\n\t\tp.spotInstance = t == \"spot\"\n\t}\n}\n"
  },
  {
    "path": "drivers/amazon/option_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage amazon\n\nimport \"testing\"\n\nfunc TestOptions(t *testing.T) {\n\tp := New(\n\t\tWithDeviceName(\"/dev/sda2\"),\n\t\tWithImage(\"ami-0aab355e1bfa1e72e\"),\n\t\tWithPrivateIP(true),\n\t\tWithRegion(\"us-west-2\"),\n\t\tWithRetries(10),\n\t\tWithSecurityGroup(\"sg-770eabe1\"),\n\t\tWithSize(\"t3.2xlarge\"),\n\t\tWithSSHKey(\"id_rsa\"),\n\t\tWithSubnets([]string{\"subnet-0b32177f\"}),\n\t\tWithTags(map[string]string{\"foo\": \"bar\", \"baz\": \"qux\"}),\n\t\tWithVolumeSize(64),\n\t\tWithVolumeType(\"io1\"),\n\t).(*provider)\n\n\tif got, want := p.deviceName, \"/dev/sda2\"; got != want {\n\t\tt.Errorf(\"Want device name %q, got %q\", want, got)\n\t}\n\tif got, want := p.image, \"ami-0aab355e1bfa1e72e\"; got != want {\n\t\tt.Errorf(\"Want image %q, got %q\", want, got)\n\t}\n\tif got, want := p.region, \"us-west-2\"; got != want {\n\t\tt.Errorf(\"Want region %q, got %q\", want, got)\n\t}\n\tif got, want := p.size, \"t3.2xlarge\"; got != want {\n\t\tt.Errorf(\"Want size %q, got %q\", want, got)\n\t}\n\tif got, want := p.key, \"id_rsa\"; got != want {\n\t\tt.Errorf(\"Want key %q, got %q\", want, got)\n\t}\n\tif got, want := p.groups[0], \"sg-770eabe1\"; got != want {\n\t\tt.Errorf(\"Want security groups %q, got %q\", want, got)\n\t}\n\tif got, want := p.subnets, []string{\"subnet-0b32177f\"}; len(got) != 1 || got[0] != want[0] {\n\t\tt.Errorf(\"Want subnet %q, got %q\", want, got)\n\t}\n\tif got, want := p.retries, 10; got != want {\n\t\tt.Errorf(\"Want %d retries, got %d\", want, got)\n\t}\n\tif got, want := p.privateIP, true; got != want {\n\t\tt.Errorf(\"Want %v privateIP, got %v\", want, got)\n\t}\n\tif got, want := len(p.tags), 2; got != want {\n\t\tt.Errorf(\"Want %d tags, got %d\", want, got)\n\t}\n\tif got, want := p.volumeSize, int64(64); got != want {\n\t\tt.Errorf(\"Want volume size %d, got %d\", want, got)\n\t}\n\tif got, want := p.volumeType, \"io1\"; got != want {\n\t\tt.Errorf(\"Want volume type %q, got %q\", want, got)\n\t}\n}\n"
  },
  {
    "path": "drivers/amazon/provider.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage amazon\n\nimport (\n\t\"sync\"\n\t\"text/template\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/drivers/internal/userdata\"\n\n\t\"github.com/aws/aws-sdk-go/aws\"\n\t\"github.com/aws/aws-sdk-go/aws/session\"\n\t\"github.com/aws/aws-sdk-go/service/ec2\"\n)\n\ntype provider struct {\n\tinit sync.Once\n\n\tdeviceName       string\n\tvolumeSize       int64\n\tvolumeType       string\n\tvolumeIops       int64\n\tvolumeThroughput int64\n\tretries          int\n\tkey              string\n\tregion           string\n\timage            string\n\tprivateIP        bool\n\tuserdata         *template.Template\n\tsize             string\n\tsizeAlt          string\n\tsubnets          []string\n\tgroups           []string\n\ttags             map[string]string\n\tiamProfileArn    string\n\tspotInstance     bool\n\timdsTokens       string\n}\n\nfunc (p *provider) getClient() *ec2.EC2 {\n\tconfig := aws.NewConfig()\n\tconfig = config.WithRegion(p.region)\n\tconfig = config.WithMaxRetries(p.retries)\n\tsession, _ := session.NewSession(config)\n\treturn ec2.New(session)\n}\n\n// New returns a new Digital Ocean provider.\nfunc New(opts ...Option) autoscaler.Provider {\n\tp := new(provider)\n\tfor _, opt := range opts {\n\t\topt(p)\n\t}\n\tif p.retries == 0 {\n\t\tp.retries = 10\n\t}\n\tif p.region == \"\" {\n\t\tp.region = \"us-east-1\"\n\t}\n\tif p.size == \"\" {\n\t\tp.size = \"t3.medium\"\n\t}\n\tif p.image == \"\" {\n\t\tp.image = defaultImage(p.region)\n\t}\n\tif p.deviceName == \"\" {\n\t\tp.deviceName = \"/dev/sda1\"\n\t}\n\tif p.volumeSize == 0 {\n\t\tp.volumeSize = 32\n\t}\n\tif p.volumeType == \"\" {\n\t\tp.volumeType = \"gp2\"\n\t}\n\tif (p.volumeType == \"io1\" || p.volumeType == \"io2\") && p.volumeIops == 0 {\n\t\tp.volumeIops = 100\n\t}\n\tif p.volumeType == \"gp3\" && p.volumeIops == 0 {\n\t\tp.volumeIops = 3000 // 3000 is the minimum for gp3\n\t}\n\tif p.volumeType == \"gp3\" && p.volumeThroughput == 0 {\n\t\tp.volumeThroughput = 125 // 125 is the minimum for gp3\n\t}\n\tif p.userdata == nil {\n\t\tp.userdata = userdata.T\n\t}\n\treturn p\n}\n"
  },
  {
    "path": "drivers/amazon/provider_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage amazon\n"
  },
  {
    "path": "drivers/amazon/setup.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage amazon\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/drone/autoscaler/logger\"\n\n\t\"github.com/aws/aws-sdk-go/service/ec2\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\nfunc (p *provider) setup(ctx context.Context) error {\n\tvar g errgroup.Group\n\tif p.key == \"\" {\n\t\tg.Go(func() error {\n\t\t\treturn p.setupKeypair(ctx)\n\t\t})\n\t}\n\tif len(p.subnets) == 0 {\n\t\t// TODO: find or create subnet\n\t}\n\tif len(p.groups) == 0 {\n\t\t// TODO: find or create security groups\n\t}\n\treturn g.Wait()\n}\n\nfunc (p *provider) setupKeypair(ctx context.Context) error {\n\tlogger := logger.FromContext(ctx)\n\n\tlogger.Debugln(\"finding default ssh key\")\n\n\topts := new(ec2.DescribeKeyPairsInput)\n\tkeys, err := p.getClient().DescribeKeyPairs(opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tindex := map[string]string{}\n\tfor _, key := range keys.KeyPairs {\n\t\tindex[*key.KeyName] = *key.KeyFingerprint\n\t}\n\n\t// if the account has multiple keys configured we will\n\t// attempt to use an existing key based on naming convention.\n\tfor _, name := range []string{\"drone\", \"id_rsa_drone\"} {\n\t\tfingerprint, ok := index[name]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tp.key = name\n\n\t\tlogger.\n\t\t\tWithField(\"name\", name).\n\t\t\tWithField(\"fingerprint\", fingerprint).\n\t\t\tDebugln(\"using default ssh key\")\n\t\treturn nil\n\t}\n\n\t// if there were no matches but the account has at least\n\t// one keypair already created we will select the first\n\t// in the list.\n\tif len(keys.KeyPairs) > 0 {\n\t\tkey := keys.KeyPairs[0]\n\t\tp.key = *key.KeyName\n\n\t\tlogger.\n\t\t\tWithField(\"name\", *key.KeyName).\n\t\t\tWithField(\"fingerprint\", *key.KeyFingerprint).\n\t\t\tDebugln(\"using default ssh key\")\n\t\treturn nil\n\t}\n\n\treturn errors.New(\"No matching keys\")\n}\n"
  },
  {
    "path": "drivers/amazon/setup_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage amazon\n"
  },
  {
    "path": "drivers/amazon/util.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage amazon\n\nimport (\n\t\"github.com/aws/aws-sdk-go/aws\"\n\t\"github.com/aws/aws-sdk-go/service/ec2\"\n)\n\n// helper function converts an array of tags in string\n// format to an array of ec2 tags.\nfunc convertTags(in map[string]string) []*ec2.Tag {\n\tvar out []*ec2.Tag\n\tfor k, v := range in {\n\t\tout = append(out, &ec2.Tag{\n\t\t\tKey:   aws.String(k),\n\t\t\tValue: aws.String(v),\n\t\t})\n\t}\n\treturn out\n}\n\n// helper function creates a copy of map[string]string\nfunc createCopy(in map[string]string) map[string]string {\n\tout := map[string]string{}\n\tfor k, v := range in {\n\t\tout[k] = v\n\t}\n\treturn out\n}\n\n// helper function returns the default image based on the\n// selected region.\nfunc defaultImage(region string) string {\n\treturn images[region]\n}\n\n// static ami id list for Ubuntu Server 20.04 LTS\n// source: https://cloud-images.ubuntu.com/locator/\n// filters:\n// - Cloud: Amazon AWS, Amazon GovCloud, Amazon AWS China\n// - Version: 20.04\n// - Instance Type: hvm-ssd\nvar images = map[string]string{\n\t// AWS Regions: Ubuntu Server 20.04 LTS\n\t// Upstream release version: 20220706\n\t\"af-south-1\":     \"ami-0f5298ccab965edeb\",\n\t\"ap-east-1\":      \"ami-0dfad1f1f65cd083b\",\n\t\"ap-northeast-1\": \"ami-0986c991cc80c6ad9\",\n\t\"ap-northeast-2\": \"ami-0565d651769eb3de5\",\n\t\"ap-northeast-3\": \"ami-0e6078093a109801c\",\n\t\"ap-south-1\":     \"ami-0325e3016099f9112\",\n\t\"ap-southeast-1\": \"ami-0eaf04122a1ae7b3b\",\n\t\"ap-southeast-2\": \"ami-048a2d001938101dd\",\n\t\"ap-southeast-3\": \"ami-09915141a4f1dafdd\",\n\t\"ca-central-1\":   \"ami-04a579d2f00bb4001\",\n\t\"eu-central-1\":   \"ami-06cac34c3836ff90b\",\n\t\"eu-north-1\":     \"ami-0ede84a5f28ec932a\",\n\t\"eu-south-1\":     \"ami-0a39f417b8836bc59\",\n\t\"eu-west-1\":      \"ami-0141514361b6a3c1b\",\n\t\"eu-west-2\":      \"ami-014b642f603e350c3\",\n\t\"eu-west-3\":      \"ami-0d0b8d91779dec1e5\",\n\t\"me-south-1\":     \"ami-0c769d841005394ee\",\n\t\"sa-east-1\":      \"ami-088afbba294231fe0\",\n\t\"us-east-1\":      \"ami-0070c5311b7677678\",\n\t\"us-east-2\":      \"ami-07f84a50d2dec2fa4\",\n\t\"us-west-1\":      \"ami-040a251ee9d7d1a9b\",\n\t\"us-west-2\":      \"ami-0aab355e1bfa1e72e\",\n\n\t// AWS GovCloud (US): Ubuntu Server 20.04 LTS\n\t// Upstream release version: 20220627.1\n\t\"us-gov-east-1\": \"ami-0d8ee446ec886f5cf\",\n\t\"us-gov-west-1\": \"ami-0cbaf57cea1d72aec\",\n\n\t// AWS China: Ubuntu Server 20.04 LTS\n\t// Upstream release version: 20210720\n\t\"cn-north-1\":     \"ami-0741e7b8b4fb0001c\",\n\t\"cn-northwest-1\": \"ami-0883e8062ff31f727\",\n}\n"
  },
  {
    "path": "drivers/amazon/util_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage amazon\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/kr/pretty\"\n)\n\nfunc TestConvertTags(t *testing.T) {\n\ta := map[string]string{\"foo\": \"bar\", \"baz\": \"qux\"}\n\tb := map[string]string{}\n\n\ttags := convertTags(a)\n\n\tif got, want := len(tags), 2; got != want {\n\t\tt.Errorf(\"Want %d tags, got %d\", want, got)\n\t}\n\n\tfor _, tag := range tags {\n\t\tb[*tag.Key] = *tag.Value\n\t}\n\n\tif !reflect.DeepEqual(a, b) {\n\t\tt.Errorf(\"unexpected tag conversion\")\n\t\tpretty.Ldiff(t, a, b)\n\t}\n}\n"
  },
  {
    "path": "drivers/azure/create.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage azure\n"
  },
  {
    "path": "drivers/azure/create_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage azure\n"
  },
  {
    "path": "drivers/azure/destroy.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage azure\n"
  },
  {
    "path": "drivers/azure/destroy_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage azure\n"
  },
  {
    "path": "drivers/azure/option.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage azure\n"
  },
  {
    "path": "drivers/azure/option_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage azure\n"
  },
  {
    "path": "drivers/azure/provider.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage azure\n"
  },
  {
    "path": "drivers/azure/provider_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage azure\n"
  },
  {
    "path": "drivers/digitalocean/create.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage digitalocean\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/logger\"\n\n\t\"github.com/digitalocean/godo\"\n)\n\nfunc (p *provider) Create(ctx context.Context, opts autoscaler.InstanceCreateOpts) (*autoscaler.Instance, error) {\n\tp.init.Do(func() {\n\t\tp.setup(ctx)\n\t})\n\n\tbuf := new(bytes.Buffer)\n\terr := p.userdata.Execute(buf, &opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq := &godo.DropletCreateRequest{\n\t\tName:              opts.Name,\n\t\tRegion:            p.region,\n\t\tSize:              p.size,\n\t\tTags:              p.tags,\n\t\tIPv6:              false,\n\t\tPrivateNetworking: p.privateIP,\n\t\tUserData:          buf.String(),\n\n\t\tSSHKeys: []godo.DropletCreateSSHKey{\n\t\t\t{Fingerprint: p.key},\n\t\t},\n\t\tImage: godo.DropletCreateImage{\n\t\t\tSlug: p.image,\n\t\t},\n\t}\n\n\tlogger := logger.FromContext(ctx).\n\t\tWithField(\"region\", req.Region).\n\t\tWithField(\"image\", req.Image.Slug).\n\t\tWithField(\"size\", req.Size).\n\t\tWithField(\"name\", req.Name)\n\n\tlogger.Debugln(\"instance create\")\n\n\tclient := newClient(ctx, p.token)\n\tdroplet, _, err := client.Droplets.Create(ctx, req)\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tErrorln(\"cannot create instance\")\n\t\treturn nil, err\n\t}\n\n\tif p.firewall != \"\" {\n\t\t_, err := client.Firewalls.AddDroplets(ctx, p.firewall, droplet.ID)\n\t\tif err != nil {\n\t\t\tlogger.WithError(err).\n\t\t\t\tErrorln(\"cannot assign instance to firewall\")\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tinstance := &autoscaler.Instance{\n\t\tProvider: autoscaler.ProviderDigitalOcean,\n\t\tID:       strconv.Itoa(droplet.ID),\n\t\tName:     droplet.Name,\n\t\tSize:     req.Size,\n\t\tRegion:   req.Region,\n\t\tImage:    req.Image.Slug,\n\t}\n\n\tlogger.WithField(\"name\", instance.Name).\n\t\tInfoln(\"instance created\")\n\n\t// poll the digitalocean endpoint for server updates\n\t// and exit when a network address is allocated.\n\tinterval := time.Duration(0)\npoller:\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tlogger.WithField(\"name\", instance.Name).\n\t\t\t\tDebugln(\"cannot ascertain network\")\n\n\t\t\treturn instance, ctx.Err()\n\t\tcase <-time.After(interval):\n\t\t\tinterval = time.Minute\n\n\t\t\tlogger.WithField(\"name\", instance.Name).\n\t\t\t\tDebugln(\"find instance network\")\n\n\t\t\tdroplet, _, err = client.Droplets.Get(ctx, droplet.ID)\n\t\t\tif err != nil {\n\t\t\t\tlogger.WithError(err).\n\t\t\t\t\tErrorln(\"cannot find instance\")\n\t\t\t\treturn instance, err\n\t\t\t}\n\n\t\t\tfor _, network := range droplet.Networks.V4 {\n\t\t\t\tif network.Type == \"public\" {\n\t\t\t\t\tinstance.Address = network.IPAddress\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif instance.Address != \"\" {\n\t\t\t\tbreak poller\n\t\t\t}\n\t\t}\n\t}\n\n\tlogger.\n\t\tWithField(\"name\", instance.Name).\n\t\tWithField(\"ip\", instance.Address).\n\t\tDebugln(\"instance network ready\")\n\n\treturn instance, nil\n}\n"
  },
  {
    "path": "drivers/digitalocean/create_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage digitalocean\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/digitalocean/godo\"\n\t\"github.com/drone/autoscaler\"\n\n\t\"github.com/h2non/gock\"\n)\n\nfunc TestCreate(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(\"https://api.digitalocean.com\").\n\t\tPost(\"/v2/droplets\").\n\t\tReply(200).\n\t\tBodyString(respDropletCreate)\n\n\tgock.New(\"https://api.digitalocean.com\").\n\t\tGet(\"/v2/droplets/3164494\").\n\t\tReply(200).\n\t\tBodyString(respDropletDesc)\n\n\tp := New(\n\t\tWithSSHKey(\"58:8e:30:66:fc:e2:ff:ad:4f:6f:02:4b:af:28:0d:c7\"),\n\t\tWithToken(\"77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3\"),\n\t).(*provider)\n\tp.init.Do(func() {}) // prevent init function\n\n\tinstance, err := p.Create(context.TODO(), autoscaler.InstanceCreateOpts{Name: \"agent1\"})\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif !gock.IsDone() {\n\t\tt.Errorf(\"Expected http requests not detected\")\n\t}\n\n\tt.Run(\"Attributes\", testInstance(instance))\n}\n\nfunc TestCreate_CreateError(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(\"https://api.digitalocean.com\").\n\t\tPost(\"/v2/droplets\").\n\t\tReply(500)\n\n\tp := New(\n\t\tWithSSHKey(\"58:8e:30:66:fc:e2:ff:ad:4f:6f:02:4b:af:28:0d:c7\"),\n\t\tWithToken(\"77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3\"),\n\t).(*provider)\n\tp.init.Do(func() {}) // prevent init function\n\n\t_, err := p.Create(context.TODO(), autoscaler.InstanceCreateOpts{Name: \"agent1\"})\n\tif err == nil {\n\t\tt.Errorf(\"Expect error returned from digital ocean\")\n\t} else if _, ok := err.(*godo.ErrorResponse); !ok {\n\t\tt.Errorf(\"Expect ErrorResponse digital ocean\")\n\t}\n\n\tif !gock.IsDone() {\n\t\tt.Errorf(\"Expected http requests not detected\")\n\t}\n}\n\nfunc TestCreate_DescribeError(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(\"https://api.digitalocean.com\").\n\t\tPost(\"/v2/droplets\").\n\t\tReply(200).\n\t\tBodyString(respDropletCreate)\n\n\tgock.New(\"https://api.digitalocean.com\").\n\t\tGet(\"/v2/droplets/3164494\").\n\t\tReply(500)\n\n\tp := New(\n\t\tWithSSHKey(\"58:8e:30:66:fc:e2:ff:ad:4f:6f:02:4b:af:28:0d:c7\"),\n\t\tWithToken(\"77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3\"),\n\t).(*provider)\n\tp.init.Do(func() {}) // prevent init function\n\n\tinstance, err := p.Create(context.TODO(), autoscaler.InstanceCreateOpts{Name: \"agent1\"})\n\tif err == nil {\n\t\tt.Errorf(\"Expect error returned from digital ocean\")\n\t} else if _, ok := err.(*godo.ErrorResponse); !ok {\n\t\tt.Errorf(\"Expect ErrorResponse digital ocean\")\n\t}\n\n\tif !gock.IsDone() {\n\t\tt.Errorf(\"Expected http requests not detected\")\n\t}\n\n\tt.Run(\"Attributes\", testInstance(instance))\n}\n\nfunc TestCreate_DescribeTimeout(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(\"https://api.digitalocean.com\").\n\t\tPost(\"/v2/droplets\").\n\t\tReply(200).\n\t\tBodyString(respDropletCreate)\n\n\tgock.New(\"https://api.digitalocean.com\").\n\t\tGet(\"/v2/droplets/3164494\").\n\t\tReply(200).\n\t\tBodyString(respDropletCreate) // no network data\n\n\tp := New(\n\t\tWithSSHKey(\"58:8e:30:66:fc:e2:ff:ad:4f:6f:02:4b:af:28:0d:c7\"),\n\t\tWithToken(\"77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3\"),\n\t).(*provider)\n\tp.init.Do(func() {}) // prevent init function\n\n\tctx, cancel := context.WithTimeout(context.Background(), time.Second)\n\tdefer cancel()\n\n\tinstance, err := p.Create(ctx, autoscaler.InstanceCreateOpts{Name: \"agent1\"})\n\tif err == nil {\n\t\tt.Errorf(\"Expected context deadline exceeded, got nil\")\n\t} else if err.Error() != \"context deadline exceeded\" {\n\t\tt.Errorf(\"Expected context deadline exceeded, got %s\", err)\n\t}\n\n\tif !gock.IsDone() {\n\t\tt.Errorf(\"Expected http requests not detected\")\n\t}\n\n\tt.Run(\"Attributes\", testInstance(instance))\n}\n\nfunc testInstance(instance *autoscaler.Instance) func(t *testing.T) {\n\treturn func(t *testing.T) {\n\t\tif instance == nil {\n\t\t\tt.Errorf(\"Expect non-nil instance even if error\")\n\t\t}\n\t\tif got, want := instance.ID, \"3164494\"; got != want {\n\t\t\tt.Errorf(\"Want droplet ID %v, got %v\", want, got)\n\t\t}\n\t\tif got, want := instance.Image, \"docker-18-04\"; got != want {\n\t\t\tt.Errorf(\"Want droplet Image %v, got %v\", want, got)\n\t\t}\n\t\tif got, want := instance.Name, \"example.com\"; got != want {\n\t\t\tt.Errorf(\"Want droplet Name %v, got %v\", want, got)\n\t\t}\n\t\tif got, want := instance.Region, \"nyc1\"; got != want {\n\t\t\tt.Errorf(\"Want droplet Region %v, got %v\", want, got)\n\t\t}\n\t\tif got, want := instance.Provider, autoscaler.ProviderDigitalOcean; got != want {\n\t\t\tt.Errorf(\"Want droplet Provider %v, got %v\", want, got)\n\t\t}\n\t}\n}\n\nfunc testInstanceAddress(instance *autoscaler.Instance) func(t *testing.T) {\n\treturn func(t *testing.T) {\n\t\tif instance == nil {\n\t\t\tt.Errorf(\"Expect non-nil instance even if error\")\n\t\t}\n\t\tif got, want := instance.Address, \"104.131.186.241\"; got != want {\n\t\t\tt.Errorf(\"Want droplet Address %v, got %v\", want, got)\n\t\t}\n\t}\n}\n\n// sample response for POST /v2/droplets\nconst respDropletCreate = `\n{\n  \"droplet\": {\n    \"id\": 3164494,\n    \"name\": \"example.com\",\n    \"memory\": 1024,\n    \"vcpus\": 1,\n    \"disk\": 25,\n    \"locked\": true,\n    \"status\": \"new\",\n    \"kernel\": {\n      \"id\": 2233,\n      \"name\": \"Ubuntu 14.04 x64 vmlinuz-3.13.0-37-generic\",\n      \"version\": \"3.13.0-37-generic\"\n    },\n    \"created_at\": \"2014-11-14T16:36:31Z\",\n    \"features\": [\n      \"virtio\"\n    ],\n    \"backup_ids\": [\n      \n    ],\n    \"snapshot_ids\": [\n      \n    ],\n    \"image\": {\n      \n    },\n    \"volume_ids\": [\n      \n    ],\n    \"size\": {\n      \n    },\n    \"size_slug\": \"s-1vcpu-1gb\",\n    \"networks\": {\n\n    },\n    \"region\": {\n      \n    },\n    \"tags\": [\n      \"web\"\n    ]\n  },\n  \"links\": {\n    \"actions\": [\n      {\n        \"id\": 36805096,\n        \"rel\": \"create\",\n        \"href\": \"https:\\/\\/api.digitalocean.com\\/v2\\/actions\\/36805096\"\n      }\n    ]\n  }\n}\n`\n\n// sample response for POST /v2/droplets/:id\nconst respDropletDesc = `\n{\n  \"droplet\": {\n    \"id\": 3164494,\n    \"name\": \"example.com\",\n    \"memory\": 1024,\n    \"vcpus\": 1,\n    \"disk\": 25,\n    \"locked\": true,\n    \"status\": \"new\",\n    \"kernel\": {\n      \"id\": 2233,\n      \"name\": \"Ubuntu 14.04 x64 vmlinuz-3.13.0-37-generic\",\n      \"version\": \"3.13.0-37-generic\"\n    },\n    \"created_at\": \"2014-11-14T16:36:31Z\",\n    \"features\": [\n      \"virtio\"\n    ],\n    \"backup_ids\": [\n      \n    ],\n    \"snapshot_ids\": [\n      \n    ],\n    \"image\": {\n      \n    },\n    \"volume_ids\": [\n      \n    ],\n    \"size\": {\n      \n    },\n    \"size_slug\": \"s-1vcpu-1gb\",\n    \"networks\": {\n      \"v4\": [\n        {\n          \"ip_address\": \"104.131.186.241\",\n          \"netmask\": \"255.255.240.0\",\n          \"gateway\": \"104.131.176.1\",\n          \"type\": \"public\"\n        }\n      ]\n    },\n    \"region\": {\n      \n    },\n    \"tags\": [\n      \"web\"\n    ]\n  },\n  \"links\": {\n    \"actions\": [\n      {\n        \"id\": 36805096,\n        \"rel\": \"create\",\n        \"href\": \"https:\\/\\/api.digitalocean.com\\/v2\\/actions\\/36805096\"\n      }\n    ]\n  }\n}\n`\n"
  },
  {
    "path": "drivers/digitalocean/destroy.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage digitalocean\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/logger\"\n)\n\nfunc (p *provider) Destroy(ctx context.Context, instance *autoscaler.Instance) error {\n\tlogger := logger.FromContext(ctx).\n\t\tWithField(\"region\", instance.Region).\n\t\tWithField(\"image\", instance.Image).\n\t\tWithField(\"size\", instance.Size).\n\t\tWithField(\"name\", instance.Name)\n\n\tclient := newClient(ctx, p.token)\n\tid, err := strconv.Atoi(instance.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, res, err := client.Droplets.Get(ctx, id)\n\tif err != nil && res.StatusCode == 404 {\n\t\tlogger.WithError(err).\n\t\t\tWarnln(\"droplet does not exist\")\n\t\treturn autoscaler.ErrInstanceNotFound\n\t} else if err != nil {\n\t\tlogger.WithError(err).\n\t\t\tErrorln(\"cannot find droplet\")\n\t\treturn err\n\t}\n\n\tlogger.Debugln(\"deleting droplet\")\n\n\t_, err = client.Droplets.Delete(ctx, id)\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tErrorln(\"deleting droplet failed\")\n\t\treturn err\n\t}\n\n\tlogger.Debugln(\"droplet deleted\")\n\n\treturn nil\n}\n"
  },
  {
    "path": "drivers/digitalocean/destroy_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage digitalocean\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/digitalocean/godo\"\n\t\"github.com/drone/autoscaler\"\n\n\t\"github.com/golang/mock/gomock\"\n\t\"github.com/h2non/gock\"\n)\n\nfunc TestDestroy(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(\"https://api.digitalocean.com\").\n\t\tGet(\"/v2/droplets/3164494\").\n\t\tReply(200).\n\t\tBodyString(respDropletCreate)\n\n\tgock.New(\"https://api.digitalocean.com\").\n\t\tDelete(\"/v2/droplets/3164494\").\n\t\tReply(204)\n\n\tmockContext := context.TODO()\n\tmockInstance := &autoscaler.Instance{\n\t\tID: \"3164494\",\n\t}\n\n\tp := New(\n\t\tWithSSHKey(\"58:8e:30:66:fc:e2:ff:ad:4f:6f:02:4b:af:28:0d:c7\"),\n\t\tWithToken(\"77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3\"),\n\t)\n\n\terr := p.Destroy(mockContext, mockInstance)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif !gock.IsDone() {\n\t\tt.Errorf(\"Expected http requests not detected\")\n\t}\n}\n\nfunc TestDestroyDeleteError(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tdefer gock.Off()\n\n\tgock.New(\"https://api.digitalocean.com\").\n\t\tGet(\"/v2/droplets/3164494\").\n\t\tReply(200).\n\t\tBodyString(respDropletCreate)\n\n\tgock.New(\"https://api.digitalocean.com\").\n\t\tDelete(\"/v2/droplets/3164494\").\n\t\tReply(500)\n\n\tmockContext := context.TODO()\n\tmockInstance := &autoscaler.Instance{\n\t\tID: \"3164494\",\n\t}\n\n\tp := New(\n\t\tWithSSHKey(\"58:8e:30:66:fc:e2:ff:ad:4f:6f:02:4b:af:28:0d:c7\"),\n\t\tWithToken(\"77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3\"),\n\t)\n\n\terr := p.Destroy(mockContext, mockInstance)\n\tif err == nil {\n\t\tt.Errorf(\"Expect error returned from digital ocean\")\n\t} else if _, ok := err.(*godo.ErrorResponse); !ok {\n\t\tt.Errorf(\"Expect ErrorResponse digital ocean\")\n\t}\n\n\tif !gock.IsDone() {\n\t\tt.Errorf(\"Expected http requests not detected\")\n\t}\n}\n\nfunc TestDestroyFindError(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tdefer gock.Off()\n\n\tgock.New(\"https://api.digitalocean.com\").\n\t\tGet(\"/v2/droplets/3164494\").\n\t\tReply(500)\n\n\tmockContext := context.TODO()\n\tmockInstance := &autoscaler.Instance{\n\t\tID: \"3164494\",\n\t}\n\n\tp := New(\n\t\tWithSSHKey(\"58:8e:30:66:fc:e2:ff:ad:4f:6f:02:4b:af:28:0d:c7\"),\n\t\tWithToken(\"77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3\"),\n\t)\n\n\terr := p.Destroy(mockContext, mockInstance)\n\tif err == nil {\n\t\tt.Errorf(\"Expect error returned from digital ocean\")\n\t} else if _, ok := err.(*godo.ErrorResponse); !ok {\n\t\tt.Errorf(\"Expect ErrorResponse digital ocean\")\n\t}\n\n\tif !gock.IsDone() {\n\t\tt.Errorf(\"Expected http requests not detected\")\n\t}\n}\n\nfunc TestDestroyNotFound(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tdefer gock.Off()\n\n\tgock.New(\"https://api.digitalocean.com\").\n\t\tGet(\"/v2/droplets/3164494\").\n\t\tReply(404)\n\n\tmockContext := context.TODO()\n\tmockInstance := &autoscaler.Instance{\n\t\tID: \"3164494\",\n\t}\n\n\tp := New(\n\t\tWithSSHKey(\"58:8e:30:66:fc:e2:ff:ad:4f:6f:02:4b:af:28:0d:c7\"),\n\t\tWithToken(\"77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3\"),\n\t)\n\n\terr := p.Destroy(mockContext, mockInstance)\n\tif err == nil {\n\t\tt.Errorf(\"Expect error returned from digital ocean\")\n\t} else if err != autoscaler.ErrInstanceNotFound {\n\t\tt.Errorf(\"Expect ErrInstanceNotFound\")\n\t}\n\n\tif !gock.IsDone() {\n\t\tt.Errorf(\"Expected http requests not detected\")\n\t}\n}\n\nfunc TestDestroyInvalidInput(t *testing.T) {\n\ti := &autoscaler.Instance{}\n\tp := provider{}\n\terr := p.Destroy(context.TODO(), i)\n\tif _, ok := err.(*strconv.NumError); !ok {\n\t\tt.Errorf(\"Expected invalid or missing ID error\")\n\t}\n}\n"
  },
  {
    "path": "drivers/digitalocean/option.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage digitalocean\n\nimport (\n\t\"io/ioutil\"\n\n\t\"github.com/drone/autoscaler/drivers/internal/userdata\"\n)\n\n// Option configures a Digital Ocean provider option.\ntype Option func(*provider)\n\n// WithImage returns an option to set the image.\nfunc WithImage(image string) Option {\n\treturn func(p *provider) {\n\t\tp.image = image\n\t}\n}\n\n// WithRegion returns an option to set the target region.\nfunc WithRegion(region string) Option {\n\treturn func(p *provider) {\n\t\tp.region = region\n\t}\n}\n\n// WithSize returns an option to set the instance size.\nfunc WithSize(size string) Option {\n\treturn func(p *provider) {\n\t\tp.size = size\n\t}\n}\n\n// WithSSHKey returns an option to set the ssh key.\nfunc WithSSHKey(key string) Option {\n\treturn func(p *provider) {\n\t\tp.key = key\n\t}\n}\n\n// WithTags returns an option to set the image.\nfunc WithTags(tags ...string) Option {\n\treturn func(p *provider) {\n\t\tp.tags = tags\n\t}\n}\n\n// WithToken returns an option to set the auth token.\nfunc WithToken(token string) Option {\n\treturn func(p *provider) {\n\t\tp.token = token\n\t}\n}\n\n// WithFirewall returns an option to set the droplet firewall.\nfunc WithFirewall(firewall string) Option {\n\treturn func(p *provider) {\n\t\tp.firewall = firewall\n\t}\n}\n\n// WithPrivateIP returns an option to set the private IP address.\nfunc WithPrivateIP(private bool) Option {\n\treturn func(p *provider) {\n\t\tp.privateIP = private\n\t}\n}\n\n// WithUserData returns an option to set the cloud-init\n// template from text.\nfunc WithUserData(text string) Option {\n\treturn func(p *provider) {\n\t\tif text != \"\" {\n\t\t\tp.userdata = userdata.Parse(text)\n\t\t}\n\t}\n}\n\n// WithUserDataFile returns an option to set the cloud-init\n// template from file.\nfunc WithUserDataFile(filepath string) Option {\n\treturn func(p *provider) {\n\t\tif filepath != \"\" {\n\t\t\tb, err := ioutil.ReadFile(filepath)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tp.userdata = userdata.Parse(string(b))\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "drivers/digitalocean/option_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage digitalocean\n\nimport \"testing\"\n\nfunc TestOptions(t *testing.T) {\n\tp := New(\n\t\tWithImage(\"ubuntu-18-04-x64\"),\n\t\tWithRegion(\"nyc3\"),\n\t\tWithSize(\"s-8vcpu-32gb\"),\n\t\tWithSSHKey(\"58:8e:30:66:fc:e2:ff:ad:4f:6f:02:4b:af:28:0d:c7\"),\n\t\tWithTags(\"drone\", \"agent\"),\n\t\tWithFirewall(\"f33e7128-f3e7-4229-b6cc-a4751381a104\"),\n\t\tWithToken(\"77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3\"),\n\t\tWithPrivateIP(false),\n\t).(*provider)\n\n\tif got, want := p.image, \"ubuntu-18-04-x64\"; got != want {\n\t\tt.Errorf(\"Want image %q, got %q\", want, got)\n\t}\n\tif got, want := p.region, \"nyc3\"; got != want {\n\t\tt.Errorf(\"Want region %q, got %q\", want, got)\n\t}\n\tif got, want := p.size, \"s-8vcpu-32gb\"; got != want {\n\t\tt.Errorf(\"Want size %q, got %q\", want, got)\n\t}\n\tif got, want := p.key, \"58:8e:30:66:fc:e2:ff:ad:4f:6f:02:4b:af:28:0d:c7\"; got != want {\n\t\tt.Errorf(\"Want key %q, got %q\", want, got)\n\t}\n\tif got, want := p.token, \"77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3\"; got != want {\n\t\tt.Errorf(\"Want token %q, got %q\", want, got)\n\t}\n\tif got, want := p.firewall, \"f33e7128-f3e7-4229-b6cc-a4751381a104\"; got != want {\n\t\tt.Errorf(\"Want token %q, got %q\", want, got)\n\t}\n\tif got, want := p.privateIP, false; got != want {\n\t\tt.Errorf(\"Want %v privateIP, got %v\", want, got)\n\t}\n\tif got, want := len(p.tags), 2; got != want {\n\t\tt.Errorf(\"Want %d tags, got %d\", want, got)\n\t}\n}\n"
  },
  {
    "path": "drivers/digitalocean/provider.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage digitalocean\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"text/template\"\n\n\t\"github.com/drone/autoscaler\"\n\n\t\"github.com/digitalocean/godo\"\n\t\"golang.org/x/oauth2\"\n)\n\n// provider implements a DigitalOcean provider.\ntype provider struct {\n\tinit sync.Once\n\n\tkey       string\n\tregion    string\n\ttoken     string\n\tsize      string\n\timage     string\n\tfirewall  string\n\tprivateIP bool\n\tuserdata  *template.Template\n\ttags      []string\n}\n\n// New returns a new Digital Ocean provider.\nfunc New(opts ...Option) autoscaler.Provider {\n\tp := new(provider)\n\tfor _, opt := range opts {\n\t\topt(p)\n\t}\n\tif p.region == \"\" {\n\t\tp.region = \"nyc1\"\n\t}\n\tif p.size == \"\" {\n\t\tp.size = \"s-2vcpu-4gb\"\n\t}\n\tif p.image == \"\" {\n\t\tp.image = \"docker-18-04\"\n\t}\n\tif p.userdata == nil {\n\t\tp.userdata = userdataT\n\t}\n\treturn p\n}\n\n// helper function returns a new digitalocean client.\nfunc newClient(ctx context.Context, token string) *godo.Client {\n\treturn godo.NewClient(\n\t\toauth2.NewClient(ctx, oauth2.StaticTokenSource(\n\t\t\t&oauth2.Token{\n\t\t\t\tAccessToken: token,\n\t\t\t},\n\t\t)),\n\t)\n}\n"
  },
  {
    "path": "drivers/digitalocean/provider_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage digitalocean\n\nimport \"testing\"\n\nfunc TestDefaults(t *testing.T) {\n\tp := New().(*provider)\n\tif got, want := p.image, \"docker-18-04\"; got != want {\n\t\tt.Errorf(\"Want image %q, got %q\", want, got)\n\t}\n\tif got, want := p.region, \"nyc1\"; got != want {\n\t\tt.Errorf(\"Want region %q, got %q\", want, got)\n\t}\n\tif got, want := p.size, \"s-2vcpu-4gb\"; got != want {\n\t\tt.Errorf(\"Want size %q, got %q\", want, got)\n\t}\n\tif got, want := p.key, \"\"; got != want {\n\t\tt.Errorf(\"Want key %q, got %q\", want, got)\n\t}\n\tif got, want := p.token, \"\"; got != want {\n\t\tt.Errorf(\"Want token %q, got %q\", want, got)\n\t}\n\tif got, want := len(p.tags), 0; got != want {\n\t\tt.Errorf(\"Want %d tags, got %d\", want, got)\n\t}\n}\n"
  },
  {
    "path": "drivers/digitalocean/setup.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage digitalocean\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/digitalocean/godo\"\n\t\"github.com/drone/autoscaler/logger\"\n\n\t\"golang.org/x/sync/errgroup\"\n)\n\nfunc (p *provider) setup(ctx context.Context) error {\n\tvar g errgroup.Group\n\tif p.key == \"\" {\n\t\tg.Go(func() error {\n\t\t\treturn p.setupKeypair(ctx)\n\t\t})\n\t}\n\treturn g.Wait()\n}\n\nfunc (p *provider) setupKeypair(ctx context.Context) error {\n\tlogger := logger.FromContext(ctx)\n\n\tlogger.Debugln(\"finding default ssh key\")\n\n\tclient := newClient(ctx, p.token)\n\tkeys, _, err := client.Keys.List(ctx, &godo.ListOptions{})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tindex := map[string]string{}\n\tfor _, key := range keys {\n\t\tindex[key.Name] = key.Fingerprint\n\t}\n\n\t// if the account has multiple keys configured we will\n\t// attempt to use an existing key based on naming convention.\n\tfor _, name := range []string{\"drone\", \"id_rsa_drone\"} {\n\t\tfingerprint, ok := index[name]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tp.key = fingerprint\n\n\t\tlogger.\n\t\t\tWithField(\"name\", name).\n\t\t\tWithField(\"fingerprint\", fingerprint).\n\t\t\tDebugln(\"using default ssh key\")\n\t\treturn nil\n\t}\n\n\t// if there were no matches but the account has at least\n\t// one keypair already created we will select the first\n\t// in the list.\n\tif len(keys) > 0 {\n\t\tkey := keys[0]\n\t\tp.key = key.Fingerprint\n\n\t\tlogger.\n\t\t\tWithField(\"name\", key.Name).\n\t\t\tWithField(\"fingerprint\", key.Fingerprint).\n\t\t\tDebugln(\"using default ssh key\")\n\t\treturn nil\n\t}\n\n\treturn errors.New(\"No matching keys\")\n}\n"
  },
  {
    "path": "drivers/digitalocean/setup_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage digitalocean\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/h2non/gock\"\n)\n\nfunc TestSetupKey_Single(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(\"https://api.digitalocean.com\").\n\t\tGet(\"/v2/account/keys\").\n\t\tReply(200).\n\t\tBodyString(respSingleKey)\n\n\tp := New(\n\t\tWithToken(\"77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3\"),\n\t).(*provider)\n\n\terr := p.setup(context.TODO())\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif got, want := p.key, \"3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa\"; got != want {\n\t\tt.Errorf(\"Want fingerprint %s, got %s\", want, got)\n\t}\n}\n\nfunc TestSetupKey_FoundMatch(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(\"https://api.digitalocean.com\").\n\t\tGet(\"/v2/account/keys\").\n\t\tReply(200).\n\t\tBodyString(respMultiKey)\n\n\tp := New(\n\t\tWithToken(\"77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3\"),\n\t).(*provider)\n\n\terr := p.setup(context.TODO())\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif got, want := p.key, \"00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff\"; got != want {\n\t\tt.Errorf(\"Want fingerprint %s, got %s\", want, got)\n\t}\n\n\tif !gock.IsDone() {\n\t\tt.Errorf(\"Expected http requests not detected\")\n\t}\n}\n\nfunc TestSetupKey_NoMatch(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(\"https://api.digitalocean.com\").\n\t\tGet(\"/v2/account/keys\").\n\t\tReply(200).\n\t\tBodyString(respMultiKeyNoMatch)\n\n\tp := New(\n\t\tWithToken(\"77e027c7447f468068a7d4fea41e7149a75a94088082c66fcf555de3977f69d3\"),\n\t).(*provider)\n\n\terr := p.setup(context.TODO())\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif got, want := p.key, \"3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa\"; got != want {\n\t\tt.Errorf(\"Want fingerprint %s, got %s\", want, got)\n\t}\n\n\tif !gock.IsDone() {\n\t\tt.Errorf(\"Expected http requests not detected\")\n\t}\n}\n\nvar respSingleKey = `\n{\n  \"ssh_keys\": [\n    {\n      \"id\": 512189,\n      \"fingerprint\": \"3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa\",\n      \"public_key\": \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDDHr/jh2Jy4yALcK4JyWbVkPRaWmhck3IgCoeOO3z1e2dBowLh64QAM+Qb72pxekALga2oi4GvT+TlWNhzPH4V example\",\n      \"name\": \"My SSH Public Key\"\n    }\n  ],\n  \"links\": {\n  },\n  \"meta\": {\n    \"total\": 1\n  }\n}\n`\n\nvar respMultiKey = `\n{\n  \"ssh_keys\": [\n    {\n      \"id\": 512189,\n      \"fingerprint\": \"3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa\",\n      \"public_key\": \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDDHr/jh2Jy4yALcK4JyWbVkPRaWmhck3IgCoeOO3z1e2dBowLh64QAM+Qb72pxekALga2oi4GvT+TlWNhzPH4V example\",\n      \"name\": \"My SSH Public Key\"\n    },\n    {\n      \"id\": 513199,\n      \"fingerprint\": \"00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff\",\n      \"public_key\": \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDDHr/jh2Jy4yALcK4JyWbVkPRaWmhck3IgCoeOO3z1e2dBowLh64QAM+Qb72pxekALga2oi4GvT+TlWNhzPH4V example\",\n      \"name\": \"id_rsa_drone\"\n    }\n  ],\n  \"links\": {\n  },\n  \"meta\": {\n    \"total\": 2\n  }\n}\n`\n\nvar respMultiKeyNoMatch = `\n{\n  \"ssh_keys\": [\n    {\n      \"id\": 512189,\n      \"fingerprint\": \"3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa\",\n      \"public_key\": \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDDHr/jh2Jy4yALcK4JyWbVkPRaWmhck3IgCoeOO3z1e2dBowLh64QAM+Qb72pxekALga2oi4GvT+TlWNhzPH4V example\",\n      \"name\": \"My SSH Public Key\"\n    },\n    {\n      \"id\": 513199,\n      \"fingerprint\": \"00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff\",\n      \"public_key\": \"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDDHr/jh2Jy4yALcK4JyWbVkPRaWmhck3IgCoeOO3z1e2dBowLh64QAM+Qb72pxekALga2oi4GvT+TlWNhzPH4V example\",\n      \"name\": \"My SSH Public Key2\"\n    }\n  ],\n  \"links\": {\n  },\n  \"meta\": {\n    \"total\": 2\n  }\n}\n`\n"
  },
  {
    "path": "drivers/digitalocean/userdata.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage digitalocean\n\nimport \"github.com/drone/autoscaler/drivers/internal/userdata\"\n\nvar userdataT = userdata.Parse(`#cloud-config\nwrite_files:\n  - path: /etc/systemd/system/docker.service.d/override.conf\n    content: |\n      [Service]\n      ExecStart=\n      ExecStart=/usr/bin/dockerd\n  - path: /etc/default/docker\n    content: |\n      DOCKER_OPTS=\"\"\n  - path: /etc/docker/daemon.json\n    content: |\n      {\n        \"dns\": [ \"8.8.8.8\", \"8.8.4.4\" ],\n        \"hosts\": [ \"0.0.0.0:2376\", \"unix:///var/run/docker.sock\" ],\n        \"tls\": true,\n        \"tlsverify\": true,\n        \"tlscacert\": \"/etc/docker/ca.pem\",\n        \"tlscert\": \"/etc/docker/server-cert.pem\",\n        \"tlskey\": \"/etc/docker/server-key.pem\"\n      }\n  - path: /etc/docker/ca.pem\n    encoding: b64\n    content: {{ .CACert | base64 }}\n  - path: /etc/docker/server-cert.pem\n    encoding: b64\n    content: {{ .TLSCert | base64 }}\n  - path: /etc/docker/server-key.pem\n    encoding: b64\n    content: {{ .TLSKey | base64 }}\n\nruncmd:\n  - [ systemctl, daemon-reload ]\n  - [ systemctl, restart, docker ]\n`)\n"
  },
  {
    "path": "drivers/google/create.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage google\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/logger\"\n\n\t\"google.golang.org/api/compute/v1\"\n\t\"google.golang.org/api/googleapi\"\n)\n\nfunc (p *provider) Create(ctx context.Context, opts autoscaler.InstanceCreateOpts) (*autoscaler.Instance, error) {\n\tp.init.Do(func() {\n\t\tp.setup(ctx)\n\t})\n\n\tbuf := new(bytes.Buffer)\n\terr := p.userdata.Execute(buf, &opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tname := strings.ToLower(opts.Name)\n\n\t// select random zone from the list\n\tzone := p.zones[rand.Intn(len(p.zones))]\n\n\tlogger := logger.FromContext(ctx).\n\t\tWithField(\"zone\", zone).\n\t\tWithField(\"image\", p.image).\n\t\tWithField(\"size\", p.size).\n\t\tWithField(\"name\", opts.Name)\n\n\tlogger.Debugln(\"instance insert\")\n\n\tnetworkConfig := []*compute.AccessConfig{}\n\tif !p.privateIP {\n\t\tnetworkConfig = []*compute.AccessConfig{\n\t\t\t{\n\t\t\t\tName: \"External NAT\",\n\t\t\t\tType: \"ONE_TO_ONE_NAT\",\n\t\t\t},\n\t\t}\n\t}\n\n\tin := &compute.Instance{\n\t\tName:           name,\n\t\tZone:           fmt.Sprintf(\"projects/%s/zones/%s\", p.project, zone),\n\t\tMinCpuPlatform: \"Automatic\",\n\t\tMachineType:    fmt.Sprintf(\"projects/%s/zones/%s/machineTypes/%s\", p.project, zone, p.size),\n\t\tMetadata: &compute.Metadata{\n\t\t\tItems: []*compute.MetadataItems{\n\t\t\t\t{\n\t\t\t\t\tKey:   p.userdataKey,\n\t\t\t\t\tValue: googleapi.String(buf.String()),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tTags: &compute.Tags{\n\t\t\tItems: p.tags,\n\t\t},\n\t\tDisks: []*compute.AttachedDisk{\n\t\t\t{\n\t\t\t\tType:       \"PERSISTENT\",\n\t\t\t\tBoot:       true,\n\t\t\t\tMode:       \"READ_WRITE\",\n\t\t\t\tAutoDelete: true,\n\t\t\t\tDeviceName: name,\n\t\t\t\tInitializeParams: &compute.AttachedDiskInitializeParams{\n\t\t\t\t\tSourceImage: fmt.Sprintf(\"https://www.googleapis.com/compute/v1/projects/%s\", p.image),\n\t\t\t\t\tDiskType:    fmt.Sprintf(\"projects/%s/zones/%s/diskTypes/%s\", p.project, zone, p.diskType),\n\t\t\t\t\tDiskSizeGb:  p.diskSize,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tCanIpForward: false,\n\t\tNetworkInterfaces: []*compute.NetworkInterface{\n\t\t\t{\n\t\t\t\tNetwork:       p.network,\n\t\t\t\tSubnetwork:    p.subnetwork,\n\t\t\t\tStackType:     p.stackType,\n\t\t\t\tAccessConfigs: networkConfig,\n\t\t\t},\n\t\t},\n\t\tLabels: p.labels,\n\t\tScheduling: &compute.Scheduling{\n\t\t\tPreemptible:       false,\n\t\t\tOnHostMaintenance: \"MIGRATE\",\n\t\t\tAutomaticRestart:  googleapi.Bool(true),\n\t\t},\n\t\tDeletionProtection: false,\n\t\tServiceAccounts: []*compute.ServiceAccount{\n\t\t\t{\n\t\t\t\tScopes: p.scopes,\n\t\t\t\tEmail:  p.serviceAccountEmail,\n\t\t\t},\n\t\t},\n\t}\n\n\t// Cannot add this in the same way as v4 access configs since the instance creation\n\t// fails if any v6 access configs are specified for an instance with IPV4_ONLY stack type\n\tif p.stackType == \"IPV4_IPV6\" {\n\t\tin.NetworkInterfaces[0].Ipv6AccessConfigs = []*compute.AccessConfig{\n\t\t\t{\n\t\t\t\tName:        \"external-ipv6\",\n\t\t\t\tType:        \"DIRECT_IPV6\",\n\t\t\t\tNetworkTier: \"PREMIUM\",\n\t\t\t},\n\t\t}\n\t}\n\n\top, err := p.service.Instances.Insert(p.project, zone, in).Do()\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tErrorln(\"instance insert failed\")\n\t\treturn nil, err\n\t}\n\n\tlogger.Debugln(\"pending instance insert operation\")\n\n\terr = p.waitZoneOperation(ctx, op.Name, zone)\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tErrorln(\"instance insert operation failed\")\n\t\treturn nil, err\n\t}\n\n\tlogger.Debugln(\"instance insert operation complete\")\n\n\tresp, err := p.service.Instances.Get(p.project, zone, name).Do()\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tErrorln(\"cannot get instance details\")\n\t\treturn nil, err\n\t}\n\n\taddress := resp.NetworkInterfaces[0].NetworkIP\n\n\tif !p.privateIP {\n\t\taddress = resp.NetworkInterfaces[0].AccessConfigs[0].NatIP\n\t}\n\n\tinstance := &autoscaler.Instance{\n\t\tProvider:            autoscaler.ProviderGoogle,\n\t\tID:                  name,\n\t\tName:                opts.Name,\n\t\tImage:               p.image,\n\t\tRegion:              zone,\n\t\tSize:                p.size,\n\t\tAddress:             address,\n\t\tServiceAccountEmail: p.serviceAccountEmail,\n\t\tScopes:              p.scopes,\n\t}\n\n\tlogger.\n\t\tWithField(\"name\", instance.Name).\n\t\tWithField(\"ip\", instance.Address).\n\t\tDebugln(\"instance inserted\")\n\n\treturn instance, nil\n}\n"
  },
  {
    "path": "drivers/google/create_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage google\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/h2non/gock\"\n\n\t\"google.golang.org/api/compute/v1\"\n\t\"google.golang.org/api/googleapi\"\n)\n\nfunc TestCreate(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(\"https://compute.googleapis.com\").\n\t\tPost(\"/compute/v1/projects/my-project/zones/us-central1-a/instances\").\n\t\tJSON(insertInstanceMock).\n\t\tReply(200).\n\t\tBodyString(`{ \"name\": \"operation-name\" }`)\n\n\tgock.New(\"https://compute.googleapis.com\").\n\t\tGet(\"/compute/v1/projects/my-project/zones/us-central1-a/instances/agent-807jvfwj\").\n\t\tReply(200).\n\t\tBodyString(`{ \"networkInterfaces\": [ { \"accessConfigs\": [ { \"natIP\": \"1.2.3.4\" } ] } ] }`)\n\n\tgock.New(\"https://compute.googleapis.com\").\n\t\tGet(\"/compute/v1/projects/my-project/zones/us-central1-a/operations/operation-name\").\n\t\tReply(200).\n\t\tBodyString(`{ \"status\": \"DONE\" }`)\n\n\tv, err := New(\n\t\tWithClient(http.DefaultClient),\n\t\tWithZones(\"us-central1-a\"),\n\t\tWithProject(\"my-project\"),\n\t\tWithUserData(\"#cloud-init\"),\n\t)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tp := v.(*provider)\n\tp.init.Do(func() {})\n\n\tinstance, err := p.Create(context.TODO(), autoscaler.InstanceCreateOpts{Name: \"agent-807jVFwj\"})\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif want, got := instance.Address, \"1.2.3.4\"; got != want {\n\t\tt.Errorf(\"Want instance IP %q, got %q\", want, got)\n\t}\n\tif want, got := instance.Image, \"ubuntu-os-cloud/global/images/ubuntu-2004-focal-v20220712\"; got != want {\n\t\tt.Errorf(\"Want instance ID %q, got %q\", want, got)\n\t}\n\tif want, got := instance.ID, \"agent-807jvfwj\"; got != want {\n\t\tt.Errorf(\"Want instance ID %q, got %q\", want, got)\n\t}\n\tif want, got := instance.Name, \"agent-807jVFwj\"; got != want {\n\t\tt.Errorf(\"Want instance Name %q, got %q\", want, got)\n\t}\n\tif want, got := instance.Provider, autoscaler.ProviderGoogle; got != want {\n\t\tt.Errorf(\"Want google Provider type\")\n\t}\n\tif want, got := instance.Region, \"us-central1-a\"; got != want {\n\t\tt.Errorf(\"Want instance Region %q, got %q\", want, got)\n\t}\n\tif want, got := instance.Size, \"n1-standard-1\"; got != want {\n\t\tt.Errorf(\"Want instance Size %q, got %q\", want, got)\n\t}\n\tif want, got := instance.ServiceAccountEmail, \"default\"; got != want {\n\t\tt.Errorf(\"Want service account email  %q, got %q\", want, got)\n\t}\n}\n\nfunc TestCreateWithMultiZones(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(\"https://compute.googleapis.com\").\n\t\tPost(\"/compute/v1/projects/my-project/zones/us-central1-b/instances\").\n\t\tJSON(insertInstanceMockB).\n\t\tReply(200).\n\t\tBodyString(`{ \"name\": \"operation-name\" }`)\n\n\tgock.New(\"https://compute.googleapis.com\").\n\t\tGet(\"/compute/v1/projects/my-project/zones/us-central1-b/instances/agent-807jvfwj\").\n\t\tReply(200).\n\t\tBodyString(`{ \"networkInterfaces\": [ { \"accessConfigs\": [ { \"natIP\": \"1.2.3.4\" } ] } ] }`)\n\n\tgock.New(\"https://compute.googleapis.com\").\n\t\tGet(\"/compute/v1/projects/my-project/zones/us-central1-b/operations/operation-name\").\n\t\tReply(200).\n\t\tBodyString(`{ \"status\": \"DONE\" }`)\n\n\tv, err := New(\n\t\tWithClient(http.DefaultClient),\n\t\tWithZones(\"us-central1-b\"),\n\t\tWithProject(\"my-project\"),\n\t\tWithUserData(\"#cloud-init\"),\n\t)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tp := v.(*provider)\n\tp.init.Do(func() {})\n\n\tinstance, err := p.Create(context.TODO(), autoscaler.InstanceCreateOpts{Name: \"agent-807jVFwj\"})\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif want, got := instance.Region, \"us-central1-b\"; got != want {\n\t\tt.Errorf(\"Want region %q, got %q\", want, got)\n\t}\n}\n\nvar insertInstanceMock = &compute.Instance{\n\tName:           \"agent-807jvfwj\",\n\tZone:           \"projects/my-project/zones/us-central1-a\",\n\tMinCpuPlatform: \"Automatic\",\n\tMachineType:    \"projects/my-project/zones/us-central1-a/machineTypes/n1-standard-1\",\n\tMetadata: &compute.Metadata{\n\t\tItems: []*compute.MetadataItems{\n\t\t\t{\n\t\t\t\tKey:   \"user-data\",\n\t\t\t\tValue: googleapi.String(`#cloud-init`),\n\t\t\t},\n\t\t},\n\t},\n\tTags: &compute.Tags{\n\t\tItems: []string{\"allow-docker\"},\n\t},\n\tDisks: []*compute.AttachedDisk{\n\t\t{\n\t\t\tType:       \"PERSISTENT\",\n\t\t\tBoot:       true,\n\t\t\tMode:       \"READ_WRITE\",\n\t\t\tAutoDelete: true,\n\t\t\tDeviceName: \"agent-807jvfwj\",\n\t\t\tInitializeParams: &compute.AttachedDiskInitializeParams{\n\t\t\t\tSourceImage: \"https://www.googleapis.com/compute/v1/projects/ubuntu-os-cloud/global/images/ubuntu-2004-focal-v20220712\",\n\t\t\t\tDiskType:    \"projects/my-project/zones/us-central1-a/diskTypes/pd-standard\",\n\t\t\t\tDiskSizeGb:  50,\n\t\t\t},\n\t\t},\n\t},\n\tCanIpForward: false,\n\tNetworkInterfaces: []*compute.NetworkInterface{\n\t\t{\n\t\t\tNetwork:   \"global/networks/default\",\n\t\t\tStackType: \"IPV4_ONLY\",\n\t\t\tAccessConfigs: []*compute.AccessConfig{\n\t\t\t\t{\n\t\t\t\t\tName: \"External NAT\",\n\t\t\t\t\tType: \"ONE_TO_ONE_NAT\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\tLabels: map[string]string{},\n\tScheduling: &compute.Scheduling{\n\t\tPreemptible:       false,\n\t\tOnHostMaintenance: \"MIGRATE\",\n\t\tAutomaticRestart:  googleapi.Bool(true),\n\t},\n\tDeletionProtection: false,\n\tServiceAccounts: []*compute.ServiceAccount{\n\t\t{\n\t\t\tEmail: \"default\",\n\t\t\tScopes: []string{\n\t\t\t\t\"https://www.googleapis.com/auth/devstorage.read_only\",\n\t\t\t\t\"https://www.googleapis.com/auth/logging.write\",\n\t\t\t\t\"https://www.googleapis.com/auth/monitoring.write\",\n\t\t\t\t\"https://www.googleapis.com/auth/trace.append\",\n\t\t\t},\n\t\t},\n\t},\n}\n\nvar insertInstanceMockB = &compute.Instance{\n\tName:           \"agent-807jvfwj\",\n\tZone:           \"projects/my-project/zones/us-central1-b\",\n\tMinCpuPlatform: \"Automatic\",\n\tMachineType:    \"projects/my-project/zones/us-central1-b/machineTypes/n1-standard-1\",\n\tMetadata: &compute.Metadata{\n\t\tItems: []*compute.MetadataItems{\n\t\t\t{\n\t\t\t\tKey:   \"user-data\",\n\t\t\t\tValue: googleapi.String(`#cloud-init`),\n\t\t\t},\n\t\t},\n\t},\n\tTags: &compute.Tags{\n\t\tItems: []string{\"allow-docker\"},\n\t},\n\tDisks: []*compute.AttachedDisk{\n\t\t{\n\t\t\tType:       \"PERSISTENT\",\n\t\t\tBoot:       true,\n\t\t\tMode:       \"READ_WRITE\",\n\t\t\tAutoDelete: true,\n\t\t\tDeviceName: \"agent-807jvfwj\",\n\t\t\tInitializeParams: &compute.AttachedDiskInitializeParams{\n\t\t\t\tSourceImage: \"https://www.googleapis.com/compute/v1/projects/ubuntu-os-cloud/global/images/ubuntu-2004-focal-v20220712\",\n\t\t\t\tDiskType:    \"projects/my-project/zones/us-central1-b/diskTypes/pd-standard\",\n\t\t\t\tDiskSizeGb:  50,\n\t\t\t},\n\t\t},\n\t},\n\tCanIpForward: false,\n\tNetworkInterfaces: []*compute.NetworkInterface{\n\t\t{\n\t\t\tNetwork:   \"global/networks/default\",\n\t\t\tStackType: \"IPV4_ONLY\",\n\t\t\tAccessConfigs: []*compute.AccessConfig{\n\t\t\t\t{\n\t\t\t\t\tName: \"External NAT\",\n\t\t\t\t\tType: \"ONE_TO_ONE_NAT\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\tLabels: map[string]string{},\n\tScheduling: &compute.Scheduling{\n\t\tPreemptible:       false,\n\t\tOnHostMaintenance: \"MIGRATE\",\n\t\tAutomaticRestart:  googleapi.Bool(true),\n\t},\n\tDeletionProtection: false,\n\tServiceAccounts: []*compute.ServiceAccount{\n\t\t{\n\t\t\tEmail: \"default\",\n\t\t\tScopes: []string{\n\t\t\t\t\"https://www.googleapis.com/auth/devstorage.read_only\",\n\t\t\t\t\"https://www.googleapis.com/auth/logging.write\",\n\t\t\t\t\"https://www.googleapis.com/auth/monitoring.write\",\n\t\t\t\t\"https://www.googleapis.com/auth/trace.append\",\n\t\t\t},\n\t\t},\n\t},\n}\n"
  },
  {
    "path": "drivers/google/destroy.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage google\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"google.golang.org/api/googleapi\"\n)\n\nfunc (p *provider) Destroy(ctx context.Context, instance *autoscaler.Instance) error {\n\t// An instance's Region is actually a Zone in the google provider\n\top, err := p.service.Instances.Delete(p.project, instance.Region, instance.ID).Do()\n\tif err != nil {\n\t\t// https://github.com/googleapis/google-api-go-client/blob/master/googleapi/googleapi.go#L135\n\t\tif gerr, ok := err.(*googleapi.Error); ok &&\n\t\t\tgerr.Code == http.StatusNotFound {\n\t\t\treturn autoscaler.ErrInstanceNotFound\n\t\t}\n\t\treturn err\n\t}\n\treturn p.waitZoneOperation(ctx, op.Name, instance.Region)\n}\n"
  },
  {
    "path": "drivers/google/destroy_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage google\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/h2non/gock\"\n)\n\nfunc TestDestroy(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(\"https://compute.googleapis.com\").\n\t\tDelete(\"/compute/v1/projects/my-project/zones/us-central1-a/instances/my-instance\").\n\t\tReply(200).\n\t\tBodyString(`{ \"name\": \"operation-name\" }`)\n\n\tgock.New(\"https://compute.googleapis.com\").\n\t\tGet(\"/compute/v1/projects/my-project/zones/us-central1-a/operations/operation-name\").\n\t\tReply(200).\n\t\tBodyString(`{ \"status\": \"DONE\" }`)\n\n\tmockContext := context.TODO()\n\tmockInstance := &autoscaler.Instance{\n\t\tID:     \"my-instance\",\n\t\tRegion: \"us-central1-a\",\n\t}\n\n\tp, err := New(\n\t\tWithClient(http.DefaultClient),\n\t\tWithZones(\"us-central1-a\"),\n\t\tWithProject(\"my-project\"),\n\t)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\n\terr = p.Destroy(mockContext, mockInstance)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc TestDestroy_Error(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(\"https://compute.googleapis.com\").\n\t\tDelete(\"/compute/v1/projects/my-project/zones/us-central1-a/instances/my-instance\").\n\t\tReply(404)\n\n\tmockContext := context.TODO()\n\tmockInstance := &autoscaler.Instance{\n\t\tID:     \"my-instance\",\n\t\tRegion: \"us-central1-a\",\n\t}\n\n\tp, err := New(\n\t\tWithClient(http.DefaultClient),\n\t\tWithZones(\"us-central1-a\"),\n\t\tWithProject(\"my-project\"),\n\t)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\n\terr = p.Destroy(mockContext, mockInstance)\n\tif err == nil {\n\t\tt.Errorf(\"Expect error deleting server\")\n\t}\n}\n"
  },
  {
    "path": "drivers/google/option.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage google\n\nimport (\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/drone/autoscaler/drivers/internal/userdata\"\n\t\"golang.org/x/time/rate\"\n\n\t\"google.golang.org/api/compute/v1\"\n)\n\n// Option configures a Digital Ocean provider option.\ntype Option func(*provider)\n\n// WithClient returns an option to set the default http\n// Client used with the Google Compute provider.\nfunc WithClient(client *http.Client) Option {\n\treturn func(p *provider) {\n\t\tservice, err := compute.New(client)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tp.service = service\n\t}\n}\n\n// WithDiskSize returns an option to set the instance disk\n// size in gigabytes.\nfunc WithDiskSize(diskSize int64) Option {\n\treturn func(p *provider) {\n\t\tp.diskSize = diskSize\n\t}\n}\n\n// WithDiskType returns an option to set the instance disk type.\nfunc WithDiskType(diskType string) Option {\n\treturn func(p *provider) {\n\t\tp.diskType = diskType\n\t}\n}\n\n// WithLabels returns an option to set the metadata labels.\nfunc WithLabels(labels map[string]string) Option {\n\treturn func(p *provider) {\n\t\tp.labels = labels\n\t}\n}\n\n// WithMachineImage returns an option to set the image.\nfunc WithMachineImage(image string) Option {\n\treturn func(p *provider) {\n\t\tp.image = image\n\t}\n}\n\n// WithMachineType returns an option to set the instance type.\nfunc WithMachineType(size string) Option {\n\treturn func(p *provider) {\n\t\tp.size = size\n\t}\n}\n\n// WithNetwork returns an option to set the network.\nfunc WithNetwork(network string) Option {\n\treturn func(p *provider) {\n\t\tp.network = network\n\t}\n}\n\n// WithSubNetwork returns an option to set the subnetwork.\nfunc WithSubnetwork(subnetwork string) Option {\n\treturn func(p *provider) {\n\t\tp.subnetwork = subnetwork\n\t}\n}\n\n// WithStackType returns an option to set the stack type for the instance.\nfunc WithStackType(stackType string) Option {\n\treturn func(p *provider) {\n\t\tp.stackType = stackType\n\t}\n}\n\n// WithPrivateIP returns an option to set the private IP address.\nfunc WithPrivateIP(private bool) Option {\n\treturn func(p *provider) {\n\t\tp.privateIP = private\n\t}\n}\n\n// WithProject returns an option to set the project.\nfunc WithProject(project string) Option {\n\treturn func(p *provider) {\n\t\tp.project = project\n\t}\n}\n\n// WithTags returns an option to set the resource tags.\nfunc WithTags(tags ...string) Option {\n\treturn func(p *provider) {\n\t\tp.tags = tags\n\t}\n}\n\n// WithUserData returns an option to set the cloud-init\n// template from text.\nfunc WithUserData(text string) Option {\n\treturn func(p *provider) {\n\t\tif text != \"\" {\n\t\t\tp.userdata = userdata.Parse(text)\n\t\t}\n\t}\n}\n\n// WithUserDataFile returns an option to set the cloud-init\n// template from file.\nfunc WithUserDataFile(filepath string) Option {\n\treturn func(p *provider) {\n\t\tif filepath != \"\" {\n\t\t\tb, err := ioutil.ReadFile(filepath)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tp.userdata = userdata.Parse(string(b))\n\t\t}\n\t}\n}\n\n// WithUserDataKey allows to set the user data key for Google Cloud Platform\n// This allows user to set either user-data or a startup script\nfunc WithUserDataKey(text string) Option {\n\treturn func(p *provider) {\n\t\tif text != \"\" {\n\t\t\tp.userdataKey = text\n\t\t}\n\t}\n}\n\n// WithZone returns an option to set the target zone.\nfunc WithZones(zones ...string) Option {\n\treturn func(p *provider) {\n\t\tp.zones = zones\n\t}\n}\n\n// WithScopes returns an option to set the scopes.\nfunc WithScopes(scopes ...string) Option {\n\treturn func(p *provider) {\n\t\tp.scopes = scopes\n\t}\n}\n\n// WithServiceAccountEmail returns an option to set the ServiceAccountEmail.\nfunc WithServiceAccountEmail(email string) Option {\n\treturn func(p *provider) {\n\t\tp.serviceAccountEmail = email\n\t}\n}\n\nfunc WithRateLimit(limitAmount int) Option {\n\treturn func(p *provider) {\n\t\tlimit := rate.Every(1 * time.Second / time.Duration(limitAmount))\n\t\tp.rateLimiter = rate.NewLimiter(limit, 1)\n\t}\n}\n"
  },
  {
    "path": "drivers/google/option_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage google\n\nimport (\n\t\"net/http\"\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestOptions(t *testing.T) {\n\tv, err := New(\n\t\tWithClient(http.DefaultClient),\n\t\tWithDiskSize(100),\n\t\tWithDiskType(\"local-ssd\"),\n\t\tWithMachineImage(\"ubuntu-1604-lts\"),\n\t\tWithMachineType(\"c3.large\"),\n\t\tWithNetwork(\"global/defaults/foo\"),\n\t\tWithPrivateIP(false),\n\t\tWithServiceAccountEmail(\"default\"),\n\t\tWithProject(\"my-project\"),\n\t\tWithTags(\"drone\", \"agent\"),\n\t\tWithZones(\"us-central1-f\"),\n\t\tWithScopes(\"scope1\", \"scope2\"),\n\t\tWithUserDataKey(\"test-key\"),\n\t)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tp := v.(*provider)\n\n\tif got, want := p.diskSize, int64(100); got != want {\n\t\tt.Errorf(\"Want diskSize %d, got %d\", want, got)\n\t}\n\tif got, want := p.diskType, \"local-ssd\"; got != want {\n\t\tt.Errorf(\"Want diskType %s, got %s\", want, got)\n\t}\n\tif got, want := p.image, \"ubuntu-1604-lts\"; got != want {\n\t\tt.Errorf(\"Want image %q, got %q\", want, got)\n\t}\n\tif got, want := p.network, \"global/defaults/foo\"; got != want {\n\t\tt.Errorf(\"Want network %q, got %q\", want, got)\n\t}\n\tif got, want := p.privateIP, false; got != want {\n\t\tt.Errorf(\"Want %v privateIP, got %v\", want, got)\n\t}\n\tif got, want := p.project, \"my-project\"; got != want {\n\t\tt.Errorf(\"Want project %q, got %q\", want, got)\n\t}\n\tif got, want := p.size, \"c3.large\"; got != want {\n\t\tt.Errorf(\"Want size %q, got %q\", want, got)\n\t}\n\tif got, want := len(p.tags), 2; got != want {\n\t\tt.Errorf(\"Want %d tags, got %d\", want, got)\n\t}\n\tif got, want := p.zones, []string{\"us-central1-f\"}; !reflect.DeepEqual(want, got) {\n\t\tt.Errorf(\"Want zone %q, got %q\", want, got)\n\t}\n\tif got, want := len(p.scopes), 2; got != want {\n\t\tt.Errorf(\"Want %d scopes, got %d\", want, got)\n\t}\n\tif got, want := p.serviceAccountEmail, \"default\"; got != want {\n\t\tt.Errorf(\"Want service account name %q, got %q\", want, got)\n\t}\n\tif got, want := p.userdataKey, \"test-key\"; got != want {\n\t\tt.Errorf(\"Want userdata key %q, got %q\", want, got)\n\t}\n}\n"
  },
  {
    "path": "drivers/google/provider.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage google\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net/http\"\n\t\"sync\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/drivers/internal/userdata\"\n\t\"golang.org/x/oauth2\"\n\t\"golang.org/x/oauth2/google\"\n\t\"golang.org/x/time/rate\"\n\tcompute \"google.golang.org/api/compute/v1\"\n\t\"google.golang.org/api/googleapi\"\n)\n\nvar (\n\tdefaultTags = []string{\n\t\t\"allow-docker\",\n\t}\n\n\tdefaultScopes = []string{\n\t\t\"https://www.googleapis.com/auth/devstorage.read_only\",\n\t\t\"https://www.googleapis.com/auth/logging.write\",\n\t\t\"https://www.googleapis.com/auth/monitoring.write\",\n\t\t\"https://www.googleapis.com/auth/trace.append\",\n\t}\n)\n\n// provider implements a Google Cloud Platform provider.\ntype provider struct {\n\tinit sync.Once\n\n\tdiskSize            int64\n\tdiskType            string\n\timage               string\n\tlabels              map[string]string\n\tnetwork             string\n\tsubnetwork          string\n\tstackType           string\n\tproject             string\n\tprivateIP           bool\n\tscopes              []string\n\tserviceAccountEmail string\n\tsize                string\n\ttags                []string\n\tzones               []string\n\tuserdata            *template.Template\n\tuserdataKey         string\n\n\trateLimiter *rate.Limiter\n\n\tservice *compute.Service\n}\n\n// New returns a new Google Cloud Platform provider.\nfunc New(opts ...Option) (autoscaler.Provider, error) {\n\tp := new(provider)\n\tfor _, opt := range opts {\n\t\topt(p)\n\t}\n\tif p.diskSize == 0 {\n\t\tp.diskSize = 50\n\t}\n\tif p.diskType == \"\" {\n\t\tp.diskType = \"pd-standard\"\n\t}\n\tif len(p.zones) == 0 {\n\t\tp.zones = []string{\"us-central1-a\"}\n\t}\n\tif p.size == \"\" {\n\t\tp.size = \"n1-standard-1\"\n\t}\n\tif p.image == \"\" {\n\t\tp.image = \"ubuntu-os-cloud/global/images/ubuntu-2004-focal-v20220712\"\n\t}\n\tif p.network == \"\" {\n\t\tp.network = \"global/networks/default\"\n\t}\n\tif p.stackType == \"\" {\n\t\tp.stackType = \"IPV4_ONLY\"\n\t}\n\tif p.userdata == nil {\n\t\tp.userdata = userdata.T\n\t}\n\tif p.userdataKey == \"\" {\n\t\tp.userdataKey = \"user-data\"\n\t}\n\tif len(p.tags) == 0 {\n\t\tp.tags = defaultTags\n\t}\n\tif len(p.scopes) == 0 {\n\t\tp.scopes = defaultScopes\n\t}\n\tif p.serviceAccountEmail == \"\" {\n\t\tp.serviceAccountEmail = \"default\"\n\t}\n\n\tif p.rateLimiter == nil {\n\t\t// If unspecified, set to the max read rate limit for the API 25/s\n\t\t// Source: https://cloud.google.com/compute/docs/api-rate-limits\n\t\tp.rateLimiter = rate.NewLimiter(rate.Every(time.Second/25), 1)\n\t}\n\n\tif p.service == nil {\n\t\tclient, err := google.DefaultClient(oauth2.NoContext, compute.ComputeScope)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tp.service, err = compute.New(client)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn p, nil\n}\n\nfunc (p *provider) waitZoneOperation(ctx context.Context, name string, zone string) error {\n\tfor {\n\t\tif p.rateLimiter.Allow() {\n\t\t\top, err := p.service.ZoneOperations.Get(p.project, zone, name).Do()\n\t\t\tif err != nil {\n\t\t\t\tif gerr, ok := err.(*googleapi.Error); ok &&\n\t\t\t\t\tgerr.Code == http.StatusNotFound {\n\t\t\t\t\treturn autoscaler.ErrInstanceNotFound\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif op.Error != nil {\n\t\t\t\treturn errors.New(op.Error.Errors[0].Message)\n\t\t\t}\n\t\t\tif op.Status == \"DONE\" {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\ttime.Sleep(time.Second)\n\t}\n}\n\nfunc (p *provider) waitGlobalOperation(ctx context.Context, name string) error {\n\tfor {\n\t\tif p.rateLimiter.Allow() {\n\t\t\top, err := p.service.GlobalOperations.Get(p.project, name).Do()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif op.Error != nil {\n\t\t\t\treturn errors.New(op.Error.Errors[0].Message)\n\t\t\t}\n\t\t\tif op.Status == \"DONE\" {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\ttime.Sleep(time.Second)\n\t}\n}\n"
  },
  {
    "path": "drivers/google/provider_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage google\n\nimport (\n\t\"net/http\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/drone/autoscaler/drivers/internal/userdata\"\n)\n\nfunc TestDefaults(t *testing.T) {\n\tv, err := New(\n\t\tWithClient(http.DefaultClient),\n\t)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tp := v.(*provider)\n\n\tif got, want := p.diskSize, int64(50); got != want {\n\t\tt.Errorf(\"Want diskSize %d, got %d\", want, got)\n\t}\n\tif got, want := p.diskType, \"pd-standard\"; got != want {\n\t\tt.Errorf(\"Want diskType %s, got %s\", want, got)\n\t}\n\tif got, want := p.image, \"ubuntu-os-cloud/global/images/ubuntu-2004-focal-v20220712\"; got != want {\n\t\tt.Errorf(\"Want image %q, got %q\", want, got)\n\t}\n\tif got, want := p.network, \"global/networks/default\"; got != want {\n\t\tt.Errorf(\"Want network %q, got %q\", want, got)\n\t}\n\tif !reflect.DeepEqual(p.scopes, defaultScopes) {\n\t\tt.Errorf(\"Want default scopes\")\n\t}\n\tif got, want := p.size, \"n1-standard-1\"; got != want {\n\t\tt.Errorf(\"Want size %q, got %q\", want, got)\n\t}\n\tif !reflect.DeepEqual(p.tags, defaultTags) {\n\t\tt.Errorf(\"Want default tags\")\n\t}\n\tif p.userdata != userdata.T {\n\t\tt.Errorf(\"Want default userdata template\")\n\t}\n\tif p.userdataKey != \"user-data\" {\n\t\tt.Errorf(\"Want default userdata key\")\n\t}\n\tif got, want := p.zones, []string{\"us-central1-a\"}; !reflect.DeepEqual(got, want) {\n\t\tt.Errorf(\"Want region %q, got %q\", want, got)\n\t}\n}\n"
  },
  {
    "path": "drivers/google/setup.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage google\n\nimport (\n\t\"context\"\n\t\"reflect\"\n\n\t\"github.com/drone/autoscaler/logger\"\n\n\tcompute \"google.golang.org/api/compute/v1\"\n)\n\nfunc (p *provider) setup(ctx context.Context) error {\n\tif reflect.DeepEqual(p.tags, defaultTags) {\n\t\treturn p.setupFirewall(ctx)\n\t}\n\treturn nil\n}\n\nfunc (p *provider) setupFirewall(ctx context.Context) error {\n\tlogger := logger.FromContext(ctx)\n\n\tlogger.Debugln(\"finding default firewall rules\")\n\n\t_, err := p.service.Firewalls.Get(p.project, \"default-allow-docker\").Context(ctx).Do()\n\tif err == nil {\n\t\tlogger.Debugln(\"found default firewall rule\")\n\t\treturn nil\n\t}\n\n\trule := &compute.Firewall{\n\t\tAllowed: []*compute.FirewallAllowed{\n\t\t\t{\n\t\t\t\tIPProtocol: \"tcp\",\n\t\t\t\tPorts:      []string{\"2376\"},\n\t\t\t},\n\t\t},\n\t\tDirection:    \"INGRESS\",\n\t\tName:         \"default-allow-docker\",\n\t\tNetwork:      p.network,\n\t\tPriority:     1000,\n\t\tSourceRanges: []string{\"0.0.0.0/0\"},\n\t\tTargetTags:   []string{\"allow-docker\"},\n\t}\n\n\top, err := p.service.Firewalls.Insert(p.project, rule).Context(ctx).Do()\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tErrorln(\"cannot create firewall operation\")\n\t\treturn err\n\t}\n\n\terr = p.waitGlobalOperation(ctx, op.Name)\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tErrorln(\"cannot create firewall rule\")\n\t}\n\n\treturn err\n}\n"
  },
  {
    "path": "drivers/google/setup_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage google\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/h2non/gock\"\n\tcompute \"google.golang.org/api/compute/v1\"\n)\n\nfunc TestSetupFirewall(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(\"https://compute.googleapis.com\").\n\t\tGet(\"/compute/v1/projects/my-project/global/firewalls/default-allow-docker\").\n\t\tReply(404)\n\n\tgock.New(\"https://compute.googleapis.com\").\n\t\tPost(\"/compute/v1/projects/my-project/global/firewalls\").\n\t\tJSON(createFirewallMock).\n\t\tReply(200).\n\t\tBodyString(`{ \"name\": \"operation-name\" }`)\n\n\tgock.New(\"https://compute.googleapis.com\").\n\t\tGet(\"/compute/v1/projects/my-project/global/operations/operation-name\").\n\t\tReply(200).\n\t\tBodyString(`{ \"status\": \"DONE\" }`)\n\n\tp, err := New(\n\t\tWithClient(http.DefaultClient),\n\t\tWithZones(\"us-central1-a\"),\n\t\tWithProject(\"my-project\"),\n\t)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\n\terr = p.(*provider).setupFirewall(context.TODO())\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc TestSetupFirewall_Exists(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(\"https://compute.googleapis.com\").\n\t\tGet(\"/compute/v1/projects/my-project/global/firewalls/default-allow-docker\").\n\t\tReply(200).\n\t\tBodyString(findFirewallRes)\n\n\tp, err := New(\n\t\tWithClient(http.DefaultClient),\n\t\tWithZones(\"us-central1-a\"),\n\t\tWithProject(\"my-project\"),\n\t)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\n\terr = p.(*provider).setupFirewall(context.TODO())\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nvar createFirewallMock = &compute.Firewall{\n\tAllowed: []*compute.FirewallAllowed{\n\t\t{\n\t\t\tIPProtocol: \"tcp\",\n\t\t\tPorts:      []string{\"2376\"},\n\t\t},\n\t},\n\tDirection:    \"INGRESS\",\n\tName:         \"default-allow-docker\",\n\tNetwork:      \"global/networks/default\",\n\tPriority:     1000,\n\tSourceRanges: []string{\"0.0.0.0/0\"},\n\tTargetTags:   []string{\"allow-docker\"},\n}\n\nvar findFirewallRes = `\n{\n  \"allowed\": [\n    {\n      \"IPProtocol\": \"tcp\",\n      \"ports\": [\n        \"2376\"\n      ]\n    }\n  ],\n  \"creationTimestamp\": \"2018-03-10T11:31:09.445-08:00\",\n  \"description\": \"\",\n  \"direction\": \"INGRESS\",\n  \"id\": \"3206167972979853122\",\n  \"kind\": \"compute#firewall\",\n  \"name\": \"default-allow-docker\",\n  \"network\": \"projects/my-project/global/networks/default\",\n  \"priority\": 1000,\n  \"selfLink\": \"projects/my-project/global/firewalls/default-allow-docker\",\n  \"sourceRanges\": [\n    \"0.0.0.0/0\"\n  ],\n  \"targetTags\": [\n    \"allow-docker\"\n  ]\n}\n`\n"
  },
  {
    "path": "drivers/hetznercloud/create.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage hetznercloud\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"strconv\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/logger\"\n\n\t\"github.com/hetznercloud/hcloud-go/hcloud\"\n)\n\nfunc (p *provider) Create(ctx context.Context, opts autoscaler.InstanceCreateOpts) (*autoscaler.Instance, error) {\n\tp.init.Do(func() {\n\t\tp.setup(ctx)\n\t})\n\n\tbuf := new(bytes.Buffer)\n\terr := p.userdata.Execute(buf, &opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq := hcloud.ServerCreateOpts{\n\t\tName:     opts.Name,\n\t\tUserData: buf.String(),\n\t\tServerType: &hcloud.ServerType{\n\t\t\tName: p.serverType,\n\t\t},\n\t\tImage: &hcloud.Image{\n\t\t\tName: p.image,\n\t\t},\n\t\tSSHKeys: []*hcloud.SSHKey{\n\t\t\t{\n\t\t\t\tID: p.key,\n\t\t\t},\n\t\t},\n\t}\n\n\tdatacenter := \"unknown\"\n\n\tif p.datacenter != \"\" {\n\t\treq.Datacenter = &hcloud.Datacenter{\n\t\t\tName: p.datacenter,\n\t\t}\n\n\t\tdatacenter = p.datacenter\n\t}\n\n\tlogger := logger.FromContext(ctx).\n\t\tWithField(\"datacenter\", datacenter).\n\t\tWithField(\"image\", req.Image.Name).\n\t\tWithField(\"serverType\", req.ServerType.Name).\n\t\tWithField(\"name\", req.Name)\n\n\tlogger.Debugln(\"instance create\")\n\n\tresp, _, err := p.client.Server.Create(ctx, req)\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tErrorln(\"cannot create instance\")\n\t\treturn nil, err\n\t}\n\n\tlogger.\n\t\tWithField(\"name\", req.Name).\n\t\tInfoln(\"instance created\")\n\n\treturn &autoscaler.Instance{\n\t\tProvider: autoscaler.ProviderHetznerCloud,\n\t\tID:       strconv.Itoa(resp.Server.ID),\n\t\tName:     resp.Server.Name,\n\t\tAddress:  resp.Server.PublicNet.IPv4.IP.String(),\n\t\tSize:     req.ServerType.Name,\n\t\tRegion:   datacenter,\n\t\tImage:    req.Image.Name,\n\t}, nil\n}\n"
  },
  {
    "path": "drivers/hetznercloud/create_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage hetznercloud\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/drone/autoscaler\"\n\n\t\"github.com/h2non/gock\"\n)\n\nfunc TestCreate(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(\"https://api.hetzner.cloud\").\n\t\tPost(\"/v1/servers\").\n\t\tReply(200).\n\t\tBodyString(respInstanceCreate)\n\n\tp := New(\n\t\tWithToken(\"LRK9DAWQ1ZAEFSrCNEEzLCUwhYX1U3g7wMg4dTlkkDC96fyDuyJ39nVbVjCKSDfj\"),\n\t).(*provider)\n\tp.init.Do(func() {}) // pre-initialize\n\n\tinstance, err := p.Create(context.TODO(), autoscaler.InstanceCreateOpts{Name: \"agent1\"})\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tt.Run(\"Attributes\", testInstance(instance))\n}\n\nfunc TestCreate_CreateError(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(\"https://api.hetzner.cloud\").\n\t\tPost(\"/v1/servers\").\n\t\tReply(500)\n\n\tp := New(\n\t\tWithToken(\"LRK9DAWQ1ZAEFSrCNEEzLCUwhYX1U3g7wMg4dTlkkDC96fyDuyJ39nVbVjCKSDfj\"),\n\t).(*provider)\n\tp.init.Do(func() {}) // pre-initialize\n\n\t_, err := p.Create(context.TODO(), autoscaler.InstanceCreateOpts{Name: \"agent1\"})\n\tif err == nil {\n\t\tt.Errorf(\"Expect error returned from hetzner cloud\")\n\t}\n}\n\nfunc testInstance(instance *autoscaler.Instance) func(t *testing.T) {\n\treturn func(t *testing.T) {\n\t\tif instance == nil {\n\t\t\tt.Errorf(\"Expect non-nil instance even if error\")\n\t\t}\n\t\tif got, want := instance.ID, \"544037\"; got != want {\n\t\t\tt.Errorf(\"Want instance ID %v, got %v\", want, got)\n\t\t}\n\t\tif got, want := instance.Image, \"ubuntu-20.04\"; got != want {\n\t\t\tt.Errorf(\"Want instance Image %v, got %v\", want, got)\n\t\t}\n\t\tif got, want := instance.Name, \"test\"; got != want {\n\t\t\tt.Errorf(\"Want instance Name %v, got %v\", want, got)\n\t\t}\n\t\tif got, want := instance.Region, \"unknown\"; got != want {\n\t\t\tt.Errorf(\"Want instance Region %v, got %v\", want, got)\n\t\t}\n\t\tif got, want := instance.Provider, autoscaler.ProviderHetznerCloud; got != want {\n\t\t\tt.Errorf(\"Want instance Provider %v, got %v\", want, got)\n\t\t}\n\t}\n}\n\nfunc testInstanceAddress(instance *autoscaler.Instance) func(t *testing.T) {\n\treturn func(t *testing.T) {\n\t\tif instance == nil {\n\t\t\tt.Errorf(\"Expect non-nil instance even if error\")\n\t\t}\n\t\tif got, want := instance.Address, \"195.201.93.137\"; got != want {\n\t\t\tt.Errorf(\"Want instance Address %v, got %v\", want, got)\n\t\t}\n\t}\n}\n\n// sample response for POST /v1/servers\nconst respInstanceCreate = `\n{\n  \"server\": {\n    \"id\": 544037,\n    \"name\": \"test\",\n    \"status\": \"initializing\",\n    \"created\": \"2018-03-02T08:44:07+00:00\",\n    \"public_net\": {\n      \"ipv4\": {\n        \"ip\": \"195.201.93.137\",\n        \"blocked\": false,\n        \"dns_ptr\": \"static.137.93.201.195.clients.your-server.de\"\n      },\n      \"ipv6\": {\n        \"ip\": \"2a01:4f8:1c0c:6996::/64\",\n        \"blocked\": false,\n        \"dns_ptr\": []\n      },\n      \"floating_ips\": []\n    },\n    \"server_type\": {\n      \"id\": 1,\n      \"name\": \"cx11\",\n      \"description\": \"CX11\",\n      \"cores\": 1,\n      \"memory\": 2.0,\n      \"disk\": 20,\n      \"prices\": [\n        {\n          \"location\": \"fsn1\",\n          \"price_hourly\": {\n            \"net\": \"0.0040000000\",\n            \"gross\": \"0.0047600000000000\"\n          },\n          \"price_monthly\": {\n            \"net\": \"2.4900000000\",\n            \"gross\": \"2.9631000000000000\"\n          }\n        },\n        {\n          \"location\": \"nbg1\",\n          \"price_hourly\": {\n            \"net\": \"0.0040000000\",\n            \"gross\": \"0.0047600000000000\"\n          },\n          \"price_monthly\": {\n            \"net\": \"2.4900000000\",\n            \"gross\": \"2.9631000000000000\"\n          }\n        }\n      ],\n      \"storage_type\": \"local\"\n    },\n    \"datacenter\": {\n      \"id\": 2,\n      \"name\": \"nbg1-dc3\",\n      \"description\": \"Nuremberg 1 DC 3\",\n      \"location\": {\n        \"id\": 2,\n        \"name\": \"nbg1\",\n        \"description\": \"Nuremberg DC Park 1\",\n        \"country\": \"DE\",\n        \"city\": \"Nuremberg\",\n        \"latitude\": 49.452102,\n        \"longitude\": 11.076665\n      },\n      \"server_types\": {\n        \"supported\": [\n          1,\n          2,\n          3,\n          4,\n          5,\n          6,\n          7,\n          8,\n          9,\n          10\n        ],\n        \"available\": [\n          1,\n          2,\n          3,\n          4,\n          5,\n          6,\n          7,\n          8,\n          9,\n          10\n        ]\n      }\n    },\n    \"image\": {\n      \"id\": 1,\n      \"type\": \"system\",\n      \"status\": \"available\",\n      \"name\": \"ubuntu-20.04\",\n      \"description\": \"Ubuntu 20.04\",\n      \"image_size\": null,\n      \"disk_size\": 5,\n      \"created\": \"2018-01-15T11:34:45+00:00\",\n      \"created_from\": null,\n      \"bound_to\": null,\n      \"os_flavor\": \"ubuntu\",\n      \"os_version\": \"20.04\",\n      \"rapid_deploy\": true\n    },\n    \"iso\": null,\n    \"rescue_enabled\": false,\n    \"locked\": false,\n    \"backup_window\": null,\n    \"outgoing_traffic\": 0,\n    \"ingoing_traffic\": 0,\n    \"included_traffic\": 21990232555520\n  },\n  \"action\": {\n    \"id\": 279192,\n    \"command\": \"create_server\",\n    \"status\": \"running\",\n    \"progress\": 0,\n    \"started\": \"2018-03-02T08:44:07+00:00\",\n    \"finished\": null,\n    \"resources\": [\n      {\n        \"id\": 544037,\n        \"type\": \"server\"\n      }\n    ],\n    \"error\": null\n  },\n  \"root_password\": null\n}\n`\n"
  },
  {
    "path": "drivers/hetznercloud/destroy.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage hetznercloud\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/logger\"\n\n\t\"github.com/hetznercloud/hcloud-go/hcloud\"\n)\n\nfunc (p *provider) Destroy(ctx context.Context, instance *autoscaler.Instance) error {\n\tlogger := logger.FromContext(ctx).\n\t\tWithField(\"region\", instance.Region).\n\t\tWithField(\"image\", instance.Image).\n\t\tWithField(\"size\", instance.Size).\n\t\tWithField(\"name\", instance.Name)\n\n\tid, err := strconv.Atoi(instance.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlogger.Debugln(\"deleting instance\")\n\n\t_, err = p.client.Server.Delete(ctx, &hcloud.Server{ID: id})\n\n\tif err != nil {\n\t\tif err.Error() == \"hcloud: server responded with status code 404\" {\n\t\t\tlogger.WithError(err).\n\t\t\t\tDebugln(\"instance does not exist\")\n\t\t\treturn autoscaler.ErrInstanceNotFound\n\t\t}\n\n\t\tlogger.WithError(err).\n\t\t\tErrorln(\"deleting instance failed\")\n\t\treturn err\n\t}\n\n\tlogger.Debugln(\"instance deleted\")\n\n\treturn nil\n}\n"
  },
  {
    "path": "drivers/hetznercloud/destroy_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage hetznercloud\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/drone/autoscaler\"\n\n\t\"github.com/h2non/gock\"\n)\n\nfunc TestDestroy(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(\"https://api.hetzner.cloud\").\n\t\tDelete(\"/v1/servers/3164494\").\n\t\tReply(200)\n\n\tmockContext := context.TODO()\n\tmockInstance := &autoscaler.Instance{\n\t\tID: \"3164494\",\n\t}\n\n\tp := New(\n\t\tWithToken(\"LRK9DAWQ1ZAEFSrCNEEzLCUwhYX1U3g7wMg4dTlkkDC96fyDuyJ39nVbVjCKSDfj\"),\n\t)\n\terr := p.Destroy(mockContext, mockInstance)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc TestDestroyDeleteError(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(\"https://api.hetzner.cloud\").\n\t\tDelete(\"/v1/servers/3164494\").\n\t\tReply(500)\n\n\tmockContext := context.TODO()\n\tmockInstance := &autoscaler.Instance{\n\t\tID: \"3164494\",\n\t}\n\n\tp := New(\n\t\tWithToken(\"LRK9DAWQ1ZAEFSrCNEEzLCUwhYX1U3g7wMg4dTlkkDC96fyDuyJ39nVbVjCKSDfj\"),\n\t)\n\terr := p.Destroy(mockContext, mockInstance)\n\tif err == nil {\n\t\tt.Errorf(\"Expect error returned from hetzner cloud\")\n\t}\n}\n\nfunc TestDestroyNotFound(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(\"https://api.hetzner.cloud\").\n\t\tDelete(\"/v1/servers/3164494\").\n\t\tReply(404).\n\t\tBodyString(destroyNotFoundResponse)\n\n\tmockContext := context.TODO()\n\tmockInstance := &autoscaler.Instance{\n\t\tID: \"3164494\",\n\t}\n\n\tp := New(\n\t\tWithToken(\"LRK9DAWQ1ZAEFSrCNEEzLCUwhYX1U3g7wMg4dTlkkDC96fyDuyJ39nVbVjCKSDfj\"),\n\t)\n\n\terr := p.Destroy(mockContext, mockInstance)\n\tif err == nil {\n\t\tt.Errorf(\"Expect error returned from hetzner cloud\")\n\t}\n\tif err != autoscaler.ErrInstanceNotFound {\n\t\tt.Errorf(\"Expect instance not found returned from hetzner cloud\")\n\t}\n}\n\nfunc TestDestroyInvalidInput(t *testing.T) {\n\ti := &autoscaler.Instance{}\n\tp := provider{}\n\terr := p.Destroy(context.TODO(), i)\n\tif _, ok := err.(*strconv.NumError); !ok {\n\t\tt.Errorf(\"Expected invalid or missing ID error\")\n\t}\n}\n\nvar destroyNotFoundResponse = `{\n  \"error\": {\n    \"message\": \"server with ID '3164494' not found\",\n    \"code\": \"not_found\",\n    \"details\": null\n  }\n}`\n"
  },
  {
    "path": "drivers/hetznercloud/option.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage hetznercloud\n\nimport (\n\t\"io/ioutil\"\n\n\t\"github.com/drone/autoscaler/drivers/internal/userdata\"\n\t\"github.com/hetznercloud/hcloud-go/hcloud\"\n)\n\n// Option configures a Digital Ocean provider option.\ntype Option func(*provider)\n\n// WithClient returns an option to set the Hetzner client.\nfunc WithClient(client *hcloud.Client) Option {\n\treturn func(p *provider) {\n\t\tp.client = client\n\t}\n}\n\n// WithDatacenter returns an option to set the datacenter.\nfunc WithDatacenter(datacenter string) Option {\n\treturn func(p *provider) {\n\t\tp.datacenter = datacenter\n\t}\n}\n\n// WithImage returns an option to set the image.\nfunc WithImage(image string) Option {\n\treturn func(p *provider) {\n\t\tp.image = image\n\t}\n}\n\n// WithServerType returns an option to set the server type.\nfunc WithServerType(serverType string) Option {\n\treturn func(p *provider) {\n\t\tp.serverType = serverType\n\t}\n}\n\n// WithSSHKey returns an option to set the ssh key.\nfunc WithSSHKey(key int) Option {\n\treturn func(p *provider) {\n\t\tp.key = key\n\t}\n}\n\n// WithToken returns an option to set the auth token.\nfunc WithToken(token string) Option {\n\treturn WithClient(\n\t\thcloud.NewClient(\n\t\t\thcloud.WithToken(\n\t\t\t\ttoken,\n\t\t\t),\n\t\t),\n\t)\n}\n\n// WithUserData returns an option to set the cloud-init\n// template from text.\nfunc WithUserData(text string) Option {\n\treturn func(p *provider) {\n\t\tif text != \"\" {\n\t\t\tp.userdata = userdata.Parse(text)\n\t\t}\n\t}\n}\n\n// WithUserDataFile returns an option to set the cloud-init\n// template from file.\nfunc WithUserDataFile(filepath string) Option {\n\treturn func(p *provider) {\n\t\tif filepath != \"\" {\n\t\t\tb, err := ioutil.ReadFile(filepath)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tp.userdata = userdata.Parse(string(b))\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "drivers/hetznercloud/option_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage hetznercloud\n\nimport \"testing\"\n\nfunc TestOptions(t *testing.T) {\n\tp := New(\n\t\tWithImage(\"ubuntu-17.04\"),\n\t\tWithDatacenter(\"fsn1-dc8\"),\n\t\tWithServerType(\"cx20\"),\n\t\tWithSSHKey(23234),\n\t).(*provider)\n\n\tif got, want := p.image, \"ubuntu-17.04\"; got != want {\n\t\tt.Errorf(\"Want image %q, got %q\", want, got)\n\t}\n\tif got, want := p.datacenter, \"fsn1-dc8\"; got != want {\n\t\tt.Errorf(\"Want region %q, got %q\", want, got)\n\t}\n\tif got, want := p.serverType, \"cx20\"; got != want {\n\t\tt.Errorf(\"Want serverType %q, got %q\", want, got)\n\t}\n\tif got, want := p.key, 23234; got != want {\n\t\tt.Errorf(\"Want key %d, got %d\", want, got)\n\t}\n}\n"
  },
  {
    "path": "drivers/hetznercloud/provider.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage hetznercloud\n\nimport (\n\t\"sync\"\n\t\"text/template\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/drivers/internal/userdata\"\n\n\t\"github.com/hetznercloud/hcloud-go/hcloud\"\n)\n\n// provider implement a Hetzner Cloud provider.\ntype provider struct {\n\tinit sync.Once\n\n\ttoken      string\n\tdatacenter string\n\tserverType string\n\timage      string\n\tuserdata   *template.Template\n\tkey        int\n\n\tclient *hcloud.Client\n}\n\n// New returns a new Digital Ocean provider.\nfunc New(opts ...Option) autoscaler.Provider {\n\tp := new(provider)\n\tfor _, opt := range opts {\n\t\topt(p)\n\t}\n\tif p.serverType == \"\" {\n\t\tp.serverType = \"cx11\"\n\t}\n\tif p.image == \"\" {\n\t\tp.image = \"ubuntu-20.04\"\n\t}\n\tif p.userdata == nil {\n\t\tp.userdata = userdata.T\n\t}\n\treturn p\n}\n"
  },
  {
    "path": "drivers/hetznercloud/provider_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage hetznercloud\n\nimport \"testing\"\n\nfunc TestDefaults(t *testing.T) {\n\tp := New().(*provider)\n\tif got, want := p.image, \"ubuntu-20.04\"; got != want {\n\t\tt.Errorf(\"Want image %q, got %q\", want, got)\n\t}\n\tif got, want := p.datacenter, \"\"; got != want {\n\t\tt.Errorf(\"Want datacenter %q, got %q\", want, got)\n\t}\n\tif got, want := p.serverType, \"cx11\"; got != want {\n\t\tt.Errorf(\"Want server type %q, got %q\", want, got)\n\t}\n}\n"
  },
  {
    "path": "drivers/hetznercloud/setup.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage hetznercloud\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/drone/autoscaler/logger\"\n\t\"github.com/hetznercloud/hcloud-go/hcloud\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\nfunc (p *provider) setup(ctx context.Context) error {\n\tvar g errgroup.Group\n\tif p.key == 0 {\n\t\tg.Go(func() error {\n\t\t\treturn p.setupKeypair(ctx)\n\t\t})\n\t}\n\treturn g.Wait()\n}\n\nfunc (p *provider) setupKeypair(ctx context.Context) error {\n\tlogger := logger.FromContext(ctx)\n\n\tlogger.Debugln(\"finding default ssh key\")\n\n\tkeys, _, err := p.client.SSHKey.List(ctx, hcloud.SSHKeyListOpts{})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tindex := map[string]*hcloud.SSHKey{}\n\tfor _, key := range keys {\n\t\tindex[key.Name] = key\n\t}\n\n\t// if the account has multiple keys configured we will\n\t// attempt to use an existing key based on naming convention.\n\tfor _, name := range []string{\"drone\", \"id_rsa_drone\"} {\n\t\tkey, ok := index[name]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tp.key = key.ID\n\n\t\tlogger.\n\t\t\tWithField(\"name\", name).\n\t\t\tWithField(\"fingerprint\", key.Fingerprint).\n\t\t\tDebugln(\"using default ssh key\")\n\t\treturn nil\n\t}\n\n\t// if there were no matches but the account has at least\n\t// one keypair already created we will select the first\n\t// in the list.\n\tif len(keys) > 0 {\n\t\tkey := keys[0]\n\t\tp.key = key.ID\n\n\t\tlogger.\n\t\t\tWithField(\"name\", key.Name).\n\t\t\tWithField(\"fingerprint\", key.Fingerprint).\n\t\t\tDebugln(\"using default ssh key\")\n\t\treturn nil\n\t}\n\n\treturn errors.New(\"No matching keys\")\n}\n"
  },
  {
    "path": "drivers/hetznercloud/setup_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage hetznercloud\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/h2non/gock\"\n)\n\nfunc TestSetupKey_ChooseFirst(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(\"https://api.hetzner.cloud\").\n\t\tGet(\"/v1/ssh_keys\").\n\t\tReply(200).\n\t\tBodyString(respSingleKey)\n\n\tp := New(\n\t\tWithToken(\"LRK9DAWQ1ZAEFSrCNEEzLCUwhYX1U3g7wMg4dTlkkDC96fyDuyJ39nVbVjCKSDfj\"),\n\t).(*provider)\n\n\terr := p.setup(context.TODO())\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif got, want := p.key, 2323; got != want {\n\t\tt.Errorf(\"Want key id %d, got %d\", want, got)\n\t}\n}\n\nfunc TestSetupKey_ChooseMatch(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(\"https://api.hetzner.cloud\").\n\t\tGet(\"/v1/ssh_keys\").\n\t\tReply(200).\n\t\tBodyString(respMultiKey)\n\n\tp := New(\n\t\tWithToken(\"LRK9DAWQ1ZAEFSrCNEEzLCUwhYX1U3g7wMg4dTlkkDC96fyDuyJ39nVbVjCKSDfj\"),\n\t).(*provider)\n\n\terr := p.setup(context.TODO())\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif got, want := p.key, 2324; got != want {\n\t\tt.Errorf(\"Want key id %d, got %d\", want, got)\n\t}\n}\n\nconst respSingleKey = `\n{\n  \"ssh_keys\": [\n    {\n      \"id\": 2323,\n      \"name\": \"My ssh key\",\n      \"fingerprint\": \"b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f\",\n      \"public_key\": \"ssh-rsa AAAjjk76kgf...Xt\"\n    }\n  ]\n}\n`\n\nconst respMultiKey = `\n{\n  \"ssh_keys\": [\n    {\n      \"id\": 2323,\n      \"name\": \"My ssh key\",\n      \"fingerprint\": \"b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f\",\n      \"public_key\": \"ssh-rsa AAAjjk76kgf...Xt\"\n    },\n    {\n      \"id\": 2324,\n      \"name\": \"drone\",\n      \"fingerprint\": \"b7:2f:30:a0:2f:6c:58:6c:21:04:58:61:ba:06:3b:2f\",\n      \"public_key\": \"ssh-rsa AAAjjk76kgf...Xt\"\n    }\n  ]\n}\n`\n"
  },
  {
    "path": "drivers/internal/userdata/userdata.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage userdata\n\nimport (\n\t\"encoding/base64\"\n\t\"text/template\"\n\n\t\"github.com/drone/funcmap\"\n)\n\nvar funcs = map[string]interface{}{\n\t\"base64\": func(src []byte) string {\n\t\treturn base64.StdEncoding.EncodeToString(src)\n\t},\n}\n\n// Parse parses the userdata template.\nfunc Parse(text string) *template.Template {\n\tif decoded, err := base64.StdEncoding.DecodeString(text); err == nil {\n\t\treturn template.Must(\n\t\t\ttemplate.New(\"_\").Funcs(funcs).Funcs(funcmap.Funcs).Parse(string(decoded)),\n\t\t)\n\t}\n\n\treturn template.Must(\n\t\ttemplate.New(\"_\").Funcs(funcs).Funcs(funcmap.Funcs).Parse(text),\n\t)\n}\n\n// T is the default userdata template.\nvar T = Parse(`#cloud-config\n\napt_reboot_if_required: false\npackage_update: false\npackage_upgrade: false\n\napt:\n  sources:\n    docker.list:\n      source: deb [arch=amd64] https://download.docker.com/linux/ubuntu $RELEASE stable\n      keyid: 0EBFCD88\n\npackages:\n  - docker-ce\n\nwrite_files:\n  - path: /etc/systemd/system/docker.service.d/override.conf\n    content: |\n      [Service]\n      ExecStart=\n      ExecStart=/usr/bin/dockerd\n  - path: /etc/default/docker\n    content: |\n      DOCKER_OPTS=\"\"\n  - path: /etc/docker/daemon.json\n    content: |\n      {\n        \"hosts\": [ \"0.0.0.0:2376\", \"unix:///var/run/docker.sock\" ],\n        \"tls\": true,\n        \"tlsverify\": true,\n        \"tlscacert\": \"/etc/docker/ca.pem\",\n        \"tlscert\": \"/etc/docker/server-cert.pem\",\n        \"tlskey\": \"/etc/docker/server-key.pem\"\n      }\n  - path: /etc/docker/ca.pem\n    encoding: b64\n    content: {{ .CACert | base64 }}\n  - path: /etc/docker/server-cert.pem\n    encoding: b64\n    content: {{ .TLSCert | base64 }}\n  - path: /etc/docker/server-key.pem\n    encoding: b64\n    content: {{ .TLSKey | base64 }}\n\nruncmd:\n  - [ systemctl, daemon-reload ]\n  - [ systemctl, restart, docker ]\n`)\n"
  },
  {
    "path": "drivers/internal/userdata/userdata_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage userdata\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/drone/autoscaler\"\n)\n\nfunc TestUserdata(t *testing.T) {\n\tbuf := new(bytes.Buffer)\n\terr := T.Execute(buf, &autoscaler.InstanceCreateOpts{\n\t\tName:    \"agent-123456\",\n\t\tCACert:  []byte(dummyCA),\n\t\tTLSKey:  []byte(dummykey),\n\t\tTLSCert: []byte(dummyCert),\n\t})\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n}\n\nfunc TestUserdataFuncmap(t *testing.T) {\n\tbuf := new(bytes.Buffer)\n\terr := UD.Execute(buf, &map[string]interface{}{\n\t\t\"Content\": \"foo\",\n\t})\n\tfmt.Println(buf.String())\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tif buf.String() != UDExpected {\n\t\tt.Errorf(\"expected '%s', got '%s'\", UDExpected, buf.String())\n\t}\n}\n\nvar dummyCA = `-----BEGIN CERTIFICATE-----\nMIIGOTCCBCGgAwIBAgIJAOE/vJd8EB24MA0GCSqGSIb3DQEBBQUAMIGyMQswCQYD\nVQQGEwJGUjEPMA0GA1UECAwGQWxzYWNlMRMwEQYDVQQHDApTdHJhc2JvdXJnMRgw\nFgYDVQQKDA93d3cuZnJlZWxhbi5vcmcxEDAOBgNVBAsMB2ZyZWVsYW4xLTArBgNV\nBAMMJEZyZWVsYW4gU2FtcGxlIENlcnRpZmljYXRlIEF1dGhvcml0eTEiMCAGCSqG\nKvbxUcDaVvXB0EU0bg==\n-----END CERTIFICATE-----`\n\nvar dummykey = `-----BEGIN RSA PRIVATE KEY-----\nMIIJKQIBAAKCAgEA3W29+ID6194bH6ejLrIC4hb2Ugo8v6ZC+Mrck2dNYMNPjcOK\nABvxxEtBamnSaeU/IY7FC/giN622LEtV/3oDcrua0+yWuVafyxmZyTKUb4/GUgaf\nRQPf/eiX9urWurtIK7XgNGFNUjYPq4dSJQPPhwCHE/LKAykWnZBXRrX0Dq4XyApN\nku0IpjIjEXH+8ixE12wH8wt7DEvdO7T3N3CfUbaITl1qBX+Nm2Z6q4Ag/u5rl8NJ\nv3TGd3xXD9yQIjmugNgxNiwAZzhJs/ZJy++fPSJ1XQxbd9qPghgGoe/ff6G7\n-----END RSA PRIVATE KEY-----`\n\nvar dummyCert = `-----BEGIN CERTIFICATE-----\nMIIGJzCCBA+gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBsjELMAkGA1UEBhMCRlIx\nd3d3LmZyZWVsYW4ub3JnMRAwDgYDVQQLDAdmcmVlbGFuMS0wKwYDVQQDDCRGcmVl\nbGFuIFNhbXBsZSBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxIjAgBgkqhkiG9w0BCQEW\nE2NvbnRhY3RAZnJlZWxhbi5vcmcwHhcNMTIwNDI3MTAzMTE4WhcNMjIwNDI1MTAz\nDiH5uEqBXExjrj0FslxcVKdVj5glVcSmkLwZKbEU1OKwleT/iXFhvooWhQ==\n-----END CERTIFICATE-----`\n\nvar UD = Parse(`#cloud-config\n\napt_reboot_if_required: \npackage_update: false\npackage_upgrade: false\n\nwrite_files:\n  - path: /etc/systemd/system/docker.service.d/override.conf\n    content: | {{nindent .Content 6 }}\n`)\n\nvar UDExpected = `#cloud-config\n\napt_reboot_if_required: \npackage_update: false\npackage_upgrade: false\n\nwrite_files:\n  - path: /etc/systemd/system/docker.service.d/override.conf\n    content: | \n      foo\n`\n"
  },
  {
    "path": "drivers/openstack/create.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage openstack\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/logger\"\n\t\"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips\"\n\t\"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs\"\n\t\"github.com/gophercloud/gophercloud/openstack/compute/v2/servers\"\n\t\"github.com/gophercloud/gophercloud/openstack/networking/v2/networks\"\n\t\"github.com/gophercloud/gophercloud/pagination\"\n)\n\n// Create creates an OpenStack instance\nfunc (p *provider) Create(ctx context.Context, opts autoscaler.InstanceCreateOpts) (*autoscaler.Instance, error) {\n\tp.init.Do(func() {\n\t\t_ = p.setup(ctx)\n\t})\n\n\tbuf := new(bytes.Buffer)\n\terr := p.userdata.Execute(buf, &opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tlogger := logger.FromContext(ctx).\n\t\tWithField(\"region\", p.region).\n\t\tWithField(\"image\", p.image).\n\t\tWithField(\"flavor\", p.flavor).\n\t\tWithField(\"network\", p.network).\n\t\tWithField(\"pool\", p.pool).\n\t\tWithField(\"name\", opts.Name)\n\n\tlogger.Debugln(\"instance create\")\n\n\tnets := make([]servers.Network, 0)\n\n\tif p.network != \"\" {\n\t\tnetwork, err := networks.Get(p.networkClient, p.network).Extract()\n\t\tif err != nil {\n\t\t\tlogger.WithError(err).\n\t\t\t\tDebugln(\"failed to find network\")\n\t\t\treturn nil, err\n\t\t}\n\n\t\tnets = append(nets, servers.Network{\n\t\t\tUUID: network.ID,\n\t\t})\n\t}\n\n\tserverCreateOpts := servers.CreateOpts{\n\t\tName:           opts.Name,\n\t\tImageRef:       p.image,\n\t\tFlavorRef:      p.flavor,\n\t\tNetworks:       nets,\n\t\tUserData:       buf.Bytes(),\n\t\tServiceClient:  p.computeClient,\n\t\tMetadata:       p.metadata,\n\t\tSecurityGroups: p.groups,\n\t}\n\tcreateOpts := keypairs.CreateOptsExt{\n\t\tCreateOptsBuilder: serverCreateOpts,\n\t\tKeyName:           p.key,\n\t}\n\tserver, err := servers.Create(p.computeClient, createOpts).Extract()\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tDebugln(\"failed to create server\")\n\t\treturn nil, err\n\t}\n\n\terr = servers.WaitForStatus(p.computeClient, server.ID, \"ACTIVE\", 300)\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tDebugln(\"failed waiting for server\")\n\t\treturn nil, err\n\t}\n\n\tinstance := &autoscaler.Instance{\n\t\tProvider: autoscaler.ProviderOpenStack,\n\t\tID:       server.ID,\n\t\tName:     server.Name,\n\t\tRegion:   p.region,\n\t\tImage:    p.image,\n\t\tSize:     p.flavor,\n\t}\n\n\tif p.network != \"\" {\n\t\tnetwork, err := networks.Get(p.networkClient, p.network).Extract()\n\t\tif err != nil {\n\t\t\tlogger.WithError(err).\n\t\t\t\tDebugln(\"failed to find network\")\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif err := servers.ListAddresses(p.computeClient, server.ID).EachPage(func(page pagination.Page) (bool, error) {\n\t\t\tresult, err := servers.ExtractAddresses(page)\n\t\t\tif err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\n\t\t\tfor name, addresses := range result {\n\t\t\t\tif name == network.Name {\n\t\t\t\t\tfor _, address := range addresses {\n\t\t\t\t\t\tinstance.Address = address.Address\n\t\t\t\t\t\treturn true, nil\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t\treturn false, nil\n\t\t}); err != nil {\n\t\t\tlogger.WithError(err).\n\t\t\t\tDebugln(\"failed to fetch address\")\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif p.pool != \"\" {\n\t\tip, err := floatingips.Create(p.computeClient, floatingips.CreateOpts{\n\t\t\tPool: p.pool,\n\t\t}).Extract()\n\t\tif err != nil {\n\t\t\tlogger.WithError(err).\n\t\t\t\tDebugln(\"failed to create floating ip\")\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif err := floatingips.AssociateInstance(p.computeClient, server.ID, floatingips.AssociateOpts{\n\t\t\tFloatingIP: ip.IP,\n\t\t}).ExtractErr(); err != nil {\n\t\t\tlogger.WithError(err).\n\t\t\t\tDebugln(\"failed to associate floating ip\")\n\t\t\treturn nil, err\n\t\t}\n\n\t\tinstance.Address = ip.IP\n\t}\n\n\tlogger.\n\t\tWithField(\"name\", instance.Name).\n\t\tWithField(\"ip\", instance.Address).\n\t\tDebugln(\"instance network ready\")\n\n\treturn instance, nil\n}\n"
  },
  {
    "path": "drivers/openstack/create_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage openstack\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/h2non/gock\"\n)\n\nfunc TestCreate(t *testing.T) {\n\tdefer gock.Off()\n\tsetupEnv(t)\n\n\tauthResp1 := helperLoad(t, \"authresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tGet(\"/identity\").\n\t\tReply(300).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tBodyString(string(authResp1))\n\n\ttokenResp1 := helperLoad(t, \"tokenresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tPost(\"/identity/v3/auth/tokens\").\n\t\tReply(201).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"X-Subject-Token\", authToken).\n\t\tBodyString(string(tokenResp1))\n\n\tauthResp2 := helperLoad(t, \"authresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tGet(\"/identity\").\n\t\tReply(300).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tBodyString(string(authResp2))\n\n\ttokenResp2 := helperLoad(t, \"tokenresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tPost(\"/identity/v3/auth/tokens\").\n\t\tReply(201).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"X-Subject-Token\", authToken).\n\t\tBodyString(string(tokenResp2))\n\n\tfipResp1 := helperLoad(t, \"fipresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tPost(\"/compute/v2.1/os-floating-ips\").\n\t\tMatchHeader(\"X-Auth-Token\", authToken).\n\t\tReply(200).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tBodyString(string(fipResp1))\n\n\timageListResp := helperLoad(t, \"imagelistresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tGet(\"/compute/v2.1/images/detail\").\n\t\tMatchHeader(\"X-Auth-Token\", authToken).\n\t\tReply(200).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tBodyString(string(imageListResp))\n\n\tflavorListResp1 := helperLoad(t, \"flavorlistresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tGet(\"/compute/v2.1/flavors/detail\").\n\t\tMatchHeader(\"X-Auth-Token\", authToken).\n\t\tReply(200).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tBodyString(string(flavorListResp1))\n\n\tserverCreateResp1 := helperLoad(t, \"servercreateresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tPost(\"/compute/v2.1/servers\").\n\t\tMatchHeader(\"X-Auth-Token\", authToken).\n\t\tReply(202).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tBodyString(string(serverCreateResp1))\n\n\tserverStatusResp1 := helperLoad(t, \"serverstatusresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tGet(\"/compute/v2.1/servers/56046f6d-3184-495b-938b-baa450db970d\").\n\t\tMatchHeader(\"X-Auth-Token\", authToken).\n\t\tReply(200).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tBodyString(string(serverStatusResp1))\n\n\tassociateResp1 := helperLoad(t, \"associateresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tPost(\"/compute/v2.1/servers/56046f6d-3184-495b-938b-baa450db970d/action\").\n\t\tMatchHeader(\"X-Auth-Token\", authToken).\n\t\tBodyString(string(\"{\\\"addFloatingIp\\\":{\\\"address\\\":\\\"172.24.4.5\\\"}}\")).\n\t\tReply(202).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tBodyString(string(associateResp1))\n\n\tv, err := New(\n\t\tWithRegion(\"RegionOne\"),\n\t\tWithFlavor(\"m1.small\"),\n\t\tWithImage(\"ubuntu-16.04-server-latest\"),\n\t\tWithFloatingIpPool(\"public\"),\n\t\tWithSSHKey(\"drone-ci-key\"),\n\t)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tp := v.(*provider)\n\tp.init.Do(func() {}) // prevent init function\n\n\tinstance, err := p.Create(context.TODO(), autoscaler.InstanceCreateOpts{Name: \"agent-RjISb5v1\"})\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif !gock.IsDone() {\n\t\tt.Error(\"Not all expected http requests completed\")\n\t}\n\tt.Run(\"Instance Attributes\", testInstance(instance))\n}\n\nfunc TestAuthFail(t *testing.T) {\n\tdefer gock.Off()\n\tsetupEnv(t)\n\n\terr := os.Setenv(\"OS_PASSWORD\", \"BAADF00D\")\n\tif err != nil {\n\t\tt.Error(\"Unable to set OS_PASSWORD\")\n\t}\n\n\tauthResp1 := helperLoad(t, \"authresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tGet(\"/identity\").\n\t\tReply(300).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tBodyString(string(authResp1))\n\n\tgock.New(\"http://ops.my.cloud\").\n\t\tPost(\"/identity/v3/auth/tokens\").\n\t\tReply(401)\n\n\t_, err = New(\n\t\tWithRegion(\"RegionOne\"),\n\t\tWithFlavor(\"m1.small\"),\n\t\tWithImage(\"ubuntu-16.04-server-latest\"),\n\t\tWithFloatingIpPool(\"public\"),\n\t\tWithSSHKey(\"drone-ci-key\"),\n\t)\n\n\tif err == nil {\n\t\tt.Error(\"Expected authentication error from OpenStack\")\n\t}\n\n\tif !gock.IsDone() {\n\t\tt.Error(\"Not all expected http requests completed\")\n\t}\n}\n\nfunc TestCreateFail(t *testing.T) {\n\tdefer gock.Off()\n\tsetupEnv(t)\n\n\tauthResp1 := helperLoad(t, \"authresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tGet(\"/identity\").\n\t\tReply(300).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tBodyString(string(authResp1))\n\n\ttokenResp1 := helperLoad(t, \"tokenresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tPost(\"/identity/v3/auth/tokens\").\n\t\tReply(201).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"X-Subject-Token\", authToken).\n\t\tBodyString(string(tokenResp1))\n\n\tauthResp2 := helperLoad(t, \"authresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tGet(\"/identity\").\n\t\tReply(300).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tBodyString(string(authResp2))\n\n\ttokenResp2 := helperLoad(t, \"tokenresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tPost(\"/identity/v3/auth/tokens\").\n\t\tReply(201).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"X-Subject-Token\", authToken).\n\t\tBodyString(string(tokenResp2))\n\n\timageListResp := helperLoad(t, \"imagelistresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tGet(\"/compute/v2.1/images/detail\").\n\t\tMatchHeader(\"X-Auth-Token\", authToken).\n\t\tReply(200).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tBodyString(string(imageListResp))\n\n\tflavorListResp1 := helperLoad(t, \"flavorlistresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tGet(\"/compute/v2.1/flavors/detail\").\n\t\tMatchHeader(\"X-Auth-Token\", authToken).\n\t\tReply(200).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tBodyString(string(flavorListResp1))\n\n\tgock.New(\"http://ops.my.cloud\").\n\t\tPost(\"/compute/v2.1/servers\").\n\t\tMatchHeader(\"X-Auth-Token\", authToken).\n\t\tReply(500)\n\n\tv, err := New(\n\t\tWithRegion(\"RegionOne\"),\n\t\tWithFlavor(\"m1.small\"),\n\t\tWithImage(\"ubuntu-16.04-server-latest\"),\n\t\tWithFloatingIpPool(\"public\"),\n\t\tWithSSHKey(\"drone-ci-key\"),\n\t)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tp := v.(*provider)\n\tp.init.Do(func() {}) // prevent init function\n\n\t_, err = p.Create(context.TODO(), autoscaler.InstanceCreateOpts{Name: \"agent-RjISb5v1\"})\n\tif err == nil {\n\t\tt.Error(\"Expected error creating instance\")\n\t}\n\n\tif !gock.IsDone() {\n\t\tt.Error(\"Not all expected http requests completed\")\n\t}\n}\n\nfunc setupEnv(t *testing.T) {\n\terr := os.Setenv(\"OS_AUTH_URL\", \"http://ops.my.cloud/identity\")\n\tif err != nil {\n\t\tt.Error(\"Unable to set OS_AUTH_URL\")\n\t}\n\terr = os.Setenv(\"OS_USERNAME\", \"admin\")\n\tif err != nil {\n\t\tt.Error(\"Unable to set OS_USERNAME\")\n\t}\n\terr = os.Setenv(\"OS_PASSWORD\", \"admin\")\n\tif err != nil {\n\t\tt.Error(\"Unable to set OS_USERNAME\")\n\t}\n\terr = os.Setenv(\"OS_DOMAIN_NAME\", \"demo\")\n\tif err != nil {\n\t\tt.Error(\"Unable to set OS_DOMAIN_NAME\")\n\t}\n}\n\nfunc testInstance(instance *autoscaler.Instance) func(t *testing.T) {\n\treturn func(t *testing.T) {\n\t\tif instance == nil {\n\t\t\tt.Errorf(\"Expect non-nil instance even if error\")\n\t\t}\n\t\tif want, got := instance.Address, \"172.24.4.5\"; got != want {\n\t\t\tt.Errorf(\"Want instance IP %q, got %q\", want, got)\n\t\t}\n\t\tif want, got := instance.Image, \"4ef19958-ee2d-44a7-a100-de0b8afdbc8e\"; got != want {\n\t\t\tt.Errorf(\"Want instance ID %q, got %q\", want, got)\n\t\t}\n\t\tif want, got := instance.ID, \"56046f6d-3184-495b-938b-baa450db970d\"; got != want {\n\t\t\tt.Errorf(\"Want instance ID %q, got %q\", want, got)\n\t\t}\n\t\tif want, got := instance.Name, \"agent-RjISb5v1\"; got != want {\n\t\t\tt.Errorf(\"Want instance Name %q, got %q\", want, got)\n\t\t}\n\t\tif want, got := instance.Provider, autoscaler.ProviderOpenStack; got != want {\n\t\t\tt.Errorf(\"Want OpenStack Provider type\")\n\t\t}\n\t\tif want, got := instance.Region, \"RegionOne\"; got != want {\n\t\t\tt.Errorf(\"Want instance Region %q, got %q\", want, got)\n\t\t}\n\t\tif want, got := instance.Size, \"29e3cce3-d771-4220-80fe-3edf0e8dd466\"; got != want {\n\t\t\tt.Errorf(\"Want instance Size %q, got %q\", want, got)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "drivers/openstack/destroy.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage openstack\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/logger\"\n\n\t\"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips\"\n\t\"github.com/gophercloud/gophercloud/openstack/compute/v2/servers\"\n\t\"github.com/gophercloud/gophercloud/pagination\"\n)\n\nfunc (p *provider) Destroy(ctx context.Context, instance *autoscaler.Instance) error {\n\tlogger := logger.FromContext(ctx).\n\t\tWithField(\"region\", instance.Region).\n\t\tWithField(\"image\", instance.Image).\n\t\tWithField(\"flavor\", instance.Size).\n\t\tWithField(\"name\", instance.Name)\n\n\tlogger.Debugln(\"deleting instance\")\n\n\terr := p.deleteFloatingIps(instance)\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tDebugln(\"failed to delete floating ips\")\n\n\t\treturn err\n\t}\n\n\terr = servers.Delete(p.computeClient, instance.ID).ExtractErr()\n\tif err == nil {\n\t\tlogger.Debugln(\"instance deleted\")\n\t\treturn nil\n\t}\n\n\tif err.Error() == \"Resource not found\" {\n\t\tlogger.WithError(err).\n\t\t\tDebugln(\"instance does not exist\")\n\t\treturn autoscaler.ErrInstanceNotFound\n\t}\n\n\tlogger.WithError(err).\n\t\tErrorln(\"attempting to force delete\")\n\n\terr = servers.ForceDelete(p.computeClient, instance.ID).ExtractErr()\n\tif err == nil {\n\t\tlogger.Debugln(\"instance deleted\")\n\t\treturn nil\n\t}\n\n\tif err.Error() == \"Resource not found\" {\n\t\tlogger.WithError(err).\n\t\t\tDebugln(\"instance does not exist\")\n\t\treturn autoscaler.ErrInstanceNotFound\n\t}\n\n\tlogger.WithError(err).\n\t\tErrorln(\"force-deleting instance failed\")\n\n\treturn err\n}\n\nfunc (p *provider) deleteFloatingIps(instance *autoscaler.Instance) error {\n\treturn floatingips.List(p.computeClient).EachPage(func(page pagination.Page) (bool, error) {\n\t\tips, err := floatingips.ExtractFloatingIPs(page)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\tfor _, ip := range ips {\n\t\t\tif ip.InstanceID == instance.ID {\n\t\t\t\tif err := floatingips.DisassociateInstance(p.computeClient, instance.ID, floatingips.DisassociateOpts{\n\t\t\t\t\tFloatingIP: ip.IP,\n\t\t\t\t}).ExtractErr(); err != nil {\n\t\t\t\t\treturn false, fmt.Errorf(\"failed to disassociate floating ip: %s\", err)\n\t\t\t\t}\n\n\t\t\t\tif err := floatingips.Delete(p.computeClient, ip.ID).ExtractErr(); err != nil {\n\t\t\t\t\treturn false, fmt.Errorf(\"failed to delete floating ip: %s\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn true, nil\n\t})\n}\n"
  },
  {
    "path": "drivers/openstack/destroy_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage openstack\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/h2non/gock\"\n)\n\nfunc TestDestroy(t *testing.T) {\n\tdefer gock.Off()\n\tsetupEnv(t)\n\n\tauthResp1 := helperLoad(t, \"authresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tGet(\"/identity\").\n\t\tReply(300).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tBodyString(string(authResp1))\n\n\ttokenResp1 := helperLoad(t, \"tokenresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tPost(\"/identity/v3/auth/tokens\").\n\t\tReply(201).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"X-Subject-Token\", authToken).\n\t\tBodyString(string(tokenResp1))\n\n\tauthResp2 := helperLoad(t, \"authresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tGet(\"/identity\").\n\t\tReply(300).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tBodyString(string(authResp2))\n\n\ttokenResp2 := helperLoad(t, \"tokenresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tPost(\"/identity/v3/auth/tokens\").\n\t\tReply(201).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tSetHeader(\"X-Subject-Token\", authToken).\n\t\tBodyString(string(tokenResp2))\n\n\tfipResp1 := helperLoad(t, \"fipresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tMatchHeader(\"X-Auth-Token\", authToken).\n\t\tGet(\"/compute/v2.1/os-floating-ips\").\n\t\tReply(200).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tBodyString(string(fipResp1))\n\n\tgock.New(\"http://ops.my.cloud\").\n\t\tMatchHeader(\"X-Auth-Token\", authToken).\n\t\tDelete(\"/compute/v2.1/servers/56046f6d-3184-495b-938b-baa450db970d\").\n\t\tReply(204)\n\n\timageListResp := helperLoad(t, \"imagelistresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tGet(\"/compute/v2.1/images/detail\").\n\t\tMatchHeader(\"X-Auth-Token\", authToken).\n\t\tReply(200).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tBodyString(string(imageListResp))\n\n\tflavorListResp1 := helperLoad(t, \"flavorlistresp1.json\")\n\tgock.New(\"http://ops.my.cloud\").\n\t\tGet(\"/compute/v2.1/flavors/detail\").\n\t\tMatchHeader(\"X-Auth-Token\", authToken).\n\t\tReply(200).\n\t\tSetHeader(\"Content-Type\", \"application/json\").\n\t\tBodyString(string(flavorListResp1))\n\n\tmockContext := context.TODO()\n\tmockInstance := &autoscaler.Instance{\n\t\tID:      \"56046f6d-3184-495b-938b-baa450db970d\",\n\t\tAddress: \"172.24.4.5\",\n\t}\n\n\tv, err := New(\n\t\tWithRegion(\"RegionOne\"),\n\t\tWithFlavor(\"m1.small\"),\n\t\tWithImage(\"ubuntu-16.04-server-latest\"),\n\t\tWithFloatingIpPool(\"public\"),\n\t\tWithSSHKey(\"drone-ci-key\"),\n\t)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tp := v.(*provider)\n\tp.init.Do(func() {}) //\n\n\terr = p.Destroy(mockContext, mockInstance)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif !gock.IsDone() {\n\t\tt.Error(\"Not all expected http requests completed\")\n\t}\n}\n"
  },
  {
    "path": "drivers/openstack/doc.go",
    "content": "/*\nPackage openstack contains a autoscaler driver for OpenStack\nConfiguration:\n\nAuthenticate with the usual OpenStack environment variables.\n(Not all of these may be necessary:\nsee https://github.com/gophercloud/gophercloud/blob/master/openstack/auth_env.go)\n\nOS_AUTH_URL=https://my.openstack.cloud:5000\nOS_ENDPOINT_TYPE=publicURL\nOS_IDENTITY_API_VERSION=2\nOS_PASSWORD=<mypassword>\nOS_DOMAIN_ID=default\nOS_REGION_NAME=my-region\nOS_TENANT_ID=my-tenant-id\nOS_TENANT_NAME=my-tenant-name\nOS_USERNAME=my-username\n\nConfigure driver with:\nDRONE_OPENSTACK_SSHKEY=drone-key-name\nDRONE_OPENSTACK_SECURITY_GROUP=my-security-group\n# Pool for floating ips\nDRONE_OPENSTACK_IP_POOL=my-ip-pool\nDRONE_OPENSTACK_FLAVOR=v1-standard-2\nDRONE_OPENSTACK_IMAGE=ubuntu-16.04-server-latest\nDRONE_OPENSTACK_METADATA=name:agent,owner:drone-ci\n\n*/\npackage openstack\n"
  },
  {
    "path": "drivers/openstack/option.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage openstack\n\nimport (\n\t\"io/ioutil\"\n\n\t\"github.com/drone/autoscaler/drivers/internal/userdata\"\n\t\"github.com/gophercloud/gophercloud\"\n)\n\ntype Option func(*provider)\n\n// WithImage returns an option to set the instance image.\nfunc WithImage(image string) Option {\n\treturn func(p *provider) {\n\t\tp.image = image\n\t}\n}\n\n// WithRegion returns an option to set the OpenStack target region.\nfunc WithRegion(region string) Option {\n\treturn func(p *provider) {\n\t\tp.region = region\n\t}\n}\n\n// WithFlavor returns an option to set the instance flavor.\nfunc WithFlavor(flavor string) Option {\n\treturn func(p *provider) {\n\t\tp.flavor = flavor\n\t}\n}\n\n// WithSecurityGroup returns an option to set the instance security groups.\nfunc WithSecurityGroup(group ...string) Option {\n\treturn func(p *provider) {\n\t\tp.groups = group\n\t}\n}\n\n// WithComputeClient returns an option to set the\n// GopherCloud ServiceClient.\nfunc WithComputeClient(computeClient *gophercloud.ServiceClient) Option {\n\treturn func(p *provider) {\n\t\tp.computeClient = computeClient\n\t}\n}\n\n// WithNetworkClient returns an option to set the\n// GopherCloud ServiceClient.\nfunc WithNetworkClient(networkClient *gophercloud.ServiceClient) Option {\n\treturn func(p *provider) {\n\t\tp.networkClient = networkClient\n\t}\n}\n\n// WithSSHKey returns an option to set the ssh key.\nfunc WithSSHKey(key string) Option {\n\treturn func(p *provider) {\n\t\tp.key = key\n\t}\n}\n\n// WithNetwork returns an option to set the network id.\nfunc WithNetwork(id string) Option {\n\treturn func(p *provider) {\n\t\tp.network = id\n\t}\n}\n\nfunc WithFloatingIpPool(pool string) Option {\n\treturn func(p *provider) {\n\t\tp.pool = pool\n\t}\n}\n\n// WithMetadata returns an option to set the instance metadata.\nfunc WithMetadata(metadata map[string]string) Option {\n\treturn func(p *provider) {\n\t\tp.metadata = metadata\n\t}\n}\n\n// WithUserData returns an option to set the cloud-init\n// template from text.\nfunc WithUserData(text string) Option {\n\treturn func(p *provider) {\n\t\tif text != \"\" {\n\t\t\tp.userdata = userdata.Parse(text)\n\t\t}\n\t}\n}\n\n// WithUserDataFile returns an option to set the cloud-init\n// template from file.\nfunc WithUserDataFile(filepath string) Option {\n\treturn func(p *provider) {\n\t\tif filepath != \"\" {\n\t\t\tb, err := ioutil.ReadFile(filepath)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tp.userdata = userdata.Parse(string(b))\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "drivers/openstack/option_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage openstack\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gophercloud/gophercloud\"\n)\n\nfunc TestOptions(t *testing.T) {\n\tv, err := New(\n\t\tWithComputeClient(&gophercloud.ServiceClient{}),\n\t\tWithNetworkClient(&gophercloud.ServiceClient{}),\n\t\tWithFloatingIpPool(\"ext-ips-1\"),\n\t\tWithFlavor(\"053dc448-045b-4c15-a4a0-1908b6b9310d\"),\n\t\tWithSecurityGroup(\"drone-ci\"),\n\t\tWithSSHKey(\"drone-ci\"),\n\t\tWithRegion(\"sto-01\"),\n\t\tWithImage(\"0e9fe318-568f-417e-b2c1-f1218aa2712f\"),\n\t\tWithMetadata(map[string]string{\"foo\": \"bar\", \"baz\": \"qux\"}),\n\t\tWithNetwork(\"c7d172c8-96e6-40ab-aaaa-4a555e247c73\"),\n\t)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tp := v.(*provider)\n\n\tif got, want := p.pool, \"ext-ips-1\"; got != want {\n\t\tt.Errorf(\"Want pool %q, got %q\", want, got)\n\t}\n\tif got, want := p.region, \"sto-01\"; got != want {\n\t\tt.Errorf(\"Want region %q, got %q\", want, got)\n\t}\n\tif got, want := p.flavor, \"053dc448-045b-4c15-a4a0-1908b6b9310d\"; got != want {\n\t\tt.Errorf(\"Want flavor %q, got %q\", want, got)\n\t}\n\tif got, want := p.image, \"0e9fe318-568f-417e-b2c1-f1218aa2712f\"; got != want {\n\t\tt.Errorf(\"Want image %q, got %q\", want, got)\n\t}\n\tif got, want := p.network, \"c7d172c8-96e6-40ab-aaaa-4a555e247c73\"; got != want {\n\t\tt.Errorf(\"Want network %q, got %q\", want, got)\n\t}\n\tif got, want := p.key, \"drone-ci\"; got != want {\n\t\tt.Errorf(\"Want key %q, got %q\", want, got)\n\t}\n\tif got, want := len(p.metadata), 2; got != want {\n\t\tt.Errorf(\"Want %d tags, got %d\", want, got)\n\t}\n\tif got, want := p.metadata[\"foo\"], \"bar\"; got != want {\n\t\tt.Errorf(\"Want foo=%q metadata, got foo=%q\", want, got)\n\t}\n\tif got, want := p.metadata[\"baz\"], \"qux\"; got != want {\n\t\tt.Errorf(\"Want baz=%q metadata, got baz=%q\", want, got)\n\t}\n}\n"
  },
  {
    "path": "drivers/openstack/provider.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage openstack\n\nimport (\n\t\"regexp\"\n\t\"sync\"\n\t\"text/template\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/drivers/internal/userdata\"\n\t\"github.com/gophercloud/gophercloud\"\n\t\"github.com/gophercloud/gophercloud/openstack\"\n\t\"github.com/gophercloud/gophercloud/openstack/compute/v2/flavors\"\n\t\"github.com/gophercloud/gophercloud/openstack/compute/v2/images\"\n\t\"github.com/gophercloud/gophercloud/openstack/networking/v2/networks\"\n)\n\n// provider implements an OpenStack provider\ntype provider struct {\n\tinit sync.Once\n\n\tkey      string\n\tregion   string\n\timage    string\n\tflavor   string\n\tnetwork  string\n\tpool     string\n\tuserdata *template.Template\n\tgroups   []string\n\tmetadata map[string]string\n\n\tcomputeClient *gophercloud.ServiceClient\n\tnetworkClient *gophercloud.ServiceClient\n}\n\n// New returns a new OpenStack provider.\nfunc New(opts ...Option) (autoscaler.Provider, error) {\n\tp := new(provider)\n\tfor _, opt := range opts {\n\t\topt(p)\n\t}\n\n\tif p.userdata == nil {\n\t\tp.userdata = userdata.T\n\t}\n\n\tif p.computeClient == nil {\n\t\tauthOpts, err := openstack.AuthOptionsFromEnv()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tauthClient, err := openstack.AuthenticatedClient(authOpts)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tp.computeClient, err = openstack.NewComputeV2(authClient, gophercloud.EndpointOpts{\n\t\t\tRegion: p.region,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif p.networkClient == nil {\n\t\tauthOpts, err := openstack.AuthOptionsFromEnv()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tauthClient, err := openstack.AuthenticatedClient(authOpts)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tp.networkClient, err = openstack.NewNetworkV2(authClient, gophercloud.EndpointOpts{\n\t\t\tRegion: p.region,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif p.image != \"\" && !isUUID(p.image) {\n\t\tuuid, err := images.IDFromName(p.computeClient, p.image)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tp.image = uuid\n\t}\n\n\tif p.flavor != \"\" && !isUUID(p.flavor) {\n\t\tuuid, err := flavors.IDFromName(p.computeClient, p.flavor)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tp.flavor = uuid\n\t}\n\n\tif p.network != \"\" && !isUUID(p.network) {\n\t\tuuid, err := networks.IDFromName(p.networkClient, p.network)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tp.network = uuid\n\t}\n\n\treturn p, nil\n}\n\nfunc isUUID(uuid string) bool {\n\tr := 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}$\")\n\treturn r.MatchString(uuid)\n}\n"
  },
  {
    "path": "drivers/openstack/provider_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage openstack\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gophercloud/gophercloud\"\n)\n\nfunc TestDefaults(t *testing.T) {\n\tv, err := New(\n\t\tWithComputeClient(&gophercloud.ServiceClient{}),\n\t\tWithNetworkClient(&gophercloud.ServiceClient{}),\n\t)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tp := v.(*provider)\n\t// Add tests if we set some actual defaults in the future.\n\t_ = p\n}\n"
  },
  {
    "path": "drivers/openstack/setup.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage openstack\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/drone/autoscaler/logger\"\n\n\t\"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\nfunc (p *provider) setup(ctx context.Context) error {\n\tvar g errgroup.Group\n\tif p.key == \"\" {\n\t\tg.Go(func() error {\n\t\t\treturn p.findKeyPair(ctx)\n\t\t})\n\t}\n\n\treturn g.Wait()\n}\n\nfunc (p *provider) findKeyPair(ctx context.Context) error {\n\tlogger := logger.FromContext(ctx)\n\n\tlogger.Debugln(\"finding default ssh key\")\n\n\tallPages, err := keypairs.List(p.computeClient).AllPages()\n\tif err != nil {\n\t\treturn err\n\t}\n\tkeys, err := keypairs.ExtractKeyPairs(allPages)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tindex := map[string]keypairs.KeyPair{}\n\tfor _, key := range keys {\n\t\tindex[key.Name] = key\n\t}\n\n\t// if the account has multiple keys configured we will\n\t// attempt to use an existing key based on naming convention.\n\tfor _, name := range []string{\"drone\", \"id_rsa_drone\"} {\n\t\tkey, ok := index[name]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tp.key = key.Name\n\n\t\tlogger.\n\t\t\tWithField(\"name\", name).\n\t\t\tWithField(\"fingerprint\", key.Fingerprint).\n\t\t\tDebugln(\"using default ssh key\")\n\t\treturn nil\n\t}\n\n\t// if there were no matches but the account has at least\n\t// one keypair already created we will select the first\n\t// in the list.\n\tif len(keys) > 0 {\n\t\tkey := keys[0]\n\t\tp.key = key.Name\n\n\t\tlogger.\n\t\t\tWithField(\"name\", key.Name).\n\t\t\tWithField(\"fingerprint\", key.Fingerprint).\n\t\t\tDebugln(\"using default ssh key\")\n\t\treturn nil\n\t}\n\treturn errors.New(\"no matching keys\")\n}\n"
  },
  {
    "path": "drivers/openstack/setup_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage openstack\n\nimport (\n\t\"io/ioutil\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc helperLoad(t *testing.T, name string) []byte {\n\tpath := filepath.Join(\"testdata\", name) // relative path\n\tbytes, err := ioutil.ReadFile(path)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn bytes\n}\n\nconst authToken = \"gAAAAABb1tQPtYVBv68airR0dgKC2vXpkLNfEHx0w1EL89dOOjKrtdYHR7IZrDd4VjwZapC5Sri4CndpPscw-nHoh0VQsrvFjtuvT6M64RdrrOljmJbvP0o7PbV713-Pi8OpRIfunvsQFnEQ2DxDH56QC6fsLEcF14VtogOQwTRBod0SkeOCpi4\""
  },
  {
    "path": "drivers/openstack/testdata/associateresp1.json",
    "content": ""
  },
  {
    "path": "drivers/openstack/testdata/authresp1.json",
    "content": "{\n  \"versions\": {\n    \"values\": [\n      {\n        \"status\": \"stable\",\n        \"updated\": \"2018-10-15T00:00:00Z\",\n        \"media-types\": [\n          {\n            \"base\": \"application/json\",\n            \"type\": \"application/vnd.openstack.identity-v3+json\"\n          }\n        ],\n        \"id\": \"v3.11\",\n        \"links\": [\n          {\n            \"href\": \"http://ops.my.cloud/identity/v3/\",\n            \"rel\": \"self\"\n          }\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "drivers/openstack/testdata/fipresp1.json",
    "content": "{\n  \"floating_ip\": {\n    \"instance_id\": null,\n    \"ip\": \"172.24.4.5\",\n    \"fixed_ip\": null,\n    \"id\": \"0f013e62-42b1-461c-af7c-8aa3c705ff29\",\n    \"pool\": \"public\"\n  }\n}\n"
  },
  {
    "path": "drivers/openstack/testdata/flavorlistresp1.json",
    "content": "{\n  \"flavors\": [\n    {\n      \"name\": \"m1.tiny\",\n      \"links\": [\n        {\n          \"href\": \"http://ops.my.cloud/compute/v2.1/flavors/20f8acd8-5660-45d2-a176-1dafe98d591f\",\n          \"rel\": \"self\"\n        },\n        {\n          \"href\": \"http://ops.my.cloud/compute/flavors/20f8acd8-5660-45d2-a176-1dafe98d591f\",\n          \"rel\": \"bookmark\"\n        }\n      ],\n      \"ram\": 512,\n      \"OS-FLV-DISABLED:disabled\": false,\n      \"vcpus\": 1,\n      \"swap\": \"\",\n      \"os-flavor-access:is_public\": true,\n      \"rxtx_factor\": 1.0,\n      \"OS-FLV-EXT-DATA:ephemeral\": 0,\n      \"disk\": 1,\n      \"id\": \"20f8acd8-5660-45d2-a176-1dafe98d591f\"\n    },\n    {\n      \"name\": \"m1.small\",\n      \"links\": [\n        {\n          \"href\": \"http://ops.my.cloud/compute/v2.1/flavors/29e3cce3-d771-4220-80fe-3edf0e8dd466\",\n          \"rel\": \"self\"\n        },\n        {\n          \"href\": \"http://ops.my.cloud/compute/flavors/29e3cce3-d771-4220-80fe-3edf0e8dd466\",\n          \"rel\": \"bookmark\"\n        }\n      ],\n      \"ram\": 2048,\n      \"OS-FLV-DISABLED:disabled\": false,\n      \"vcpus\": 1,\n      \"swap\": \"\",\n      \"os-flavor-access:is_public\": true,\n      \"rxtx_factor\": 1.0,\n      \"OS-FLV-EXT-DATA:ephemeral\": 0,\n      \"disk\": 20,\n      \"id\": \"29e3cce3-d771-4220-80fe-3edf0e8dd466\"\n    },\n    {\n      \"name\": \"m1.medium\",\n      \"links\": [\n        {\n          \"href\": \"http://ops.my.cloud/compute/v2.1/flavors/2fe9e665-cf0f-4bff-a2a7-a0c19b15da7b\",\n          \"rel\": \"self\"\n        },\n        {\n          \"href\": \"http://ops.my.cloud/compute/flavors/2fe9e665-cf0f-4bff-a2a7-a0c19b15da7b\",\n          \"rel\": \"bookmark\"\n        }\n      ],\n      \"ram\": 4096,\n      \"OS-FLV-DISABLED:disabled\": false,\n      \"vcpus\": 2,\n      \"swap\": \"\",\n      \"os-flavor-access:is_public\": true,\n      \"rxtx_factor\": 1.0,\n      \"OS-FLV-EXT-DATA:ephemeral\": 0,\n      \"disk\": 40,\n      \"id\": \"2fe9e665-cf0f-4bff-a2a7-a0c19b15da7b\"\n    },\n    {\n      \"name\": \"m1.large\",\n      \"links\": [\n        {\n          \"href\": \"http://ops.my.cloud/compute/v2.1/flavors/43832d64-56ed-401f-953c-d4d4c156f33a\",\n          \"rel\": \"self\"\n        },\n        {\n          \"href\": \"http://ops.my.cloud/compute/flavors/43832d64-56ed-401f-953c-d4d4c156f33a\",\n          \"rel\": \"bookmark\"\n        }\n      ],\n      \"ram\": 8192,\n      \"OS-FLV-DISABLED:disabled\": false,\n      \"vcpus\": 4,\n      \"swap\": \"\",\n      \"os-flavor-access:is_public\": true,\n      \"rxtx_factor\": 1.0,\n      \"OS-FLV-EXT-DATA:ephemeral\": 0,\n      \"disk\": 80,\n      \"id\": \"43832d64-56ed-401f-953c-d4d4c156f33a\"\n    },\n    {\n      \"name\": \"m1.xlarge\",\n      \"links\": [\n        {\n          \"href\": \"http://ops.my.cloud/compute/v2.1/flavors/618945e9-beb4-4f20-88fd-e044a228156f\",\n          \"rel\": \"self\"\n        },\n        {\n          \"href\": \"http://ops.my.cloud/compute/flavors/618945e9-beb4-4f20-88fd-e044a228156f\",\n          \"rel\": \"bookmark\"\n        }\n      ],\n      \"ram\": 16384,\n      \"OS-FLV-DISABLED:disabled\": false,\n      \"vcpus\": 8,\n      \"swap\": \"\",\n      \"os-flavor-access:is_public\": true,\n      \"rxtx_factor\": 1.0,\n      \"OS-FLV-EXT-DATA:ephemeral\": 0,\n      \"disk\": 160,\n      \"id\": \"618945e9-beb4-4f20-88fd-e044a228156f\"\n    },\n    {\n      \"name\": \"cirros256\",\n      \"links\": [\n        {\n          \"href\": \"http://ops.my.cloud/compute/v2.1/flavors/67a446b2-53c3-460b-a2da-533ce9a0c527\",\n          \"rel\": \"self\"\n        },\n        {\n          \"href\": \"http://ops.my.cloud/compute/flavors/67a446b2-53c3-460b-a2da-533ce9a0c527\",\n          \"rel\": \"bookmark\"\n        }\n      ],\n      \"ram\": 256,\n      \"OS-FLV-DISABLED:disabled\": false,\n      \"vcpus\": 1,\n      \"swap\": \"\",\n      \"os-flavor-access:is_public\": true,\n      \"rxtx_factor\": 1.0,\n      \"OS-FLV-EXT-DATA:ephemeral\": 0,\n      \"disk\": 0,\n      \"id\": \"67a446b2-53c3-460b-a2da-533ce9a0c527\"\n    },\n    {\n      \"name\": \"ds512M\",\n      \"links\": [\n        {\n          \"href\": \"http://ops.my.cloud/compute/v2.1/flavors/c7d172c8-96e6-40ab-aaaa-4a555e247c73\",\n          \"rel\": \"self\"\n        },\n        {\n          \"href\": \"http://ops.my.cloud/compute/flavors/c7d172c8-96e6-40ab-aaaa-4a555e247c73\",\n          \"rel\": \"bookmark\"\n        }\n      ],\n      \"ram\": 512,\n      \"OS-FLV-DISABLED:disabled\": false,\n      \"vcpus\": 1,\n      \"swap\": \"\",\n      \"os-flavor-access:is_public\": true,\n      \"rxtx_factor\": 1.0,\n      \"OS-FLV-EXT-DATA:ephemeral\": 0,\n      \"disk\": 5,\n      \"id\": \"c7d172c8-96e6-40ab-aaaa-4a555e247c73\"\n    },\n    {\n      \"name\": \"ds1G\",\n      \"links\": [\n        {\n          \"href\": \"http://ops.my.cloud/compute/v2.1/flavors/c3a01655-1b6a-44aa-b095-cc28dd407e70\",\n          \"rel\": \"self\"\n        },\n        {\n          \"href\": \"http://ops.my.cloud/compute/flavors/c3a01655-1b6a-44aa-b095-cc28dd407e70\",\n          \"rel\": \"bookmark\"\n        }\n      ],\n      \"ram\": 1024,\n      \"OS-FLV-DISABLED:disabled\": false,\n      \"vcpus\": 1,\n      \"swap\": \"\",\n      \"os-flavor-access:is_public\": true,\n      \"rxtx_factor\": 1.0,\n      \"OS-FLV-EXT-DATA:ephemeral\": 0,\n      \"disk\": 10,\n      \"id\": \"c3a01655-1b6a-44aa-b095-cc28dd407e70\"\n    },\n    {\n      \"name\": \"ds2G\",\n      \"links\": [\n        {\n          \"href\": \"http://ops.my.cloud/compute/v2.1/flavors/053dc448-045b-4c15-a4a0-1908b6b9310d\",\n          \"rel\": \"self\"\n        },\n        {\n          \"href\": \"http://ops.my.cloud/compute/flavors/053dc448-045b-4c15-a4a0-1908b6b9310d\",\n          \"rel\": \"bookmark\"\n        }\n      ],\n      \"ram\": 2048,\n      \"OS-FLV-DISABLED:disabled\": false,\n      \"vcpus\": 2,\n      \"swap\": \"\",\n      \"os-flavor-access:is_public\": true,\n      \"rxtx_factor\": 1.0,\n      \"OS-FLV-EXT-DATA:ephemeral\": 0,\n      \"disk\": 10,\n      \"id\": \"053dc448-045b-4c15-a4a0-1908b6b9310d\"\n    },\n    {\n      \"name\": \"ds4G\",\n      \"links\": [\n        {\n          \"href\": \"http://ops.my.cloud/compute/v2.1/flavors/0e9fe318-568f-417e-b2c1-f1218aa2712f\",\n          \"rel\": \"self\"\n        },\n        {\n          \"href\": \"http://ops.my.cloud/compute/flavors/0e9fe318-568f-417e-b2c1-f1218aa2712f\",\n          \"rel\": \"bookmark\"\n        }\n      ],\n      \"ram\": 4096,\n      \"OS-FLV-DISABLED:disabled\": false,\n      \"vcpus\": 4,\n      \"swap\": \"\",\n      \"os-flavor-access:is_public\": true,\n      \"rxtx_factor\": 1.0,\n      \"OS-FLV-EXT-DATA:ephemeral\": 0,\n      \"disk\": 20,\n      \"id\": \"0e9fe318-568f-417e-b2c1-f1218aa2712f\"\n    }\n  ]\n}\n"
  },
  {
    "path": "drivers/openstack/testdata/imagelistresp1.json",
    "content": "{\n  \"images\": [\n    {\n      \"status\": \"ACTIVE\",\n      \"updated\": \"2018-10-26T14:29:41Z\",\n      \"links\": [\n        {\n          \"href\": \"http://ops.my.cloud/compute/v2.1/images/ee7d6850-0592-4036-bd6e-198b41df7381\",\n          \"rel\": \"self\"\n        },\n        {\n          \"href\": \"http://ops.my.cloud/compute/images/ee7d6850-0592-4036-bd6e-198b41df7381\",\n          \"rel\": \"bookmark\"\n        },\n        {\n          \"href\": \"http://ops.my.cloud/image/images/ee7d6850-0592-4036-bd6e-198b41df7381\",\n          \"type\": \"application/vnd.openstack.image\",\n          \"rel\": \"alternate\"\n        }\n      ],\n      \"id\": \"ee7d6850-0592-4036-bd6e-198b41df7381\",\n      \"OS-EXT-IMG-SIZE:size\": 74448896,\n      \"name\": \"rancheros-v1.4.1\",\n      \"created\": \"2018-10-26T14:29:39Z\",\n      \"minDisk\": 0,\n      \"progress\": 100,\n      \"minRam\": 0,\n      \"metadata\": {\n        \"description\": \"RancherOS v1.4.1\"\n      }\n    },\n    {\n      \"status\": \"ACTIVE\",\n      \"updated\": \"2018-10-23T13:20:03Z\",\n      \"links\": [\n        {\n          \"href\": \"http://ops.my.cloud/compute/v2.1/images/4ef19958-ee2d-44a7-a100-de0b8afdbc8e\",\n          \"rel\": \"self\"\n        },\n        {\n          \"href\": \"http://ops.my.cloud/compute/images/4ef19958-ee2d-44a7-a100-de0b8afdbc8e\",\n          \"rel\": \"bookmark\"\n        },\n        {\n          \"href\": \"http://ops.my.cloud/image/images/4ef19958-ee2d-44a7-a100-de0b8afdbc8e\",\n          \"type\": \"application/vnd.openstack.image\",\n          \"rel\": \"alternate\"\n        }\n      ],\n      \"id\": \"4ef19958-ee2d-44a7-a100-de0b8afdbc8e\",\n      \"OS-EXT-IMG-SIZE:size\": 296943616,\n      \"name\": \"ubuntu-16.04-server-latest\",\n      \"created\": \"2018-10-23T13:19:58Z\",\n      \"minDisk\": 0,\n      \"progress\": 100,\n      \"minRam\": 0,\n      \"metadata\": {\n        \"description\": \"Ubuntu 16.04 LTS\"\n      }\n    },\n    {\n      \"status\": \"ACTIVE\",\n      \"updated\": \"2018-10-22T12:03:52Z\",\n      \"links\": [\n        {\n          \"href\": \"http://ops.my.cloud/compute/v2.1/images/7fd93141-c387-4859-bc79-b92fac420473\",\n          \"rel\": \"self\"\n        },\n        {\n          \"href\": \"http://ops.my.cloud/compute/images/7fd93141-c387-4859-bc79-b92fac420473\",\n          \"rel\": \"bookmark\"\n        },\n        {\n          \"href\": \"http://ops.my.cloud/image/images/7fd93141-c387-4859-bc79-b92fac420473\",\n          \"type\": \"application/vnd.openstack.image\",\n          \"rel\": \"alternate\"\n        }\n      ],\n      \"id\": \"7fd93141-c387-4859-bc79-b92fac420473\",\n      \"OS-EXT-IMG-SIZE:size\": 13267968,\n      \"name\": \"cirros-0.3.5-x86_64-disk\",\n      \"created\": \"2018-10-22T12:03:51Z\",\n      \"minDisk\": 0,\n      \"progress\": 100,\n      \"minRam\": 0,\n      \"metadata\": {}\n    }\n  ]\n}"
  },
  {
    "path": "drivers/openstack/testdata/servercreateresp1.json",
    "content": "{\n  \"server\": {\n    \"OS-EXT-STS:task_state\": null,\n    \"addresses\": {\n      \"private\": [\n        {\n          \"OS-EXT-IPS-MAC:mac_addr\": \"fa:16:3e:7a:f3:1f\",\n          \"version\": 4,\n          \"addr\": \"10.0.0.14\",\n          \"OS-EXT-IPS:type\": \"fixed\"\n        }\n      ]\n    },\n    \"links\": [\n      {\n        \"href\": \"http://ops.my.cloud/compute/v2.1/servers/56046f6d-3184-495b-938b-baa450db970d\",\n        \"rel\": \"self\"\n      },\n      {\n        \"href\": \"http://ops.my.cloud/compute/servers/56046f6d-3184-495b-938b-baa450db970d\",\n        \"rel\": \"bookmark\"\n      }\n    ],\n    \"image\": {\n      \"id\": \"4ef19958-ee2d-44a7-a100-de0b8afdbc8e\",\n      \"links\": [\n        {\n          \"href\": \"http://ops.my.cloud/compute/images/4ef19958-ee2d-44a7-a100-de0b8afdbc8e\",\n          \"rel\": \"bookmark\"\n        }\n      ]\n    },\n    \"OS-EXT-STS:vm_state\": \"active\",\n    \"OS-EXT-SRV-ATTR:instance_name\": \"instance-0000000d\",\n    \"OS-SRV-USG:launched_at\": \"2018-10-29T09:37:05.000000\",\n    \"flavor\": {\n      \"id\": \"2\",\n      \"links\": [\n        {\n          \"href\": \"http://ops.my.cloud/compute/flavors/2\",\n          \"rel\": \"bookmark\"\n        }\n      ]\n    },\n    \"id\": \"56046f6d-3184-495b-938b-baa450db970d\",\n    \"security_groups\": [\n      {\n        \"name\": \"drone-agent\"\n      }\n    ],\n    \"user_id\": \"898384bb1b5e4d5a9ff816f7ea911943\",\n    \"OS-DCF:diskConfig\": \"MANUAL\",\n    \"accessIPv4\": \"\",\n    \"accessIPv6\": \"\",\n    \"progress\": 0,\n    \"OS-EXT-STS:power_state\": 1,\n    \"OS-EXT-AZ:availability_zone\": \"nova\",\n    \"config_drive\": \"\",\n    \"status\": \"ACTIVE\",\n    \"updated\": \"2018-10-29T09:37:06Z\",\n    \"hostId\": \"1e678c454d7593d464d1a0c1c15111119ae841d11d3f7ba66f9aaee9\",\n    \"OS-EXT-SRV-ATTR:host\": \"devstack\",\n    \"OS-SRV-USG:terminated_at\": null,\n    \"key_name\": \"drone-ci-key\",\n    \"OS-EXT-SRV-ATTR:hypervisor_hostname\": \"devstack\",\n    \"name\": \"agent-RjISb5v1\",\n    \"created\": \"2018-10-29T09:37:01Z\",\n    \"tenant_id\": \"661f707340b0486caf878f9cc2bc1fab\",\n    \"os-extended-volumes:volumes_attached\": [],\n    \"metadata\": {\n      \"owner\": \"drone-ci\",\n      \"name\": \"agent\"\n    }\n  }\n}\n"
  },
  {
    "path": "drivers/openstack/testdata/serverstatusresp1.json",
    "content": "{\n  \"server\": {\n    \"OS-EXT-STS:task_state\": null,\n    \"addresses\": {\n      \"private\": [\n        {\n          \"OS-EXT-IPS-MAC:mac_addr\": \"fa:16:3e:7a:f3:1f\",\n          \"version\": 4,\n          \"addr\": \"10.0.0.14\",\n          \"OS-EXT-IPS:type\": \"fixed\"\n        }\n      ]\n    },\n    \"links\": [\n      {\n        \"href\": \"http://ops.my.cloud/compute/v2.1/servers/56046f6d-3184-495b-938b-baa450db970d\",\n        \"rel\": \"self\"\n      },\n      {\n        \"href\": \"http://ops.my.cloud/compute/servers/56046f6d-3184-495b-938b-baa450db970d\",\n        \"rel\": \"bookmark\"\n      }\n    ],\n    \"image\": {\n      \"id\": \"4ef19958-ee2d-44a7-a100-de0b8afdbc8e\",\n      \"links\": [\n        {\n          \"href\": \"http://ops.my.cloud/compute/images/4ef19958-ee2d-44a7-a100-de0b8afdbc8e\",\n          \"rel\": \"bookmark\"\n        }\n      ]\n    },\n    \"OS-EXT-STS:vm_state\": \"active\",\n    \"OS-EXT-SRV-ATTR:instance_name\": \"instance-0000000d\",\n    \"OS-SRV-USG:launched_at\": \"2018-10-29T09:37:05.000000\",\n    \"flavor\": {\n      \"id\": \"2\",\n      \"links\": [\n        {\n          \"href\": \"http://ops.my.cloud/compute/flavors/2\",\n          \"rel\": \"bookmark\"\n        }\n      ]\n    },\n    \"id\": \"56046f6d-3184-495b-938b-baa450db970d\",\n    \"security_groups\": [\n      {\n        \"name\": \"drone-agent\"\n      }\n    ],\n    \"user_id\": \"898384bb1b5e4d5a9ff816f7ea911943\",\n    \"OS-DCF:diskConfig\": \"MANUAL\",\n    \"accessIPv4\": \"\",\n    \"accessIPv6\": \"\",\n    \"progress\": 0,\n    \"OS-EXT-STS:power_state\": 1,\n    \"OS-EXT-AZ:availability_zone\": \"nova\",\n    \"config_drive\": \"\",\n    \"status\": \"ACTIVE\",\n    \"updated\": \"2018-10-29T09:37:06Z\",\n    \"hostId\": \"1e678c454d7593d464d1a0c1c15111119ae841d11d3f7ba66f9aaee9\",\n    \"OS-EXT-SRV-ATTR:host\": \"devstack\",\n    \"OS-SRV-USG:terminated_at\": null,\n    \"key_name\": \"drone-ci-key\",\n    \"OS-EXT-SRV-ATTR:hypervisor_hostname\": \"devstack\",\n    \"name\": \"agent-RjISb5v1\",\n    \"created\": \"2018-10-29T09:37:01Z\",\n    \"tenant_id\": \"661f707340b0486caf878f9cc2bc1fab\",\n    \"os-extended-volumes:volumes_attached\": [],\n    \"metadata\": {\n      \"owner\": \"drone-ci\",\n      \"name\": \"agent\"\n    }\n  }\n}\n"
  },
  {
    "path": "drivers/openstack/testdata/tokenresp1.json",
    "content": "{\n  \"token\": {\n    \"is_domain\": false,\n    \"methods\": [\n      \"password\"\n    ],\n    \"roles\": [\n      {\n        \"id\": \"ed9e3145d25241579d09ee3aec891bb8\",\n        \"name\": \"reader\"\n      },\n      {\n        \"id\": \"74de3a0909ff455dbe3b4d3e8605c517\",\n        \"name\": \"admin\"\n      },\n      {\n        \"id\": \"702053a51c4543029dd6bc3a6141f5ae\",\n        \"name\": \"member\"\n      }\n    ],\n    \"expires_at\": \"2018-10-29T10:34:07.000000Z\",\n    \"project\": {\n      \"domain\": {\n        \"id\": \"default\",\n        \"name\": \"Default\"\n      },\n      \"id\": \"661f707340b0486caf878f9cc2bc1fab\",\n      \"name\": \"demo\"\n    },\n    \"catalog\": [\n      {\n        \"endpoints\": [\n          {\n            \"url\": \"http://ops.my.cloud/identity\",\n            \"interface\": \"admin\",\n            \"region\": \"RegionOne\",\n            \"region_id\": \"RegionOne\",\n            \"id\": \"4433d85dd7f64f74afdb920bc17d92b6\"\n          },\n          {\n            \"url\": \"http://ops.my.cloud/identity\",\n            \"interface\": \"public\",\n            \"region\": \"RegionOne\",\n            \"region_id\": \"RegionOne\",\n            \"id\": \"ad24909f182a46e78238ea2611ece548\"\n          }\n        ],\n        \"type\": \"identity\",\n        \"id\": \"092f43883e8944c6807ff71d59b52b94\",\n        \"name\": \"keystone\"\n      },\n      {\n        \"endpoints\": [\n          {\n            \"url\": \"http://ops.my.cloud/compute/v2.1\",\n            \"interface\": \"public\",\n            \"region\": \"RegionOne\",\n            \"region_id\": \"RegionOne\",\n            \"id\": \"2df37853876f471e9e65aaced4b5d799\"\n          }\n        ],\n        \"type\": \"compute\",\n        \"id\": \"0a8db85efccc4c59bb023c78a932b1fb\",\n        \"name\": \"nova\"\n      },\n      {\n        \"endpoints\": [\n          {\n            \"url\": \"http://ops.my.cloud:9696/\",\n            \"interface\": \"public\",\n            \"region\": \"RegionOne\",\n            \"region_id\": \"RegionOne\",\n            \"id\": \"c82f85c8599e4c379829d5d03245c3e1\"\n          }\n        ],\n        \"type\": \"network\",\n        \"id\": \"473577f6c81d4dccbaad596da46d9d23\",\n        \"name\": \"neutron\"\n      },\n      {\n        \"endpoints\": [\n          {\n            \"url\": \"http://ops.my.cloud/placement\",\n            \"interface\": \"public\",\n            \"region\": \"RegionOne\",\n            \"region_id\": \"RegionOne\",\n            \"id\": \"710613f4370f4985931621f746b2bdac\"\n          }\n        ],\n        \"type\": \"placement\",\n        \"id\": \"549aa251294d4b0d8e8e875b65fdb038\",\n        \"name\": \"placement\"\n      },\n      {\n        \"endpoints\": [\n          {\n            \"url\": \"http://ops.my.cloud/compute/v2/661f707340b0486caf878f9cc2bc1fab\",\n            \"interface\": \"public\",\n            \"region\": \"RegionOne\",\n            \"region_id\": \"RegionOne\",\n            \"id\": \"ad8fc82f56a543b7a45faa64642218f8\"\n          }\n        ],\n        \"type\": \"compute_legacy\",\n        \"id\": \"6ee1053e7751458280cd8312548ad788\",\n        \"name\": \"nova_legacy\"\n      },\n      {\n        \"endpoints\": [\n          {\n            \"url\": \"http://ops.my.cloud/image\",\n            \"interface\": \"public\",\n            \"region\": \"RegionOne\",\n            \"region_id\": \"RegionOne\",\n            \"id\": \"b9cb65ade0774693aad0ed409d5956bf\"\n          }\n        ],\n        \"type\": \"image\",\n        \"id\": \"ea4559f84b804a679aba2a165671bb5e\",\n        \"name\": \"glance\"\n      }\n    ],\n    \"user\": {\n      \"password_expires_at\": null,\n      \"domain\": {\n        \"id\": \"default\",\n        \"name\": \"Default\"\n      },\n      \"id\": \"898384bb1b5e4d5a9ff816f7ea911943\",\n      \"name\": \"admin\"\n    },\n    \"audit_ids\": [\n      \"Ga_Iq4XvQ26BMLL2k8Mk2Q\"\n    ],\n    \"issued_at\": \"2018-10-29T09:34:07.000000Z\"\n  }\n}\n"
  },
  {
    "path": "drivers/packet/create.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage packet\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/logger\"\n\n\t\"github.com/packethost/packngo\"\n)\n\nfunc (p *provider) Create(ctx context.Context, opts autoscaler.InstanceCreateOpts) (*autoscaler.Instance, error) {\n\tp.init.Do(func() {\n\t\tp.setup(ctx)\n\t})\n\n\tbuf := new(bytes.Buffer)\n\terr := p.userdata.Execute(buf, &opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tlogger := logger.FromContext(ctx).\n\t\tWithField(\"project\", p.project).\n\t\tWithField(\"facility\", p.facility).\n\t\tWithField(\"billing\", p.billing).\n\t\tWithField(\"plan\", p.plan).\n\t\tWithField(\"os\", p.os).\n\t\tWithField(\"hostname\", p.hostname)\n\n\tcr := &packngo.DeviceCreateRequest{\n\t\tHostName:     p.hostname,\n\t\tFacility:     p.facility,\n\t\tPlan:         p.plan,\n\t\tOS:           p.os,\n\t\tProjectID:    p.project,\n\t\tBillingCycle: p.billing,\n\t\tUserData:     buf.String(),\n\t}\n\n\tlogger.Debugln(\"instance create\")\n\n\td, _, err := p.client.Devices.Create(cr)\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tErrorln(\"cannot create instance\")\n\t\treturn nil, err\n\t}\n\n\tinstance := &autoscaler.Instance{\n\t\tProvider: autoscaler.ProviderPacket,\n\t\tID:       d.ID,\n\t\tName:     opts.Name,\n\t\tImage:    d.OS.Slug,\n\t\tRegion:   d.Facility.Code,\n\t\tSize:     d.Plan.Slug,\n\t}\n\n\t// poll the packet endpoint for server updates\n\t// and exit when a network address is allocated.\n\tinterval := time.Duration(0)\npoller:\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tlogger.WithField(\"name\", instance.Name).\n\t\t\t\tDebugln(\"cannot ascertain network\")\n\n\t\t\treturn instance, ctx.Err()\n\t\tcase <-time.After(interval):\n\t\t\tinterval = time.Minute\n\n\t\t\tlogger.WithField(\"name\", instance.Name).\n\t\t\t\tDebugln(\"find instance network\")\n\n\t\t\td, _, err := p.client.Devices.Get(d.ID)\n\t\t\tif err != nil {\n\t\t\t\tlogger.WithError(err).\n\t\t\t\t\tErrorln(\"cannot find instance\")\n\t\t\t\treturn instance, err\n\t\t\t}\n\n\t\t\tif d.State == \"active\" {\n\t\t\t\tfor _, ip := range d.Network {\n\t\t\t\t\tif ip.Public && ip.AddressFamily == 4 {\n\t\t\t\t\t\tinstance.Address = ip.Address\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif instance.Address != \"\" {\n\t\t\t\t\tbreak poller\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tlogger.\n\t\tWithField(\"name\", instance.Name).\n\t\tWithField(\"ip\", instance.Address).\n\t\tDebugln(\"instance network ready\")\n\n\treturn instance, nil\n}\n"
  },
  {
    "path": "drivers/packet/create_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage packet\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/h2non/gock\"\n\t\"github.com/packethost/packngo\"\n)\n\nfunc TestCreate(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(baseURL).\n\t\tMatchHeader(\"X-Auth-Token\", apiKey).\n\t\tPost(createDevice).\n\t\tReply(200).\n\t\tBodyString(respCreate)\n\n\tgock.New(baseURL).\n\t\tMatchHeader(\"X-Auth-Token\", apiKey).\n\t\tGet(getDevice).\n\t\tMatchParam(\"include\", \"facility\").\n\t\tReply(200).\n\t\tDelay(10 * time.Second).\n\t\tBodyString(respCreate)\n\n\tinstance, err := prov.Create(context.TODO(), autoscaler.InstanceCreateOpts{Name: prov.os})\n\tif err != nil {\n\t\tt.Error(err)\n\t\tt.FailNow()\n\t}\n\n\tt.Run(\"Attributes\", testInstance(instance))\n}\n\nfunc testInstance(instance *autoscaler.Instance) func(t *testing.T) {\n\treturn func(t *testing.T) {\n\t\tif instance == nil {\n\t\t\tt.Errorf(\"Expect non-nil instance even if error\")\n\t\t}\n\t\tif got, want := instance.ID, instanceID; got != want {\n\t\t\tt.Errorf(\"Want ID %v, got %v\", want, got)\n\t\t}\n\t\tif got, want := instance.Image, prov.os; got != want {\n\t\t\tt.Errorf(\"Want Image %v, got %v\", want, got)\n\t\t}\n\t\tif got, want := instance.Name, prov.os; got != want {\n\t\t\tt.Errorf(\"Want Name %v, got %v\", want, got)\n\t\t}\n\t\tif got, want := instance.Region, prov.facility; got != want {\n\t\t\tt.Errorf(\"Want Region %v, got %v\", want, got)\n\t\t}\n\t\tif got, want := instance.Provider, autoscaler.ProviderPacket; got != want {\n\t\t\tt.Errorf(\"Want Provider %v, got %v\", want, got)\n\t\t}\n\t}\n}\n\nfunc TestCreate_Timeout(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(baseURL).\n\t\tMatchHeader(\"X-Auth-Token\", apiKey).\n\t\tPost(getDevice).\n\t\tReply(200).\n\t\tBodyString(respCreateInactive)\n\tgock.New(baseURL).\n\t\tMatchHeader(\"X-Auth-Token\", apiKey).\n\t\tGet(getDevice).\n\t\tReply(200).\n\t\tBodyString(respCreateInactive)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tcancel()\n\n\tif _, err := prov.Create(ctx, autoscaler.InstanceCreateOpts{Name: prov.os}); err != context.Canceled {\n\t\tt.Errorf(\"Expected error creating a device\")\n\t}\n}\n\nfunc TestCreate_Erro(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(baseURL).\n\t\tMatchHeader(\"X-Auth-Token\", apiKey).\n\t\tPost(getDevice).\n\t\tReply(400)\n\n\t_, err := prov.Create(context.Background(), autoscaler.InstanceCreateOpts{Name: prov.os})\n\tif err == nil {\n\t\tt.Errorf(\"Expect error returned when creatiung the device\")\n\t} else if _, ok := err.(*packngo.ErrorResponse); !ok {\n\t\tt.Errorf(\"Expect error to be of type  ErrorResponse\")\n\t}\n}\n\nfunc TestCreate_WaitToBecomeActive(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(baseURL).\n\t\tMatchHeader(\"X-Auth-Token\", apiKey).\n\t\tPost(getDevice).\n\t\tReply(200).\n\t\tBodyString(respCreateInactive)\n\tgock.New(baseURL).\n\t\tMatchHeader(\"X-Auth-Token\", apiKey).\n\t\tGet(getDevice).\n\t\tReply(200).\n\t\tBodyString(respCreateInactive)\n\n\twait := make(chan struct{})\n\tgo func() {\n\t\tprov.Create(context.Background(), autoscaler.InstanceCreateOpts{Name: prov.os})\n\t\tclose(wait)\n\t}()\n\n\tselect {\n\tcase <-wait:\n\t\tt.Errorf(\"Expected device creation to block when device is not set to active\")\n\tcase <-time.After(50 * time.Millisecond):\n\t}\n}\n"
  },
  {
    "path": "drivers/packet/destroy.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage packet\n\nimport (\n\t\"context\"\n\n\t\"github.com/drone/autoscaler\"\n)\n\nfunc (p *provider) Destroy(ctx context.Context, instance *autoscaler.Instance) error {\n\t_, err := p.client.Devices.Delete(instance.ID)\n\treturn err\n}\n"
  },
  {
    "path": "drivers/packet/destroy_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage packet\n\nimport (\n\t\"context\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/h2non/gock\"\n\t\"github.com/packethost/packngo\"\n)\n\nfunc TestDestroyError(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(baseURL).\n\t\tDelete(getDevice + \"/\" + instanceID).\n\t\tReply(400)\n\n\terr := prov.Destroy(context.Background(), &autoscaler.Instance{ID: instanceID})\n\n\tif err == nil {\n\t\tt.Errorf(\"Expect error when deleting a device\")\n\t} else if _, ok := err.(*packngo.ErrorResponse); !ok {\n\t\tt.Errorf(\"expected: %s , got: %s \", reflect.TypeOf(&packngo.ErrorResponse{}), reflect.TypeOf(err))\n\t}\n}\n"
  },
  {
    "path": "drivers/packet/option.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage packet\n\nimport (\n\t\"io/ioutil\"\n\n\t\"github.com/drone/autoscaler/drivers/internal/userdata\"\n)\n\n// Option configures a Digital Ocean provider option.\ntype Option func(*provider)\n\n// WithAPIKey returns an option to set the api key.\nfunc WithAPIKey(apikey string) Option {\n\treturn func(p *provider) {\n\t\tp.apikey = apikey\n\t}\n}\n\n// WithFacility returns an option to set the target facility.\nfunc WithFacility(facility string) Option {\n\treturn func(p *provider) {\n\t\tp.facility = facility\n\t}\n}\n\n// WithPlan returns an option to set the plan.\nfunc WithPlan(plan string) Option {\n\treturn func(p *provider) {\n\t\tp.plan = plan\n\t}\n}\n\n// WithOS returns an option to set the operating system.\nfunc WithOS(os string) Option {\n\treturn func(p *provider) {\n\t\tp.os = os\n\t}\n}\n\n// WithProject returns an option to set the project id.\nfunc WithProject(project string) Option {\n\treturn func(p *provider) {\n\t\tp.project = project\n\t}\n}\n\n// WithSSHKey returns an option to set the ssh key.\nfunc WithSSHKey(sshkey string) Option {\n\treturn func(p *provider) {\n\t\tp.sshkey = sshkey\n\t}\n}\n\n// WithHostname returns an option to set the hostname\nfunc WithHostname(hostname string) Option {\n\treturn func(p *provider) {\n\t\tif hostname != \"\" {\n\t\t\tp.hostname = hostname\n\t\t}\n\t}\n}\n\n// WithTags returns an option to set the image.\nfunc WithTags(tags ...string) Option {\n\treturn func(p *provider) {\n\t\tp.tags = tags\n\t}\n}\n\n// WithUserData returns an option to set the cloud-init\n// template from text.\nfunc WithUserData(text string) Option {\n\treturn func(p *provider) {\n\t\tif text != \"\" {\n\t\t\tp.userdata = userdata.Parse(text)\n\t\t}\n\t}\n}\n\n// WithUserDataFile returns an option to set the cloud-init\n// template from file.\nfunc WithUserDataFile(filepath string) Option {\n\treturn func(p *provider) {\n\t\tif filepath != \"\" {\n\t\t\tb, err := ioutil.ReadFile(filepath)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tp.userdata = userdata.Parse(string(b))\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "drivers/packet/option_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage packet\n\nimport \"testing\"\n\nfunc TestOptions(t *testing.T) {\n\tp := New(\n\t\tWithAPIKey(\"my_authentication_token\"),\n\t\tWithFacility(\"sjc1\"),\n\t\tWithOS(\"ubuntu_16_10\"),\n\t\tWithPlan(\"baremetal_1\"),\n\t\tWithProject(\"my_project\"),\n\t\tWithSSHKey(\"id_rsa\"),\n\t\tWithHostname(\"agent-abcdef\"),\n\t\tWithTags(\"drone\", \"agent\"),\n\t).(*provider)\n\n\tif got, want := p.apikey, \"my_authentication_token\"; got != want {\n\t\tt.Errorf(\"Want api key %q, got %q\", want, got)\n\t}\n\tif got, want := p.facility, \"sjc1\"; got != want {\n\t\tt.Errorf(\"Want facility %q, got %q\", want, got)\n\t}\n\tif got, want := p.os, \"ubuntu_16_10\"; got != want {\n\t\tt.Errorf(\"Want os %q, got %q\", want, got)\n\t}\n\tif got, want := p.plan, \"baremetal_1\"; got != want {\n\t\tt.Errorf(\"Want plan %q, got %q\", want, got)\n\t}\n\tif got, want := p.project, \"my_project\"; got != want {\n\t\tt.Errorf(\"Want project %q, got %q\", want, got)\n\t}\n\tif got, want := p.sshkey, \"id_rsa\"; got != want {\n\t\tt.Errorf(\"Want sshkey %q, got %q\", want, got)\n\t}\n\tif got, want := p.hostname, \"agent-abcdef\"; got != want {\n\t\tt.Errorf(\"Want hostname %q, got %q\", want, got)\n\t}\n\tif got, want := len(p.tags), 2; got != want {\n\t\tt.Errorf(\"Want %d tags, got %d\", want, got)\n\t}\n}\n"
  },
  {
    "path": "drivers/packet/provider.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage packet\n\nimport (\n\t\"sync\"\n\t\"text/template\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/drivers/internal/userdata\"\n\t\"github.com/packethost/packngo\"\n)\n\nconst consumerToken = \"24e70949af5ecd17fe8e867b335fc88e7de8bd4ad617c0403d8769a376ddea72\"\n\n// provider implements a Packet.net provider.\ntype provider struct {\n\tinit sync.Once\n\n\tapikey   string\n\tbilling  string\n\tfacility string\n\tos       string\n\tplan     string\n\tproject  string\n\tsshkey   string\n\thostname string\n\ttags     []string\n\tuserdata *template.Template\n\n\tclient *packngo.Client\n}\n\n// New returns a new Packet.net provider.\nfunc New(opts ...Option) autoscaler.Provider {\n\tp := new(provider)\n\tfor _, opt := range opts {\n\t\topt(p)\n\t}\n\tif p.facility == \"\" {\n\t\tp.facility = \"ewr1\"\n\t}\n\tif p.os == \"\" {\n\t\tp.os = \"ubuntu_18_04\"\n\t}\n\tif p.plan == \"\" {\n\t\tp.plan = \"baremetal_0\"\n\t}\n\tif p.billing == \"\" {\n\t\tp.billing = \"hourly\"\n\t}\n\tif p.userdata == nil {\n\t\tp.userdata = userdata.T\n\t}\n\tif p.client == nil {\n\t\tp.client = packngo.NewClient(\n\t\t\tconsumerToken, p.apikey, nil)\n\t}\n\treturn p\n}\n"
  },
  {
    "path": "drivers/packet/provider_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage packet\n\nimport (\n\t\"testing\"\n\n\t\"github.com/drone/autoscaler/drivers/internal/userdata\"\n)\n\nfunc TestDefaults(t *testing.T) {\n\tp := New().(*provider)\n\n\tif got, want := p.plan, \"baremetal_0\"; got != want {\n\t\tt.Errorf(\"Want plan %q, got %q\", want, got)\n\t}\n\tif got, want := p.facility, \"ewr1\"; got != want {\n\t\tt.Errorf(\"Want region %q, got %q\", want, got)\n\t}\n\tif got, want := p.billing, \"hourly\"; got != want {\n\t\tt.Errorf(\"Want billing %q, got %q\", want, got)\n\t}\n\tif got, want := p.os, \"ubuntu_18_04\"; got != want {\n\t\tt.Errorf(\"Want os %q, got %q\", want, got)\n\t}\n\tif p.userdata != userdata.T {\n\t\tt.Errorf(\"Want default userdata template\")\n\t}\n}\n"
  },
  {
    "path": "drivers/packet/setup.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage packet\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/drone/autoscaler/logger\"\n\n\t\"github.com/packethost/packngo\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\nfunc (p *provider) setup(ctx context.Context) error {\n\tvar g errgroup.Group\n\tif p.sshkey == \"\" {\n\t\tg.Go(func() error {\n\t\t\treturn p.setupKeypair(ctx)\n\t\t})\n\t}\n\treturn g.Wait()\n}\n\n// helper funciton to ascertain the ID of an existing SSH\n// key to use when provisioning instances. This is only\n// necessary when the user has not provided the ID.\nfunc (p *provider) setupKeypair(ctx context.Context) error {\n\tlogger := logger.FromContext(ctx)\n\tlogger.Debugln(\"finding default ssh key\")\n\n\tkeys, _, err := p.client.SSHKeys.List()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tindex := map[string]packngo.SSHKey{}\n\tfor _, key := range keys {\n\t\tindex[key.Label] = key\n\t}\n\n\t// if the account has multiple keys configured we will\n\t// attempt to use an existing key based on naming convention.\n\tfor _, name := range []string{\"drone\", \"id_rsa_drone\"} {\n\t\tkey, ok := index[name]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tp.sshkey = key.Key\n\n\t\tlogger.\n\t\t\tWithField(\"id\", key.ID).\n\t\t\tWithField(\"label\", key.Key).\n\t\t\tWithField(\"fingerprint\", key.FingerPrint).\n\t\t\tDebugln(\"using default ssh key\")\n\t\treturn nil\n\t}\n\n\t// if there were no matches but the account has at least\n\t// one keypair already created we will select the first\n\t// in the list.\n\tif len(keys) > 0 {\n\t\tkey := keys[0]\n\t\tp.sshkey = key.ID\n\n\t\tlogger.\n\t\t\tWithField(\"id\", key.ID).\n\t\t\tWithField(\"label\", key.Label).\n\t\t\tWithField(\"fingerprint\", key.FingerPrint).\n\t\t\tDebugln(\"using default ssh key\")\n\t\treturn nil\n\t}\n\n\treturn errors.New(\"No matching keys\")\n}\n"
  },
  {
    "path": "drivers/packet/setup_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage packet\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/h2non/gock\"\n)\n\nconst (\n\tbaseURL      = \"https://api.packet.net/\"\n\tgetDevice    = \"/devices\"\n\tgetSSH       = \"/ssh-keys\"\n\tprojectID    = \"x\"\n\tcreateDevice = \"/projects/\" + projectID + getDevice\n\tinstanceID   = \"92b0facf-189e-4bbf-81a8-bc56c0c4dc88\"\n\tapiKey       = \"apiKey\"\n\tsshKey       = \"sshKey\"\n\thostname     = \"hostname\"\n\ttag          = \"tag\"\n)\n\nvar (\n\tprov               *provider\n\trespCreate         string\n\trespCreateInactive string\n\trespSSHKeys        string\n)\n\nfunc TestMain(m *testing.M) {\n\tprov = New(\n\t\tWithProject(projectID),\n\t\tWithTags(tag),\n\t\tWithHostname(hostname),\n\t\tWithAPIKey(apiKey),\n\t).(*provider)\n\n\trespCreate = `\n{\n  \"id\": \"` + instanceID + `\",\n  \"state\": \"active\",\n  \"tags\": [\"` + tag + `\"],\n  \"hostname\": \"` + hostname + `\",\n  \"operating_system\": {\n    \"slug\": \"` + prov.os + `\"\n  },\n  \"facility\": {\n    \"code\": \"ewr1\"\n  },\n  \"ip_addresses\": [\n    {\n      \"address_family\": 4,\n      \"public\": true,\n      \"address\": \"147.75.77.155\"\n    }\n  ],\n  \"plan\": {\n    \"slug\": \"baremetal_0\"\n  }\n}\n`\n\trespCreateInactive = `\n{\n  \"id\": \"` + instanceID + `\",\n  \"state\": \"inactive\",\n  \"tags\": [\"` + tag + `\"],\n  \"hostname\": \"` + hostname + `\",\n  \"operating_system\": {\n    \"slug\": \"` + prov.os + `\"\n  },\n  \"facility\": {\n    \"code\": \"ewr1\"\n  },\n  \"ip_addresses\": [\n    {\n      \"address_family\": 4,\n      \"public\": true,\n      \"address\": \"147.75.77.155\"\n    }\n  ],\n  \"plan\": {\n    \"slug\": \"baremetal_0\"\n  }\n}\n`\n\n\trespSSHKeys = `\n{\n  \"ssh_keys\": [\n    {\n      \"id\": \"` + sshKey + `\",\n      \"label\": \"label\",\n      \"key\": \"key\",\n      \"fingerprint\": \"fingerprint\"\n    }\n  ]\n}\n`\n\tos.Exit(m.Run())\n\n}\n\nfunc TestSetup_Keypair(t *testing.T) {\n\tdefer gock.Off()\n\n\tgock.New(baseURL).\n\t\tMatchHeader(\"X-Auth-Token\", apiKey).\n\t\tGet(getSSH).\n\t\tReply(200).\n\t\tBodyString(respSSHKeys)\n\n\tif err := prov.setupKeypair(context.Background()); err != nil {\n\t\tt.Error(err)\n\t\tt.FailNow()\n\t}\n\n\tif prov.sshkey != sshKey {\n\t\tt.Errorf(\"expected: %s, got: %s\", sshKey, prov.sshkey)\n\t}\n}\n"
  },
  {
    "path": "drivers/scaleway/create.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage scaleway\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/logger\"\n\n\t\"github.com/scaleway/scaleway-sdk-go/api/instance/v1\"\n\t\"github.com/scaleway/scaleway-sdk-go/scw\"\n)\n\nfunc (p *provider) Create(ctx context.Context, opts autoscaler.InstanceCreateOpts) (*autoscaler.Instance, error) {\n\tp.init.Do(func() {\n\t\tp.setup(ctx)\n\t})\n\n\tapi := instance.NewAPI(p.client)\n\n\treq := &instance.CreateServerRequest{\n\t\tName:              opts.Name,\n\t\tDynamicIPRequired: scw.BoolPtr(p.dynamicIP),\n\t\tCommercialType:    p.size,\n\t\tImage:             p.image,\n\t\tTags:              p.tags,\n\t\tSecurityGroup:     p.securityGroup,\n\t}\n\n\tlogger := logger.FromContext(ctx).\n\t\tWithField(\"datacenter\", string(p.zone)).\n\t\tWithField(\"image\", req.Image).\n\t\tWithField(\"size\", req.CommercialType).\n\t\tWithField(\"name\", req.Name)\n\n\tlogger.Infoln(\"instance create\")\n\n\tresp, err := api.CreateServer(req)\n\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tErrorln(\"cannot create instance\")\n\t\treturn nil, err\n\t}\n\n\tbuf := new(bytes.Buffer)\n\terr = p.userdata.Execute(buf, &opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = api.SetServerUserData(&instance.SetServerUserDataRequest{\n\t\tZone:     req.Zone,\n\t\tServerID: resp.Server.ID,\n\t\tKey:      \"cloud-init\",\n\t\tContent:  buf,\n\t})\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tlogger.WithField(\"name\", req.Name).\n\t\tDebugln(\"powering instance on\")\n\n\tserver, err := serverPowerAction(api, ctx, instance.ServerActionPoweron, resp.Server.ID)\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tErrorln(\"cannot power on instance\")\n\t\treturn nil, err\n\t}\n\tif server.State != instance.ServerStateRunning {\n\t\treturn nil, errors.New(\"instance in invalid state: \" + string(server.State))\n\t}\n\n\tlogger.WithField(\"name\", req.Name).\n\t\tInfoln(\"instance created\")\n\n\tip := server.PublicIP\n\tif ip == nil {\n\t\treturn nil, errors.New(\"server not assigned ip\")\n\t}\n\n\treturn &autoscaler.Instance{\n\t\tProvider: autoscaler.ProviderScaleway,\n\t\tID:       server.ID,\n\t\tName:     server.Name,\n\t\tAddress:  ip.Address.String(),\n\t\tRegion:   string(req.Zone),\n\t\tImage:    req.Image,\n\t\tSize:     req.CommercialType,\n\t}, nil\n}\n\nfunc serverPowerAction(api *instance.API, ctx context.Context, action instance.ServerAction, serverID string) (*instance.Server, error) {\n\n\tsaReq := &instance.ServerActionRequest{\n\t\tServerID: serverID,\n\t\tAction:   action,\n\t}\n\n\tgsReq := &instance.GetServerRequest{\n\t\tServerID: serverID,\n\t}\n\n\tterminal := map[instance.ServerState]struct{}{\n\t\tinstance.ServerStateStopped:        {},\n\t\tinstance.ServerStateStoppedInPlace: {},\n\t\tinstance.ServerStateLocked:         {},\n\t\tinstance.ServerStateRunning:        {},\n\t}\n\n\t// Call to power the server on\n\t_, err := api.ServerAction(saReq, scw.WithContext(ctx))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar complete bool\n\tvar server *instance.Server\n\n\t// Wait for context end, or poll every 3 seconds for\n\t// server status, until it is powered on\n\tfor !complete {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil, ctx.Err()\n\t\tcase <-time.After(3 * time.Second):\n\t\t\tgsResp, err := api.GetServer(gsReq, scw.WithContext(ctx))\n\t\t\tif err != nil {\n\t\t\t\treturn server, err\n\t\t\t}\n\t\t\tif _, complete = terminal[gsResp.Server.State]; complete {\n\t\t\t\tserver = gsResp.Server\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\tif server == nil {\n\t\treturn nil, errors.New(\"server is nil\")\n\t}\n\n\treturn server, nil\n}\n"
  },
  {
    "path": "drivers/scaleway/create_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage scaleway\n"
  },
  {
    "path": "drivers/scaleway/destroy.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage scaleway\n\nimport (\n\t\"context\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/logger\"\n\n\t\"github.com/scaleway/scaleway-sdk-go/api/instance/v1\"\n\t\"github.com/scaleway/scaleway-sdk-go/scw\"\n)\n\nfunc (p *provider) Destroy(ctx context.Context, inst *autoscaler.Instance) error {\n\tp.init.Do(func() {\n\t\tp.setup(ctx)\n\t})\n\n\tlogger := logger.FromContext(ctx).\n\t\tWithField(\"datacenter\", inst.Region).\n\t\tWithField(\"image\", inst.Image).\n\t\tWithField(\"size\", inst.Size).\n\t\tWithField(\"name\", inst.Name)\n\n\tapi := instance.NewAPI(p.client)\n\n\tsrvReq := &instance.GetServerRequest{\n\t\tServerID: inst.ID,\n\t}\n\t_, err := api.GetServer(srvReq, scw.WithContext(ctx))\n\tif err != nil {\n\t\tscwErr, ok := err.(*scw.ResponseError)\n\t\tif ok && scwErr.StatusCode == 404 {\n\t\t\treturn autoscaler.ErrInstanceNotFound\n\t\t} else {\n\t\t\tlogger.WithError(err).\n\t\t\t\tErrorln(\"cannot get server\")\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Issue \"terminate\" action, instead of DeleteServer, as terminate\n\t// cleans up volumes and IP addresses attached, too\n\treq := &instance.ServerActionRequest{\n\t\tServerID: inst.ID,\n\t\tAction:   instance.ServerActionTerminate,\n\t}\n\n\tlogger.Debugln(\"terminating server\")\n\n\t_, err = api.ServerAction(req, scw.WithContext(ctx))\n\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tErrorln(\"terminating server failed\")\n\t\treturn err\n\t}\n\n\tlogger.Infoln(\"server terminated\")\n\n\treturn err\n}\n"
  },
  {
    "path": "drivers/scaleway/destroy_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage scaleway\n"
  },
  {
    "path": "drivers/scaleway/option.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage scaleway\n\nimport (\n\t\"io/ioutil\"\n\n\t\"github.com/drone/autoscaler/drivers/internal/userdata\"\n\t\"github.com/scaleway/scaleway-sdk-go/scw\"\n)\n\n// Option configures a Scaleway provider option.\ntype Option func(*provider) error\n\n// WithAccessKey returns an option to set the user access key\nfunc WithAccessKey(accessKey string) Option {\n\treturn func(p *provider) error {\n\t\tp.accessKey = accessKey\n\t\treturn nil\n\t}\n}\n\n// WithSecretKey returns an option to set the user secret key\nfunc WithSecretKey(secretKey string) Option {\n\treturn func(p *provider) error {\n\t\tp.secretKey = secretKey\n\t\treturn nil\n\t}\n}\n\n// WithOrganisationID returns an option to set the user organisation id\nfunc WithOrganisationID(orgId string) Option {\n\treturn func(p *provider) error {\n\t\tp.orgID = orgId\n\t\treturn nil\n\t}\n}\n\n// WithImage returns an option to set the image.\nfunc WithImage(image string) Option {\n\treturn func(p *provider) error {\n\t\tp.image = image\n\t\treturn nil\n\t}\n}\n\n// WithDynamicIP returns an option to enable a dynamic IP.\nfunc WithDynamicIP(dynamicIP bool) Option {\n\treturn func(p *provider) error {\n\t\tp.dynamicIP = dynamicIP\n\t\treturn nil\n\t}\n}\n\n// WithTags returns an option to set the server tags.\nfunc WithTags(tags ...string) Option {\n\treturn func(p *provider) error {\n\t\tp.tags = tags\n\t\treturn nil\n\t}\n}\n\n// WithZone returns an option to set the target zone.\nfunc WithZone(name string) Option {\n\treturn func(p *provider) error {\n\t\tif name == \"\" {\n\t\t\treturn nil\n\t\t}\n\t\tzone, err := scw.ParseZone(name)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.zone = zone\n\t\treturn nil\n\t}\n}\n\n// WithSize returns an option to set the instance size.\nfunc WithSize(size string) Option {\n\treturn func(p *provider) error {\n\t\tp.size = size\n\t\treturn nil\n\t}\n}\n\n// WithUserData returns an option to set the cloud-init\n// template from text.\nfunc WithUserData(text string) Option {\n\treturn func(p *provider) error {\n\t\tif text != \"\" {\n\t\t\tp.userdata = userdata.Parse(text)\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// WithUserDataFile returns an option to set the cloud-init\n// template from file.\nfunc WithUserDataFile(filepath string) Option {\n\treturn func(p *provider) error {\n\t\tif filepath != \"\" {\n\t\t\tb, err := ioutil.ReadFile(filepath)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tp.userdata = userdata.Parse(string(b))\n\t\t}\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "drivers/scaleway/option_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage scaleway\n"
  },
  {
    "path": "drivers/scaleway/provider.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage scaleway\n\nimport (\n\t\"sync\"\n\t\"text/template\"\n\n\t\"github.com/drone/autoscaler/drivers/internal/userdata\"\n\t\"github.com/scaleway/scaleway-sdk-go/scw\"\n\n\t\"github.com/drone/autoscaler\"\n)\n\n// provider implements a Scaleway provider.\ntype provider struct {\n\tinit sync.Once\n\n\taccessKey     string\n\tsecretKey     string\n\torgID         string\n\tsecurityGroup string\n\tdynamicIP     bool\n\tzone          scw.Zone // fr-par-1 or nl-ams-1\n\tsize          string\n\timage         string\n\ttags          []string\n\tuserdata      *template.Template\n\n\tclient *scw.Client\n}\n\n// New returns a new Scaleway provider.\nfunc New(opts ...Option) (autoscaler.Provider, error) {\n\tp := new(provider)\n\tfor _, opt := range opts {\n\t\terr := opt(p)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif p.zone == \"\" {\n\t\tp.zone = scw.ZoneFrPar1\n\t}\n\tif p.size == \"\" {\n\t\tp.size = \"dev1-l\"\n\t}\n\tif p.image == \"\" {\n\t\tp.image = \"ubuntu-bionic\"\n\t}\n\tif p.userdata == nil {\n\t\tp.userdata = userdata.T\n\t}\n\n\treturn p, nil\n}\n"
  },
  {
    "path": "drivers/scaleway/provider_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage scaleway\n"
  },
  {
    "path": "drivers/scaleway/setup.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage scaleway\n\nimport (\n\t\"context\"\n\n\t\"github.com/scaleway/scaleway-sdk-go/scw\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\nfunc (p *provider) setup(ctx context.Context) error {\n\tvar g errgroup.Group\n\tif p.client == nil {\n\t\tg.Go(func() error {\n\t\t\treturn p.newClient(ctx)\n\t\t})\n\t}\n\treturn g.Wait()\n}\n\nfunc (p *provider) newClient(ctx context.Context) error {\n\tclient, err := scw.NewClient(\n\t\tscw.WithDefaultOrganizationID(p.orgID),\n\t\tscw.WithAuth(p.accessKey, p.secretKey),\n\t\tscw.WithDefaultZone(p.zone),\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tp.client = client\n\treturn nil\n}\n"
  },
  {
    "path": "engine/alloc.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage engine\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/engine/certs\"\n\t\"github.com/drone/autoscaler/logger\"\n\t\"github.com/drone/autoscaler/metrics\"\n)\n\ntype allocator struct {\n\twg sync.WaitGroup\n\n\tservers  autoscaler.ServerStore\n\tprovider autoscaler.Provider\n\tmetrics  metrics.Collector\n}\n\nfunc (a *allocator) Allocate(ctx context.Context) error {\n\tlogger := logger.FromContext(ctx)\n\n\tservers, err := a.servers.ListState(ctx, autoscaler.StatePending)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, server := range servers {\n\t\tserver.State = autoscaler.StateCreating\n\t\terr = a.servers.Update(ctx, server)\n\t\tif err != nil {\n\t\t\tlogger.WithError(err).\n\t\t\t\tWithField(\"server\", server.Name).\n\t\t\t\tWithField(\"state\", \"creating\").\n\t\t\t\tErrorln(\"failed to update server state\")\n\t\t\treturn err\n\t\t}\n\n\t\ta.wg.Add(1)\n\t\tgo func(server *autoscaler.Server) {\n\t\t\ta.allocate(ctx, server)\n\t\t\ta.wg.Done()\n\t\t}(server)\n\t}\n\treturn nil\n}\n\nfunc (a *allocator) allocate(ctx context.Context, server *autoscaler.Server) error {\n\tlogger := logger.FromContext(ctx)\n\tdefer func() {\n\t\tif err := recover(); err != nil {\n\t\t\tlogger.WithError(err.(error)).\n\t\t\t\tWithField(\"server\", server.Name).\n\t\t\t\tErrorln(\"unexpected panic\")\n\t\t}\n\t}()\n\n\tca, err := certs.GenerateCA()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcert, err := certs.GenerateCert(server.Name, ca)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tctx, cancel := context.WithTimeout(ctx, time.Hour)\n\tdefer cancel()\n\n\topts := autoscaler.InstanceCreateOpts{\n\t\tName:    server.Name,\n\t\tCAKey:   ca.Key,\n\t\tCACert:  ca.Cert,\n\t\tTLSKey:  cert.Key,\n\t\tTLSCert: cert.Cert,\n\t}\n\n\tstart := time.Now()\n\tinstance, err := a.provider.Create(ctx, opts)\n\tif err != nil {\n\t\ta.metrics.IncrServerCreateError()\n\t\tlogger.WithError(err).\n\t\t\tWithField(\"server\", server.Name).\n\t\t\tErrorln(\"failed to provision server\")\n\n\t\tserver.Error = err.Error()\n\t\tserver.State = autoscaler.StateError\n\t} else {\n\t\ta.metrics.TrackServerCreateTime(start)\n\t\tlogger.WithField(\"server\", server.Name).\n\t\t\tDebugln(\"provisioned server\")\n\n\t\tserver.State = autoscaler.StateCreated\n\t}\n\n\tif instance != nil {\n\t\tserver.ID = instance.ID\n\t\tserver.Address = instance.Address\n\t\tserver.Image = instance.Image\n\t\tserver.Provider = instance.Provider\n\t\tserver.Region = instance.Region\n\t\tserver.Size = instance.Size\n\t\tserver.CACert = opts.CACert\n\t\tserver.CAKey = opts.CAKey\n\t\tserver.TLSCert = opts.TLSCert\n\t\tserver.TLSKey = opts.TLSKey\n\t\tserver.Started = time.Now().Unix()\n\t}\n\n\terr = a.servers.Update(ctx, server)\n\tif err != nil {\n\t\ta.metrics.IncrServerCreateError()\n\t\tlogger.WithError(err).\n\t\t\tWithField(\"server\", server.Name).\n\t\t\tErrorln(\"failed to update server state\")\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "engine/alloc_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage engine\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/metrics\"\n\t\"github.com/drone/autoscaler/mocks\"\n\n\t\"github.com/golang/mock/gomock\"\n)\n\nfunc TestAllocate(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tmockctx := context.Background()\n\tmockInstance := &autoscaler.Instance{}\n\tmockServers := []*autoscaler.Server{\n\t\t{State: autoscaler.StatePending},\n\t}\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().ListState(mockctx, autoscaler.StatePending).Return(mockServers, nil)\n\tstore.EXPECT().Update(mockctx, mockServers[0]).Return(nil)\n\tstore.EXPECT().Update(gomock.Any(), mockServers[0]).Return(nil)\n\n\tprovider := mocks.NewMockProvider(controller)\n\tprovider.EXPECT().Create(gomock.Any(), gomock.Any()).Return(mockInstance, nil)\n\n\ta := allocator{servers: store, provider: provider, metrics: &metrics.NopCollector{}}\n\terr := a.Allocate(mockctx)\n\ta.wg.Wait()\n\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tif got, want := mockServers[0].State, autoscaler.StateCreated; got != want {\n\t\tt.Errorf(\"Want server state Created, got %v\", got)\n\t}\n}\n\nfunc TestAllocate_ServerCreateError(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tmockctx := context.Background()\n\tmockerr := errors.New(\"mock error\")\n\tmockServers := []*autoscaler.Server{\n\t\t{State: autoscaler.StatePending},\n\t}\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().ListState(mockctx, autoscaler.StatePending).Return(mockServers, nil)\n\tstore.EXPECT().Update(mockctx, mockServers[0]).Return(nil)\n\tstore.EXPECT().Update(gomock.Any(), mockServers[0]).Return(nil)\n\n\tprovider := mocks.NewMockProvider(controller)\n\tprovider.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil, mockerr)\n\n\ta := allocator{servers: store, provider: provider, metrics: &metrics.NopCollector{}}\n\ta.Allocate(mockctx)\n\ta.wg.Wait()\n\n\tif got, want := mockServers[0].State, autoscaler.StateError; got != want {\n\t\tt.Errorf(\"Want server state Error, got %v\", got)\n\t}\n}\n\nfunc TestAllocate_ServerListError(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tmockctx := context.Background()\n\tmockerr := errors.New(\"mock error\")\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().ListState(mockctx, autoscaler.StatePending).Return(nil, mockerr)\n\n\ta := allocator{servers: store}\n\tif got, want := a.Allocate(mockctx), mockerr; got != want {\n\t\tt.Errorf(\"Want error getting server list\")\n\t}\n}\n\nfunc TestAllocate_ServerUpdateError(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tmockctx := context.Background()\n\tmockerr := errors.New(\"mock error\")\n\tmockServers := []*autoscaler.Server{\n\t\t{State: autoscaler.StatePending},\n\t}\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().ListState(mockctx, autoscaler.StatePending).Return(mockServers, nil)\n\tstore.EXPECT().Update(mockctx, mockServers[0]).Return(mockerr)\n\n\ta := allocator{servers: store, metrics: &metrics.NopCollector{}}\n\tif got, want := a.Allocate(mockctx), mockerr; got != want {\n\t\tt.Errorf(\"Want error updating server\")\n\t}\n\tif got, want := mockServers[0].State, autoscaler.StateCreating; got != want {\n\t\tt.Errorf(\"Want server state Staging, got %v\", got)\n\t}\n}\n"
  },
  {
    "path": "engine/calc.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage engine\n\nimport \"math\"\n\n// helper function returns the absolute value of x.\nfunc abs(x int) int {\n\tif x < 0 {\n\t\tx = x * -1\n\t}\n\treturn x\n}\n\n// helper function returns the larger of x or y.\nfunc max(x, y int) int {\n\tif x > y {\n\t\treturn x\n\t}\n\treturn y\n}\n\n// helper function calculates the different between the existing\n// server count and required server count to handle queue volume.\nfunc serverDiff(pending, available, concurrency int) int {\n\treturn int(\n\t\tmath.Ceil(\n\t\t\tfloat64(pending-available) /\n\t\t\t\tfloat64(concurrency),\n\t\t),\n\t)\n}\n\n// helper function adjusts the number of servers to provision\n// to ensure it does not exceed the max server count.\nfunc serverCeil(count, additions, ceiling int) int {\n\tif count+additions >= ceiling {\n\t\tadditions = ceiling - count\n\t}\n\treturn additions\n}\n\n// helper function adjusts the number of servers to provision\n// to ensure the minimum server count is maintained.\nfunc serverFloor(count, deletions, floor int) int {\n\tif deletions == 0 {\n\t\treturn 0\n\t}\n\tif floor > count-deletions {\n\t\tdeletions = count - floor\n\t}\n\treturn max(deletions, 0)\n}\n"
  },
  {
    "path": "engine/calc_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage engine\n\nimport \"testing\"\n\nfunc TestAbs(t *testing.T) {\n\ttests := []struct {\n\t\tx, want int\n\t}{\n\t\t{0, 0},\n\t\t{1, 1},\n\t\t{-1, 1},\n\t}\n\tfor _, test := range tests {\n\t\tif got, want := abs(test.x), test.want; got != want {\n\t\t\tt.Errorf(\"Want abs value %d, got %d\", want, got)\n\t\t}\n\t}\n}\n\nfunc TestMax(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want int\n\t}{\n\t\t{0, 1, 1},\n\t\t{0, 0, 0},\n\t\t{1, 1, 1},\n\t\t{-1, 0, 0},\n\t\t{-1, 1, 1},\n\t}\n\tfor _, test := range tests {\n\t\tif got, want := max(test.x, test.y), test.want; got != want {\n\t\t\tt.Errorf(\"Want max value %d, got %d\", want, got)\n\t\t}\n\t}\n}\n\nfunc TestServerDiff(t *testing.T) {\n\ttests := []struct {\n\t\tpending, // count pending builds\n\t\tavailable, // count available capacity\n\t\tconcurrency, // per-server concurrency\n\t\twant int\n\t}{\n\t\t// use 2 of 2 existing\n\t\t{\n\t\t\tpending:     2,\n\t\t\tavailable:   2,\n\t\t\tconcurrency: 2,\n\t\t\twant:        0,\n\t\t},\n\t\t// use 1 of 2 existing\n\t\t{\n\t\t\tpending:     1,\n\t\t\tavailable:   2,\n\t\t\tconcurrency: 2,\n\t\t\twant:        0,\n\t\t},\n\t\t// want 1 server\n\t\t{\n\t\t\tpending:     4,\n\t\t\tavailable:   2,\n\t\t\tconcurrency: 2,\n\t\t\twant:        1,\n\t\t},\n\t\t// want 2 servers\n\t\t{\n\t\t\tpending:     4,\n\t\t\tavailable:   2,\n\t\t\tconcurrency: 1,\n\t\t\twant:        2,\n\t\t},\n\t\t// want 2 servers (round-up)\n\t\t{\n\t\t\tpending:     5,\n\t\t\tavailable:   2,\n\t\t\tconcurrency: 2,\n\t\t\twant:        2,\n\t\t},\n\n\t\t//\n\t\t// the following test cases check for instances when\n\t\t// we have exceess server capacity and want to remove\n\t\t// server instances.\n\t\t//\n\n\t\t// want 0 servers removed, at capacity\n\t\t{\n\t\t\tpending:     1,\n\t\t\tavailable:   1,\n\t\t\tconcurrency: 2,\n\t\t\twant:        0,\n\t\t},\n\t\t// want 0 servers removed, at capacity (server partially used)\n\t\t{\n\t\t\tpending:     1,\n\t\t\tavailable:   2,\n\t\t\tconcurrency: 2,\n\t\t\twant:        0,\n\t\t},\n\t\t// want 1 server removed, pending builds, but excess capacity\n\t\t{\n\t\t\tpending:     2,\n\t\t\tavailable:   4,\n\t\t\tconcurrency: 2,\n\t\t\twant:        -1,\n\t\t},\n\t\t// want 2 servers removed (round down)\n\t\t{\n\t\t\tpending:     0,\n\t\t\tavailable:   5,\n\t\t\tconcurrency: 2,\n\t\t\twant:        -2,\n\t\t},\n\t\t// want 10 servers removed\n\t\t{\n\t\t\tpending:     4,\n\t\t\tavailable:   24,\n\t\t\tconcurrency: 2,\n\t\t\twant:        -10,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tdiff := serverDiff(\n\t\t\ttest.pending,\n\t\t\ttest.available,\n\t\t\ttest.concurrency,\n\t\t)\n\t\tif got, want := diff, test.want; got != want {\n\t\t\tt.Errorf(\"Got server diff %d, want %d\", got, want)\n\t\t}\n\t}\n}\n\nfunc TestSeverCeil(t *testing.T) {\n\ttests := []struct {\n\t\tcurr, // count of servers running\n\t\tdiff, // count of servers to add\n\t\tceil, // max number of servers\n\t\twant int\n\t}{\n\t\t// add 0 servers\n\t\t{\n\t\t\tcurr: 2,\n\t\t\tdiff: 0,\n\t\t\tceil: 2,\n\t\t\twant: 0,\n\t\t},\n\t\t// add 0 servers, handle 0 current count\n\t\t{\n\t\t\tcurr: 0,\n\t\t\tdiff: 0,\n\t\t\tceil: 1,\n\t\t\twant: 0,\n\t\t},\n\t\t// add 1 server\n\t\t{\n\t\t\tcurr: 2,\n\t\t\tdiff: 1,\n\t\t\tceil: 4,\n\t\t\twant: 1,\n\t\t},\n\t\t// add 1 server, handle 0 current count\n\t\t{\n\t\t\tcurr: 0,\n\t\t\tdiff: 2,\n\t\t\tceil: 1,\n\t\t\twant: 1,\n\t\t},\n\t\t// add 2 servers\n\t\t{\n\t\t\tcurr: 2,\n\t\t\tdiff: 2,\n\t\t\tceil: 4,\n\t\t\twant: 2,\n\t\t},\n\t\t// add 2 servers, adjust to ceil\n\t\t{\n\t\t\tcurr: 2,\n\t\t\tdiff: 4,\n\t\t\tceil: 4,\n\t\t\twant: 2,\n\t\t},\n\t\t// add 4 servers, adjust to ceil\n\t\t{\n\t\t\tcurr: 0,\n\t\t\tdiff: 10,\n\t\t\tceil: 4,\n\t\t\twant: 4,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tdiff := serverCeil(\n\t\t\ttest.curr,\n\t\t\ttest.diff,\n\t\t\ttest.ceil,\n\t\t)\n\t\tif got, want := diff, test.want; got != want {\n\t\t\tt.Errorf(\"Got server diff %d, want %d\", got, want)\n\t\t}\n\t}\n}\n\nfunc TestSeverFloor(t *testing.T) {\n\ttests := []struct {\n\t\tcurr, // count of servers running\n\t\tdiff, // count of servers to remove\n\t\tfloor, // min number of servers\n\t\twant int\n\t}{\n\t\t// remove 0 servers\n\t\t{\n\t\t\tcurr:  2,\n\t\t\tdiff:  0,\n\t\t\tfloor: 2,\n\t\t\twant:  0,\n\t\t},\n\t\t// remove 1 server\n\t\t{\n\t\t\tcurr:  4,\n\t\t\tdiff:  1,\n\t\t\tfloor: 2,\n\t\t\twant:  1,\n\t\t},\n\t\t// remove 2 servers\n\t\t{\n\t\t\tcurr:  4,\n\t\t\tdiff:  2,\n\t\t\tfloor: 2,\n\t\t\twant:  2,\n\t\t},\n\t\t// remove 2 servers, adjust to floor\n\t\t{\n\t\t\tcurr:  4,\n\t\t\tdiff:  3,\n\t\t\tfloor: 2,\n\t\t\twant:  2,\n\t\t},\n\t\t// remove 0 servers, adjust to floor\n\t\t{\n\t\t\tcurr:  2,\n\t\t\tdiff:  1,\n\t\t\tfloor: 2,\n\t\t\twant:  0,\n\t\t},\n\t\t// should not remove non-existent servers\n\t\t{\n\t\t\tcurr:  0,\n\t\t\tdiff:  4,\n\t\t\tfloor: 2,\n\t\t\twant:  0,\n\t\t},\n\t\t{\n\t\t\tcurr:  1,\n\t\t\tdiff:  4,\n\t\t\tfloor: 2,\n\t\t\twant:  0,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tdiff := serverFloor(\n\t\t\ttest.curr,\n\t\t\ttest.diff,\n\t\t\ttest.floor,\n\t\t)\n\t\tif got, want := diff, test.want; got != want {\n\t\t\tt.Errorf(\"Got server diff %d, want %d\", got, want)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "engine/certs/cert.go",
    "content": "// Copyright Docker.IO, Inc. All rights reserved.\n// https://github.com/docker/machine\n\npackage certs\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/pem\"\n\t\"math/big\"\n\t\"time\"\n)\n\nconst (\n\t// default key size.\n\tsize = 2048\n\n\t// default organization name for certificates.\n\torganization = \"drone.autoscaler.generated\"\n)\n\n// Certificate stores a certificate and private key.\ntype Certificate struct {\n\tCert []byte\n\tKey  []byte\n}\n\n// GenerateCert generates a certificate for the host address.\nfunc GenerateCert(host string, ca *Certificate) (*Certificate, error) {\n\ttemplate, err := newCertificate(organization)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttemplate.DNSNames = append(template.DNSNames, host)\n\n\ttlsCert, err := tls.X509KeyPair(ca.Cert, ca.Key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpriv, err := rsa.GenerateKey(rand.Reader, size)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tx509Cert, err := x509.ParseCertificate(tlsCert.Certificate[0])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tderBytes, err := x509.CreateCertificate(\n\t\trand.Reader, template, x509Cert, &priv.PublicKey, tlsCert.PrivateKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcertOut := new(bytes.Buffer)\n\tpem.Encode(certOut, &pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: derBytes,\n\t})\n\n\tkeyOut := new(bytes.Buffer)\n\tpem.Encode(keyOut, &pem.Block{\n\t\tType:  \"RSA PRIVATE KEY\",\n\t\tBytes: x509.MarshalPKCS1PrivateKey(priv),\n\t})\n\n\treturn &Certificate{\n\t\tCert: certOut.Bytes(),\n\t\tKey:  keyOut.Bytes(),\n\t}, nil\n}\n\n// GenerateCA generates a CA certificate.\nfunc GenerateCA() (*Certificate, error) {\n\ttemplate, err := newCertificate(organization)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttemplate.IsCA = true\n\ttemplate.KeyUsage |= x509.KeyUsageCertSign\n\ttemplate.KeyUsage |= x509.KeyUsageKeyEncipherment\n\ttemplate.KeyUsage |= x509.KeyUsageKeyAgreement\n\n\tpriv, err := rsa.GenerateKey(rand.Reader, size)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tderBytes, err := x509.CreateCertificate(\n\t\trand.Reader, template, template, &priv.PublicKey, priv)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcertOut := new(bytes.Buffer)\n\tpem.Encode(certOut, &pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: derBytes,\n\t})\n\n\tkeyOut := new(bytes.Buffer)\n\tpem.Encode(keyOut, &pem.Block{\n\t\tType:  \"RSA PRIVATE KEY\",\n\t\tBytes: x509.MarshalPKCS1PrivateKey(priv),\n\t})\n\n\treturn &Certificate{\n\t\tCert: certOut.Bytes(),\n\t\tKey:  keyOut.Bytes(),\n\t}, nil\n}\n\nfunc newCertificate(org string) (*x509.Certificate, error) {\n\tnow := time.Now()\n\t// need to set notBefore slightly in the past to account for time\n\t// skew in the VMs otherwise the certs sometimes are not yet valid\n\tnotBefore := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute()-5, 0, 0, time.Local)\n\tnotAfter := notBefore.Add(time.Hour * 24 * 1080)\n\n\tserialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)\n\tserialNumber, err := rand.Int(rand.Reader, serialNumberLimit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &x509.Certificate{\n\t\tSerialNumber: serialNumber,\n\t\tSubject: pkix.Name{\n\t\t\tOrganization: []string{org},\n\t\t},\n\t\tNotBefore: notBefore,\n\t\tNotAfter:  notAfter,\n\n\t\tKeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageKeyAgreement,\n\t\tBasicConstraintsValid: true,\n\t}, nil\n}\n"
  },
  {
    "path": "engine/certs/cert_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage certs\n\nimport (\n\t\"testing\"\n)\n\nfunc TestGenerate(t *testing.T) {\n\tca, err := GenerateCA()\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\t_, err = GenerateCert(\"company.com\", ca)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n"
  },
  {
    "path": "engine/collect.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage engine\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/docker/docker/api/types/container\"\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/logger\"\n)\n\ntype collector struct {\n\twg sync.WaitGroup\n\n\ttimeout  time.Duration\n\tservers  autoscaler.ServerStore\n\tprovider autoscaler.Provider\n\tclient   clientFunc\n}\n\nfunc (c *collector) Collect(ctx context.Context) error {\n\tlogger := logger.FromContext(ctx)\n\n\tservers, err := c.servers.ListState(ctx, autoscaler.StateShutdown)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, server := range servers {\n\t\tserver.State = autoscaler.StateStopping\n\t\terr = c.servers.Update(ctx, server)\n\t\tif err != nil {\n\t\t\tlogger.WithError(err).\n\t\t\t\tWithField(\"server\", server.Name).\n\t\t\t\tWithField(\"state\", \"stopping\").\n\t\t\t\tErrorln(\"failed to update server state\")\n\t\t\treturn err\n\t\t}\n\n\t\tc.wg.Add(1)\n\t\tgo func(server *autoscaler.Server) {\n\t\t\tc.collect(ctx, server)\n\t\t\tc.wg.Done()\n\t\t}(server)\n\t}\n\treturn nil\n}\n\nfunc (c *collector) collect(ctx context.Context, server *autoscaler.Server) error {\n\tlogger := logger.FromContext(ctx)\n\tlogger.WithField(\"server\", server.Name).\n\t\tDebugln(\"destroying server\")\n\n\tdefer func() {\n\t\tif err := recover(); err != nil {\n\t\t\tlogger.WithField(\"error\", err).\n\t\t\t\tWithField(\"server\", server.Name).\n\t\t\t\tErrorln(\"unexpected panic\")\n\t\t}\n\t}()\n\n\t// if the server was never created there is nothing\n\t// to terminate, so we can just set the agent state\n\t// to term\n\tif server.ID == \"\" {\n\t\tlogger.WithField(\"server\", server.Name).\n\t\t\tDebugln(\"server never provisioned. nothing to stop\")\n\n\t\tserver.Stopped = time.Now().Unix()\n\t\tserver.State = autoscaler.StateStopped\n\n\t\terr := c.servers.Update(ctx, server)\n\t\tif err != nil {\n\t\t\tlogger.WithError(err).\n\t\t\t\tWithField(\"server\", server.Name).\n\t\t\t\tErrorln(\"cannot update server state\")\n\t\t} else {\n\t\t\tlogger.WithField(\"server\", server.Name).\n\t\t\t\tDebugln(\"updated server state to stopped\")\n\t\t}\n\t\treturn err\n\t}\n\n\t// first we need to gracefully shutdown the runner so\n\t// that in-progress pipelines can complete. They will\n\t// have up to 60 minutes to complete before being\n\t// force-killed.\n\tif server.Address != \"\" {\n\t\tclient, closer, err := c.client(server)\n\t\tif closer != nil {\n\t\t\tdefer closer.Close()\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tlogger.WithField(\"server\", server.Name).\n\t\t\tDebugln(\"stopping the agent\")\n\n\t\tctxStop, cancel := context.WithTimeout(ctx, c.timeout)\n\t\tdefer cancel()\n\n\t\t// 1 minute offset between docker stop timeout and\n\t\t// the context timeout.\n\t\ttimeout := c.timeout - time.Minute\n\t\ttimeoutSeconds := int(timeout.Seconds())\n\t\terr = client.ContainerStop(ctxStop, \"agent\", container.StopOptions{Timeout: &timeoutSeconds})\n\t\tif err != nil {\n\t\t\tlogger.WithError(err).\n\t\t\t\tWithField(\"server\", server.Name).\n\t\t\t\tErrorln(\"cannot stop the agent\")\n\t\t} else {\n\t\t\tlogger.WithField(\"server\", server.Name).\n\t\t\t\tDebugln(\"stopped the agent\")\n\t\t}\n\t}\n\n\t// next we need to terminate the remote instance (e.g. in aws).\n\t// It is possible the server was terminated out-of-band in which\n\t// case there is nothing to terminate.\n\n\tin := &autoscaler.Instance{\n\t\tID:       server.ID,\n\t\tProvider: server.Provider,\n\t\tName:     server.Name,\n\t\tAddress:  server.Address,\n\t\tRegion:   server.Region,\n\t\tImage:    server.Image,\n\t\tSize:     server.Size,\n\t}\n\n\tctx, cancel := context.WithTimeout(ctx, time.Hour)\n\tdefer cancel()\n\n\terr := c.provider.Destroy(ctx, in)\n\tif err == autoscaler.ErrInstanceNotFound {\n\t\tlogger.\n\t\t\tWithField(\"state\", \"error\").\n\t\t\tWithField(\"server\", server.Name).\n\t\t\tInfoln(\"server no longer exists. nothing to destroy\")\n\n\t\tserver.Stopped = time.Now().Unix()\n\t\tserver.State = autoscaler.StateStopped\n\t} else if err != nil {\n\t\tlogger.WithField(\"server\", server.Name).\n\t\t\tErrorln(\"failed to destroy server\")\n\n\t\tserver.Error = err.Error()\n\t\tserver.State = autoscaler.StateError\n\t} else {\n\t\tlogger.WithField(\"server\", server.Name).\n\t\t\tDebugln(\"destroyed server\")\n\n\t\tserver.Stopped = time.Now().Unix()\n\t\tserver.State = autoscaler.StateStopped\n\t}\n\n\terr = c.servers.Update(ctx, server)\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tWithField(\"server\", server.Name).\n\t\t\tErrorln(\"failed to update server state\")\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "engine/collect_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage engine\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/mocks\"\n\n\tdocker \"github.com/docker/docker/client\"\n\t\"github.com/golang/mock/gomock\"\n)\n\nfunc TestCollect(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tmockctx := context.Background()\n\tmockServers := []*autoscaler.Server{\n\t\t{\n\t\t\tID:      \"i-1234567890abcdef0\",\n\t\t\tAddress: \"1.2.3.4\",\n\t\t\tState:   autoscaler.StateShutdown,\n\t\t},\n\t}\n\n\tclient := mocks.NewMockAPIClient(controller)\n\tclient.EXPECT().ContainerStop(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().ListState(mockctx, autoscaler.StateShutdown).Return(mockServers, nil)\n\tstore.EXPECT().Update(mockctx, mockServers[0]).Return(nil)\n\tstore.EXPECT().Update(gomock.Any(), mockServers[0]).Return(nil)\n\n\tprovider := mocks.NewMockProvider(controller)\n\tprovider.EXPECT().Destroy(gomock.Any(), gomock.Any()).Return(nil)\n\n\tc := collector{\n\t\tservers:  store,\n\t\tprovider: provider,\n\t\tclient: func(*autoscaler.Server) (docker.APIClient, io.Closer, error) {\n\t\t\treturn client, nil, nil\n\t\t},\n\t}\n\terr := c.Collect(mockctx)\n\tc.wg.Wait()\n\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tif got, want := mockServers[0].State, autoscaler.StateStopped; got != want {\n\t\tt.Errorf(\"Want server state Stopped, got %v\", got)\n\t}\n}\n\nfunc TestCollect_DockerStopError(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tmockerr := errors.New(\"oh no\")\n\tmockctx := context.Background()\n\tmockServers := []*autoscaler.Server{\n\t\t{\n\t\t\tID:      \"i-1234567890abcdef0\",\n\t\t\tAddress: \"1.2.3.4\",\n\t\t\tState:   autoscaler.StateShutdown,\n\t\t},\n\t}\n\n\tclient := mocks.NewMockAPIClient(controller)\n\tclient.EXPECT().ContainerStop(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockerr)\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().ListState(mockctx, autoscaler.StateShutdown).Return(mockServers, nil)\n\tstore.EXPECT().Update(mockctx, mockServers[0]).Return(nil)\n\tstore.EXPECT().Update(gomock.Any(), mockServers[0]).Return(nil)\n\n\tprovider := mocks.NewMockProvider(controller)\n\tprovider.EXPECT().Destroy(gomock.Any(), gomock.Any()).Return(nil)\n\n\tc := collector{\n\t\tservers:  store,\n\t\tprovider: provider,\n\t\tclient: func(*autoscaler.Server) (docker.APIClient, io.Closer, error) {\n\t\t\treturn client, nil, nil\n\t\t},\n\t}\n\terr := c.Collect(mockctx)\n\tc.wg.Wait()\n\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tif got, want := mockServers[0].State, autoscaler.StateStopped; got != want {\n\t\tt.Errorf(\"Want server state Stopped, got %v\", got)\n\t}\n}\n\nfunc TestCollect_ServerDestroyError(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tmockctx := context.Background()\n\tmockerr := errors.New(\"mock error\")\n\tmockServers := []*autoscaler.Server{\n\t\t{\n\t\t\tID:      \"i-1234567890abcdef0\",\n\t\t\tName:    \"agent-807jVFwj\",\n\t\t\tAddress: \"1.2.3.4\",\n\t\t\tState:   autoscaler.StateShutdown,\n\t\t},\n\t}\n\n\tclient := mocks.NewMockAPIClient(controller)\n\tclient.EXPECT().ContainerStop(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().ListState(mockctx, autoscaler.StateShutdown).Return(mockServers, nil)\n\tstore.EXPECT().Update(mockctx, mockServers[0]).Return(nil)\n\tstore.EXPECT().Update(gomock.Any(), mockServers[0]).Return(nil)\n\n\tprovider := mocks.NewMockProvider(controller)\n\tprovider.EXPECT().Destroy(gomock.Any(), gomock.Any()).Return(mockerr)\n\n\tc := collector{\n\t\tservers:  store,\n\t\tprovider: provider,\n\t\tclient: func(*autoscaler.Server) (docker.APIClient, io.Closer, error) {\n\t\t\treturn client, nil, nil\n\t\t},\n\t}\n\tc.Collect(mockctx)\n\tc.wg.Wait()\n\n\tif got, want := mockServers[0].State, autoscaler.StateError; got != want {\n\t\tt.Errorf(\"Want server state Error, got %v\", got)\n\t}\n}\n\nfunc TestCollect_ServerListError(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tmockctx := context.Background()\n\tmockerr := errors.New(\"mock error\")\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().ListState(mockctx, autoscaler.StateShutdown).Return(nil, mockerr)\n\n\tc := collector{servers: store}\n\tif got, want := c.Collect(mockctx), mockerr; got != want {\n\t\tt.Errorf(\"Want error getting server list\")\n\t}\n}\n\nfunc TestCollect_ServerUpdateError(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tmockctx := context.Background()\n\tmockerr := errors.New(\"mock error\")\n\tmockServers := []*autoscaler.Server{\n\t\t{\n\t\t\tID:      \"i-1234567890abcdef0\",\n\t\t\tAddress: \"1.2.3.4\",\n\t\t\tState:   autoscaler.StateShutdown,\n\t\t},\n\t}\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().ListState(mockctx, autoscaler.StateShutdown).Return(mockServers, nil)\n\tstore.EXPECT().Update(mockctx, mockServers[0]).Return(mockerr)\n\n\tc := collector{servers: store}\n\tif got, want := c.Collect(mockctx), mockerr; got != want {\n\t\tt.Errorf(\"Want error updating server\")\n\t}\n\tif got, want := mockServers[0].State, autoscaler.StateStopping; got != want {\n\t\tt.Errorf(\"Want server state Stopping, got %v\", got)\n\t}\n}\n\nfunc TestCollect_ServerNeverProvisioned(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tmockctx := context.Background()\n\tmockServer := &autoscaler.Server{\n\t\tID:    \"\",\n\t\tState: autoscaler.StateShutdown,\n\t}\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().Update(gomock.Any(), mockServer).Return(nil).Times(1)\n\n\tc := collector{servers: store}\n\tif err := c.collect(mockctx, mockServer); err != nil {\n\t\tt.Error(err)\n\t}\n\tif got, want := mockServer.State, autoscaler.StateStopped; got != want {\n\t\tt.Errorf(\"Want server state Stopping, got %v\", got)\n\t}\n}\n"
  },
  {
    "path": "engine/docker.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage engine\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\n\tdocker \"github.com/docker/docker/client\"\n\t\"github.com/drone/autoscaler\"\n)\n\n// clientFunc defines a builder funciton used to build and return\n// the docker client from a Server. This is primarily used for\n// mock unit testing.\ntype clientFunc func(*autoscaler.Server) (docker.APIClient, io.Closer, error)\n\n// newDockerClient returns a new Docker client configured for the\n// Server host and certificate chain.\nfunc newDockerClient(server *autoscaler.Server) (docker.APIClient, io.Closer, error) {\n\ttlsCert, err := tls.X509KeyPair(server.TLSCert, server.TLSKey)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\ttlsConfig := &tls.Config{\n\t\tServerName:   server.Name,\n\t\tCertificates: []tls.Certificate{tlsCert},\n\t}\n\ttlsConfig.RootCAs = x509.NewCertPool()\n\ttlsConfig.RootCAs.AppendCertsFromPEM(server.CACert)\n\tclient := &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tTLSClientConfig: tlsConfig,\n\t\t},\n\t}\n\tdockerClient, err := docker.NewClientWithOpts(\n\t   docker.WithAPIVersionNegotiation(),\n\t   docker.WithHTTPClient(client),\n\t   docker.WithHost(fmt.Sprintf(\"https://%s:2376\", server.Address)),\n\t)\n\treturn dockerClient, dockerClient, err\n}\n"
  },
  {
    "path": "engine/engine.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage engine\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/config\"\n\t\"github.com/drone/autoscaler/logger\"\n\t\"github.com/drone/autoscaler/metrics\"\n\t\"github.com/drone/drone-go/drone\"\n)\n\n// defines the interval at which terminated instances are\n// purged from the database.\nconst purge = time.Hour * 24\n\ntype engine struct {\n\tmu sync.Mutex\n\n\tallocator *allocator\n\tcollector *collector\n\tinstaller *installer\n\tpinger    *pinger\n\tplanner   *planner\n\treaper    *reaper\n\tmetrics   metrics.Collector\n\n\tinterval time.Duration\n\tpaused   bool\n}\n\n// New returns a new autoscale Engine.\nfunc New(\n\tclient drone.Client,\n\tconfig config.Config,\n\tservers autoscaler.ServerStore,\n\tprovider autoscaler.Provider,\n\tmetrics metrics.Collector,\n) autoscaler.Engine {\n\treturn &engine{\n\t\tpaused:   false,\n\t\tinterval: config.Interval,\n\t\tallocator: &allocator{\n\t\t\tservers:  servers,\n\t\t\tprovider: provider,\n\t\t\tmetrics:  metrics,\n\t\t},\n\t\tcollector: &collector{\n\t\t\ttimeout:  config.Timeout.Stop,\n\t\t\tservers:  servers,\n\t\t\tprovider: provider,\n\t\t\tclient:   newDockerClient,\n\t\t},\n\t\tinstaller: &installer{\n\t\t\tmetrics:                 metrics,\n\t\t\tservers:                 servers,\n\t\t\tos:                      config.Agent.OS,\n\t\t\tarch:                    config.Agent.Arch,\n\t\t\timage:                   config.Agent.Image,\n\t\t\tsecret:                  config.Agent.Token,\n\t\t\tenvs:                    config.Agent.Environ,\n\t\t\tvolumes:                 config.Agent.Volumes,\n\t\t\tports:                   config.Agent.Ports,\n\t\t\tlabels:                  config.Agent.Labels,\n\t\t\tproto:                   config.Server.Proto,\n\t\t\thost:                    config.Server.Host,\n\t\t\tclient:                  newDockerClient,\n\t\t\trunner:                  config.Runner,\n\t\t\tcheckInterval:           config.Check.Interval,\n\t\t\tcheckDeadline:           config.Check.Deadline,\n\t\t\tgcEnabled:               config.GC.Enabled,\n\t\t\tgcDebug:                 config.GC.Debug,\n\t\t\tgcImage:                 config.GC.Image,\n\t\t\tgcIgnore:                config.GC.Images,\n\t\t\tgcInterval:              config.GC.Interval,\n\t\t\tgcCache:                 config.GC.Cache,\n\t\t\twatchtowerEnabled:       config.Watchtower.Enabled,\n\t\t\twatchtowerImage:         config.Watchtower.Image,\n\t\t\twatchtowerStopSignal:    config.Watchtower.Signal,\n\t\t\twatchtowerSignalEnabled: config.Watchtower.SignalEnabled,\n\t\t\twatchtowerTimeout:       config.Watchtower.Timeout,\n\t\t\twatchtowerInterval:      config.Watchtower.Interval,\n\t\t},\n\t\tpinger: &pinger{\n\t\t\tservers:  servers,\n\t\t\tclient:   newDockerClient,\n\t\t\tenabled:  config.Pinger.Enabled,\n\t\t\tinterval: config.Pinger.Interval,\n\t\t},\n\t\tplanner: &planner{\n\t\t\tclient:     client,\n\t\t\tservers:    servers,\n\t\t\tos:         config.Agent.OS,\n\t\t\tarch:       config.Agent.Arch,\n\t\t\tversion:    config.Agent.Version,\n\t\t\tkernel:     config.Agent.Kernel,\n\t\t\tbuffer:     config.CapacityBuffer,\n\t\t\tttu:        config.Pool.MinAge,\n\t\t\tmin:        config.Pool.Min,\n\t\t\tmax:        config.Pool.Max,\n\t\t\tcap:        config.Agent.Concurrency,\n\t\t\tlabels:     config.Agent.Labels,\n\t\t\tnamePrefix: config.Agent.NamePrefix,\n\t\t},\n\t\treaper: &reaper{\n\t\t\tservers:  servers,\n\t\t\tprovider: provider,\n\t\t\tinterval: config.Reaper.Interval,\n\t\t\tenabled:  config.Reaper.Enabled,\n\t\t},\n\t}\n}\n\n// Pause paueses the scaler.\nfunc (e *engine) Pause() {\n\te.mu.Lock()\n\te.paused = true\n\te.mu.Unlock()\n}\n\n// Paused returns true if scaling is paused.\nfunc (e *engine) Paused() bool {\n\te.mu.Lock()\n\tdefer e.mu.Unlock()\n\treturn e.paused\n}\n\n// Resume resumes the scaler.\nfunc (e *engine) Resume() {\n\te.mu.Lock()\n\te.paused = false\n\te.mu.Unlock()\n}\n\nfunc (e *engine) Start(ctx context.Context) {\n\te.reset(ctx)\n\n\tvar wg sync.WaitGroup\n\twg.Add(7)\n\tgo func() {\n\t\te.allocate(ctx)\n\t\twg.Done()\n\t}()\n\tgo func() {\n\t\te.install(ctx)\n\t\twg.Done()\n\t}()\n\tgo func() {\n\t\te.collect(ctx)\n\t\twg.Done()\n\t}()\n\tgo func() {\n\t\te.plan(ctx)\n\t\twg.Done()\n\t}()\n\tgo func() {\n\t\te.purge(ctx)\n\t\twg.Done()\n\t}()\n\tgo func() {\n\t\te.reap(ctx)\n\t\twg.Done()\n\t}()\n\tgo func() {\n\t\te.ping(ctx)\n\t\twg.Done()\n\t}()\n\twg.Wait()\n}\n\n// runs the allocation process.\nfunc (e *engine) allocate(ctx context.Context) {\n\tconst interval = time.Second * 10\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase <-time.After(interval):\n\t\t\te.allocator.Allocate(ctx)\n\t\t}\n\t}\n}\n\n// runs the installation process.\nfunc (e *engine) install(ctx context.Context) {\n\tconst interval = time.Second * 10\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase <-time.After(interval):\n\t\t\te.installer.Install(ctx)\n\t\t}\n\t}\n}\n\n// runs the collection process.\nfunc (e *engine) collect(ctx context.Context) {\n\tconst interval = time.Second * 10\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase <-time.After(interval):\n\t\t\te.collector.Collect(ctx)\n\t\t}\n\t}\n}\n\n// runs the planning process.\nfunc (e *engine) plan(ctx context.Context) {\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase <-time.After(e.interval):\n\t\t\tif !e.Paused() {\n\t\t\t\te.planner.Plan(ctx)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// runs the ping process.\nfunc (e *engine) ping(ctx context.Context) {\n\t// by default, run the pinger every 10m.\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase <-time.After(e.pinger.interval):\n\t\t\te.pinger.Ping(ctx)\n\t\t}\n\t}\n}\n\n// runs the purge process.\nfunc (e *engine) purge(ctx context.Context) {\n\tconst interval = time.Hour * 24\n\tconst retain = time.Hour * 24 * -1\n\n\tlogger := logger.FromContext(ctx)\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase <-time.After(interval):\n\t\t\tlogger.WithField(\"ttl\", retain.String()).\n\t\t\t\tDebugln(\"clear stopped servers from database\")\n\t\t\te.planner.servers.Purge(ctx, time.Now().Add(retain).Unix())\n\t\t}\n\t}\n}\n\n// runs the reaper process.\nfunc (e *engine) reap(ctx context.Context) {\n\t// by default, the reaper is run hourly since in general this\n\t// should happen infrequently.\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase <-time.After(e.reaper.interval):\n\t\t\te.reaper.Reap(ctx)\n\t\t}\n\t}\n}\n\nfunc (e *engine) reset(ctx context.Context) {\n\t// handle the situation where the autoscaler is stopped or\n\t// restarted during instance setup or teardown. If this happens\n\t// reset the instance state to resume.\n\tservers, _ := e.allocator.servers.List(ctx)\n\tfor _, s := range servers {\n\t\tswitch s.State {\n\t\tcase autoscaler.StateStaging:\n\t\t\tlog := logger.FromContext(ctx).\n\t\t\t\tWithField(\"instance\", s.Name).\n\t\t\t\tWithField(\"address\", s.Address).\n\t\t\t\tWithField(\"from-state\", \"staging\").\n\t\t\t\tWithField(\"to-state\", \"created\")\n\t\t\tlog.Infoln(\"reset instance state\")\n\t\t\ts.State = autoscaler.StateCreated\n\t\t\tif err := e.allocator.servers.Update(ctx, s); err != nil {\n\t\t\t\tlog.WithError(err).\n\t\t\t\t\tError(\"failed to reset instance state\")\n\t\t\t}\n\t\tcase autoscaler.StateStopping:\n\t\t\tlog := logger.FromContext(ctx).\n\t\t\t\tWithField(\"instance\", s.Name).\n\t\t\t\tWithField(\"address\", s.Address).\n\t\t\t\tWithField(\"from-state\", \"stopping\").\n\t\t\t\tWithField(\"to-state\", \"shutdown\")\n\t\t\tlog.Infoln(\"reset instance state\")\n\t\t\ts.State = autoscaler.StateShutdown\n\t\t\tif err := e.allocator.servers.Update(ctx, s); err != nil {\n\t\t\t\tlog.WithError(err).\n\t\t\t\t\tErrorln(\"failed to reset instance state\")\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "engine/install.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage engine\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/config\"\n\t\"github.com/drone/autoscaler/logger\"\n\t\"github.com/drone/autoscaler/metrics\"\n\n\t\"github.com/docker/docker/api/types/container\"\n\t\"github.com/docker/docker/api/types/image\"\n\t\"github.com/docker/docker/api/types/mount\"\n\tdocker \"github.com/docker/docker/client\"\n\t\"github.com/docker/go-connections/nat\"\n)\n\ntype installer struct {\n\twg sync.WaitGroup\n\n\tos               string\n\tarch             string\n\timage            string\n\tsecret           string\n\tvolumes          []string\n\tports            []string\n\thost             string\n\tproto            string\n\tenvs             []string\n\tkeepaliveTime    time.Duration\n\tkeepaliveTimeout time.Duration\n\trunner           config.Runner\n\tlabels           map[string]string\n\n\tcheckInterval time.Duration\n\tcheckDeadline time.Duration\n\n\tgcEnabled  bool\n\tgcDebug    bool\n\tgcImage    string\n\tgcIgnore   []string\n\tgcInterval time.Duration\n\tgcCache    string\n\n\twatchtowerEnabled       bool\n\twatchtowerImage         string\n\twatchtowerStopSignal    string\n\twatchtowerSignalEnabled bool\n\twatchtowerInterval      int\n\twatchtowerTimeout       time.Duration\n\n\tservers autoscaler.ServerStore\n\tmetrics metrics.Collector\n\tclient  clientFunc\n}\n\nfunc (i *installer) Install(ctx context.Context) error {\n\tlogger := logger.FromContext(ctx)\n\n\tservers, err := i.servers.ListState(ctx, autoscaler.StateCreated)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, server := range servers {\n\t\tserver.State = autoscaler.StateStaging\n\t\terr = i.servers.Update(ctx, server)\n\t\tif err != nil {\n\t\t\tlogger.WithError(err).\n\t\t\t\tWithField(\"server\", server.Name).\n\t\t\t\tWithField(\"state\", \"staging\").\n\t\t\t\tErrorln(\"failed to update server state\")\n\t\t\treturn err\n\t\t}\n\n\t\ti.wg.Add(1)\n\t\tgo func(server *autoscaler.Server) {\n\t\t\ti.install(ctx, server)\n\t\t\ti.wg.Done()\n\t\t}(server)\n\t}\n\treturn nil\n}\n\nfunc (i *installer) install(ctx context.Context, instance *autoscaler.Server) error {\n\tlogger := logger.FromContext(ctx).\n\t\tWithField(\"ip\", instance.Address).\n\t\tWithField(\"name\", instance.Name)\n\n\tclient, closer, err := i.client(instance)\n\tif closer != nil {\n\t\tdefer closer.Close()\n\t}\n\tif err != nil {\n\t\ti.metrics.IncrServerInitError()\n\t\tlogger.WithError(err).\n\t\t\tErrorln(\"cannot create docker client\")\n\t\treturn i.errorUpdate(ctx, instance, err)\n\t}\n\n\tlogger.WithField(\"name\", instance.Name).\n\t\tDebugln(\"check docker connectivity\")\n\n\ttimeout, cancel := context.WithTimeout(ctx, i.checkDeadline)\n\tdefer cancel()\n\n\tstart := time.Now()\n\tinterval := time.Duration(0)\npoller:\n\tfor {\n\t\tselect {\n\t\tcase <-timeout.Done():\n\t\t\ti.metrics.IncrServerInitError()\n\t\t\tlogger.WithField(\"name\", instance.Name).\n\t\t\t\tDebugln(\"connection timeout\")\n\n\t\t\treturn i.errorUpdate(ctx, instance, timeout.Err())\n\t\tcase <-time.After(interval):\n\t\t\tinterval = i.checkInterval\n\n\t\t\tlogger.WithField(\"name\", instance.Name).\n\t\t\t\tDebugln(\"connecting to docker\")\n\n\t\t\t_, err := client.ContainerList(ctx, container.ListOptions{})\n\t\t\tif err != nil {\n\t\t\t\tlogger.\n\t\t\t\t\tWithField(\"error\", err.Error()).\n\t\t\t\t\tWithField(\"name\", instance.Name).\n\t\t\t\t\tDebugf(\"cannot connect, retry in %v\", interval)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tbreak poller\n\t\t}\n\t}\n\n\t// track elapsed time to establish a connection\n\ti.metrics.TrackServerInitTime(start)\n\n\tlogger.WithField(\"image\", i.image).\n\t\tDebugln(\"pull docker image\")\n\n\tstart = time.Now()\n\trc, err := client.ImagePull(ctx, i.image, image.PullOptions{})\n\tif err != nil {\n\t\ti.metrics.IncrServerSetupError()\n\t\tlogger.WithError(err).\n\t\t\tWithField(\"image\", i.image).\n\t\t\tErrorln(\"cannot pull docker image\")\n\t\treturn i.errorUpdate(ctx, instance, err)\n\t}\n\tio.Copy(ioutil.Discard, rc)\n\trc.Close()\n\n\tlogger.WithField(\"image\", i.image).\n\t\tDebugln(\"create agent container\")\n\n\tenvs := append(i.envs,\n\t\tfmt.Sprintf(\"DRONE_RPC_HOST=%s\", i.host),\n\t\tfmt.Sprintf(\"DRONE_RPC_PROTO=%s\", i.proto),\n\t\tfmt.Sprintf(\"DRONE_RPC_SERVER=%s://%s\", i.proto, i.host),\n\t\tfmt.Sprintf(\"DRONE_RPC_SECRET=%s\", i.secret),\n\t\tfmt.Sprintf(\"DRONE_RUNNER_CAPACITY=%v\", instance.Capacity),\n\t\tfmt.Sprintf(\"DRONE_RUNNER_NAME=%s\", instance.Name),\n\t)\n\n\tif s := i.runner.Volumes; s != \"\" {\n\t\tenvs = append(envs, fmt.Sprintf(\"DRONE_RUNNER_VOLUMES=%s\", s))\n\t}\n\tif s := i.runner.Devices; s != \"\" {\n\t\tenvs = append(envs, fmt.Sprintf(\"DRONE_RUNNER_DEVICES=%s\", s))\n\t}\n\tif s := i.runner.EnvFile; s != \"\" {\n\t\tenvs = append(envs, fmt.Sprintf(\"DRONE_RUNNER_ENV_FILE=%s\", s))\n\t}\n\tif s := i.runner.Privileged; s != \"\" {\n\t\tenvs = append(envs, fmt.Sprintf(\"DRONE_RUNNER_PRIVILEGED_IMAGES=%s\", s))\n\t}\n\n\tif len(i.labels) > 0 {\n\t\tvar stringLabels []string\n\n\t\tfor key, val := range i.labels {\n\t\t\tstringLabels = append(stringLabels, fmt.Sprintf(\"%s:%s\", key, val))\n\t\t}\n\n\t\tenvs = append(envs,\n\t\t\tfmt.Sprintf(\"DRONE_RUNNER_LABELS=%s\", strings.Join(stringLabels, \",\")),\n\t\t)\n\t}\n\n\tvar mounts []mount.Mount\n\tvolumes := i.volumes\n\tswitch i.os {\n\tcase \"windows\":\n\t\tmounts = append(mounts, mount.Mount{\n\t\t\tSource: `\\\\.\\pipe\\docker_engine`,\n\t\t\tTarget: `\\\\.\\pipe\\docker_engine`,\n\t\t\tType:   mount.TypeNamedPipe,\n\t\t})\n\tdefault:\n\t\tvolumes = append(volumes,\n\t\t\t\"/var/run/docker.sock:/var/run/docker.sock\",\n\t\t)\n\n\t\t// if memory serves me correctly, we need to explicitly\n\t\t// set this to nil to ensure the json representation\n\t\t// of this value is null. but I could be wrong in which\n\t\t// case this can be removed. ‾\\_(ツ)_/‾\n\t\tmounts = nil\n\t}\n\n\texposedPorts, portBindings, err := nat.ParsePortSpecs(i.ports)\n\tif err != nil {\n\t\ti.metrics.IncrServerInitError()\n\t\tlogger.WithError(err).Errorln(\"could not create port binding\")\n\t\treturn i.errorUpdate(ctx, instance, err)\n\t}\n\n\tres, err := client.ContainerCreate(ctx,\n\t\t&container.Config{\n\t\t\tImage:        i.image,\n\t\t\tAttachStdout: true,\n\t\t\tAttachStderr: true,\n\t\t\tEnv:          envs,\n\t\t\tVolumes:      toVol(volumes),\n\t\t\tExposedPorts: exposedPorts,\n\t\t\tLabels: map[string]string{\n\t\t\t\t\"com.centurylinklabs.watchtower.enable\":      fmt.Sprint(i.watchtowerSignalEnabled),\n\t\t\t\t\"com.centurylinklabs.watchtower.stop-signal\": i.watchtowerStopSignal,\n\t\t\t\t\"io.drone.agent.name\":                        instance.Name,\n\t\t\t\t\"io.drone.agent.zone\":                        instance.Region,\n\t\t\t\t\"io.drone.agent.size\":                        instance.Size,\n\t\t\t\t\"io.drone.agent.instance\":                    instance.ID,\n\t\t\t\t\"io.drone.agent.capacity\":                    fmt.Sprint(instance.Capacity),\n\t\t\t},\n\t\t},\n\t\t&container.HostConfig{\n\t\t\tBinds:        volumes,\n\t\t\tMounts:       mounts,\n\t\t\tPortBindings: portBindings,\n\t\t\tRestartPolicy: container.RestartPolicy{\n\t\t\t\tName: \"always\",\n\t\t\t},\n\t\t}, nil, nil, \"agent\")\n\n\tif err != nil {\n\t\ti.metrics.IncrServerSetupError()\n\t\tlogger.WithField(\"image\", i.image).\n\t\t\tErrorln(\"cannot create agent container\")\n\t\treturn i.errorUpdate(ctx, instance, err)\n\t}\n\n\tlogger.WithField(\"image\", i.image).\n\t\tDebugln(\"start the agent container\")\n\n\terr = client.ContainerStart(ctx, res.ID, container.StartOptions{})\n\tif err != nil {\n\t\ti.metrics.IncrServerSetupError()\n\t\tlogger.WithField(\"image\", i.image).\n\t\t\tDebugln(\"cannot start the agent container\")\n\t\treturn i.errorUpdate(ctx, instance, err)\n\t}\n\n\tlogger.WithField(\"image\", i.image).\n\t\tDebugln(\"agent container started\")\n\n\tif i.gcEnabled {\n\t\tlogger.WithField(\"image\", i.image).\n\t\t\tDebugln(\"setup the garbage collector\")\n\t\terr = i.setupGarbageCollector(ctx, client)\n\t\tif err != nil {\n\t\t\tlogger.WithError(err).\n\t\t\t\tWithField(\"image\", i.image).\n\t\t\t\tWarnln(\"cannot setup the garbage collector\")\n\t\t}\n\t}\n\n\tif i.watchtowerEnabled {\n\t\tlogger.WithField(\"image\", i.image).\n\t\t\tDebugln(\"setup watchtower\")\n\t\terr = i.setupWatchtower(ctx, client)\n\t\tif err != nil {\n\t\t\tlogger.WithError(err).\n\t\t\t\tWithField(\"image\", i.image).\n\t\t\t\tWarnln(\"cannot setup watchtwoer\")\n\t\t}\n\t}\n\n\t// track elapsed time to install software.\n\ti.metrics.TrackServerSetupTime(start)\n\n\tinstance.State = autoscaler.StateRunning\n\terr = i.servers.Update(ctx, instance)\n\tif err != nil {\n\t\ti.metrics.IncrServerSetupError()\n\t\tlogger.WithError(err).\n\t\t\tWithField(\"server\", instance.Name).\n\t\t\tWithField(\"state\", \"running\").\n\t\t\tErrorln(\"failed to update server state\")\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (i *installer) setupWatchtower(ctx context.Context, client docker.APIClient) error {\n\tvols := []string{\"/var/run/docker.sock:/var/run/docker.sock\"}\n\tres, err := client.ContainerCreate(ctx,\n\t\t&container.Config{\n\t\t\tImage:        i.watchtowerImage,\n\t\t\tAttachStdout: true,\n\t\t\tAttachStderr: true,\n\t\t\tVolumes:      toVol(vols),\n\t\t\tEnv: []string{\n\t\t\t\tfmt.Sprintf(\"WATCHTOWER_POLL_INTERVAL=%d\", i.watchtowerInterval),\n\t\t\t\tfmt.Sprintf(\"WATCHTOWER_TIMEOUT=%s\", i.watchtowerTimeout),\n\t\t\t\tfmt.Sprintf(\"WATCHTOWER_CLEANUP=true\"),\n\t\t\t\tfmt.Sprintf(\"WATCHTOWER_LABEL_ENABLE=true\"),\n\t\t\t},\n\t\t},\n\t\t&container.HostConfig{\n\t\t\tBinds: vols,\n\t\t\tRestartPolicy: container.RestartPolicy{\n\t\t\t\tName: \"always\",\n\t\t\t},\n\t\t}, nil, nil, \"watchtower\")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn client.ContainerStart(ctx, res.ID, container.StartOptions{})\n}\n\nfunc (i *installer) setupGarbageCollector(ctx context.Context, client docker.APIClient) error {\n\tlogger := logger.FromContext(ctx)\n\tvols := []string{\"/var/run/docker.sock:/var/run/docker.sock\"}\n\tenvs := []string{\n\t\tfmt.Sprintf(\"GC_CACHE=%s\", i.gcCache),\n\t\tfmt.Sprintf(\"GC_DEBUG=%v\", i.gcDebug),\n\t\tfmt.Sprintf(\"GC_INTERVAL=%s\", i.gcInterval),\n\t}\n\tif len(i.gcIgnore) > 0 {\n\t\tenvs = append(envs,\n\t\t\tfmt.Sprintf(\"GC_IGNORE=%s\", strings.Join(i.gcIgnore, \",\")),\n\t\t)\n\t}\n\n\tlogger.WithField(\"image\", i.gcImage).\n\t\tDebugln(\"pull gc image\")\n\n\trc, err := client.ImagePull(ctx, i.gcImage, image.PullOptions{})\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tWithField(\"image\", i.gcImage).\n\t\t\tErrorln(\"cannot pull gc image\")\n\t\treturn err\n\t}\n\tio.Copy(ioutil.Discard, rc)\n\trc.Close()\n\n\tres, err := client.ContainerCreate(ctx,\n\t\t&container.Config{\n\t\t\tImage:        i.gcImage,\n\t\t\tAttachStdout: true,\n\t\t\tAttachStderr: true,\n\t\t\tVolumes:      toVol(vols),\n\t\t\tEnv:          envs,\n\t\t\tLabels: map[string]string{\n\t\t\t\t\"com.centurylinklabs.watchtower.enable\": fmt.Sprint(i.watchtowerSignalEnabled),\n\t\t\t},\n\t\t},\n\t\t&container.HostConfig{\n\t\t\tBinds: vols,\n\t\t\tRestartPolicy: container.RestartPolicy{\n\t\t\t\tName: \"always\",\n\t\t\t},\n\t\t}, nil, nil, \"drone-gc\")\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn client.ContainerStart(ctx, res.ID, container.StartOptions{})\n}\n\nfunc (i *installer) errorUpdate(ctx context.Context, server *autoscaler.Server, err error) error {\n\tif err != nil {\n\t\tserver.State = autoscaler.StateError\n\t\tserver.Error = err.Error()\n\t\txerr := i.servers.Update(ctx, server)\n\t\tif xerr != nil {\n\t\t\tlogger.FromContext(ctx).\n\t\t\t\tWithError(xerr).\n\t\t\t\tWithField(\"server\", server.Name).\n\t\t\t\tWithField(\"state\", \"error\").\n\t\t\t\tErrorln(\"failed to update server state\")\n\t\t}\n\t}\n\treturn err\n}\n\n// helper function that converts a slice of volume paths to a set of\n// unique volume names.\nfunc toVol(paths []string) map[string]struct{} {\n\tset := map[string]struct{}{}\n\tfor _, path := range paths {\n\t\tparts, err := splitVolumeParts(path)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif len(parts) < 2 {\n\t\t\tcontinue\n\t\t}\n\t\tset[parts[1]] = struct{}{}\n\t}\n\treturn set\n}\n\n// helper function that split volume path\nfunc splitVolumeParts(volumeParts string) ([]string, error) {\n\tpattern := `^((?:[\\w]\\:)?[^\\:]*)\\:((?:[\\w]\\:)?[^\\:]*)(?:\\:([rwom]*))?`\n\tr, err := regexp.Compile(pattern)\n\tif err != nil {\n\t\treturn []string{}, err\n\t}\n\tif r.MatchString(volumeParts) {\n\t\tresults := r.FindStringSubmatch(volumeParts)[1:]\n\t\tcleanResults := []string{}\n\t\tfor _, item := range results {\n\t\t\tif item != \"\" {\n\t\t\t\tcleanResults = append(cleanResults, item)\n\t\t\t}\n\t\t}\n\t\treturn cleanResults, nil\n\t} else {\n\t\treturn strings.Split(volumeParts, \":\"), nil\n\t}\n}\n"
  },
  {
    "path": "engine/install_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage engine\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestSplitVolumeParts(t *testing.T) {\n\ttestdata := []struct {\n\t\tfrom    string\n\t\tto      []string\n\t\tsuccess bool\n\t}{\n\t\t{\n\t\t\tfrom:    `Z::Z::rw`,\n\t\t\tto:      []string{`Z:`, `Z:`, `rw`},\n\t\t\tsuccess: true,\n\t\t},\n\t\t{\n\t\t\tfrom:    `Z:\\:Z:\\:rw`,\n\t\t\tto:      []string{`Z:\\`, `Z:\\`, `rw`},\n\t\t\tsuccess: true,\n\t\t},\n\t\t{\n\t\t\tfrom:    `Z:\\git\\refs:Z:\\git\\refs:rw`,\n\t\t\tto:      []string{`Z:\\git\\refs`, `Z:\\git\\refs`, `rw`},\n\t\t\tsuccess: true,\n\t\t},\n\t\t{\n\t\t\tfrom:    `Z:\\git\\refs:Z:\\git\\refs`,\n\t\t\tto:      []string{`Z:\\git\\refs`, `Z:\\git\\refs`},\n\t\t\tsuccess: true,\n\t\t},\n\t\t{\n\t\t\tfrom:    `Z:/:Z:/:rw`,\n\t\t\tto:      []string{`Z:/`, `Z:/`, `rw`},\n\t\t\tsuccess: true,\n\t\t},\n\t\t{\n\t\t\tfrom:    `Z:/git/refs:Z:/git/refs:rw`,\n\t\t\tto:      []string{`Z:/git/refs`, `Z:/git/refs`, `rw`},\n\t\t\tsuccess: true,\n\t\t},\n\t\t{\n\t\t\tfrom:    `Z:/git/refs:Z:/git/refs`,\n\t\t\tto:      []string{`Z:/git/refs`, `Z:/git/refs`},\n\t\t\tsuccess: true,\n\t\t},\n\t\t{\n\t\t\tfrom:    `/test:/test`,\n\t\t\tto:      []string{`/test`, `/test`},\n\t\t\tsuccess: true,\n\t\t},\n\t\t{\n\t\t\tfrom:    `test:/test`,\n\t\t\tto:      []string{`test`, `/test`},\n\t\t\tsuccess: true,\n\t\t},\n\t\t{\n\t\t\tfrom:    `test:test`,\n\t\t\tto:      []string{`test`, `test`},\n\t\t\tsuccess: true,\n\t\t},\n\t}\n\tfor _, test := range testdata {\n\t\tresults, err := splitVolumeParts(test.from)\n\t\tif test.success == (err != nil) {\n\t\t} else {\n\t\t\tif reflect.DeepEqual(results, test.to) != test.success {\n\t\t\t\tt.Errorf(\"Expect %q matches %q is %v\", test.from, results, test.to)\n\t\t\t}\n\t\t}\n\t}\n}"
  },
  {
    "path": "engine/pinger.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage engine\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/logger\"\n)\n\ntype pinger struct {\n\twg sync.WaitGroup\n\n\tservers  autoscaler.ServerStore\n\tclient   clientFunc\n\tinterval time.Duration\n\tenabled  bool\n}\n\nfunc (p *pinger) Ping(ctx context.Context) error {\n\t// this is a feature flag that can be used to enable\n\t// experimental pinging and detection of zombie instances.\n\tif !p.enabled {\n\t\treturn nil\n\t}\n\n\tservers, err := p.servers.ListState(ctx, autoscaler.StateRunning)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, server := range servers {\n\t\tp.wg.Add(1)\n\t\tgo func(server *autoscaler.Server) {\n\t\t\tp.ping(ctx, server)\n\t\t\tp.wg.Done()\n\t\t}(server)\n\t}\n\treturn nil\n}\n\nfunc (p *pinger) ping(ctx context.Context, server *autoscaler.Server) error {\n\tlogger := logger.FromContext(ctx).\n\t\tWithField(\"ip\", server.Address).\n\t\tWithField(\"name\", server.Name)\n\n\tclient, closer, err := p.client(server)\n\tif closer != nil {\n\t\tdefer closer.Close()\n\t}\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tErrorln(\"cannot create docker client\")\n\t\treturn nil\n\t}\n\n\t// the system will attempt to ping the server a maximum of\n\t// five times, with a 1 minute timeout for each ping. If the\n\t// server cannot be reached, it will be placed in an error\n\t// state.\n\n\tfor i := 0; i < 5; i++ {\n\t\tlogger.Debugln(\"pinging the server\")\n\n\t\ttimeout, cancel := context.WithTimeout(ctx, time.Minute)\n\t\t_, err := client.Ping(timeout)\n\t\tcancel()\n\n\t\t// If the global context is in an error state we\n\t\t// should assume this is because the program is\n\t\t// being gracefully terminated. This could cause\n\t\t// false positive ping errors, so we ignore and\n\t\t// exit the routine.\n\t\tif ctx.Err() != nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tif err == nil {\n\t\t\tlogger.WithField(\"state\", \"healthy\").\n\t\t\t\tDebugln(\"server ping successful\")\n\t\t\treturn nil\n\t\t} else {\n\t\t\tlogger.WithError(err).\n\t\t\t\tWarnln(\"server ping unsuccessful\")\n\t\t}\n\t}\n\n\tserver, err = p.servers.Find(ctx, server.Name)\n\tif err != nil {\n\t\t// if the server no longer exists in the database\n\t\t// it is possible it was mutated by another goroutine.\n\t\treturn err\n\t}\n\n\tif server.State != autoscaler.StateRunning {\n\t\t// if the server was mutated by another goroutine\n\t\t// we should exit without making any changes.\n\t\treturn nil\n\t}\n\n\tlogger.WithField(\"state\", \"unhealthy\").\n\t\tDebugln(\"failed to reach server\")\n\n\tserver.Error = \"Failed to ping the server\"\n\tserver.Stopped = time.Now().Unix()\n\tserver.State = autoscaler.StateError\n\terr = p.servers.Update(ctx, server)\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tWithField(\"server\", server.Name).\n\t\t\tWithField(\"state\", \"error\").\n\t\t\tErrorln(\"failed to update server state\")\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "engine/pinger_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage engine\n"
  },
  {
    "path": "engine/planner.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage engine\n\nimport (\n\t\"context\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/logger\"\n\t\"github.com/drone/drone-go/drone\"\n\n\t\"github.com/dchest/uniuri\"\n)\n\n// a planner is responsible for capacity planning. It will assess\n// current build volume and plan the creation or termination of\n// server resources accordingly.\ntype planner struct {\n\tos         string\n\tarch       string\n\tversion    string\n\tkernel     string\n\tnamePrefix string\n\tmin        int           // min number of servers\n\tmax        int           // max number of servers to allocate\n\tcap        int           // capacity per-server\n\tbuffer     int           // buffer capacity to have warm and ready\n\tttu        time.Duration // minimum server age\n\tlabels     map[string]string\n\n\tclient  drone.Client\n\tservers autoscaler.ServerStore\n}\n\nfunc (p *planner) Plan(ctx context.Context) error {\n\t// generate a unique identifier for the current\n\t// execution cycle for tracing and grouping logs.\n\tcycle := uniuri.New()\n\n\tlog := logger.FromContext(ctx).WithField(\"id\", cycle)\n\tlog.Debugln(\"calculate unfinished jobs\")\n\n\tpending, running, err := p.count(ctx)\n\tif err != nil {\n\t\tlog.WithError(err).\n\t\t\tErrorln(\"cannot calculate unfinished jobs\")\n\t\treturn err\n\t}\n\n\tlog.Debugln(\"calculate server capacity\")\n\n\tcapacity, servers, err := p.capacity(ctx)\n\tif err != nil {\n\t\tlog.WithError(err).\n\t\t\tErrorln(\"cannot calculate server capacity\")\n\t\treturn err\n\t}\n\n\tlog.\n\t\tWithField(\"min-pool\", p.min).\n\t\tWithField(\"max-pool\", p.max).\n\t\tWithField(\"server-buffer\", p.buffer).\n\t\tWithField(\"server-capacity\", capacity).\n\t\tWithField(\"server-count\", servers).\n\t\tWithField(\"pending-builds\", pending).\n\t\tWithField(\"running-builds\", running).\n\t\tDebugln(\"check capacity\")\n\n\tdefer func() {\n\t\tlog.Debugln(\"check capacity complete\")\n\t}()\n\n\tctx = logger.WithContext(ctx, log)\n\n\tfree := max(capacity-running-p.buffer, 0)\n\tdiff := serverDiff(pending, free, p.cap)\n\n\t// if the server differential to handle the build volume\n\t// is positive, we can reduce server capacity.\n\tif diff < 0 {\n\t\treturn p.mark(ctx,\n\t\t\t// we should adjust the desired capacity to ensure\n\t\t\t// we maintain the minimum required server count.\n\t\t\tserverFloor(servers, abs(diff), p.min),\n\t\t)\n\t}\n\n\t// if the server differential to handle the build volume\n\t// is positive, we need to allocate more server capacity.\n\tif diff > 0 {\n\t\treturn p.alloc(ctx,\n\t\t\t// we should adjust the desired capacity to ensure\n\t\t\t// it does not exceed the max server count.\n\t\t\tserverCeil(servers, diff, p.max),\n\t\t)\n\t}\n\n\tlog.Debugln(\"no capacity changes required\")\n\n\treturn nil\n}\n\n// helper function allocates n new server instances.\nfunc (p *planner) alloc(ctx context.Context, n int) error {\n\tlogger := logger.FromContext(ctx)\n\tlogger.Debugf(\"allocate %d servers\", n)\n\n\tnamePrefix := p.namePrefix\n\tif namePrefix == \"\" {\n\t\tnamePrefix = \"agent-\"\n\t}\n\tfor i := 0; i < n; i++ {\n\t\tserver := &autoscaler.Server{\n\t\t\tName:     p.namePrefix + uniuri.NewLen(8),\n\t\t\tState:    autoscaler.StatePending,\n\t\t\tSecret:   uniuri.New(),\n\t\t\tCapacity: p.cap,\n\t\t}\n\n\t\terr := p.servers.Create(ctx, server)\n\t\tif err != nil {\n\t\t\tlogger.WithError(err).\n\t\t\t\tErrorln(\"cannot create server\")\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// helper function marks instances for termination.\nfunc (p *planner) mark(ctx context.Context, n int) error {\n\tlogger := logger.FromContext(ctx)\n\tlogger.Debugf(\"terminate %d servers\", n)\n\n\tif n == 0 {\n\t\treturn nil\n\t}\n\n\tservers, err := p.servers.ListState(ctx, autoscaler.StateRunning)\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tErrorln(\"cannot fetch server list\")\n\t\treturn err\n\t}\n\tsort.Sort(sort.Reverse(byCreated(servers)))\n\n\t// Abort marking servers for termination if the total\n\t// number of running servers, minus the total number\n\t// of servers to terminate, falls below the minimum\n\t// number of servers (including the buffer).\n\tif len(servers)-n < p.min {\n\t\tlogger.WithField(\"servers-to-terminate\", n).\n\t\t\tWithField(\"servers-running\", len(servers)).\n\t\t\tWithField(\"min-pool\", p.min).\n\t\t\tDebugf(\"abort terminating instances to ensure minimum capacity met\")\n\t\treturn nil\n\t}\n\n\tbusy, err := p.listBusy(ctx)\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tErrorln(\"cannot ascertain busy server list\")\n\t\treturn err\n\t}\n\n\tvar idle []*autoscaler.Server\n\tfor _, server := range servers {\n\t\t// skip busy servers\n\t\tif _, ok := busy[server.Name]; ok {\n\t\t\tlogger.WithField(\"server\", server.Name).\n\t\t\t\tDebugln(\"server is busy\")\n\t\t\tcontinue\n\t\t}\n\n\t\t// skip servers less than minage\n\t\tif time.Now().Before(time.Unix(server.Created, 0).Add(p.ttu)) {\n\t\t\tlogger.\n\t\t\t\tWithField(\"server\", server.Name).\n\t\t\t\tWithField(\"age\", timeDiff(time.Now(), time.Unix(server.Created, 0))).\n\t\t\t\tWithField(\"min-age\", p.ttu).\n\t\t\t\tDebugln(\"server min-age not reached\")\n\t\t\tcontinue\n\t\t}\n\n\t\tidle = append(idle, server)\n\t\tlogger.WithField(\"server\", server.Name).\n\t\t\tDebugln(\"server is idle\")\n\t}\n\n\t// if there are no idle servers, there are no servers\n\t// to retire, we can exit.\n\tif len(idle) == 0 {\n\t\tlogger.Debugln(\"no idle servers to shutdown\")\n\t}\n\n\tif len(idle) > n {\n\t\tidle = idle[:n]\n\t}\n\n\tfor _, server := range idle {\n\t\tserver.State = autoscaler.StateShutdown\n\t\terr := p.servers.Update(ctx, server)\n\t\tif err != nil {\n\t\t\tlogger.WithError(err).\n\t\t\t\tWithField(\"server\", server.Name).\n\t\t\t\tWithField(\"state\", \"shutdown\").\n\t\t\t\tErrorln(\"cannot update server state\")\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// helper function returns the number of pending and\n// running builds in the remote Drone installation.\nfunc (p *planner) count(ctx context.Context) (pending, running int, err error) {\n\tstages, err := p.client.Queue()\n\tif err != nil {\n\t\treturn pending, running, err\n\t}\n\tfor _, stage := range stages {\n\t\tif p.match(stage) == false {\n\t\t\tcontinue\n\t\t}\n\t\tswitch stage.Status {\n\t\tcase drone.StatusPending:\n\t\t\tpending++\n\t\tcase drone.StatusRunning:\n\t\t\trunning++\n\t\t}\n\t}\n\treturn\n}\n\n// helper function returns our current capacity.\nfunc (p *planner) capacity(ctx context.Context) (capacity, count int, err error) {\n\tservers, err := p.servers.List(ctx)\n\tif err != nil {\n\t\treturn capacity, count, err\n\t}\n\tfor _, server := range servers {\n\t\tswitch server.State {\n\t\tcase autoscaler.StateStopped:\n\t\t\t// ignore state\n\t\tdefault:\n\t\t\tcount++\n\t\t\tcapacity += server.Capacity\n\t\t}\n\t}\n\treturn\n}\n\n// helper function returns a list of busy servers.\nfunc (p *planner) listBusy(ctx context.Context) (map[string]struct{}, error) {\n\tbusy := map[string]struct{}{}\n\tstages, err := p.client.Queue()\n\tif err != nil {\n\t\treturn busy, err\n\t}\n\tfor _, stage := range stages {\n\t\tif p.match(stage) == false {\n\t\t\tcontinue\n\t\t}\n\t\tif stage.Status == drone.StatusRunning || stage.Status == drone.StatusPending {\n\t\t\tbusy[stage.Machine] = struct{}{}\n\t\t}\n\t}\n\treturn busy, nil\n}\n\n// helper function returns true if the os, arch, variant\n// and kernel match the stage.\nfunc (p *planner) match(stage *drone.Stage) bool {\n\tlabelMatch := true\n\n\tif len(p.labels) > 0 || len(stage.Labels) > 0 {\n\t\tlabelMatch = checkLabels(p.labels, stage.Labels)\n\t}\n\n\treturn stage.OS == p.os &&\n\t\tstage.Arch == p.arch &&\n\t\tstage.Variant == p.version &&\n\t\tstage.Kernel == p.kernel &&\n\t\tlabelMatch\n}\n\nfunc checkLabels(a, b map[string]string) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor k, v := range a {\n\t\tif w, ok := b[k]; !ok || v != w {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc timeDiff(t time.Time, start time.Time) time.Duration {\n\tvar d time.Duration\n\tif t.After(start) {\n\t\td = t.Sub(start)\n\t}\n\treturn d\n}\n"
  },
  {
    "path": "engine/planner_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage engine\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/config\"\n\t\"github.com/drone/autoscaler/mocks\"\n\t\"github.com/drone/drone-go/drone\"\n\n\t\"github.com/golang/mock/gomock\"\n)\n\n// This test verifies that if the server capacity is\n// >= the pending count, and the server capacity is\n// <= the pool minimum size, no actions are taken.\nfunc TestPlan_Noop(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tservers := []*autoscaler.Server{\n\t\t{Name: \"server1\", Capacity: 2, State: autoscaler.StateRunning},\n\t\t{Name: \"server2\", Capacity: 2, State: autoscaler.StateRunning},\n\t}\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().List(gomock.Any()).Return(servers, nil)\n\n\tclient := mocks.NewMockClient(controller)\n\tclient.EXPECT().Queue().Return([]*drone.Stage{\n\t\t{Status: drone.StatusRunning},\n\t\t{Status: drone.StatusPending},\n\t\t{Status: drone.StatusPending},\n\t}, nil)\n\n\tp := planner{\n\t\tcap:     2,\n\t\tmin:     2,\n\t\tmax:     10,\n\t\tclient:  client,\n\t\tservers: store,\n\t}\n\n\terr := p.Plan(context.TODO())\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\n// This test verifies that if that no servers are\n// destroyed if there is excess capacity and the\n// the server count <= the min pool size.\nfunc TestPlan_MinBufferCapacity(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\t// x2 capacity\n\tservers := []*autoscaler.Server{\n\t\t{Name: \"server1\", Capacity: 1, State: autoscaler.StateRunning},\n\t\t{Name: \"server2\", Capacity: 1, State: autoscaler.StateRunning},\n\t}\n\n\t// x0 running builds\n\t// x0 pending builds\n\tbuilds := []*drone.Stage{}\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().List(gomock.Any()).Return(servers, nil)\n\n\tclient := mocks.NewMockClient(controller)\n\tclient.EXPECT().Queue().Return(builds, nil)\n\n\tp := planner{\n\t\tcap:     2,\n\t\tbuffer:  1,\n\t\tmin:     2,\n\t\tmax:     4,\n\t\tclient:  client,\n\t\tservers: store,\n\t}\n\n\terr := p.Plan(context.TODO())\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\n// This test verifies that if the server capacity minus buffer is\n// less than the pending count, and the server capacity is\n// >= the pool maximum, no actions are taken.\nfunc TestPlan_MaxBufferCapacity(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\t// x4 capacity\n\tservers := []*autoscaler.Server{\n\t\t{Name: \"server1\", Capacity: 1, State: autoscaler.StateRunning},\n\t\t{Name: \"server2\", Capacity: 1, State: autoscaler.StateRunning},\n\t\t{Name: \"server3\", Capacity: 1, State: autoscaler.StateRunning},\n\t\t{Name: \"server4\", Capacity: 1, State: autoscaler.StateRunning},\n\t}\n\n\t// x3 running builds\n\t// x1 pending builds\n\tbuilds := []*drone.Stage{\n\t\t{Status: drone.StatusRunning},\n\t\t{Status: drone.StatusRunning},\n\t\t{Status: drone.StatusRunning},\n\t\t{Status: drone.StatusPending},\n\t}\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().List(gomock.Any()).Return(servers, nil)\n\n\tclient := mocks.NewMockClient(controller)\n\tclient.EXPECT().Queue().Return(builds, nil)\n\n\tp := planner{\n\t\tcap:     1,\n\t\tbuffer:  2,\n\t\tmin:     2,\n\t\tmax:     4,\n\t\tclient:  client,\n\t\tservers: store,\n\t}\n\n\terr := p.Plan(context.TODO())\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\n// This test verifies that if the server capacity minus buffer is\n// less than the pending count, and the server capacity is\n// < the pool maximum, additional servers are provisioned.\nfunc TestPlan_MoreBufferCapacity(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\t// x4 capacity\n\tservers := []*autoscaler.Server{\n\t\t{Name: \"server1\", Capacity: 2, State: autoscaler.StateRunning},\n\t\t{Name: \"server2\", Capacity: 2, State: autoscaler.StateRunning},\n\t}\n\n\t// x2 running builds\n\t// x1 pending builds\n\tbuilds := []*drone.Stage{\n\t\t{Status: drone.StatusRunning},\n\t\t{Status: drone.StatusRunning},\n\t\t{Status: drone.StatusPending},\n\t}\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().List(gomock.Any()).Return(servers, nil)\n\tstore.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil)\n\n\tclient := mocks.NewMockClient(controller)\n\tclient.EXPECT().Queue().Return(builds, nil)\n\n\tp := planner{\n\t\tcap:     2,\n\t\tbuffer:  2,\n\t\tmin:     2,\n\t\tmax:     4,\n\t\tclient:  client,\n\t\tservers: store,\n\t}\n\n\terr := p.Plan(context.TODO())\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\n// This test verifies that if the server capacity is\n// < than the pending count, and the server capacity is\n// >= the pool maximum, no actions are taken.\nfunc TestPlan_MaxCapacity(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\t// x4 capacity\n\tservers := []*autoscaler.Server{\n\t\t{Name: \"server1\", Capacity: 1, State: autoscaler.StateRunning},\n\t\t{Name: \"server2\", Capacity: 1, State: autoscaler.StateRunning},\n\t\t{Name: \"server3\", Capacity: 1, State: autoscaler.StateRunning},\n\t\t{Name: \"server4\", Capacity: 1, State: autoscaler.StateRunning},\n\t}\n\n\t// x4 running builds\n\t// x3 pending builds\n\tbuilds := []*drone.Stage{\n\t\t{Status: drone.StatusRunning},\n\t\t{Status: drone.StatusRunning},\n\t\t{Status: drone.StatusRunning},\n\t\t{Status: drone.StatusRunning},\n\t\t{Status: drone.StatusPending},\n\t\t{Status: drone.StatusPending},\n\t\t{Status: drone.StatusPending},\n\t}\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().List(gomock.Any()).Return(servers, nil)\n\n\tclient := mocks.NewMockClient(controller)\n\tclient.EXPECT().Queue().Return(builds, nil)\n\n\tconfig := config.Config{}\n\tconfig.Pool.Min = 2\n\tconfig.Pool.Max = 4\n\tconfig.Agent.Concurrency = 2\n\n\tp := planner{\n\t\tcap:     2,\n\t\tmin:     2,\n\t\tmax:     4,\n\t\tclient:  client,\n\t\tservers: store,\n\t}\n\n\terr := p.Plan(context.TODO())\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\n// This test verifies that if the server capacity is\n// less than the pending count, and the server capacity is\n// < the pool maximum, additional servers are provisioned.\nfunc TestPlan_MoreCapacity(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\t// x2 capacity\n\tservers := []*autoscaler.Server{\n\t\t{Name: \"server1\", Capacity: 1, State: autoscaler.StateRunning},\n\t\t{Name: \"server2\", Capacity: 1, State: autoscaler.StateRunning},\n\t}\n\n\t// x2 running builds\n\t// x3 pending builds\n\tbuilds := []*drone.Stage{\n\t\t{Status: drone.StatusRunning},\n\t\t{Status: drone.StatusRunning},\n\t\t{Status: drone.StatusPending},\n\t\t{Status: drone.StatusPending},\n\t\t{Status: drone.StatusPending},\n\t\t{Status: drone.StatusPending}, // ignore, would exceed max pool size\n\t\t{Status: drone.StatusPending}, // ignore, would exceed max pool size\n\t\t{Status: drone.StatusPending}, // ignore, would exceed max pool size\n\t\t{Status: drone.StatusPending}, // ignore, would exceed max pool size\n\t\t{Status: drone.StatusPending}, // ignore, would exceed max pool size\n\t\t{Status: drone.StatusPending}, // ignore, would exceed max pool size\n\t}\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().List(gomock.Any()).Return(servers, nil)\n\tstore.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil)\n\tstore.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil)\n\n\tclient := mocks.NewMockClient(controller)\n\tclient.EXPECT().Queue().Return(builds, nil)\n\n\tp := planner{\n\t\tcap:     2,\n\t\tmin:     2,\n\t\tmax:     4,\n\t\tclient:  client,\n\t\tservers: store,\n\t}\n\n\terr := p.Plan(context.TODO())\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\n// This test verifies that if that no servers are\n// destroyed if there is excess capacity and the\n// the server count <= the min pool size.\nfunc TestPlan_MinPool(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\t// x2 capacity\n\tservers := []*autoscaler.Server{\n\t\t{Name: \"server1\", Capacity: 1, State: autoscaler.StateRunning},\n\t\t{Name: \"server2\", Capacity: 1, State: autoscaler.StateRunning},\n\t}\n\n\t// x0 running builds\n\t// x0 pending builds\n\tbuilds := []*drone.Stage{}\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().List(gomock.Any()).Return(servers, nil)\n\n\tclient := mocks.NewMockClient(controller)\n\tclient.EXPECT().Queue().Return(builds, nil)\n\n\tp := planner{\n\t\tcap:     2,\n\t\tmin:     2,\n\t\tmax:     4,\n\t\tclient:  client,\n\t\tservers: store,\n\t}\n\n\terr := p.Plan(context.TODO())\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\n// This test verifies that no servers are\n// destroyed if no idle servers exist.\nfunc TestPlan_NoIdle(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\t// x2 capacity\n\tservers := []*autoscaler.Server{\n\t\t{Name: \"server1\", Capacity: 2, State: autoscaler.StateRunning},\n\t\t{Name: \"server2\", Capacity: 2, State: autoscaler.StateRunning},\n\t}\n\n\t// x2 running builds\n\t// x0 pending builds\n\tbuilds := []*drone.Stage{\n\t\t{Status: drone.StatusRunning, Machine: \"server1\"},\n\t\t{Status: drone.StatusRunning, Machine: \"server2\"},\n\t}\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().List(gomock.Any()).Return(servers, nil)\n\tstore.EXPECT().ListState(gomock.Any(), autoscaler.StateRunning).Return(servers, nil)\n\n\tclient := mocks.NewMockClient(controller)\n\tclient.EXPECT().Queue().Return(builds, nil)\n\tclient.EXPECT().Queue().Return(builds, nil)\n\n\tp := planner{\n\t\tcap:     2,\n\t\tmin:     1,\n\t\tmax:     4,\n\t\tclient:  client,\n\t\tservers: store,\n\t}\n\n\terr := p.Plan(context.Background())\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\n// This test verifies that idle servers are not\n// garbage collected until the min-age is reached.\nfunc TestScale_MinAge(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\t// x2 capacity\n\tservers := []*autoscaler.Server{\n\t\t{Name: \"server1\", Capacity: 1, State: autoscaler.StateRunning, Created: time.Now().Unix()},\n\t\t{Name: \"server2\", Capacity: 1, State: autoscaler.StateRunning, Created: time.Now().Unix()},\n\t}\n\n\t// x0 running builds\n\t// x0 pending builds\n\tbuilds := []*drone.Stage{}\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().List(gomock.Any()).Return(servers, nil)\n\tstore.EXPECT().ListState(gomock.Any(), autoscaler.StateRunning).Return(servers, nil)\n\n\tclient := mocks.NewMockClient(controller)\n\tclient.EXPECT().Queue().Return(builds, nil)\n\tclient.EXPECT().Queue().Return(builds, nil)\n\n\tp := planner{\n\t\tcap:     2,\n\t\tmin:     1,\n\t\tmax:     4,\n\t\tttu:     time.Hour,\n\t\tclient:  client,\n\t\tservers: store,\n\t}\n\n\terr := p.Plan(context.TODO())\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc TestPlan_ShutdownIdle(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\t// x3 capacity\n\tservers := []*autoscaler.Server{\n\t\t{Name: \"server1\", Capacity: 2, Created: 1, State: autoscaler.StateRunning},\n\t\t{Name: \"server2\", Capacity: 2, Created: 2, State: autoscaler.StateRunning},\n\t\t{Name: \"server3\", Capacity: 2, Created: 3, State: autoscaler.StateRunning},\n\t}\n\n\t// x0 running builds\n\t// x0 pending builds\n\tbuilds := []*drone.Stage{}\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().List(gomock.Any()).Return(servers, nil)\n\tstore.EXPECT().ListState(gomock.Any(), autoscaler.StateRunning).Return(servers, nil)\n\tstore.EXPECT().Update(gomock.Any(), servers[2]).Return(nil)\n\tstore.EXPECT().Update(gomock.Any(), servers[1]).Return(nil)\n\n\tclient := mocks.NewMockClient(controller)\n\tclient.EXPECT().Queue().Return(builds, nil)\n\tclient.EXPECT().Queue().Return(builds, nil)\n\n\tp := planner{\n\t\tcap:     2,\n\t\tmin:     1,\n\t\tmax:     4,\n\t\tclient:  client,\n\t\tservers: store,\n\t}\n\n\terr := p.Plan(context.TODO())\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc TestPlan_ExcludePendingWhenTerminating(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tservers := []*autoscaler.Server{\n\t\t// x3 capacity\n\t\t{Name: \"server1\", Capacity: 2, Created: 1, State: autoscaler.StateRunning},\n\t\t{Name: \"server2\", Capacity: 2, Created: 2, State: autoscaler.StateRunning},\n\t\t{Name: \"server3\", Capacity: 2, Created: 3, State: autoscaler.StateRunning},\n\n\t\t// x3 pending / staging / starting\n\t\t{Name: \"server4\", Capacity: 2, Created: 4, State: autoscaler.StateCreating},\n\t\t{Name: \"server5\", Capacity: 2, Created: 5, State: autoscaler.StateCreated},\n\t\t{Name: \"server6\", Capacity: 2, Created: 6, State: autoscaler.StateStaging},\n\t}\n\n\t// x0 running builds\n\t// x0 pending builds\n\tbuilds := []*drone.Stage{}\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().List(gomock.Any()).Return(servers, nil)\n\tstore.EXPECT().ListState(gomock.Any(), autoscaler.StateRunning).Return(servers[:3], nil)\n\n\tclient := mocks.NewMockClient(controller)\n\tclient.EXPECT().Queue().Return(builds, nil)\n\n\tp := planner{\n\t\tcap:     2,\n\t\tmin:     3,\n\t\tmax:     10,\n\t\tclient:  client,\n\t\tservers: store,\n\t}\n\n\terr := p.Plan(context.TODO())\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n\nfunc TestListBusy(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tclient := mocks.NewMockClient(controller)\n\tclient.EXPECT().Queue().Return([]*drone.Stage{\n\t\t{Status: drone.StatusPending, Machine: \"machine1\"},\n\t\t{Status: drone.StatusRunning, Machine: \"machine2\"},\n\t}, nil)\n\n\tp := planner{\n\t\tclient: client,\n\t}\n\n\tbusy, err := p.listBusy(context.TODO())\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tif got, want := len(busy), 2; got != want {\n\t\tt.Errorf(\"Want busy server count %d, got %d\", want, got)\n\t}\n\tif _, ok := busy[\"machine1\"]; !ok {\n\t\tt.Errorf(\"Expected server not in busy list\")\n\t}\n\tif _, ok := busy[\"machine2\"]; !ok {\n\t\tt.Errorf(\"Expected server not in busy list\")\n\t}\n}\n\nfunc TestListBusyWithPendingAndRunning(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tclient := mocks.NewMockClient(controller)\n\tclient.EXPECT().Queue().Return([]*drone.Stage{\n\t\t{Status: drone.StatusPending, Machine: \"machine1\"},\n\t\t{Status: drone.StatusRunning, Machine: \"machine2\"},\n\t\t{Status: drone.StatusPending, Machine: \"machine3\"},\n\t}, nil)\n\n\tp := planner{\n\t\tclient: client,\n\t}\n\n\tbusy, err := p.listBusy(context.TODO())\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tif got, want := len(busy), 3; got != want {\n\t\tt.Errorf(\"Want busy server count %d, got %d\", want, got)\n\t}\n\tif _, ok := busy[\"machine1\"]; !ok {\n\t\tt.Errorf(\"Machine1 not found in the busy server list\")\n\t}\n\tif _, ok := busy[\"machine2\"]; !ok {\n\t\tt.Errorf(\"Machine2 not found in the busy server list\")\n\t}\n\tif _, ok := busy[\"machine3\"]; !ok {\n\t\tt.Errorf(\"Machine3 not found in the busy server list\")\n\t}\n}\n\nfunc TestCapacity(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tservers := []*autoscaler.Server{\n\t\t{Name: \"server1\", Capacity: 4},\n\t\t{Name: \"server2\", Capacity: 3},\n\t\t{Name: \"server3\", Capacity: 2},\n\t\t{Name: \"server4\", Capacity: 1},\n\t}\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().List(gomock.Any()).Return(servers, nil)\n\n\tp := planner{\n\t\tservers: store,\n\t}\n\n\tcapacity, count, err := p.capacity(context.TODO())\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tif got, want := capacity, 10; got != want {\n\t\tt.Errorf(\"Want capacity count %d, got %d\", want, got)\n\t}\n\tif got, want := count, 4; got != want {\n\t\tt.Errorf(\"Want server count %d, got %d\", want, got)\n\t}\n}\n\nfunc TestCount(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tclient := mocks.NewMockClient(controller)\n\tclient.EXPECT().Queue().Return([]*drone.Stage{\n\t\t{Status: drone.StatusPending},\n\t\t{Status: drone.StatusPending},\n\t\t{Status: drone.StatusPending},\n\t\t{Status: drone.StatusRunning},\n\t\t{Status: drone.StatusRunning},\n\t}, nil)\n\n\tp := planner{\n\t\tclient: client,\n\t}\n\n\tpending, running, err := p.count(context.TODO())\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tif got, want := pending, 3; got != want {\n\t\tt.Errorf(\"Want pending count %d, got %d\", want, got)\n\t}\n\tif got, want := running, 2; got != want {\n\t\tt.Errorf(\"Want running count %d, got %d\", want, got)\n\t}\n}\n\nfunc TestMatch(t *testing.T) {\n\ttests := []struct {\n\t\tmatch   bool\n\t\tos      string\n\t\tarch    string\n\t\tversion string\n\t\tkernel  string\n\t\tlabels  map[string]string\n\t\tstage   *drone.Stage\n\t}{\n\t\t{\n\t\t\tmatch: true,\n\t\t\tos:    \"linux\",\n\t\t\tarch:  \"amd64\",\n\t\t\tstage: &drone.Stage{\n\t\t\t\tOS:   \"linux\",\n\t\t\t\tArch: \"amd64\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tmatch: false,\n\t\t\tos:    \"linux\",\n\t\t\tarch:  \"amd64\",\n\t\t\tstage: &drone.Stage{\n\t\t\t\tOS:   \"linux\",\n\t\t\t\tArch: \"arm\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tmatch: false,\n\t\t\tos:    \"linux\",\n\t\t\tarch:  \"amd64\",\n\t\t\tstage: &drone.Stage{\n\t\t\t\tOS:   \"linux\",\n\t\t\t\tArch: \"amd64\",\n\t\t\t\tLabels: map[string]string{\n\t\t\t\t\t\"region\": \"us-west-2\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tmatch: false,\n\t\t\tos:    \"linux\",\n\t\t\tarch:  \"amd64\",\n\t\t\tlabels: map[string]string{\n\t\t\t\t\"region\": \"us-west-2\",\n\t\t\t},\n\t\t\tstage: &drone.Stage{\n\t\t\t\tOS:   \"linux\",\n\t\t\t\tArch: \"amd64\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tmatch: true,\n\t\t\tos:    \"linux\",\n\t\t\tarch:  \"amd64\",\n\t\t\tlabels: map[string]string{\n\t\t\t\t\"region\": \"us-west-2\",\n\t\t\t},\n\t\t\tstage: &drone.Stage{\n\t\t\t\tOS:   \"linux\",\n\t\t\t\tArch: \"amd64\",\n\t\t\t\tLabels: map[string]string{\n\t\t\t\t\t\"region\": \"us-west-2\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tmatch: true,\n\t\t\tos:    \"linux\",\n\t\t\tarch:  \"amd64\",\n\t\t\tlabels: map[string]string{\n\t\t\t\t\"region\": \"us-west-2\",\n\t\t\t\t\"mem\":    \"high\",\n\t\t\t},\n\t\t\tstage: &drone.Stage{\n\t\t\t\tOS:   \"linux\",\n\t\t\t\tArch: \"amd64\",\n\t\t\t\tLabels: map[string]string{\n\t\t\t\t\t\"region\": \"us-west-2\",\n\t\t\t\t\t\"mem\":    \"high\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tmatch: false,\n\t\t\tos:    \"linux\",\n\t\t\tarch:  \"amd64\",\n\t\t\tlabels: map[string]string{\n\t\t\t\t\"region\": \"us-east-2\",\n\t\t\t},\n\t\t\tstage: &drone.Stage{\n\t\t\t\tOS:   \"linux\",\n\t\t\t\tArch: \"amd64\",\n\t\t\t\tLabels: map[string]string{\n\t\t\t\t\t\"region\": \"us-west-2\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tmatch: false,\n\t\t\tos:    \"linux\",\n\t\t\tarch:  \"amd64\",\n\t\t\tlabels: map[string]string{\n\t\t\t\t\"region\": \"us-east-2\",\n\t\t\t\t\"mem\":    \"high\",\n\t\t\t},\n\t\t\tstage: &drone.Stage{\n\t\t\t\tOS:   \"linux\",\n\t\t\t\tArch: \"amd64\",\n\t\t\t\tLabels: map[string]string{\n\t\t\t\t\t\"region\": \"us-west-2\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tp := &planner{\n\t\t\tos:      test.os,\n\t\t\tarch:    test.arch,\n\t\t\tversion: test.version,\n\t\t\tkernel:  test.kernel,\n\t\t\tlabels:  test.labels,\n\t\t}\n\t\tif p.match(test.stage) != test.match {\n\t\t\tt.Fail()\n\t\t\treturn\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "engine/reaper.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage engine\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/logger\"\n)\n\n//\n// The reaper looks for and removes errored instances. The\n// collector, on the other hand, is responsible for garbage\n// collecting running instances that are no longer required.\n//\n// Note that I am open to using a more descriptive name if\n// anyone has a better suggestion.\n//\n\ntype reaper struct {\n\twg sync.WaitGroup\n\n\tservers  autoscaler.ServerStore\n\tprovider autoscaler.Provider\n\tinterval time.Duration\n\tenabled  bool\n}\n\nfunc (r *reaper) Reap(ctx context.Context) error {\n\t// this is a feature flag that can be used to enable\n\t// experimental reaping of errored instances.\n\tif !r.enabled {\n\t\treturn nil\n\t}\n\n\tservers, err := r.servers.ListState(ctx, autoscaler.StateError)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, server := range servers {\n\t\tr.wg.Add(1)\n\t\tgo func(server *autoscaler.Server) {\n\t\t\tr.reap(ctx, server)\n\t\t\tr.wg.Done()\n\t\t}(server)\n\t}\n\treturn nil\n}\n\nfunc (r *reaper) reap(ctx context.Context, server *autoscaler.Server) error {\n\tlogger := logger.FromContext(ctx)\n\tlogger.\n\t\tWithField(\"state\", \"error\").\n\t\tWithField(\"server\", server.Name).\n\t\tDebugln(\"inspecting failed server\")\n\n\t// if the server ID is an empty string it indicates\n\t// the server was never provisioned, but still has an\n\t// entry in the database. In this case, we can simply\n\t// delete the database entry\n\tif server.ID == \"\" {\n\t\tlogger.\n\t\t\tWithField(\"state\", \"error\").\n\t\t\tWithField(\"server\", server.Name).\n\t\t\tInfoln(\"server never provisioned. nothing to destroy\")\n\t} else {\n\t\tlogger.\n\t\t\tWithField(\"state\", \"error\").\n\t\t\tWithField(\"server\", server.Name).\n\t\t\tInfoln(\"destroy provisioned server\")\n\n\t\tin := &autoscaler.Instance{\n\t\t\tID:       server.ID,\n\t\t\tProvider: server.Provider,\n\t\t\tName:     server.Name,\n\t\t\tAddress:  server.Address,\n\t\t\tRegion:   server.Region,\n\t\t\tImage:    server.Image,\n\t\t\tSize:     server.Size,\n\t\t}\n\n\t\terr := r.provider.Destroy(ctx, in)\n\t\t// TODO implement ErrInstanceNotFound in Google driver\n\t\t// TODO implement ErrInstanceNotFound in Hetzner driver\n\t\t// TODO implement ErrInstanceNotFound in Packet driver\n\t\tif err == autoscaler.ErrInstanceNotFound {\n\t\t\tlogger.\n\t\t\t\tWithField(\"state\", \"error\").\n\t\t\t\tWithField(\"server\", server.Name).\n\t\t\t\tInfoln(\"server no longer exists. nothing to destroy\")\n\n\t\t\t// this accounts for the fact that the server can be\n\t\t\t// manually terminated outside of the autoscaler. In\n\t\t\t// this case the reaper continues and updates the\n\t\t\t// server state to stopped (below)\n\n\t\t} else if err != nil {\n\t\t\tlogger.WithError(err).\n\t\t\t\tWithField(\"state\", \"error\").\n\t\t\t\tWithField(\"server\", server.Name).\n\t\t\t\tErrorln(\"cannot destroy server\")\n\t\t\treturn err\n\t\t}\n\t}\n\n\tserver.Stopped = time.Now().Unix()\n\tserver.State = autoscaler.StateStopped\n\terr := r.servers.Update(ctx, server)\n\tif err != nil {\n\t\tlogger.WithError(err).\n\t\t\tWithField(\"server\", server.Name).\n\t\t\tWithField(\"state\", \"stopped\").\n\t\t\tErrorln(\"failed to update server state\")\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "engine/reaper_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage engine\n"
  },
  {
    "path": "engine/sort.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage engine\n\nimport \"github.com/drone/autoscaler\"\n\n// byCreated sorts the server list by created date.\ntype byCreated []*autoscaler.Server\n\nfunc (a byCreated) Len() int           { return len(a) }\nfunc (a byCreated) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }\nfunc (a byCreated) Less(i, j int) bool { return a[i].Created < a[j].Created }\n"
  },
  {
    "path": "engine/sort_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage engine\n\nimport (\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/drone/autoscaler\"\n)\n\nfunc TestSortByCreated(t *testing.T) {\n\tservers := []*autoscaler.Server{\n\t\t{Created: 4, Name: \"fourth\"},\n\t\t{Created: 2, Name: \"second\"},\n\t\t{Created: 3, Name: \"third\"},\n\t\t{Created: 5, Name: \"fifth\"},\n\t\t{Created: 1, Name: \"first\"},\n\t}\n\n\tsort.Sort(byCreated(servers))\n\n\tfor i, server := range servers {\n\t\tif server.Created != int64(i+1) {\n\t\t\tt.Errorf(\"Invalid sort order %d for %q\", i, server.Name)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "engine.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage autoscaler\n\nimport \"context\"\n\n// An Engine is responsible for running the scaling\n// alogirthm to provision and shutdown instances according\n// to build volume.\ntype Engine interface {\n\t// Start starts the Engine. The context can be used\n\t// to cancel a running engine.\n\tStart(context.Context)\n\t// Pause pauses the Engine.\n\tPause()\n\t// Paused returns true if th Engine is paused.\n\tPaused() bool\n\t// Resume resumes the Engine if paused.\n\tResume()\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/drone/autoscaler\n\ngo 1.22.4\n\nreplace (\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp => go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0\n\tgo.opentelemetry.io/otel => go.opentelemetry.io/otel v1.32.0\n\tgo.opentelemetry.io/otel/metric => go.opentelemetry.io/otel/metric v1.32.0\n\tgo.opentelemetry.io/otel/trace => go.opentelemetry.io/otel/trace v1.32.0\n)\n\nrequire (\n\tgithub.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d\n\tgithub.com/Microsoft/go-winio v0.5.1 // indirect\n\tgithub.com/avast/retry-go v3.0.0+incompatible\n\tgithub.com/aws/aws-sdk-go v1.44.205\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/bluele/slack v0.0.0-20171128075526-307046097ee9\n\tgithub.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9\n\tgithub.com/digitalocean/godo v1.1.1\n\tgithub.com/docker/docker v28.0.2+incompatible\n\tgithub.com/docker/go-connections v0.4.0\n\tgithub.com/drone/drone-go v1.0.5-0.20190504210458-4d6116b897ba\n\tgithub.com/drone/envconfig v1.4.1\n\tgithub.com/drone/funcmap v0.0.0-20220929084810-72602997d16f\n\tgithub.com/drone/signal v0.0.0-20170915013802-ac5d07ef1315\n\tgithub.com/dustin/go-humanize v1.0.0\n\tgithub.com/go-chi/chi v3.3.2+incompatible\n\tgithub.com/go-sql-driver/mysql v1.3.0\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/mock v1.6.0\n\tgithub.com/google/go-cmp v0.6.0\n\tgithub.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 // indirect\n\tgithub.com/google/uuid v1.3.0 // indirect\n\tgithub.com/gophercloud/gophercloud v0.0.0-20181014043407-c8947f7d1c51\n\tgithub.com/h2non/gock v1.2.0\n\tgithub.com/hetznercloud/hcloud-go v1.4.0\n\tgithub.com/jmoiron/sqlx v0.0.0-20180228184624-cf35089a1979\n\tgithub.com/joho/godotenv v1.2.0\n\tgithub.com/kr/pretty v0.2.1\n\tgithub.com/lib/pq v1.10.4\n\tgithub.com/mattn/go-sqlite3 v1.14.16\n\tgithub.com/packethost/packngo v0.1.0\n\tgithub.com/prometheus/client_golang v1.14.0\n\tgithub.com/scaleway/scaleway-sdk-go v1.0.0-beta.3\n\tgithub.com/sirupsen/logrus v1.9.3\n\tgolang.org/x/crypto v0.14.0\n\tgolang.org/x/oauth2 v0.10.0\n\tgolang.org/x/sync v0.3.0\n\tgolang.org/x/time v0.1.0\n\tgoogle.golang.org/api v0.126.0\n)\n\nrequire (\n\tcloud.google.com/go/compute v1.21.0 // indirect\n\tcloud.google.com/go/compute/metadata v0.2.3 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.2.0 // indirect\n\tgithub.com/docker/go-units v0.4.0 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect\n\tgithub.com/golang/protobuf v1.5.3 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.11.0 // indirect\n\tgithub.com/jmespath/go-jmespath v0.4.0 // indirect\n\tgithub.com/kr/text v0.2.0 // indirect\n\tgithub.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.0.2 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/prometheus/client_model v0.3.0 // indirect\n\tgithub.com/prometheus/common v0.37.0 // indirect\n\tgithub.com/prometheus/procfs v0.8.0 // indirect\n\tgithub.com/tent/http-link-go v0.0.0-20130702225549-ac974c61c2f9 // indirect\n)\n\nrequire (\n\tgithub.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect\n\tgithub.com/containerd/log v0.1.0 // indirect\n\tgithub.com/distribution/reference v0.6.0 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/go-logr/logr v1.4.2 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/google/s2a-go v0.1.4 // indirect\n\tgithub.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect\n\tgithub.com/moby/docker-image-spec v1.3.1 // indirect\n\tgithub.com/moby/term v0.0.0-20210610120745-9d4ed1856297 // indirect\n\tgithub.com/morikuni/aec v1.0.0 // indirect\n\tgo.opencensus.io v0.24.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0 // indirect\n\tgo.opentelemetry.io/otel v1.32.0 // indirect\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.32.0 // indirect\n\tgo.opentelemetry.io/otel/sdk v1.3.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.32.0 // indirect\n\tgolang.org/x/net v0.17.0 // indirect\n\tgolang.org/x/sys v0.13.0 // indirect\n\tgolang.org/x/text v0.13.0 // indirect\n\tgoogle.golang.org/appengine v1.6.7 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect\n\tgoogle.golang.org/grpc v1.58.3 // indirect\n\tgoogle.golang.org/protobuf v1.31.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/yaml.v2 v2.4.0 // indirect\n\tgotest.tools/v3 v3.0.3 // indirect\n\tlaunchpad.net/gocheck v0.0.0-20140225173054-000000000087 // indirect\n\tsigs.k8s.io/yaml v1.2.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/compute v1.21.0 h1:JNBsyXVoOoNJtTQcnEY5uYpZIbeCTYIeDe0Xh1bySMk=\ncloud.google.com/go/compute v1.21.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=\ncloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=\ncloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\ngithub.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d h1:j6oB/WPCigdOkxtuPl1VSIiLpy7Mdsu6phQffbF19Ng=\ngithub.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d/go.mod h1:3cARGAK9CfW3HoxCy1a0G4TKrdiKke8ftOMEOHyySYs=\ngithub.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e/go.mod h1:Xa6lInWHNQnuWoF0YPSsx+INFA9qk7/7pTjwb3PInkY=\ngithub.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=\ngithub.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=\ngithub.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY=\ngithub.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=\ngithub.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=\ngithub.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=\ngithub.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=\ngithub.com/aws/aws-sdk-go v1.44.205 h1:q23NJXgLPIuBMn4zaluWWz57HPP5z7Ut8ZtK1D3N9bs=\ngithub.com/aws/aws-sdk-go v1.44.205/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bluele/slack v0.0.0-20171128075526-307046097ee9 h1:bnBrnv6TsEcPq7HIJykjAGl8jw/J1FZMYYceCQfLgEI=\ngithub.com/bluele/slack v0.0.0-20171128075526-307046097ee9/go.mod h1:W679Ri2W93VLD8cVpEY/zLH1ow4zhJcCyjzrKxfM3QM=\ngithub.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo=\ngithub.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=\ngithub.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=\ngithub.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=\ngithub.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 h1:74lLNRzvsdIlkTgfDSMuaPjBr4cf6k7pwQQANm/yLKU=\ngithub.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=\ngithub.com/digitalocean/godo v1.1.1 h1:v0A7yF3xmKLjjdJGIeBbINfMufcrrRhqZsxuVQMoT+U=\ngithub.com/digitalocean/godo v1.1.1/go.mod h1:h6faOIcZ8lWIwNQ+DN7b3CgX4Kwby5T+nbpNqkUIozU=\ngithub.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=\ngithub.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=\ngithub.com/dnaeon/go-vcr v1.0.1 h1:r8L/HqC0Hje5AXMu1ooW8oyQyOFv4GxqpL0nRP7SLLY=\ngithub.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=\ngithub.com/docker/docker v28.0.2+incompatible h1:9BILleFwug5FSSqWBgVevgL3ewDJfWWWyZVqlDMttE8=\ngithub.com/docker/docker v28.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=\ngithub.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=\ngithub.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=\ngithub.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/drone/drone-go v1.0.5-0.20190504210458-4d6116b897ba h1:GKiT4UPBligLXJAP1zRllHvTUygAAlgS3t9LM9aasp0=\ngithub.com/drone/drone-go v1.0.5-0.20190504210458-4d6116b897ba/go.mod h1:GxyeGClYohaKNYJv/ZpsmVHtMJ7WhoT+uDaJNcDIrk4=\ngithub.com/drone/envconfig v1.4.1 h1:QROCzj78Z/4kWt0+79aO9bodvmQ44jTdQJQ8uFFV920=\ngithub.com/drone/envconfig v1.4.1/go.mod h1:jECq3U/qeRJS8nuiZbcNCeXikzUMCpNfRBY1DfU6WtA=\ngithub.com/drone/funcmap v0.0.0-20220929084810-72602997d16f h1:/jEs7lulqVO2u1+XI5rW4oFwIIusxuDOVKD9PAzlW2E=\ngithub.com/drone/funcmap v0.0.0-20220929084810-72602997d16f/go.mod h1:nDRkX7PHq+p39AD5/usv3KZMerxZTYU/9rfLS5IDspU=\ngithub.com/drone/signal v0.0.0-20170915013802-ac5d07ef1315 h1:pNSCIqkfTtVWwSHCOzCdEODGIdVh399LUj3wWiB4J+8=\ngithub.com/drone/signal v0.0.0-20170915013802-ac5d07ef1315/go.mod h1:S8t92eFT0g4WUgEc/LxG+LCuiskpMNsG0ajAMGnyZpc=\ngithub.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=\ngithub.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/go-chi/chi v3.3.2+incompatible h1:uQNcQN3NsV1j4ANsPh42P4ew4t6rnRbJb8frvpp31qQ=\ngithub.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=\ngithub.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=\ngithub.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=\ngithub.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-sql-driver/mysql v1.3.0 h1:pgwjLi/dvffoP9aabwkT3AKpXQM93QARkjFhDDqC1UE=\ngithub.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=\ngithub.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=\ngithub.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 h1:zLTLjkaOFEFIOxY5BWLFLwh+cL8vOBW4XJ2aqLE/Tf0=\ngithub.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc=\ngithub.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=\ngithub.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=\ngithub.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4=\ngithub.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI=\ngithub.com/gophercloud/gophercloud v0.0.0-20181014043407-c8947f7d1c51 h1:boFYpbhy0scteX2LHVBgOwPfEMaisiv6ShNqvmi16+I=\ngithub.com/gophercloud/gophercloud v0.0.0-20181014043407-c8947f7d1c51/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4=\ngithub.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=\ngithub.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=\ngithub.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=\ngithub.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=\ngithub.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=\ngithub.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hetznercloud/hcloud-go v1.4.0 h1:TXLSp+Ft0jBqbL9CVfENSc+ynjkF+e0xoRko9CVpX9Y=\ngithub.com/hetznercloud/hcloud-go v1.4.0/go.mod h1:g5pff0YNAZywQaivY/CmhUYFVp7oP0nu3MiODC2W4Hw=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=\ngithub.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=\ngithub.com/jmoiron/sqlx v0.0.0-20180228184624-cf35089a1979 h1:2Xvj9kCxHDj//km9z+jV09L1ATggC+0pDMjqqAfyWcY=\ngithub.com/jmoiron/sqlx v0.0.0-20180228184624-cf35089a1979/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU=\ngithub.com/joho/godotenv v1.2.0 h1:vGTvz69FzUFp+X4/bAkb0j5BoLC+9bpqTWY8mjhA9pc=\ngithub.com/joho/godotenv v1.2.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=\ngithub.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk=\ngithub.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=\ngithub.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=\ngithub.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=\ngithub.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=\ngithub.com/moby/term v0.0.0-20210610120745-9d4ed1856297 h1:yH0SvLzcbZxcJXho2yh7CqdENGMQe73Cw3woZBpPli0=\ngithub.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=\ngithub.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=\ngithub.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=\ngithub.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=\ngithub.com/packethost/packngo v0.1.0 h1:G/5zumXb2fbPm5MAM3y8MmugE66Ehpio5qx0IhdhTPc=\ngithub.com/packethost/packngo v0.1.0/go.mod h1:otzZQXgoO96RTzDB/Hycg0qZcXZsWJGJRSXbmEIJ+4M=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=\ngithub.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=\ngithub.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=\ngithub.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=\ngithub.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=\ngithub.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=\ngithub.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=\ngithub.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=\ngithub.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=\ngithub.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=\ngithub.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=\ngithub.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=\ngithub.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/scaleway/scaleway-sdk-go v1.0.0-beta.3 h1:4o0hpXUFqgMR1NnGbX7SeHFODlkHOZqoeVkABmnM0P8=\ngithub.com/scaleway/scaleway-sdk-go v1.0.0-beta.3/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=\ngithub.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=\ngithub.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/tent/http-link-go v0.0.0-20130702225549-ac974c61c2f9 h1:/Bsw4C+DEdqPjt8vAqaC9LAqpAQnaCQQqmolqq3S1T4=\ngithub.com/tent/http-link-go v0.0.0-20130702225549-ac974c61c2f9/go.mod h1:RHkNRtSLfOK7qBTHaeSX1D6BNpI3qw7NTxsmNr4RvN8=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=\ngo.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=\ngo.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=\ngo.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=\ngo.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0 h1:R/OBkMoGgfy2fLhs2QhkCI1w4HLEQX92GCcJB6SSdNk=\ngo.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0 h1:giGm8w67Ja7amYNfYMdme7xSp2pIxThWopw8+QP51Yk=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0/go.mod h1:hO1KLR7jcKaDDKDkvI9dP/FIhpmna5lkqPUQdEjFAM8=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0 h1:Ydage/P0fRrSPpZeCVxzjqGcI6iVmG2xb43+IR8cjqM=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0/go.mod h1:QNX1aly8ehqqX1LEa6YniTU7VY9I6R3X/oPxhGdTceE=\ngo.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=\ngo.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=\ngo.opentelemetry.io/otel/sdk v1.3.0 h1:3278edCoH89MEJ0Ky8WQXVmDQv3FX4ZJ3Pp+9fJreAI=\ngo.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs=\ngo.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=\ngo.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=\ngo.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=\ngo.opentelemetry.io/proto/otlp v0.11.0 h1:cLDgIBTf4lLOlztkhzAEdQsJ4Lj+i5Wc9k6Nn0K1VyU=\ngo.opentelemetry.io/proto/otlp v0.11.0/go.mod h1:QpEjXPrNQzrFDZgoTo49dgHR9RYRSrg3NAKnUGl9YpQ=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=\ngolang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=\ngolang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=\ngolang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=\ngolang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=\ngolang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=\ngolang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=\ngolang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=\ngolang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=\ngolang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=\ngolang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/api v0.126.0 h1:q4GJq+cAdMAC7XP7njvQ4tvohGLiSlytuL4BQxbIZ+o=\ngoogle.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=\ngoogle.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g=\ngoogle.golang.org/genproto v0.0.0-20230711160842-782d3b101e98/go.mod h1:S7mY02OqCJTD0E1OiQy1F72PWFB4bZJ87cAtLPYgDR0=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=\ngoogle.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=\ngoogle.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ=\ngoogle.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=\ngoogle.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=\ngotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=\ngotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nlaunchpad.net/gocheck v0.0.0-20140225173054-000000000087 h1:Izowp2XBH6Ya6rv+hqbceQyw/gSGoXfH/UPoTGduL54=\nlaunchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\nsigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=\nsigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=\n"
  },
  {
    "path": "licenses/Polyform-Free-Trial.md",
    "content": "# Polyform Free Trial License 1.0.0\n\n<https://polyformproject.org/licenses/free-trial/1.0.0>\n\n## Acceptance\n\nIn order to get any license under these terms, you must agree\nto them as both strict obligations and conditions to all\nyour licenses.\n\n## Copyright License\n\nThe licensor grants you a copyright license for the software\nto do everything you might do with the software that would\notherwise infringe the licensor's copyright in it for any\npermitted purpose.  However, you may only make changes or\nnew works based on the software according to [Changes and New\nWorks License](#changes-and-new-works-license), and you may\nnot distribute copies of the software.\n\n## Changes and New Works License\n\nThe licensor grants you an additional copyright license to\nmake changes and new works based on the software for any\npermitted purpose.\n\n## Patent License\n\nThe licensor grants you a patent license for the software that\ncovers patent claims the licensor can license, or becomes able\nto license, that you would infringe by using the software.\n\n## Fair Use\n\nYou may have \"fair use\" rights for the software under the\nlaw. These terms do not limit them.\n\n## Free Trial\n\nUse to evaluate whether the software suits a particular\napplication for less than 32 consecutive calendar days, on\nbehalf of you or your company, is use for a permitted purpose.\n\n## No Other Rights\n\nThese terms do not allow you to sublicense or transfer any of\nyour licenses to anyone else, or prevent the licensor from\ngranting licenses to anyone else.  These terms do not imply\nany other licenses.\n\n## Patent Defense\n\nIf you make any written claim that the software infringes or\ncontributes to infringement of any patent, your patent license\nfor the software granted under these terms ends immediately. If\nyour company makes such a claim, your patent license ends\nimmediately for work on behalf of your company.\n\n## Violations\n\nIf you violate any of these terms, or do anything with the\nsoftware not covered by your licenses, all your licenses\nend immediately.\n\n## No Liability\n\n***As far as the law allows, the software comes as is, without\nany warranty or condition, and the licensor will not be liable\nto you for any damages arising out of these terms or the use\nor nature of the software, under any kind of legal claim.***\n\n## Definitions\n\nThe **licensor** is the individual or entity offering these\nterms, and the **software** is the software the licensor makes\navailable under these terms.\n\n**You** refers to the individual or entity agreeing to these\nterms.\n\n**Your company** is any legal entity, sole proprietorship,\nor other kind of organization that you work for, plus all\norganizations that have control over, are under the control of,\nor are under common control with that organization.  **Control**\nmeans ownership of substantially all the assets of an entity,\nor the power to direct its management and policies by vote,\ncontract, or otherwise.  Control can be direct or indirect.\n\n**Your licenses** are all the licenses granted to you for the\nsoftware under these terms.\n\n**Use** means anything you do with the software requiring one\nof your licenses."
  },
  {
    "path": "licenses/Polyform-Small-Business.md",
    "content": "# Polyform Small Business License 1.0.0\n\n<https://polyformproject.org/licenses/small-business/1.0.0>\n\n## Acceptance\n\nIn order to get any license under these terms, you must agree\nto them as both strict obligations and conditions to all\nyour licenses.\n\n## Copyright License\n\nThe licensor grants you a copyright license for the\nsoftware to do everything you might do with the software\nthat would otherwise infringe the licensor's copyright\nin it for any permitted purpose.  However, you may\nonly distribute the software according to [Distribution\nLicense](#distribution-license) and make changes or new works\nbased on the software according to [Changes and New Works\nLicense](#changes-and-new-works-license).\n\n## Distribution License\n\nThe licensor grants you an additional copyright license\nto distribute copies of the software.  Your license\nto distribute covers distributing the software with\nchanges and new works permitted by [Changes and New Works\nLicense](#changes-and-new-works-license).\n\n## Notices\n\nYou must ensure that anyone who gets a copy of any part of\nthe software from you also gets a copy of these terms or the\nURL for them above, as well as copies of any plain-text lines\nbeginning with `Required Notice:` that the licensor provided\nwith the software.  For example:\n\n> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)\n\n## Changes and New Works License\n\nThe licensor grants you an additional copyright license to\nmake changes and new works based on the software for any\npermitted purpose.\n\n## Patent License\n\nThe licensor grants you a patent license for the software that\ncovers patent claims the licensor can license, or becomes able\nto license, that you would infringe by using the software.\n\n## Fair Use\n\nYou may have \"fair use\" rights for the software under the\nlaw. These terms do not limit them.\n\n## Small Business\n\nUse of the software for the benefit of your company is use for\na permitted purpose if your company has fewer than 100 total\nindividuals working as employees and independent contractors,\nand less than 1,000,000 USD (2019) total revenue in the prior\ntax year.  Adjust this revenue threshold for inflation according\nto the United States Bureau of Labor Statistics' consumer price\nindex for all urban consumers, U.S. city average, for all items,\nnot seasonally adjusted, with 1982–1984=100 reference base.\n\n## No Other Rights\n\nThese terms do not allow you to sublicense or transfer any of\nyour licenses to anyone else, or prevent the licensor from\ngranting licenses to anyone else.  These terms do not imply\nany other licenses.\n\n## Patent Defense\n\nIf you make any written claim that the software infringes or\ncontributes to infringement of any patent, your patent license\nfor the software granted under these terms ends immediately. If\nyour company makes such a claim, your patent license ends\nimmediately for work on behalf of your company.\n\n## Violations\n\nThe first time you are notified in writing that you have\nviolated any of these terms, or done anything with the software\nnot covered by your licenses, your licenses can nonetheless\ncontinue if you come into full compliance with these terms,\nand take practical steps to correct past violations, within\n32 days of receiving notice.  Otherwise, all your licenses\nend immediately.\n\n## No Liability\n\n***As far as the law allows, the software comes as is, without\nany warranty or condition, and the licensor will not be liable\nto you for any damages arising out of these terms or the use\nor nature of the software, under any kind of legal claim.***\n\n## Definitions\n\nThe **licensor** is the individual or entity offering these\nterms, and the **software** is the software the licensor makes\navailable under these terms.\n\n**You** refers to the individual or entity agreeing to these\nterms.\n\n**Your company** is any legal entity, sole proprietorship,\nor other kind of organization that you work for, plus all\norganizations that have control over, are under the control of,\nor are under common control with that organization.  **Control**\nmeans ownership of substantially all the assets of an entity,\nor the power to direct its management and policies by vote,\ncontract, or otherwise.  Control can be direct or indirect.\n\n**Your licenses** are all the licenses granted to you for the\nsoftware under these terms.\n\n**Use** means anything you do with the software requiring one\nof your licenses.\n"
  },
  {
    "path": "logger/context.go",
    "content": "// Copyright 2019 Drone.IO Inc. All rights reserved.\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage logger\n\nimport (\n\t\"context\"\n\t\"net/http\"\n)\n\ntype loggerKey struct{}\n\n// WithContext returns a new context with the provided logger.\n// Use in combination with logger.WithField for great effect.\nfunc WithContext(ctx context.Context, logger Logger) context.Context {\n\treturn context.WithValue(ctx, loggerKey{}, logger)\n}\n\n// FromContext retrieves the current logger from the context.\nfunc FromContext(ctx context.Context) Logger {\n\tlogger := ctx.Value(loggerKey{})\n\tif logger == nil {\n\t\treturn Default\n\t}\n\treturn logger.(Logger)\n}\n\n// FromRequest retrieves the current logger from the request. If no\n// logger is available, the default logger is returned.\nfunc FromRequest(r *http.Request) Logger {\n\treturn FromContext(r.Context())\n}\n"
  },
  {
    "path": "logger/context_test.go",
    "content": "// Copyright 2019 Drone.IO Inc. All rights reserved.\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage logger\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"testing\"\n)\n\nfunc TestContext(t *testing.T) {\n\tentry := Discard()\n\n\tctx := WithContext(context.Background(), entry)\n\tgot := FromContext(ctx)\n\n\tif got != entry {\n\t\tt.Errorf(\"Expected Logger from context\")\n\t}\n}\n\nfunc TestEmptyContext(t *testing.T) {\n\tgot := FromContext(context.Background())\n\tif got == nil {\n\t\tt.Errorf(\"Expected Logger from context\")\n\t}\n\tif _, ok := got.(*discard); !ok {\n\t\tt.Errorf(\"Expected discard Logger from context\")\n\t}\n}\n\nfunc TestRequest(t *testing.T) {\n\tentry := Discard()\n\n\tctx := WithContext(context.Background(), entry)\n\treq := new(http.Request)\n\treq = req.WithContext(ctx)\n\n\tgot := FromRequest(req)\n\n\tif got != entry {\n\t\tt.Errorf(\"Expected Logger from http.Request\")\n\t}\n}\n"
  },
  {
    "path": "logger/history/history.go",
    "content": "// Copyright 2019 Drone.IO Inc. All rights reserved.\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage history\n\n// Package history implements a logrus hook that provides access\n// to log recent log activity.\nimport (\n\t\"sync\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\n// default log entry limit.\nconst defaultLimit = 250\n\n// Level is the log level.\ntype Level string\n\n// log levels.\nconst (\n\tLevelError = Level(\"error\")\n\tLevelWarn  = Level(\"warn\")\n\tLevelInfo  = Level(\"info\")\n\tLevelDebug = Level(\"debug\")\n\tLevelTrace = Level(\"trace\")\n)\n\n// Entry provides a log entry.\ntype Entry struct {\n\tLevel   Level\n\tMessage string\n\tData    map[string]interface{}\n\tUnix    int64\n}\n\n// Hook is a logrus hook that track the log history.\ntype Hook struct {\n\tsync.RWMutex\n\tlimit   int\n\tentries []*Entry\n}\n\n// New returns a new history hook.\nfunc New() *Hook {\n\treturn NewLimit(defaultLimit)\n}\n\n// NewLimit returns a new history hook with a custom\n// history limit.\nfunc NewLimit(limit int) *Hook {\n\treturn &Hook{limit: limit}\n}\n\n// Fire receives the log entry.\nfunc (h *Hook) Fire(e *logrus.Entry) error {\n\t// ignore http request logs\n\tif _, ok := e.Data[\"user-agent\"]; ok {\n\t\treturn nil\n\t}\n\th.Lock()\n\tif len(h.entries) >= h.limit {\n\t\th.entries = h.entries[1:]\n\t}\n\th.entries = append(h.entries, &Entry{\n\t\tLevel:   convertLevel(e.Level),\n\t\tData:    convertFields(e.Data),\n\t\tUnix:    e.Time.Unix(),\n\t\tMessage: e.Message,\n\t})\n\th.Unlock()\n\treturn nil\n}\n\n// Levels returns the supported log levels.\nfunc (h *Hook) Levels() []logrus.Level {\n\treturn logrus.AllLevels\n}\n\n// Entries returns a list of all entries.\nfunc (h *Hook) Entries() []*Entry {\n\th.RLock()\n\tdefer h.RUnlock()\n\tentries := make([]*Entry, len(h.entries))\n\tfor i, entry := range h.entries {\n\t\tentries[i] = copyEntry(entry)\n\t}\n\treturn entries\n}\n\n// Filter returns a list of all entries for which the filter\n// function returns true.\nfunc (h *Hook) Filter(filter func(*Entry) bool) []*Entry {\n\th.RLock()\n\tdefer h.RUnlock()\n\tvar entries []*Entry\n\tfor _, entry := range h.entries {\n\t\tif filter(entry) {\n\t\t\tentries = append(entries, copyEntry(entry))\n\t\t}\n\t}\n\treturn entries\n}\n\n// helper funtion copies an entry for threadsafe access.\nfunc copyEntry(src *Entry) *Entry {\n\tdst := new(Entry)\n\t*dst = *src\n\tdst.Data = map[string]interface{}{}\n\tfor k, v := range src.Data {\n\t\tdst.Data[k] = v\n\t}\n\treturn dst\n}\n\n// helper function converts a logrus.Level to the local type.\nfunc convertLevel(level logrus.Level) Level {\n\tswitch level {\n\tcase logrus.PanicLevel:\n\t\treturn LevelError\n\tcase logrus.FatalLevel:\n\t\treturn LevelError\n\tcase logrus.ErrorLevel:\n\t\treturn LevelError\n\tcase logrus.WarnLevel:\n\t\treturn LevelWarn\n\tcase logrus.DebugLevel:\n\t\treturn LevelDebug\n\tcase logrus.TraceLevel:\n\t\treturn LevelTrace\n\tdefault:\n\t\treturn LevelInfo\n\t}\n}\n\n// helper fucntion copies logrus.Fields to a basic map.\nfunc convertFields(src logrus.Fields) map[string]interface{} {\n\tdst := map[string]interface{}{}\n\tfor k, v := range src {\n\t\tdst[k] = v\n\t}\n\treturn dst\n}\n"
  },
  {
    "path": "logger/history/history_test.go",
    "content": "// Copyright 2019 Drone.IO Inc. All rights reserved.\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage history\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/sirupsen/logrus\"\n)\n\nfunc TestLevels(t *testing.T) {\n\thook := New()\n\tif diff := cmp.Diff(hook.Levels(), logrus.AllLevels); diff != \"\" {\n\t\tt.Errorf(\"Hook should return all levels\")\n\t\tt.Log(diff)\n\t}\n}\n\nfunc TestConvertLevels(t *testing.T) {\n\ttests := []struct {\n\t\tbefore logrus.Level\n\t\tafter  Level\n\t}{\n\t\t{logrus.PanicLevel, LevelError},\n\t\t{logrus.FatalLevel, LevelError},\n\t\t{logrus.ErrorLevel, LevelError},\n\t\t{logrus.WarnLevel, LevelWarn},\n\t\t{logrus.InfoLevel, LevelInfo},\n\t\t{logrus.DebugLevel, LevelDebug},\n\t\t{logrus.TraceLevel, LevelTrace},\n\t}\n\tif len(tests) != len(logrus.AllLevels) {\n\t\tt.Errorf(\"missing unit tests for all logrus levels\")\n\t}\n\tfor _, test := range tests {\n\t\tif got, want := convertLevel(test.before), test.after; got != want {\n\t\t\tt.Errorf(\"Want entry level %v, got %v\", want, got)\n\t\t}\n\t}\n}\n\nfunc TestLimit(t *testing.T) {\n\thook := NewLimit(4)\n\thook.Fire(&logrus.Entry{})\n\thook.Fire(&logrus.Entry{})\n\thook.Fire(&logrus.Entry{})\n\thook.Fire(&logrus.Entry{})\n\thook.Fire(&logrus.Entry{})\n\tif got, want := len(hook.entries), 4; got != want {\n\t\tt.Errorf(\"Expect entries pruned to %d, got %d\", want, got)\n\t}\n}\n\nfunc TestHistory(t *testing.T) {\n\thook := New()\n\n\tnow := time.Now()\n\thook.Fire(&logrus.Entry{\n\t\tLevel:   logrus.DebugLevel,\n\t\tMessage: \"foo\",\n\t\tData:    logrus.Fields{\"foo\": \"bar\"},\n\t\tTime:    now,\n\t})\n\n\thook.Fire(&logrus.Entry{\n\t\tLevel:   logrus.InfoLevel,\n\t\tMessage: \"bar\",\n\t\tData:    logrus.Fields{\"baz\": \"qux\"},\n\t\tTime:    now,\n\t})\n\n\tif len(hook.entries) != 2 {\n\t\tt.Errorf(\"Expected 2 hooks added to history\")\n\t}\n\n\tentries := hook.Entries()\n\tif len(entries) != 2 {\n\t\tt.Errorf(\"Expected 2 hooks returned\")\n\t}\n\tif entries[0] == hook.entries[0] {\n\t\tt.Errorf(\"Expect copy of entries, got a reference\")\n\t}\n\tif entries[1] == hook.entries[1] {\n\t\tt.Errorf(\"Expect copy of entries, got a reference\")\n\t}\n\n\texpect := []*Entry{\n\t\t{\n\t\t\tLevel:   LevelDebug,\n\t\t\tMessage: \"foo\",\n\t\t\tData:    logrus.Fields{\"foo\": \"bar\"},\n\t\t\tUnix:    now.Unix(),\n\t\t},\n\t\t{\n\t\t\tLevel:   LevelInfo,\n\t\t\tMessage: \"bar\",\n\t\t\tData:    logrus.Fields{\"baz\": \"qux\"},\n\t\t\tUnix:    now.Unix(),\n\t\t},\n\t}\n\tif diff := cmp.Diff(entries, expect); diff != \"\" {\n\t\tt.Errorf(\"Entries should return an exact copy of all entries\")\n\t\tt.Log(diff)\n\t}\n}\n\nfunc TestFilter(t *testing.T) {\n\thook := New()\n\n\tnow := time.Now()\n\thook.Fire(&logrus.Entry{\n\t\tLevel:   logrus.DebugLevel,\n\t\tMessage: \"foo\",\n\t\tData:    logrus.Fields{\"foo\": \"bar\"},\n\t\tTime:    now,\n\t})\n\n\thook.Fire(&logrus.Entry{\n\t\tLevel:   logrus.InfoLevel,\n\t\tMessage: \"bar\",\n\t\tData:    logrus.Fields{\"baz\": \"qux\"},\n\t\tTime:    now,\n\t})\n\n\texpect := []*Entry{\n\t\t{\n\t\t\tLevel:   LevelDebug,\n\t\t\tMessage: \"foo\",\n\t\t\tData:    logrus.Fields{\"foo\": \"bar\"},\n\t\t\tUnix:    now.Unix(),\n\t\t},\n\t}\n\tentries := hook.Filter(func(entry *Entry) bool {\n\t\treturn entry.Data[\"foo\"] == \"bar\"\n\t})\n\tif diff := cmp.Diff(entries, expect); diff != \"\" {\n\t\tt.Errorf(\"Entries should return an exact copy of all entries\")\n\t\tt.Log(diff)\n\t}\n}\n"
  },
  {
    "path": "logger/logger.go",
    "content": "// Copyright 2019 Drone.IO Inc. All rights reserved.\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\n// Package logger defines interfaces that logger drivers\n// implement to log messages.\npackage logger\n\n// A Logger represents an active logging object that generates\n// lines of output to an io.Writer.\ntype Logger interface {\n\tDebug(args ...interface{})\n\tDebugf(format string, args ...interface{})\n\tDebugln(args ...interface{})\n\n\tError(args ...interface{})\n\tErrorf(format string, args ...interface{})\n\tErrorln(args ...interface{})\n\n\tInfo(args ...interface{})\n\tInfof(format string, args ...interface{})\n\tInfoln(args ...interface{})\n\n\tTrace(args ...interface{})\n\tTracef(format string, args ...interface{})\n\tTraceln(args ...interface{})\n\n\tWarn(args ...interface{})\n\tWarnf(format string, args ...interface{})\n\tWarnln(args ...interface{})\n\n\tWithError(error) Logger\n\tWithField(string, interface{}) Logger\n}\n\n// Default returns the default logger.\nvar Default = Discard()\n\n// Discard returns a no-op logger\nfunc Discard() Logger {\n\treturn &discard{}\n}\n\ntype discard struct{}\n\nfunc (*discard) Debug(args ...interface{})                 {}\nfunc (*discard) Debugf(format string, args ...interface{}) {}\nfunc (*discard) Debugln(args ...interface{})               {}\nfunc (*discard) Error(args ...interface{})                 {}\nfunc (*discard) Errorf(format string, args ...interface{}) {}\nfunc (*discard) Errorln(args ...interface{})               {}\nfunc (*discard) Info(args ...interface{})                  {}\nfunc (*discard) Infof(format string, args ...interface{})  {}\nfunc (*discard) Infoln(args ...interface{})                {}\nfunc (*discard) Trace(args ...interface{})                 {}\nfunc (*discard) Tracef(format string, args ...interface{}) {}\nfunc (*discard) Traceln(args ...interface{})               {}\nfunc (*discard) Warn(args ...interface{})                  {}\nfunc (*discard) Warnf(format string, args ...interface{})  {}\nfunc (*discard) Warnln(args ...interface{})                {}\nfunc (d *discard) WithError(error) Logger                  { return d }\nfunc (d *discard) WithField(string, interface{}) Logger    { return d }\n"
  },
  {
    "path": "logger/logger_test.go",
    "content": "// Copyright 2019 Drone.IO Inc. All rights reserved.\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage logger\n\nimport \"testing\"\n\nfunc TestWithError(t *testing.T) {\n\td := &discard{}\n\tif d.WithError(nil) != d {\n\t\tt.Errorf(\"Expect WithError to return base logger\")\n\t}\n}\n\nfunc TestWithField(t *testing.T) {\n\td := &discard{}\n\tif d.WithField(\"hello\", \"world\") != d {\n\t\tt.Errorf(\"Expect WithField to return base logger\")\n\t}\n}\n"
  },
  {
    "path": "logger/logrus.go",
    "content": "// Copyright 2019 Drone.IO Inc. All rights reserved.\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage logger\n\nimport \"github.com/sirupsen/logrus\"\n\n// Logrus returns a Logger that wraps a logrus.Entry.\nfunc Logrus(entry *logrus.Entry) Logger {\n\treturn &wrapLogrus{entry}\n}\n\ntype wrapLogrus struct {\n\t*logrus.Entry\n}\n\nfunc (w *wrapLogrus) WithError(err error) Logger {\n\treturn &wrapLogrus{w.Entry.WithError(err)}\n}\n\nfunc (w *wrapLogrus) WithField(key string, value interface{}) Logger {\n\treturn &wrapLogrus{w.Entry.WithField(key, value)}\n}\n"
  },
  {
    "path": "logger/logrus_test.go",
    "content": "// Copyright 2019 Drone.IO Inc. All rights reserved.\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage logger\n\nimport (\n\t\"testing\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\nfunc TestLogrus(t *testing.T) {\n\tlogger := Logrus(\n\t\tlogrus.NewEntry(\n\t\t\tlogrus.StandardLogger(),\n\t\t),\n\t)\n\tif _, ok := logger.(*wrapLogrus); !ok {\n\t\tt.Errorf(\"Expect wrapped logrus\")\n\t}\n\tif _, ok := logger.WithError(nil).(*wrapLogrus); !ok {\n\t\tt.Errorf(\"Expect WithError wraps logrus\")\n\t}\n\tif _, ok := logger.WithField(\"foo\", \"bar\").(*wrapLogrus); !ok {\n\t\tt.Errorf(\"Expect WithField logrus\")\n\t}\n}\n"
  },
  {
    "path": "logger/request/request.go",
    "content": "// Copyright 2019 Drone.IO Inc. All rights reserved.\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage request\n\nimport (\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/drone/autoscaler/logger\"\n\t\"github.com/go-chi/chi/middleware\"\n\t\"github.com/sirupsen/logrus\"\n)\n\n// Logger provides logrus middleware.\nfunc Logger(next http.Handler) http.Handler {\n\tfn := func(w http.ResponseWriter, r *http.Request) {\n\t\tstart := time.Now()\n\t\trw := middleware.NewWrapResponseWriter(w, r.ProtoMajor)\n\n\t\tfields := logrus.Fields{\n\t\t\t\"method\":     r.Method,\n\t\t\t\"request\":    r.RequestURI,\n\t\t\t\"remote\":     r.RemoteAddr,\n\t\t\t\"referer\":    r.Referer(),\n\t\t\t\"user-agent\": r.UserAgent(),\n\t\t}\n\t\tlog := logrus.WithFields(fields)\n\t\tctx := r.Context()\n\t\tctx = logger.WithContext(ctx, logger.Logrus(log))\n\t\tnext.ServeHTTP(rw, r)\n\n\t\tfields[\"status\"] = rw.Status()\n\t\tfields[\"duration\"] = time.Since(start)\n\t\tif id := r.Context().Value(middleware.RequestIDKey); id != nil {\n\t\t\tfields[\"request-id\"] = id\n\t\t}\n\t\tlog.WithFields(fields).Debugln(\"request completed\")\n\t}\n\treturn http.HandlerFunc(fn)\n}\n"
  },
  {
    "path": "metrics/metrics.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage metrics\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n)\n\nvar noContext = context.Background()\n\n// Collector defines a metrics collector.\ntype Collector interface {\n\t// TrackServerCreateTime registers the elapsed time it takes\n\t// to provision a server instance.\n\tTrackServerCreateTime(start time.Time)\n\n\t// TrackServerInitTime registers the elapsed time it takes\n\t// for a server instance to initialize and begin accepting\n\t// network connections.\n\tTrackServerInitTime(start time.Time)\n\n\t// TrackServerSetupTime registers the elapsed time it takes\n\t// to install software (i.e. docker, runners) on the server.\n\tTrackServerSetupTime(start time.Time)\n\n\t// IncrServerCreateError keeps a count of errors encountered\n\t// when provisioning servers.\n\tIncrServerCreateError()\n\n\t// IncrServerInitError keeps a count of errors encountered\n\t// when initializing and establishing networking connections\n\t// with servers.\n\tIncrServerInitError()\n\n\t// IncrServerSetupError keeps a count of errors encountered\n\t// when installing software on servers.\n\tIncrServerSetupError()\n}\n\n// Prometheus is a Prometheus metrics collector.\ntype Prometheus struct {\n\ttrackServerCreateTime prometheus.Histogram\n\ttrackServerInitTime   prometheus.Histogram\n\ttrackServerSetupTime  prometheus.Histogram\n\tcountServerCreateErr  prometheus.Counter\n\tcountServerInitErr    prometheus.Counter\n\tcountServerSetupErr   prometheus.Counter\n}\n\n// New returns a new Prometheus metrics provider.\nfunc New() *Prometheus {\n\tp := new(Prometheus)\n\tp.trackServerCreateTime = prometheus.NewHistogram(prometheus.HistogramOpts{\n\t\tName:    \"drone_server_create_time_seconds\",\n\t\tHelp:    \"Elapsed time creating a server.\",\n\t\tBuckets: []float64{60, 150, 300, 600, 900, 1200},\n\t})\n\tp.trackServerInitTime = prometheus.NewHistogram(prometheus.HistogramOpts{\n\t\tName:    \"drone_server_boot_time_seconds\",\n\t\tHelp:    \"Elapsed time initializing a server.\",\n\t\tBuckets: []float64{60, 150, 300, 600, 900, 1200},\n\t})\n\tp.trackServerSetupTime = prometheus.NewHistogram(prometheus.HistogramOpts{\n\t\tName:    \"drone_server_install_time_seconds\",\n\t\tHelp:    \"Elapsed time installing software on a server.\",\n\t\tBuckets: []float64{60, 150, 300, 600, 900, 1200},\n\t})\n\tp.countServerCreateErr = prometheus.NewCounter(prometheus.CounterOpts{\n\t\tName: \"drone_server_create_errors_total\",\n\t\tHelp: \"Total number of errors initializing a server.\",\n\t})\n\tp.countServerInitErr = prometheus.NewCounter(prometheus.CounterOpts{\n\t\tName: \"drone_server_boot_errors_total\",\n\t\tHelp: \"Total number of errors initializing a server.\",\n\t})\n\tp.countServerSetupErr = prometheus.NewCounter(prometheus.CounterOpts{\n\t\tName: \"drone_server_install_errors_total\",\n\t\tHelp: \"Total number of errors installing software on a server.\",\n\t})\n\tprometheus.MustRegister(p.trackServerCreateTime)\n\tprometheus.MustRegister(p.trackServerInitTime)\n\tprometheus.MustRegister(p.trackServerSetupTime)\n\tprometheus.MustRegister(p.countServerCreateErr)\n\tprometheus.MustRegister(p.countServerInitErr)\n\tprometheus.MustRegister(p.countServerSetupErr)\n\treturn p\n}\n\n// TrackServerCreateTime registers the elapsed time it takes\n// to provision a server instance.\nfunc (m *Prometheus) TrackServerCreateTime(start time.Time) {\n\tm.trackServerCreateTime.Observe(\n\t\ttime.Now().Sub(start).Round(time.Second).Seconds(),\n\t)\n}\n\n// TrackServerInitTime registers the elapsed time it takes\n// for a server instance to initialize and begin accepting\n// network connections.\nfunc (m *Prometheus) TrackServerInitTime(start time.Time) {\n\tm.trackServerInitTime.Observe(\n\t\ttime.Now().Sub(start).Round(time.Second).Seconds(),\n\t)\n}\n\n// TrackServerSetupTime registers the elapsed time it takes\n// to install software (i.e. docker, runners) on the server.\nfunc (m *Prometheus) TrackServerSetupTime(start time.Time) {\n\tm.trackServerSetupTime.Observe(\n\t\ttime.Now().Sub(start).Round(time.Second).Seconds(),\n\t)\n}\n\n// IncrServerCreateError keeps a count of errors encountered\n// when provisioning servers.\nfunc (m *Prometheus) IncrServerCreateError() {\n\tm.countServerCreateErr.Inc()\n}\n\n// IncrServerInitError keeps a count of errors encountered\n// when initializing and establishing networking connections\n// with servers.\nfunc (m *Prometheus) IncrServerInitError() {\n\tm.countServerInitErr.Inc()\n}\n\n// IncrServerSetupError keeps a count of errors encountered\n// when installing software on servers.\nfunc (m *Prometheus) IncrServerSetupError() {\n\tm.countServerSetupErr.Inc()\n}\n\n// NopCollector provides a no-op metrics collector.\ntype NopCollector struct{}\n\n// TrackServerCreateTime registers the elapsed time it takes\n// to provision a server instance.\nfunc (*NopCollector) TrackServerCreateTime(start time.Time) {}\n\n// TrackServerInitTime registers the elapsed time it takes\n// for a server instance to initialize and begin accepting\n// network connections.\nfunc (*NopCollector) TrackServerInitTime(start time.Time) {}\n\n// TrackServerSetupTime registers the elapsed time it takes\n// to install software (i.e. docker, runners) on the server.\nfunc (*NopCollector) TrackServerSetupTime(start time.Time) {}\n\n// IncrServerCreateError keeps a count of errors encountered\n// when provisioning servers.\nfunc (*NopCollector) IncrServerCreateError() {}\n\n// IncrServerInitError keeps a count of errors encountered\n// when initializing and establishing networking connections\n// with servers.\nfunc (*NopCollector) IncrServerInitError() {}\n\n// IncrServerSetupError keeps a count of errors encountered\n// when installing software on servers.\nfunc (*NopCollector) IncrServerSetupError() {}\n"
  },
  {
    "path": "metrics/server_capacity.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage metrics\n\nimport (\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n)\n\n// ServerCapacity provides metrics for server capacity count.\nfunc ServerCapacity(store autoscaler.ServerStore) autoscaler.ServerStore {\n\tprometheus.MustRegister(\n\t\tprometheus.NewGaugeFunc(prometheus.GaugeOpts{\n\t\t\tName: \"drone_server_capacity\",\n\t\t\tHelp: \"Total capacity of active servers.\",\n\t\t}, func() float64 {\n\t\t\tvar capacity int\n\t\t\tservers, _ := store.ListState(noContext, autoscaler.StateRunning)\n\t\t\tfor _, server := range servers {\n\t\t\t\tcapacity += server.Capacity\n\t\t\t}\n\t\t\treturn float64(capacity)\n\t\t}),\n\t)\n\treturn store\n}\n"
  },
  {
    "path": "metrics/server_capacity_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage metrics\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/mocks\"\n\t\"github.com/golang/mock/gomock\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n)\n\nfunc TestServerCapacity(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\t// restore the default prometheus registerer\n\t// when the unit test is complete.\n\tsnapshot := prometheus.DefaultRegisterer\n\tdefer func() {\n\t\tprometheus.DefaultRegisterer = snapshot\n\t}()\n\n\t// creates a blank registry\n\tregistry := prometheus.NewRegistry()\n\tprometheus.DefaultRegisterer = registry\n\n\t// x2 server count\n\t// x3 server capacity\n\tservers := []*autoscaler.Server{\n\t\t{Name: \"server1\", Capacity: 1, Created: time.Now().Unix()},\n\t\t{Name: \"server2\", Capacity: 2, Created: time.Now().Unix()},\n\t}\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().ListState(gomock.Any(), autoscaler.StateRunning).Return(servers, nil)\n\tServerCapacity(store)\n\n\tmetrics, err := registry.Gather()\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tif want, got := len(metrics), 1; want != got {\n\t\tt.Errorf(\"Expect registered metric\")\n\t\treturn\n\t}\n\tmetric := metrics[0]\n\tif want, got := metric.GetName(), \"drone_server_capacity\"; want != got {\n\t\tt.Errorf(\"Expect metric name %s, got %s\", want, got)\n\t}\n\tif want, got := metric.Metric[0].Gauge.GetValue(), float64(3); want != got {\n\t\tt.Errorf(\"Expect metric value %f, got %f\", want, got)\n\t}\n}\n"
  },
  {
    "path": "metrics/server_count.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage metrics\n\nimport (\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n)\n\n// ServerCount provides metrics for server counts.\nfunc ServerCount(store autoscaler.ServerStore) autoscaler.ServerStore {\n\tprometheus.MustRegister(\n\t\tprometheus.NewGaugeFunc(prometheus.GaugeOpts{\n\t\t\tName: \"drone_server_count\",\n\t\t\tHelp: \"Total number of active servers.\",\n\t\t}, func() float64 {\n\t\t\tservers, _ := store.ListState(noContext, autoscaler.StateRunning)\n\t\t\treturn float64(len(servers))\n\t\t}),\n\t)\n\treturn store\n}\n"
  },
  {
    "path": "metrics/server_count_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage metrics\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/mocks\"\n\t\"github.com/golang/mock/gomock\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n)\n\nfunc TestServerCount(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\t// restore the default prometheus registerer\n\t// when the unit test is complete.\n\tsnapshot := prometheus.DefaultRegisterer\n\tdefer func() {\n\t\tprometheus.DefaultRegisterer = snapshot\n\t}()\n\n\t// creates a blank registry\n\tregistry := prometheus.NewRegistry()\n\tprometheus.DefaultRegisterer = registry\n\n\t// x2 server count\n\tservers := []*autoscaler.Server{\n\t\t{Name: \"server1\", Capacity: 1, Created: time.Now().Unix()},\n\t\t{Name: \"server2\", Capacity: 1, Created: time.Now().Unix()},\n\t}\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().ListState(gomock.Any(), autoscaler.StateRunning).Return(servers, nil).AnyTimes()\n\tServerCount(store)\n\n\tmetrics, err := registry.Gather()\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tif want, got := len(metrics), 1; want != got {\n\t\tt.Errorf(\"Expect registered metric\")\n\t\treturn\n\t}\n\tmetric := metrics[0]\n\tif want, got := metric.GetName(), \"drone_server_count\"; want != got {\n\t\tt.Errorf(\"Expect metric name %s, got %s\", want, got)\n\t}\n\tif want, got := metric.Metric[0].Gauge.GetValue(), float64(len(servers)); want != got {\n\t\tt.Errorf(\"Expect metric value %f, got %f\", want, got)\n\t}\n}\n"
  },
  {
    "path": "metrics/server_create.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage metrics\n\nimport (\n\t\"context\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n)\n\n// ServerCreate provides metrics for servers created.\nfunc ServerCreate(provider autoscaler.Provider) autoscaler.Provider {\n\tcounter := prometheus.NewCounter(prometheus.CounterOpts{\n\t\tName: \"drone_servers_created\",\n\t\tHelp: \"Total number of servers created.\",\n\t})\n\terrors := prometheus.NewCounter(prometheus.CounterOpts{\n\t\tName: \"drone_servers_created_err\",\n\t\tHelp: \"Total number of server creation errors.\",\n\t})\n\tprometheus.MustRegister(counter)\n\tprometheus.MustRegister(errors)\n\treturn &providerWrapCreate{\n\t\tProvider: provider,\n\t\tcreated:  counter,\n\t\terrors:   errors,\n\t}\n}\n\n// instruments the Provider to count server create events.\ntype providerWrapCreate struct {\n\tautoscaler.Provider\n\tcreated prometheus.Counter\n\terrors  prometheus.Counter\n}\n\nfunc (p *providerWrapCreate) Create(ctx context.Context, opts autoscaler.InstanceCreateOpts) (*autoscaler.Instance, error) {\n\tinstance, err := p.Provider.Create(ctx, opts)\n\tif err == nil {\n\t\tp.created.Add(1)\n\t} else {\n\t\tp.errors.Add(1)\n\t}\n\treturn instance, err\n}\n"
  },
  {
    "path": "metrics/server_create_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage metrics\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/mocks\"\n\t\"github.com/golang/mock/gomock\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n)\n\nfunc TestServerCreate(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\t// restore the default prometheus registerer\n\t// when the unit test is complete.\n\tsnapshot := prometheus.DefaultRegisterer\n\tdefer func() {\n\t\tprometheus.DefaultRegisterer = snapshot\n\t}()\n\n\t// creates a blank registry\n\tregistry := prometheus.NewRegistry()\n\tprometheus.DefaultRegisterer = registry\n\n\topts := autoscaler.InstanceCreateOpts{Name: \"server1\"}\n\tinstance := &autoscaler.Instance{}\n\n\tprovider := mocks.NewMockProvider(controller)\n\tprovider.EXPECT().Create(gomock.Any(), opts).Times(3).Return(instance, nil)\n\tprovider.EXPECT().Create(gomock.Any(), opts).Return(nil, errors.New(\"error\"))\n\n\tproviderInst := ServerCreate(provider)\n\tfor i := 0; i < 3; i++ {\n\t\tres, err := providerInst.Create(noContext, opts)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tif res != instance {\n\t\t\tt.Errorf(\"Expect instance returned\")\n\t\t}\n\t}\n\t_, err := providerInst.Create(noContext, opts)\n\tif err == nil {\n\t\tt.Errorf(\"Expect error returned from provider\")\n\t}\n\n\tmetrics, err := registry.Gather()\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tif want, got := len(metrics), 2; want != got {\n\t\tt.Errorf(\"Expect registered metric\")\n\t\treturn\n\t}\n\tif got, want := metrics[0].GetName(), \"drone_servers_created\"; want != got {\n\t\tt.Errorf(\"Expect metric name %s, got %s\", want, got)\n\t}\n\tif got, want := metrics[0].Metric[0].Counter.GetValue(), float64(3); want != got {\n\t\tt.Errorf(\"Expect metric value %f, got %f\", want, got)\n\t}\n\tif got, want := metrics[1].GetName(), \"drone_servers_created_err\"; want != got {\n\t\tt.Errorf(\"Expect metric name %s, got %s\", want, got)\n\t}\n\tif got, want := metrics[1].Metric[0].Counter.GetValue(), float64(1); want != got {\n\t\tt.Errorf(\"Expect metric value %f, got %f\", want, got)\n\t}\n}\n"
  },
  {
    "path": "metrics/server_delete.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage metrics\n\nimport (\n\t\"context\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n)\n\n// ServerDelete provides metrics for servers deleted.\nfunc ServerDelete(provider autoscaler.Provider) autoscaler.Provider {\n\tcreated := prometheus.NewCounter(prometheus.CounterOpts{\n\t\tName: \"drone_servers_deleted\",\n\t\tHelp: \"Total number of servers deleted.\",\n\t})\n\terrors := prometheus.NewCounter(prometheus.CounterOpts{\n\t\tName: \"drone_servers_deleted_err\",\n\t\tHelp: \"Total number of server deletion errors.\",\n\t})\n\tprometheus.MustRegister(created)\n\tprometheus.MustRegister(errors)\n\treturn &providerWrapDestroy{\n\t\tProvider: provider,\n\t\tcreated:  created,\n\t\terrors:   errors,\n\t}\n}\n\n// instruments the Provider to count server destroy events.\ntype providerWrapDestroy struct {\n\tautoscaler.Provider\n\tcreated prometheus.Counter\n\terrors  prometheus.Counter\n}\n\nfunc (p *providerWrapDestroy) Destroy(ctx context.Context, instance *autoscaler.Instance) error {\n\terr := p.Provider.Destroy(ctx, instance)\n\tif err == nil {\n\t\tp.created.Add(1)\n\t} else {\n\t\tp.errors.Add(1)\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "metrics/server_delete_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage metrics\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/mocks\"\n\t\"github.com/golang/mock/gomock\"\n\t\"github.com/prometheus/client_golang/prometheus\"\n)\n\nfunc TestServerDelete(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\t// restore the default prometheus registerer\n\t// when the unit test is complete.\n\tsnapshot := prometheus.DefaultRegisterer\n\tdefer func() {\n\t\tprometheus.DefaultRegisterer = snapshot\n\t}()\n\n\t// creates a blank registry\n\tregistry := prometheus.NewRegistry()\n\tprometheus.DefaultRegisterer = registry\n\n\tinstance := &autoscaler.Instance{Name: \"server1\"}\n\n\tprovider := mocks.NewMockProvider(controller)\n\tprovider.EXPECT().Destroy(noContext, instance).Times(3).Return(nil)\n\tprovider.EXPECT().Destroy(noContext, instance).Return(errors.New(\"error\"))\n\n\tproviderInst := ServerDelete(provider)\n\tfor i := 0; i < 3; i++ {\n\t\terr := providerInst.Destroy(noContext, instance)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t}\n\terr := providerInst.Destroy(noContext, instance)\n\tif err == nil {\n\t\tt.Errorf(\"Expect error returned from provider\")\n\t}\n\n\tmetrics, err := registry.Gather()\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tif want, got := len(metrics), 2; want != got {\n\t\tt.Errorf(\"Expect registered metric\")\n\t\treturn\n\t}\n\tif got, want := metrics[0].GetName(), \"drone_servers_deleted\"; want != got {\n\t\tt.Errorf(\"Expect metric name %s, got %s\", want, got)\n\t}\n\tif got, want := metrics[0].Metric[0].Counter.GetValue(), float64(3); want != got {\n\t\tt.Errorf(\"Expect metric value %f, got %f\", want, got)\n\t}\n\tif got, want := metrics[1].GetName(), \"drone_servers_deleted_err\"; want != got {\n\t\tt.Errorf(\"Expect metric name %s, got %s\", want, got)\n\t}\n\tif got, want := metrics[1].Metric[0].Counter.GetValue(), float64(1); want != got {\n\t\tt.Errorf(\"Expect metric value %f, got %f\", want, got)\n\t}\n}\n"
  },
  {
    "path": "mocks/mock_docker.go",
    "content": "// Code generated by MockGen. DO NOT EDIT.\n// Source: github.com/docker/docker/client (interfaces: APIClient)\n\n// Package mocks is a generated GoMock package.\npackage mocks\n\nimport (\n\tcontext \"context\"\n\tio \"io\"\n\tnet \"net\"\n\thttp \"net/http\"\n\treflect \"reflect\"\n\n\ttypes \"github.com/docker/docker/api/types\"\n\tcheckpoint \"github.com/docker/docker/api/types/checkpoint\"\n\tcommon \"github.com/docker/docker/api/types/common\"\n\tcontainer \"github.com/docker/docker/api/types/container\"\n\tevents \"github.com/docker/docker/api/types/events\"\n\tfilters \"github.com/docker/docker/api/types/filters\"\n\timage \"github.com/docker/docker/api/types/image\"\n\tnetwork \"github.com/docker/docker/api/types/network\"\n\tregistry \"github.com/docker/docker/api/types/registry\"\n\tswarm \"github.com/docker/docker/api/types/swarm\"\n\tsystem \"github.com/docker/docker/api/types/system\"\n\tvolume \"github.com/docker/docker/api/types/volume\"\n\tclient \"github.com/docker/docker/client\"\n\tgomock \"github.com/golang/mock/gomock\"\n\tv1 \"github.com/opencontainers/image-spec/specs-go/v1\"\n)\n\n// MockAPIClient is a mock of APIClient interface.\ntype MockAPIClient struct {\n\tctrl     *gomock.Controller\n\trecorder *MockAPIClientMockRecorder\n}\n\n// MockAPIClientMockRecorder is the mock recorder for MockAPIClient.\ntype MockAPIClientMockRecorder struct {\n\tmock *MockAPIClient\n}\n\n// NewMockAPIClient creates a new mock instance.\nfunc NewMockAPIClient(ctrl *gomock.Controller) *MockAPIClient {\n\tmock := &MockAPIClient{ctrl: ctrl}\n\tmock.recorder = &MockAPIClientMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *MockAPIClient) EXPECT() *MockAPIClientMockRecorder {\n\treturn m.recorder\n}\n\n// BuildCachePrune mocks base method.\nfunc (m *MockAPIClient) BuildCachePrune(arg0 context.Context, arg1 types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"BuildCachePrune\", arg0, arg1)\n\tret0, _ := ret[0].(*types.BuildCachePruneReport)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// BuildCachePrune indicates an expected call of BuildCachePrune.\nfunc (mr *MockAPIClientMockRecorder) BuildCachePrune(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"BuildCachePrune\", reflect.TypeOf((*MockAPIClient)(nil).BuildCachePrune), arg0, arg1)\n}\n\n// BuildCancel mocks base method.\nfunc (m *MockAPIClient) BuildCancel(arg0 context.Context, arg1 string) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"BuildCancel\", arg0, arg1)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// BuildCancel indicates an expected call of BuildCancel.\nfunc (mr *MockAPIClientMockRecorder) BuildCancel(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"BuildCancel\", reflect.TypeOf((*MockAPIClient)(nil).BuildCancel), arg0, arg1)\n}\n\n// CheckpointCreate mocks base method.\nfunc (m *MockAPIClient) CheckpointCreate(arg0 context.Context, arg1 string, arg2 checkpoint.CreateOptions) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"CheckpointCreate\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// CheckpointCreate indicates an expected call of CheckpointCreate.\nfunc (mr *MockAPIClientMockRecorder) CheckpointCreate(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"CheckpointCreate\", reflect.TypeOf((*MockAPIClient)(nil).CheckpointCreate), arg0, arg1, arg2)\n}\n\n// CheckpointDelete mocks base method.\nfunc (m *MockAPIClient) CheckpointDelete(arg0 context.Context, arg1 string, arg2 checkpoint.DeleteOptions) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"CheckpointDelete\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// CheckpointDelete indicates an expected call of CheckpointDelete.\nfunc (mr *MockAPIClientMockRecorder) CheckpointDelete(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"CheckpointDelete\", reflect.TypeOf((*MockAPIClient)(nil).CheckpointDelete), arg0, arg1, arg2)\n}\n\n// CheckpointList mocks base method.\nfunc (m *MockAPIClient) CheckpointList(arg0 context.Context, arg1 string, arg2 checkpoint.ListOptions) ([]checkpoint.Summary, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"CheckpointList\", arg0, arg1, arg2)\n\tret0, _ := ret[0].([]checkpoint.Summary)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// CheckpointList indicates an expected call of CheckpointList.\nfunc (mr *MockAPIClientMockRecorder) CheckpointList(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"CheckpointList\", reflect.TypeOf((*MockAPIClient)(nil).CheckpointList), arg0, arg1, arg2)\n}\n\n// ClientVersion mocks base method.\nfunc (m *MockAPIClient) ClientVersion() string {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ClientVersion\")\n\tret0, _ := ret[0].(string)\n\treturn ret0\n}\n\n// ClientVersion indicates an expected call of ClientVersion.\nfunc (mr *MockAPIClientMockRecorder) ClientVersion() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ClientVersion\", reflect.TypeOf((*MockAPIClient)(nil).ClientVersion))\n}\n\n// Close mocks base method.\nfunc (m *MockAPIClient) Close() error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Close\")\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// Close indicates an expected call of Close.\nfunc (mr *MockAPIClientMockRecorder) Close() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Close\", reflect.TypeOf((*MockAPIClient)(nil).Close))\n}\n\n// ConfigCreate mocks base method.\nfunc (m *MockAPIClient) ConfigCreate(arg0 context.Context, arg1 swarm.ConfigSpec) (types.ConfigCreateResponse, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ConfigCreate\", arg0, arg1)\n\tret0, _ := ret[0].(types.ConfigCreateResponse)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ConfigCreate indicates an expected call of ConfigCreate.\nfunc (mr *MockAPIClientMockRecorder) ConfigCreate(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ConfigCreate\", reflect.TypeOf((*MockAPIClient)(nil).ConfigCreate), arg0, arg1)\n}\n\n// ConfigInspectWithRaw mocks base method.\nfunc (m *MockAPIClient) ConfigInspectWithRaw(arg0 context.Context, arg1 string) (swarm.Config, []byte, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ConfigInspectWithRaw\", arg0, arg1)\n\tret0, _ := ret[0].(swarm.Config)\n\tret1, _ := ret[1].([]byte)\n\tret2, _ := ret[2].(error)\n\treturn ret0, ret1, ret2\n}\n\n// ConfigInspectWithRaw indicates an expected call of ConfigInspectWithRaw.\nfunc (mr *MockAPIClientMockRecorder) ConfigInspectWithRaw(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ConfigInspectWithRaw\", reflect.TypeOf((*MockAPIClient)(nil).ConfigInspectWithRaw), arg0, arg1)\n}\n\n// ConfigList mocks base method.\nfunc (m *MockAPIClient) ConfigList(arg0 context.Context, arg1 types.ConfigListOptions) ([]swarm.Config, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ConfigList\", arg0, arg1)\n\tret0, _ := ret[0].([]swarm.Config)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ConfigList indicates an expected call of ConfigList.\nfunc (mr *MockAPIClientMockRecorder) ConfigList(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ConfigList\", reflect.TypeOf((*MockAPIClient)(nil).ConfigList), arg0, arg1)\n}\n\n// ConfigRemove mocks base method.\nfunc (m *MockAPIClient) ConfigRemove(arg0 context.Context, arg1 string) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ConfigRemove\", arg0, arg1)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// ConfigRemove indicates an expected call of ConfigRemove.\nfunc (mr *MockAPIClientMockRecorder) ConfigRemove(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ConfigRemove\", reflect.TypeOf((*MockAPIClient)(nil).ConfigRemove), arg0, arg1)\n}\n\n// ConfigUpdate mocks base method.\nfunc (m *MockAPIClient) ConfigUpdate(arg0 context.Context, arg1 string, arg2 swarm.Version, arg3 swarm.ConfigSpec) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ConfigUpdate\", arg0, arg1, arg2, arg3)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// ConfigUpdate indicates an expected call of ConfigUpdate.\nfunc (mr *MockAPIClientMockRecorder) ConfigUpdate(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ConfigUpdate\", reflect.TypeOf((*MockAPIClient)(nil).ConfigUpdate), arg0, arg1, arg2, arg3)\n}\n\n// ContainerAttach mocks base method.\nfunc (m *MockAPIClient) ContainerAttach(arg0 context.Context, arg1 string, arg2 container.AttachOptions) (types.HijackedResponse, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerAttach\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(types.HijackedResponse)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ContainerAttach indicates an expected call of ContainerAttach.\nfunc (mr *MockAPIClientMockRecorder) ContainerAttach(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerAttach\", reflect.TypeOf((*MockAPIClient)(nil).ContainerAttach), arg0, arg1, arg2)\n}\n\n// ContainerCommit mocks base method.\nfunc (m *MockAPIClient) ContainerCommit(arg0 context.Context, arg1 string, arg2 container.CommitOptions) (common.IDResponse, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerCommit\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(common.IDResponse)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ContainerCommit indicates an expected call of ContainerCommit.\nfunc (mr *MockAPIClientMockRecorder) ContainerCommit(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerCommit\", reflect.TypeOf((*MockAPIClient)(nil).ContainerCommit), arg0, arg1, arg2)\n}\n\n// ContainerCreate mocks base method.\nfunc (m *MockAPIClient) ContainerCreate(arg0 context.Context, arg1 *container.Config, arg2 *container.HostConfig, arg3 *network.NetworkingConfig, arg4 *v1.Platform, arg5 string) (container.CreateResponse, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerCreate\", arg0, arg1, arg2, arg3, arg4, arg5)\n\tret0, _ := ret[0].(container.CreateResponse)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ContainerCreate indicates an expected call of ContainerCreate.\nfunc (mr *MockAPIClientMockRecorder) ContainerCreate(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerCreate\", reflect.TypeOf((*MockAPIClient)(nil).ContainerCreate), arg0, arg1, arg2, arg3, arg4, arg5)\n}\n\n// ContainerDiff mocks base method.\nfunc (m *MockAPIClient) ContainerDiff(arg0 context.Context, arg1 string) ([]container.FilesystemChange, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerDiff\", arg0, arg1)\n\tret0, _ := ret[0].([]container.FilesystemChange)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ContainerDiff indicates an expected call of ContainerDiff.\nfunc (mr *MockAPIClientMockRecorder) ContainerDiff(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerDiff\", reflect.TypeOf((*MockAPIClient)(nil).ContainerDiff), arg0, arg1)\n}\n\n// ContainerExecAttach mocks base method.\nfunc (m *MockAPIClient) ContainerExecAttach(arg0 context.Context, arg1 string, arg2 container.ExecStartOptions) (types.HijackedResponse, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerExecAttach\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(types.HijackedResponse)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ContainerExecAttach indicates an expected call of ContainerExecAttach.\nfunc (mr *MockAPIClientMockRecorder) ContainerExecAttach(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerExecAttach\", reflect.TypeOf((*MockAPIClient)(nil).ContainerExecAttach), arg0, arg1, arg2)\n}\n\n// ContainerExecCreate mocks base method.\nfunc (m *MockAPIClient) ContainerExecCreate(arg0 context.Context, arg1 string, arg2 container.ExecOptions) (common.IDResponse, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerExecCreate\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(common.IDResponse)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ContainerExecCreate indicates an expected call of ContainerExecCreate.\nfunc (mr *MockAPIClientMockRecorder) ContainerExecCreate(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerExecCreate\", reflect.TypeOf((*MockAPIClient)(nil).ContainerExecCreate), arg0, arg1, arg2)\n}\n\n// ContainerExecInspect mocks base method.\nfunc (m *MockAPIClient) ContainerExecInspect(arg0 context.Context, arg1 string) (container.ExecInspect, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerExecInspect\", arg0, arg1)\n\tret0, _ := ret[0].(container.ExecInspect)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ContainerExecInspect indicates an expected call of ContainerExecInspect.\nfunc (mr *MockAPIClientMockRecorder) ContainerExecInspect(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerExecInspect\", reflect.TypeOf((*MockAPIClient)(nil).ContainerExecInspect), arg0, arg1)\n}\n\n// ContainerExecResize mocks base method.\nfunc (m *MockAPIClient) ContainerExecResize(arg0 context.Context, arg1 string, arg2 container.ResizeOptions) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerExecResize\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// ContainerExecResize indicates an expected call of ContainerExecResize.\nfunc (mr *MockAPIClientMockRecorder) ContainerExecResize(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerExecResize\", reflect.TypeOf((*MockAPIClient)(nil).ContainerExecResize), arg0, arg1, arg2)\n}\n\n// ContainerExecStart mocks base method.\nfunc (m *MockAPIClient) ContainerExecStart(arg0 context.Context, arg1 string, arg2 container.ExecStartOptions) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerExecStart\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// ContainerExecStart indicates an expected call of ContainerExecStart.\nfunc (mr *MockAPIClientMockRecorder) ContainerExecStart(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerExecStart\", reflect.TypeOf((*MockAPIClient)(nil).ContainerExecStart), arg0, arg1, arg2)\n}\n\n// ContainerExport mocks base method.\nfunc (m *MockAPIClient) ContainerExport(arg0 context.Context, arg1 string) (io.ReadCloser, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerExport\", arg0, arg1)\n\tret0, _ := ret[0].(io.ReadCloser)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ContainerExport indicates an expected call of ContainerExport.\nfunc (mr *MockAPIClientMockRecorder) ContainerExport(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerExport\", reflect.TypeOf((*MockAPIClient)(nil).ContainerExport), arg0, arg1)\n}\n\n// ContainerInspect mocks base method.\nfunc (m *MockAPIClient) ContainerInspect(arg0 context.Context, arg1 string) (container.InspectResponse, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerInspect\", arg0, arg1)\n\tret0, _ := ret[0].(container.InspectResponse)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ContainerInspect indicates an expected call of ContainerInspect.\nfunc (mr *MockAPIClientMockRecorder) ContainerInspect(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerInspect\", reflect.TypeOf((*MockAPIClient)(nil).ContainerInspect), arg0, arg1)\n}\n\n// ContainerInspectWithRaw mocks base method.\nfunc (m *MockAPIClient) ContainerInspectWithRaw(arg0 context.Context, arg1 string, arg2 bool) (container.InspectResponse, []byte, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerInspectWithRaw\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(container.InspectResponse)\n\tret1, _ := ret[1].([]byte)\n\tret2, _ := ret[2].(error)\n\treturn ret0, ret1, ret2\n}\n\n// ContainerInspectWithRaw indicates an expected call of ContainerInspectWithRaw.\nfunc (mr *MockAPIClientMockRecorder) ContainerInspectWithRaw(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerInspectWithRaw\", reflect.TypeOf((*MockAPIClient)(nil).ContainerInspectWithRaw), arg0, arg1, arg2)\n}\n\n// ContainerKill mocks base method.\nfunc (m *MockAPIClient) ContainerKill(arg0 context.Context, arg1, arg2 string) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerKill\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// ContainerKill indicates an expected call of ContainerKill.\nfunc (mr *MockAPIClientMockRecorder) ContainerKill(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerKill\", reflect.TypeOf((*MockAPIClient)(nil).ContainerKill), arg0, arg1, arg2)\n}\n\n// ContainerList mocks base method.\nfunc (m *MockAPIClient) ContainerList(arg0 context.Context, arg1 container.ListOptions) ([]container.Summary, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerList\", arg0, arg1)\n\tret0, _ := ret[0].([]container.Summary)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ContainerList indicates an expected call of ContainerList.\nfunc (mr *MockAPIClientMockRecorder) ContainerList(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerList\", reflect.TypeOf((*MockAPIClient)(nil).ContainerList), arg0, arg1)\n}\n\n// ContainerLogs mocks base method.\nfunc (m *MockAPIClient) ContainerLogs(arg0 context.Context, arg1 string, arg2 container.LogsOptions) (io.ReadCloser, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerLogs\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(io.ReadCloser)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ContainerLogs indicates an expected call of ContainerLogs.\nfunc (mr *MockAPIClientMockRecorder) ContainerLogs(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerLogs\", reflect.TypeOf((*MockAPIClient)(nil).ContainerLogs), arg0, arg1, arg2)\n}\n\n// ContainerPause mocks base method.\nfunc (m *MockAPIClient) ContainerPause(arg0 context.Context, arg1 string) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerPause\", arg0, arg1)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// ContainerPause indicates an expected call of ContainerPause.\nfunc (mr *MockAPIClientMockRecorder) ContainerPause(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerPause\", reflect.TypeOf((*MockAPIClient)(nil).ContainerPause), arg0, arg1)\n}\n\n// ContainerRemove mocks base method.\nfunc (m *MockAPIClient) ContainerRemove(arg0 context.Context, arg1 string, arg2 container.RemoveOptions) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerRemove\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// ContainerRemove indicates an expected call of ContainerRemove.\nfunc (mr *MockAPIClientMockRecorder) ContainerRemove(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerRemove\", reflect.TypeOf((*MockAPIClient)(nil).ContainerRemove), arg0, arg1, arg2)\n}\n\n// ContainerRename mocks base method.\nfunc (m *MockAPIClient) ContainerRename(arg0 context.Context, arg1, arg2 string) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerRename\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// ContainerRename indicates an expected call of ContainerRename.\nfunc (mr *MockAPIClientMockRecorder) ContainerRename(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerRename\", reflect.TypeOf((*MockAPIClient)(nil).ContainerRename), arg0, arg1, arg2)\n}\n\n// ContainerResize mocks base method.\nfunc (m *MockAPIClient) ContainerResize(arg0 context.Context, arg1 string, arg2 container.ResizeOptions) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerResize\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// ContainerResize indicates an expected call of ContainerResize.\nfunc (mr *MockAPIClientMockRecorder) ContainerResize(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerResize\", reflect.TypeOf((*MockAPIClient)(nil).ContainerResize), arg0, arg1, arg2)\n}\n\n// ContainerRestart mocks base method.\nfunc (m *MockAPIClient) ContainerRestart(arg0 context.Context, arg1 string, arg2 container.StopOptions) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerRestart\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// ContainerRestart indicates an expected call of ContainerRestart.\nfunc (mr *MockAPIClientMockRecorder) ContainerRestart(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerRestart\", reflect.TypeOf((*MockAPIClient)(nil).ContainerRestart), arg0, arg1, arg2)\n}\n\n// ContainerStart mocks base method.\nfunc (m *MockAPIClient) ContainerStart(arg0 context.Context, arg1 string, arg2 container.StartOptions) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerStart\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// ContainerStart indicates an expected call of ContainerStart.\nfunc (mr *MockAPIClientMockRecorder) ContainerStart(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerStart\", reflect.TypeOf((*MockAPIClient)(nil).ContainerStart), arg0, arg1, arg2)\n}\n\n// ContainerStatPath mocks base method.\nfunc (m *MockAPIClient) ContainerStatPath(arg0 context.Context, arg1, arg2 string) (container.PathStat, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerStatPath\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(container.PathStat)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ContainerStatPath indicates an expected call of ContainerStatPath.\nfunc (mr *MockAPIClientMockRecorder) ContainerStatPath(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerStatPath\", reflect.TypeOf((*MockAPIClient)(nil).ContainerStatPath), arg0, arg1, arg2)\n}\n\n// ContainerStats mocks base method.\nfunc (m *MockAPIClient) ContainerStats(arg0 context.Context, arg1 string, arg2 bool) (container.StatsResponseReader, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerStats\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(container.StatsResponseReader)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ContainerStats indicates an expected call of ContainerStats.\nfunc (mr *MockAPIClientMockRecorder) ContainerStats(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerStats\", reflect.TypeOf((*MockAPIClient)(nil).ContainerStats), arg0, arg1, arg2)\n}\n\n// ContainerStatsOneShot mocks base method.\nfunc (m *MockAPIClient) ContainerStatsOneShot(arg0 context.Context, arg1 string) (container.StatsResponseReader, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerStatsOneShot\", arg0, arg1)\n\tret0, _ := ret[0].(container.StatsResponseReader)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ContainerStatsOneShot indicates an expected call of ContainerStatsOneShot.\nfunc (mr *MockAPIClientMockRecorder) ContainerStatsOneShot(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerStatsOneShot\", reflect.TypeOf((*MockAPIClient)(nil).ContainerStatsOneShot), arg0, arg1)\n}\n\n// ContainerStop mocks base method.\nfunc (m *MockAPIClient) ContainerStop(arg0 context.Context, arg1 string, arg2 container.StopOptions) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerStop\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// ContainerStop indicates an expected call of ContainerStop.\nfunc (mr *MockAPIClientMockRecorder) ContainerStop(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerStop\", reflect.TypeOf((*MockAPIClient)(nil).ContainerStop), arg0, arg1, arg2)\n}\n\n// ContainerTop mocks base method.\nfunc (m *MockAPIClient) ContainerTop(arg0 context.Context, arg1 string, arg2 []string) (container.TopResponse, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerTop\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(container.TopResponse)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ContainerTop indicates an expected call of ContainerTop.\nfunc (mr *MockAPIClientMockRecorder) ContainerTop(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerTop\", reflect.TypeOf((*MockAPIClient)(nil).ContainerTop), arg0, arg1, arg2)\n}\n\n// ContainerUnpause mocks base method.\nfunc (m *MockAPIClient) ContainerUnpause(arg0 context.Context, arg1 string) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerUnpause\", arg0, arg1)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// ContainerUnpause indicates an expected call of ContainerUnpause.\nfunc (mr *MockAPIClientMockRecorder) ContainerUnpause(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerUnpause\", reflect.TypeOf((*MockAPIClient)(nil).ContainerUnpause), arg0, arg1)\n}\n\n// ContainerUpdate mocks base method.\nfunc (m *MockAPIClient) ContainerUpdate(arg0 context.Context, arg1 string, arg2 container.UpdateConfig) (container.UpdateResponse, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerUpdate\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(container.UpdateResponse)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ContainerUpdate indicates an expected call of ContainerUpdate.\nfunc (mr *MockAPIClientMockRecorder) ContainerUpdate(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerUpdate\", reflect.TypeOf((*MockAPIClient)(nil).ContainerUpdate), arg0, arg1, arg2)\n}\n\n// ContainerWait mocks base method.\nfunc (m *MockAPIClient) ContainerWait(arg0 context.Context, arg1 string, arg2 container.WaitCondition) (<-chan container.WaitResponse, <-chan error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainerWait\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(<-chan container.WaitResponse)\n\tret1, _ := ret[1].(<-chan error)\n\treturn ret0, ret1\n}\n\n// ContainerWait indicates an expected call of ContainerWait.\nfunc (mr *MockAPIClientMockRecorder) ContainerWait(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainerWait\", reflect.TypeOf((*MockAPIClient)(nil).ContainerWait), arg0, arg1, arg2)\n}\n\n// ContainersPrune mocks base method.\nfunc (m *MockAPIClient) ContainersPrune(arg0 context.Context, arg1 filters.Args) (container.PruneReport, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ContainersPrune\", arg0, arg1)\n\tret0, _ := ret[0].(container.PruneReport)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ContainersPrune indicates an expected call of ContainersPrune.\nfunc (mr *MockAPIClientMockRecorder) ContainersPrune(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ContainersPrune\", reflect.TypeOf((*MockAPIClient)(nil).ContainersPrune), arg0, arg1)\n}\n\n// CopyFromContainer mocks base method.\nfunc (m *MockAPIClient) CopyFromContainer(arg0 context.Context, arg1, arg2 string) (io.ReadCloser, container.PathStat, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"CopyFromContainer\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(io.ReadCloser)\n\tret1, _ := ret[1].(container.PathStat)\n\tret2, _ := ret[2].(error)\n\treturn ret0, ret1, ret2\n}\n\n// CopyFromContainer indicates an expected call of CopyFromContainer.\nfunc (mr *MockAPIClientMockRecorder) CopyFromContainer(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"CopyFromContainer\", reflect.TypeOf((*MockAPIClient)(nil).CopyFromContainer), arg0, arg1, arg2)\n}\n\n// CopyToContainer mocks base method.\nfunc (m *MockAPIClient) CopyToContainer(arg0 context.Context, arg1, arg2 string, arg3 io.Reader, arg4 container.CopyToContainerOptions) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"CopyToContainer\", arg0, arg1, arg2, arg3, arg4)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// CopyToContainer indicates an expected call of CopyToContainer.\nfunc (mr *MockAPIClientMockRecorder) CopyToContainer(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"CopyToContainer\", reflect.TypeOf((*MockAPIClient)(nil).CopyToContainer), arg0, arg1, arg2, arg3, arg4)\n}\n\n// DaemonHost mocks base method.\nfunc (m *MockAPIClient) DaemonHost() string {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"DaemonHost\")\n\tret0, _ := ret[0].(string)\n\treturn ret0\n}\n\n// DaemonHost indicates an expected call of DaemonHost.\nfunc (mr *MockAPIClientMockRecorder) DaemonHost() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"DaemonHost\", reflect.TypeOf((*MockAPIClient)(nil).DaemonHost))\n}\n\n// DialHijack mocks base method.\nfunc (m *MockAPIClient) DialHijack(arg0 context.Context, arg1, arg2 string, arg3 map[string][]string) (net.Conn, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"DialHijack\", arg0, arg1, arg2, arg3)\n\tret0, _ := ret[0].(net.Conn)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// DialHijack indicates an expected call of DialHijack.\nfunc (mr *MockAPIClientMockRecorder) DialHijack(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"DialHijack\", reflect.TypeOf((*MockAPIClient)(nil).DialHijack), arg0, arg1, arg2, arg3)\n}\n\n// Dialer mocks base method.\nfunc (m *MockAPIClient) Dialer() func(context.Context) (net.Conn, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Dialer\")\n\tret0, _ := ret[0].(func(context.Context) (net.Conn, error))\n\treturn ret0\n}\n\n// Dialer indicates an expected call of Dialer.\nfunc (mr *MockAPIClientMockRecorder) Dialer() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Dialer\", reflect.TypeOf((*MockAPIClient)(nil).Dialer))\n}\n\n// DiskUsage mocks base method.\nfunc (m *MockAPIClient) DiskUsage(arg0 context.Context, arg1 types.DiskUsageOptions) (types.DiskUsage, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"DiskUsage\", arg0, arg1)\n\tret0, _ := ret[0].(types.DiskUsage)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// DiskUsage indicates an expected call of DiskUsage.\nfunc (mr *MockAPIClientMockRecorder) DiskUsage(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"DiskUsage\", reflect.TypeOf((*MockAPIClient)(nil).DiskUsage), arg0, arg1)\n}\n\n// DistributionInspect mocks base method.\nfunc (m *MockAPIClient) DistributionInspect(arg0 context.Context, arg1, arg2 string) (registry.DistributionInspect, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"DistributionInspect\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(registry.DistributionInspect)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// DistributionInspect indicates an expected call of DistributionInspect.\nfunc (mr *MockAPIClientMockRecorder) DistributionInspect(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"DistributionInspect\", reflect.TypeOf((*MockAPIClient)(nil).DistributionInspect), arg0, arg1, arg2)\n}\n\n// Events mocks base method.\nfunc (m *MockAPIClient) Events(arg0 context.Context, arg1 events.ListOptions) (<-chan events.Message, <-chan error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Events\", arg0, arg1)\n\tret0, _ := ret[0].(<-chan events.Message)\n\tret1, _ := ret[1].(<-chan error)\n\treturn ret0, ret1\n}\n\n// Events indicates an expected call of Events.\nfunc (mr *MockAPIClientMockRecorder) Events(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Events\", reflect.TypeOf((*MockAPIClient)(nil).Events), arg0, arg1)\n}\n\n// HTTPClient mocks base method.\nfunc (m *MockAPIClient) HTTPClient() *http.Client {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"HTTPClient\")\n\tret0, _ := ret[0].(*http.Client)\n\treturn ret0\n}\n\n// HTTPClient indicates an expected call of HTTPClient.\nfunc (mr *MockAPIClientMockRecorder) HTTPClient() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"HTTPClient\", reflect.TypeOf((*MockAPIClient)(nil).HTTPClient))\n}\n\n// ImageBuild mocks base method.\nfunc (m *MockAPIClient) ImageBuild(arg0 context.Context, arg1 io.Reader, arg2 types.ImageBuildOptions) (types.ImageBuildResponse, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ImageBuild\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(types.ImageBuildResponse)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ImageBuild indicates an expected call of ImageBuild.\nfunc (mr *MockAPIClientMockRecorder) ImageBuild(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ImageBuild\", reflect.TypeOf((*MockAPIClient)(nil).ImageBuild), arg0, arg1, arg2)\n}\n\n// ImageCreate mocks base method.\nfunc (m *MockAPIClient) ImageCreate(arg0 context.Context, arg1 string, arg2 image.CreateOptions) (io.ReadCloser, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ImageCreate\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(io.ReadCloser)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ImageCreate indicates an expected call of ImageCreate.\nfunc (mr *MockAPIClientMockRecorder) ImageCreate(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ImageCreate\", reflect.TypeOf((*MockAPIClient)(nil).ImageCreate), arg0, arg1, arg2)\n}\n\n// ImageHistory mocks base method.\nfunc (m *MockAPIClient) ImageHistory(arg0 context.Context, arg1 string, arg2 ...client.ImageHistoryOption) ([]image.HistoryResponseItem, error) {\n\tm.ctrl.T.Helper()\n\tvarargs := []interface{}{arg0, arg1}\n\tfor _, a := range arg2 {\n\t\tvarargs = append(varargs, a)\n\t}\n\tret := m.ctrl.Call(m, \"ImageHistory\", varargs...)\n\tret0, _ := ret[0].([]image.HistoryResponseItem)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ImageHistory indicates an expected call of ImageHistory.\nfunc (mr *MockAPIClientMockRecorder) ImageHistory(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]interface{}{arg0, arg1}, arg2...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ImageHistory\", reflect.TypeOf((*MockAPIClient)(nil).ImageHistory), varargs...)\n}\n\n// ImageImport mocks base method.\nfunc (m *MockAPIClient) ImageImport(arg0 context.Context, arg1 image.ImportSource, arg2 string, arg3 image.ImportOptions) (io.ReadCloser, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ImageImport\", arg0, arg1, arg2, arg3)\n\tret0, _ := ret[0].(io.ReadCloser)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ImageImport indicates an expected call of ImageImport.\nfunc (mr *MockAPIClientMockRecorder) ImageImport(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ImageImport\", reflect.TypeOf((*MockAPIClient)(nil).ImageImport), arg0, arg1, arg2, arg3)\n}\n\n// ImageInspect mocks base method.\nfunc (m *MockAPIClient) ImageInspect(arg0 context.Context, arg1 string, arg2 ...client.ImageInspectOption) (image.InspectResponse, error) {\n\tm.ctrl.T.Helper()\n\tvarargs := []interface{}{arg0, arg1}\n\tfor _, a := range arg2 {\n\t\tvarargs = append(varargs, a)\n\t}\n\tret := m.ctrl.Call(m, \"ImageInspect\", varargs...)\n\tret0, _ := ret[0].(image.InspectResponse)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ImageInspect indicates an expected call of ImageInspect.\nfunc (mr *MockAPIClientMockRecorder) ImageInspect(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]interface{}{arg0, arg1}, arg2...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ImageInspect\", reflect.TypeOf((*MockAPIClient)(nil).ImageInspect), varargs...)\n}\n\n// ImageInspectWithRaw mocks base method.\nfunc (m *MockAPIClient) ImageInspectWithRaw(arg0 context.Context, arg1 string) (image.InspectResponse, []byte, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ImageInspectWithRaw\", arg0, arg1)\n\tret0, _ := ret[0].(image.InspectResponse)\n\tret1, _ := ret[1].([]byte)\n\tret2, _ := ret[2].(error)\n\treturn ret0, ret1, ret2\n}\n\n// ImageInspectWithRaw indicates an expected call of ImageInspectWithRaw.\nfunc (mr *MockAPIClientMockRecorder) ImageInspectWithRaw(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ImageInspectWithRaw\", reflect.TypeOf((*MockAPIClient)(nil).ImageInspectWithRaw), arg0, arg1)\n}\n\n// ImageList mocks base method.\nfunc (m *MockAPIClient) ImageList(arg0 context.Context, arg1 image.ListOptions) ([]image.Summary, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ImageList\", arg0, arg1)\n\tret0, _ := ret[0].([]image.Summary)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ImageList indicates an expected call of ImageList.\nfunc (mr *MockAPIClientMockRecorder) ImageList(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ImageList\", reflect.TypeOf((*MockAPIClient)(nil).ImageList), arg0, arg1)\n}\n\n// ImageLoad mocks base method.\nfunc (m *MockAPIClient) ImageLoad(arg0 context.Context, arg1 io.Reader, arg2 ...client.ImageLoadOption) (image.LoadResponse, error) {\n\tm.ctrl.T.Helper()\n\tvarargs := []interface{}{arg0, arg1}\n\tfor _, a := range arg2 {\n\t\tvarargs = append(varargs, a)\n\t}\n\tret := m.ctrl.Call(m, \"ImageLoad\", varargs...)\n\tret0, _ := ret[0].(image.LoadResponse)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ImageLoad indicates an expected call of ImageLoad.\nfunc (mr *MockAPIClientMockRecorder) ImageLoad(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]interface{}{arg0, arg1}, arg2...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ImageLoad\", reflect.TypeOf((*MockAPIClient)(nil).ImageLoad), varargs...)\n}\n\n// ImagePull mocks base method.\nfunc (m *MockAPIClient) ImagePull(arg0 context.Context, arg1 string, arg2 image.PullOptions) (io.ReadCloser, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ImagePull\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(io.ReadCloser)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ImagePull indicates an expected call of ImagePull.\nfunc (mr *MockAPIClientMockRecorder) ImagePull(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ImagePull\", reflect.TypeOf((*MockAPIClient)(nil).ImagePull), arg0, arg1, arg2)\n}\n\n// ImagePush mocks base method.\nfunc (m *MockAPIClient) ImagePush(arg0 context.Context, arg1 string, arg2 image.PushOptions) (io.ReadCloser, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ImagePush\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(io.ReadCloser)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ImagePush indicates an expected call of ImagePush.\nfunc (mr *MockAPIClientMockRecorder) ImagePush(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ImagePush\", reflect.TypeOf((*MockAPIClient)(nil).ImagePush), arg0, arg1, arg2)\n}\n\n// ImageRemove mocks base method.\nfunc (m *MockAPIClient) ImageRemove(arg0 context.Context, arg1 string, arg2 image.RemoveOptions) ([]image.DeleteResponse, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ImageRemove\", arg0, arg1, arg2)\n\tret0, _ := ret[0].([]image.DeleteResponse)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ImageRemove indicates an expected call of ImageRemove.\nfunc (mr *MockAPIClientMockRecorder) ImageRemove(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ImageRemove\", reflect.TypeOf((*MockAPIClient)(nil).ImageRemove), arg0, arg1, arg2)\n}\n\n// ImageSave mocks base method.\nfunc (m *MockAPIClient) ImageSave(arg0 context.Context, arg1 []string, arg2 ...client.ImageSaveOption) (io.ReadCloser, error) {\n\tm.ctrl.T.Helper()\n\tvarargs := []interface{}{arg0, arg1}\n\tfor _, a := range arg2 {\n\t\tvarargs = append(varargs, a)\n\t}\n\tret := m.ctrl.Call(m, \"ImageSave\", varargs...)\n\tret0, _ := ret[0].(io.ReadCloser)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ImageSave indicates an expected call of ImageSave.\nfunc (mr *MockAPIClientMockRecorder) ImageSave(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]interface{}{arg0, arg1}, arg2...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ImageSave\", reflect.TypeOf((*MockAPIClient)(nil).ImageSave), varargs...)\n}\n\n// ImageSearch mocks base method.\nfunc (m *MockAPIClient) ImageSearch(arg0 context.Context, arg1 string, arg2 registry.SearchOptions) ([]registry.SearchResult, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ImageSearch\", arg0, arg1, arg2)\n\tret0, _ := ret[0].([]registry.SearchResult)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ImageSearch indicates an expected call of ImageSearch.\nfunc (mr *MockAPIClientMockRecorder) ImageSearch(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ImageSearch\", reflect.TypeOf((*MockAPIClient)(nil).ImageSearch), arg0, arg1, arg2)\n}\n\n// ImageTag mocks base method.\nfunc (m *MockAPIClient) ImageTag(arg0 context.Context, arg1, arg2 string) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ImageTag\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// ImageTag indicates an expected call of ImageTag.\nfunc (mr *MockAPIClientMockRecorder) ImageTag(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ImageTag\", reflect.TypeOf((*MockAPIClient)(nil).ImageTag), arg0, arg1, arg2)\n}\n\n// ImagesPrune mocks base method.\nfunc (m *MockAPIClient) ImagesPrune(arg0 context.Context, arg1 filters.Args) (image.PruneReport, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ImagesPrune\", arg0, arg1)\n\tret0, _ := ret[0].(image.PruneReport)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ImagesPrune indicates an expected call of ImagesPrune.\nfunc (mr *MockAPIClientMockRecorder) ImagesPrune(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ImagesPrune\", reflect.TypeOf((*MockAPIClient)(nil).ImagesPrune), arg0, arg1)\n}\n\n// Info mocks base method.\nfunc (m *MockAPIClient) Info(arg0 context.Context) (system.Info, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Info\", arg0)\n\tret0, _ := ret[0].(system.Info)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// Info indicates an expected call of Info.\nfunc (mr *MockAPIClientMockRecorder) Info(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Info\", reflect.TypeOf((*MockAPIClient)(nil).Info), arg0)\n}\n\n// NegotiateAPIVersion mocks base method.\nfunc (m *MockAPIClient) NegotiateAPIVersion(arg0 context.Context) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"NegotiateAPIVersion\", arg0)\n}\n\n// NegotiateAPIVersion indicates an expected call of NegotiateAPIVersion.\nfunc (mr *MockAPIClientMockRecorder) NegotiateAPIVersion(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"NegotiateAPIVersion\", reflect.TypeOf((*MockAPIClient)(nil).NegotiateAPIVersion), arg0)\n}\n\n// NegotiateAPIVersionPing mocks base method.\nfunc (m *MockAPIClient) NegotiateAPIVersionPing(arg0 types.Ping) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"NegotiateAPIVersionPing\", arg0)\n}\n\n// NegotiateAPIVersionPing indicates an expected call of NegotiateAPIVersionPing.\nfunc (mr *MockAPIClientMockRecorder) NegotiateAPIVersionPing(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"NegotiateAPIVersionPing\", reflect.TypeOf((*MockAPIClient)(nil).NegotiateAPIVersionPing), arg0)\n}\n\n// NetworkConnect mocks base method.\nfunc (m *MockAPIClient) NetworkConnect(arg0 context.Context, arg1, arg2 string, arg3 *network.EndpointSettings) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"NetworkConnect\", arg0, arg1, arg2, arg3)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// NetworkConnect indicates an expected call of NetworkConnect.\nfunc (mr *MockAPIClientMockRecorder) NetworkConnect(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"NetworkConnect\", reflect.TypeOf((*MockAPIClient)(nil).NetworkConnect), arg0, arg1, arg2, arg3)\n}\n\n// NetworkCreate mocks base method.\nfunc (m *MockAPIClient) NetworkCreate(arg0 context.Context, arg1 string, arg2 network.CreateOptions) (network.CreateResponse, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"NetworkCreate\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(network.CreateResponse)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// NetworkCreate indicates an expected call of NetworkCreate.\nfunc (mr *MockAPIClientMockRecorder) NetworkCreate(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"NetworkCreate\", reflect.TypeOf((*MockAPIClient)(nil).NetworkCreate), arg0, arg1, arg2)\n}\n\n// NetworkDisconnect mocks base method.\nfunc (m *MockAPIClient) NetworkDisconnect(arg0 context.Context, arg1, arg2 string, arg3 bool) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"NetworkDisconnect\", arg0, arg1, arg2, arg3)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// NetworkDisconnect indicates an expected call of NetworkDisconnect.\nfunc (mr *MockAPIClientMockRecorder) NetworkDisconnect(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"NetworkDisconnect\", reflect.TypeOf((*MockAPIClient)(nil).NetworkDisconnect), arg0, arg1, arg2, arg3)\n}\n\n// NetworkInspect mocks base method.\nfunc (m *MockAPIClient) NetworkInspect(arg0 context.Context, arg1 string, arg2 network.InspectOptions) (network.Inspect, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"NetworkInspect\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(network.Inspect)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// NetworkInspect indicates an expected call of NetworkInspect.\nfunc (mr *MockAPIClientMockRecorder) NetworkInspect(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"NetworkInspect\", reflect.TypeOf((*MockAPIClient)(nil).NetworkInspect), arg0, arg1, arg2)\n}\n\n// NetworkInspectWithRaw mocks base method.\nfunc (m *MockAPIClient) NetworkInspectWithRaw(arg0 context.Context, arg1 string, arg2 network.InspectOptions) (network.Inspect, []byte, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"NetworkInspectWithRaw\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(network.Inspect)\n\tret1, _ := ret[1].([]byte)\n\tret2, _ := ret[2].(error)\n\treturn ret0, ret1, ret2\n}\n\n// NetworkInspectWithRaw indicates an expected call of NetworkInspectWithRaw.\nfunc (mr *MockAPIClientMockRecorder) NetworkInspectWithRaw(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"NetworkInspectWithRaw\", reflect.TypeOf((*MockAPIClient)(nil).NetworkInspectWithRaw), arg0, arg1, arg2)\n}\n\n// NetworkList mocks base method.\nfunc (m *MockAPIClient) NetworkList(arg0 context.Context, arg1 network.ListOptions) ([]network.Inspect, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"NetworkList\", arg0, arg1)\n\tret0, _ := ret[0].([]network.Inspect)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// NetworkList indicates an expected call of NetworkList.\nfunc (mr *MockAPIClientMockRecorder) NetworkList(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"NetworkList\", reflect.TypeOf((*MockAPIClient)(nil).NetworkList), arg0, arg1)\n}\n\n// NetworkRemove mocks base method.\nfunc (m *MockAPIClient) NetworkRemove(arg0 context.Context, arg1 string) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"NetworkRemove\", arg0, arg1)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// NetworkRemove indicates an expected call of NetworkRemove.\nfunc (mr *MockAPIClientMockRecorder) NetworkRemove(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"NetworkRemove\", reflect.TypeOf((*MockAPIClient)(nil).NetworkRemove), arg0, arg1)\n}\n\n// NetworksPrune mocks base method.\nfunc (m *MockAPIClient) NetworksPrune(arg0 context.Context, arg1 filters.Args) (network.PruneReport, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"NetworksPrune\", arg0, arg1)\n\tret0, _ := ret[0].(network.PruneReport)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// NetworksPrune indicates an expected call of NetworksPrune.\nfunc (mr *MockAPIClientMockRecorder) NetworksPrune(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"NetworksPrune\", reflect.TypeOf((*MockAPIClient)(nil).NetworksPrune), arg0, arg1)\n}\n\n// NodeInspectWithRaw mocks base method.\nfunc (m *MockAPIClient) NodeInspectWithRaw(arg0 context.Context, arg1 string) (swarm.Node, []byte, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"NodeInspectWithRaw\", arg0, arg1)\n\tret0, _ := ret[0].(swarm.Node)\n\tret1, _ := ret[1].([]byte)\n\tret2, _ := ret[2].(error)\n\treturn ret0, ret1, ret2\n}\n\n// NodeInspectWithRaw indicates an expected call of NodeInspectWithRaw.\nfunc (mr *MockAPIClientMockRecorder) NodeInspectWithRaw(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"NodeInspectWithRaw\", reflect.TypeOf((*MockAPIClient)(nil).NodeInspectWithRaw), arg0, arg1)\n}\n\n// NodeList mocks base method.\nfunc (m *MockAPIClient) NodeList(arg0 context.Context, arg1 types.NodeListOptions) ([]swarm.Node, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"NodeList\", arg0, arg1)\n\tret0, _ := ret[0].([]swarm.Node)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// NodeList indicates an expected call of NodeList.\nfunc (mr *MockAPIClientMockRecorder) NodeList(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"NodeList\", reflect.TypeOf((*MockAPIClient)(nil).NodeList), arg0, arg1)\n}\n\n// NodeRemove mocks base method.\nfunc (m *MockAPIClient) NodeRemove(arg0 context.Context, arg1 string, arg2 types.NodeRemoveOptions) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"NodeRemove\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// NodeRemove indicates an expected call of NodeRemove.\nfunc (mr *MockAPIClientMockRecorder) NodeRemove(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"NodeRemove\", reflect.TypeOf((*MockAPIClient)(nil).NodeRemove), arg0, arg1, arg2)\n}\n\n// NodeUpdate mocks base method.\nfunc (m *MockAPIClient) NodeUpdate(arg0 context.Context, arg1 string, arg2 swarm.Version, arg3 swarm.NodeSpec) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"NodeUpdate\", arg0, arg1, arg2, arg3)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// NodeUpdate indicates an expected call of NodeUpdate.\nfunc (mr *MockAPIClientMockRecorder) NodeUpdate(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"NodeUpdate\", reflect.TypeOf((*MockAPIClient)(nil).NodeUpdate), arg0, arg1, arg2, arg3)\n}\n\n// Ping mocks base method.\nfunc (m *MockAPIClient) Ping(arg0 context.Context) (types.Ping, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Ping\", arg0)\n\tret0, _ := ret[0].(types.Ping)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// Ping indicates an expected call of Ping.\nfunc (mr *MockAPIClientMockRecorder) Ping(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Ping\", reflect.TypeOf((*MockAPIClient)(nil).Ping), arg0)\n}\n\n// PluginCreate mocks base method.\nfunc (m *MockAPIClient) PluginCreate(arg0 context.Context, arg1 io.Reader, arg2 types.PluginCreateOptions) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"PluginCreate\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// PluginCreate indicates an expected call of PluginCreate.\nfunc (mr *MockAPIClientMockRecorder) PluginCreate(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"PluginCreate\", reflect.TypeOf((*MockAPIClient)(nil).PluginCreate), arg0, arg1, arg2)\n}\n\n// PluginDisable mocks base method.\nfunc (m *MockAPIClient) PluginDisable(arg0 context.Context, arg1 string, arg2 types.PluginDisableOptions) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"PluginDisable\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// PluginDisable indicates an expected call of PluginDisable.\nfunc (mr *MockAPIClientMockRecorder) PluginDisable(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"PluginDisable\", reflect.TypeOf((*MockAPIClient)(nil).PluginDisable), arg0, arg1, arg2)\n}\n\n// PluginEnable mocks base method.\nfunc (m *MockAPIClient) PluginEnable(arg0 context.Context, arg1 string, arg2 types.PluginEnableOptions) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"PluginEnable\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// PluginEnable indicates an expected call of PluginEnable.\nfunc (mr *MockAPIClientMockRecorder) PluginEnable(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"PluginEnable\", reflect.TypeOf((*MockAPIClient)(nil).PluginEnable), arg0, arg1, arg2)\n}\n\n// PluginInspectWithRaw mocks base method.\nfunc (m *MockAPIClient) PluginInspectWithRaw(arg0 context.Context, arg1 string) (*types.Plugin, []byte, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"PluginInspectWithRaw\", arg0, arg1)\n\tret0, _ := ret[0].(*types.Plugin)\n\tret1, _ := ret[1].([]byte)\n\tret2, _ := ret[2].(error)\n\treturn ret0, ret1, ret2\n}\n\n// PluginInspectWithRaw indicates an expected call of PluginInspectWithRaw.\nfunc (mr *MockAPIClientMockRecorder) PluginInspectWithRaw(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"PluginInspectWithRaw\", reflect.TypeOf((*MockAPIClient)(nil).PluginInspectWithRaw), arg0, arg1)\n}\n\n// PluginInstall mocks base method.\nfunc (m *MockAPIClient) PluginInstall(arg0 context.Context, arg1 string, arg2 types.PluginInstallOptions) (io.ReadCloser, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"PluginInstall\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(io.ReadCloser)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// PluginInstall indicates an expected call of PluginInstall.\nfunc (mr *MockAPIClientMockRecorder) PluginInstall(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"PluginInstall\", reflect.TypeOf((*MockAPIClient)(nil).PluginInstall), arg0, arg1, arg2)\n}\n\n// PluginList mocks base method.\nfunc (m *MockAPIClient) PluginList(arg0 context.Context, arg1 filters.Args) (types.PluginsListResponse, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"PluginList\", arg0, arg1)\n\tret0, _ := ret[0].(types.PluginsListResponse)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// PluginList indicates an expected call of PluginList.\nfunc (mr *MockAPIClientMockRecorder) PluginList(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"PluginList\", reflect.TypeOf((*MockAPIClient)(nil).PluginList), arg0, arg1)\n}\n\n// PluginPush mocks base method.\nfunc (m *MockAPIClient) PluginPush(arg0 context.Context, arg1, arg2 string) (io.ReadCloser, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"PluginPush\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(io.ReadCloser)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// PluginPush indicates an expected call of PluginPush.\nfunc (mr *MockAPIClientMockRecorder) PluginPush(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"PluginPush\", reflect.TypeOf((*MockAPIClient)(nil).PluginPush), arg0, arg1, arg2)\n}\n\n// PluginRemove mocks base method.\nfunc (m *MockAPIClient) PluginRemove(arg0 context.Context, arg1 string, arg2 types.PluginRemoveOptions) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"PluginRemove\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// PluginRemove indicates an expected call of PluginRemove.\nfunc (mr *MockAPIClientMockRecorder) PluginRemove(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"PluginRemove\", reflect.TypeOf((*MockAPIClient)(nil).PluginRemove), arg0, arg1, arg2)\n}\n\n// PluginSet mocks base method.\nfunc (m *MockAPIClient) PluginSet(arg0 context.Context, arg1 string, arg2 []string) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"PluginSet\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// PluginSet indicates an expected call of PluginSet.\nfunc (mr *MockAPIClientMockRecorder) PluginSet(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"PluginSet\", reflect.TypeOf((*MockAPIClient)(nil).PluginSet), arg0, arg1, arg2)\n}\n\n// PluginUpgrade mocks base method.\nfunc (m *MockAPIClient) PluginUpgrade(arg0 context.Context, arg1 string, arg2 types.PluginInstallOptions) (io.ReadCloser, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"PluginUpgrade\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(io.ReadCloser)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// PluginUpgrade indicates an expected call of PluginUpgrade.\nfunc (mr *MockAPIClientMockRecorder) PluginUpgrade(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"PluginUpgrade\", reflect.TypeOf((*MockAPIClient)(nil).PluginUpgrade), arg0, arg1, arg2)\n}\n\n// RegistryLogin mocks base method.\nfunc (m *MockAPIClient) RegistryLogin(arg0 context.Context, arg1 registry.AuthConfig) (registry.AuthenticateOKBody, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"RegistryLogin\", arg0, arg1)\n\tret0, _ := ret[0].(registry.AuthenticateOKBody)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// RegistryLogin indicates an expected call of RegistryLogin.\nfunc (mr *MockAPIClientMockRecorder) RegistryLogin(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"RegistryLogin\", reflect.TypeOf((*MockAPIClient)(nil).RegistryLogin), arg0, arg1)\n}\n\n// SecretCreate mocks base method.\nfunc (m *MockAPIClient) SecretCreate(arg0 context.Context, arg1 swarm.SecretSpec) (types.SecretCreateResponse, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"SecretCreate\", arg0, arg1)\n\tret0, _ := ret[0].(types.SecretCreateResponse)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// SecretCreate indicates an expected call of SecretCreate.\nfunc (mr *MockAPIClientMockRecorder) SecretCreate(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"SecretCreate\", reflect.TypeOf((*MockAPIClient)(nil).SecretCreate), arg0, arg1)\n}\n\n// SecretInspectWithRaw mocks base method.\nfunc (m *MockAPIClient) SecretInspectWithRaw(arg0 context.Context, arg1 string) (swarm.Secret, []byte, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"SecretInspectWithRaw\", arg0, arg1)\n\tret0, _ := ret[0].(swarm.Secret)\n\tret1, _ := ret[1].([]byte)\n\tret2, _ := ret[2].(error)\n\treturn ret0, ret1, ret2\n}\n\n// SecretInspectWithRaw indicates an expected call of SecretInspectWithRaw.\nfunc (mr *MockAPIClientMockRecorder) SecretInspectWithRaw(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"SecretInspectWithRaw\", reflect.TypeOf((*MockAPIClient)(nil).SecretInspectWithRaw), arg0, arg1)\n}\n\n// SecretList mocks base method.\nfunc (m *MockAPIClient) SecretList(arg0 context.Context, arg1 types.SecretListOptions) ([]swarm.Secret, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"SecretList\", arg0, arg1)\n\tret0, _ := ret[0].([]swarm.Secret)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// SecretList indicates an expected call of SecretList.\nfunc (mr *MockAPIClientMockRecorder) SecretList(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"SecretList\", reflect.TypeOf((*MockAPIClient)(nil).SecretList), arg0, arg1)\n}\n\n// SecretRemove mocks base method.\nfunc (m *MockAPIClient) SecretRemove(arg0 context.Context, arg1 string) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"SecretRemove\", arg0, arg1)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// SecretRemove indicates an expected call of SecretRemove.\nfunc (mr *MockAPIClientMockRecorder) SecretRemove(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"SecretRemove\", reflect.TypeOf((*MockAPIClient)(nil).SecretRemove), arg0, arg1)\n}\n\n// SecretUpdate mocks base method.\nfunc (m *MockAPIClient) SecretUpdate(arg0 context.Context, arg1 string, arg2 swarm.Version, arg3 swarm.SecretSpec) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"SecretUpdate\", arg0, arg1, arg2, arg3)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// SecretUpdate indicates an expected call of SecretUpdate.\nfunc (mr *MockAPIClientMockRecorder) SecretUpdate(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"SecretUpdate\", reflect.TypeOf((*MockAPIClient)(nil).SecretUpdate), arg0, arg1, arg2, arg3)\n}\n\n// ServerVersion mocks base method.\nfunc (m *MockAPIClient) ServerVersion(arg0 context.Context) (types.Version, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ServerVersion\", arg0)\n\tret0, _ := ret[0].(types.Version)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ServerVersion indicates an expected call of ServerVersion.\nfunc (mr *MockAPIClientMockRecorder) ServerVersion(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ServerVersion\", reflect.TypeOf((*MockAPIClient)(nil).ServerVersion), arg0)\n}\n\n// ServiceCreate mocks base method.\nfunc (m *MockAPIClient) ServiceCreate(arg0 context.Context, arg1 swarm.ServiceSpec, arg2 types.ServiceCreateOptions) (swarm.ServiceCreateResponse, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ServiceCreate\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(swarm.ServiceCreateResponse)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ServiceCreate indicates an expected call of ServiceCreate.\nfunc (mr *MockAPIClientMockRecorder) ServiceCreate(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ServiceCreate\", reflect.TypeOf((*MockAPIClient)(nil).ServiceCreate), arg0, arg1, arg2)\n}\n\n// ServiceInspectWithRaw mocks base method.\nfunc (m *MockAPIClient) ServiceInspectWithRaw(arg0 context.Context, arg1 string, arg2 types.ServiceInspectOptions) (swarm.Service, []byte, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ServiceInspectWithRaw\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(swarm.Service)\n\tret1, _ := ret[1].([]byte)\n\tret2, _ := ret[2].(error)\n\treturn ret0, ret1, ret2\n}\n\n// ServiceInspectWithRaw indicates an expected call of ServiceInspectWithRaw.\nfunc (mr *MockAPIClientMockRecorder) ServiceInspectWithRaw(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ServiceInspectWithRaw\", reflect.TypeOf((*MockAPIClient)(nil).ServiceInspectWithRaw), arg0, arg1, arg2)\n}\n\n// ServiceList mocks base method.\nfunc (m *MockAPIClient) ServiceList(arg0 context.Context, arg1 types.ServiceListOptions) ([]swarm.Service, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ServiceList\", arg0, arg1)\n\tret0, _ := ret[0].([]swarm.Service)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ServiceList indicates an expected call of ServiceList.\nfunc (mr *MockAPIClientMockRecorder) ServiceList(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ServiceList\", reflect.TypeOf((*MockAPIClient)(nil).ServiceList), arg0, arg1)\n}\n\n// ServiceLogs mocks base method.\nfunc (m *MockAPIClient) ServiceLogs(arg0 context.Context, arg1 string, arg2 container.LogsOptions) (io.ReadCloser, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ServiceLogs\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(io.ReadCloser)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ServiceLogs indicates an expected call of ServiceLogs.\nfunc (mr *MockAPIClientMockRecorder) ServiceLogs(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ServiceLogs\", reflect.TypeOf((*MockAPIClient)(nil).ServiceLogs), arg0, arg1, arg2)\n}\n\n// ServiceRemove mocks base method.\nfunc (m *MockAPIClient) ServiceRemove(arg0 context.Context, arg1 string) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ServiceRemove\", arg0, arg1)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// ServiceRemove indicates an expected call of ServiceRemove.\nfunc (mr *MockAPIClientMockRecorder) ServiceRemove(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ServiceRemove\", reflect.TypeOf((*MockAPIClient)(nil).ServiceRemove), arg0, arg1)\n}\n\n// ServiceUpdate mocks base method.\nfunc (m *MockAPIClient) ServiceUpdate(arg0 context.Context, arg1 string, arg2 swarm.Version, arg3 swarm.ServiceSpec, arg4 types.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ServiceUpdate\", arg0, arg1, arg2, arg3, arg4)\n\tret0, _ := ret[0].(swarm.ServiceUpdateResponse)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ServiceUpdate indicates an expected call of ServiceUpdate.\nfunc (mr *MockAPIClientMockRecorder) ServiceUpdate(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ServiceUpdate\", reflect.TypeOf((*MockAPIClient)(nil).ServiceUpdate), arg0, arg1, arg2, arg3, arg4)\n}\n\n// SwarmGetUnlockKey mocks base method.\nfunc (m *MockAPIClient) SwarmGetUnlockKey(arg0 context.Context) (types.SwarmUnlockKeyResponse, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"SwarmGetUnlockKey\", arg0)\n\tret0, _ := ret[0].(types.SwarmUnlockKeyResponse)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// SwarmGetUnlockKey indicates an expected call of SwarmGetUnlockKey.\nfunc (mr *MockAPIClientMockRecorder) SwarmGetUnlockKey(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"SwarmGetUnlockKey\", reflect.TypeOf((*MockAPIClient)(nil).SwarmGetUnlockKey), arg0)\n}\n\n// SwarmInit mocks base method.\nfunc (m *MockAPIClient) SwarmInit(arg0 context.Context, arg1 swarm.InitRequest) (string, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"SwarmInit\", arg0, arg1)\n\tret0, _ := ret[0].(string)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// SwarmInit indicates an expected call of SwarmInit.\nfunc (mr *MockAPIClientMockRecorder) SwarmInit(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"SwarmInit\", reflect.TypeOf((*MockAPIClient)(nil).SwarmInit), arg0, arg1)\n}\n\n// SwarmInspect mocks base method.\nfunc (m *MockAPIClient) SwarmInspect(arg0 context.Context) (swarm.Swarm, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"SwarmInspect\", arg0)\n\tret0, _ := ret[0].(swarm.Swarm)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// SwarmInspect indicates an expected call of SwarmInspect.\nfunc (mr *MockAPIClientMockRecorder) SwarmInspect(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"SwarmInspect\", reflect.TypeOf((*MockAPIClient)(nil).SwarmInspect), arg0)\n}\n\n// SwarmJoin mocks base method.\nfunc (m *MockAPIClient) SwarmJoin(arg0 context.Context, arg1 swarm.JoinRequest) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"SwarmJoin\", arg0, arg1)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// SwarmJoin indicates an expected call of SwarmJoin.\nfunc (mr *MockAPIClientMockRecorder) SwarmJoin(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"SwarmJoin\", reflect.TypeOf((*MockAPIClient)(nil).SwarmJoin), arg0, arg1)\n}\n\n// SwarmLeave mocks base method.\nfunc (m *MockAPIClient) SwarmLeave(arg0 context.Context, arg1 bool) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"SwarmLeave\", arg0, arg1)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// SwarmLeave indicates an expected call of SwarmLeave.\nfunc (mr *MockAPIClientMockRecorder) SwarmLeave(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"SwarmLeave\", reflect.TypeOf((*MockAPIClient)(nil).SwarmLeave), arg0, arg1)\n}\n\n// SwarmUnlock mocks base method.\nfunc (m *MockAPIClient) SwarmUnlock(arg0 context.Context, arg1 swarm.UnlockRequest) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"SwarmUnlock\", arg0, arg1)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// SwarmUnlock indicates an expected call of SwarmUnlock.\nfunc (mr *MockAPIClientMockRecorder) SwarmUnlock(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"SwarmUnlock\", reflect.TypeOf((*MockAPIClient)(nil).SwarmUnlock), arg0, arg1)\n}\n\n// SwarmUpdate mocks base method.\nfunc (m *MockAPIClient) SwarmUpdate(arg0 context.Context, arg1 swarm.Version, arg2 swarm.Spec, arg3 swarm.UpdateFlags) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"SwarmUpdate\", arg0, arg1, arg2, arg3)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// SwarmUpdate indicates an expected call of SwarmUpdate.\nfunc (mr *MockAPIClientMockRecorder) SwarmUpdate(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"SwarmUpdate\", reflect.TypeOf((*MockAPIClient)(nil).SwarmUpdate), arg0, arg1, arg2, arg3)\n}\n\n// TaskInspectWithRaw mocks base method.\nfunc (m *MockAPIClient) TaskInspectWithRaw(arg0 context.Context, arg1 string) (swarm.Task, []byte, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"TaskInspectWithRaw\", arg0, arg1)\n\tret0, _ := ret[0].(swarm.Task)\n\tret1, _ := ret[1].([]byte)\n\tret2, _ := ret[2].(error)\n\treturn ret0, ret1, ret2\n}\n\n// TaskInspectWithRaw indicates an expected call of TaskInspectWithRaw.\nfunc (mr *MockAPIClientMockRecorder) TaskInspectWithRaw(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"TaskInspectWithRaw\", reflect.TypeOf((*MockAPIClient)(nil).TaskInspectWithRaw), arg0, arg1)\n}\n\n// TaskList mocks base method.\nfunc (m *MockAPIClient) TaskList(arg0 context.Context, arg1 types.TaskListOptions) ([]swarm.Task, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"TaskList\", arg0, arg1)\n\tret0, _ := ret[0].([]swarm.Task)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// TaskList indicates an expected call of TaskList.\nfunc (mr *MockAPIClientMockRecorder) TaskList(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"TaskList\", reflect.TypeOf((*MockAPIClient)(nil).TaskList), arg0, arg1)\n}\n\n// TaskLogs mocks base method.\nfunc (m *MockAPIClient) TaskLogs(arg0 context.Context, arg1 string, arg2 container.LogsOptions) (io.ReadCloser, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"TaskLogs\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(io.ReadCloser)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// TaskLogs indicates an expected call of TaskLogs.\nfunc (mr *MockAPIClientMockRecorder) TaskLogs(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"TaskLogs\", reflect.TypeOf((*MockAPIClient)(nil).TaskLogs), arg0, arg1, arg2)\n}\n\n// VolumeCreate mocks base method.\nfunc (m *MockAPIClient) VolumeCreate(arg0 context.Context, arg1 volume.CreateOptions) (volume.Volume, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"VolumeCreate\", arg0, arg1)\n\tret0, _ := ret[0].(volume.Volume)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// VolumeCreate indicates an expected call of VolumeCreate.\nfunc (mr *MockAPIClientMockRecorder) VolumeCreate(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"VolumeCreate\", reflect.TypeOf((*MockAPIClient)(nil).VolumeCreate), arg0, arg1)\n}\n\n// VolumeInspect mocks base method.\nfunc (m *MockAPIClient) VolumeInspect(arg0 context.Context, arg1 string) (volume.Volume, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"VolumeInspect\", arg0, arg1)\n\tret0, _ := ret[0].(volume.Volume)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// VolumeInspect indicates an expected call of VolumeInspect.\nfunc (mr *MockAPIClientMockRecorder) VolumeInspect(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"VolumeInspect\", reflect.TypeOf((*MockAPIClient)(nil).VolumeInspect), arg0, arg1)\n}\n\n// VolumeInspectWithRaw mocks base method.\nfunc (m *MockAPIClient) VolumeInspectWithRaw(arg0 context.Context, arg1 string) (volume.Volume, []byte, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"VolumeInspectWithRaw\", arg0, arg1)\n\tret0, _ := ret[0].(volume.Volume)\n\tret1, _ := ret[1].([]byte)\n\tret2, _ := ret[2].(error)\n\treturn ret0, ret1, ret2\n}\n\n// VolumeInspectWithRaw indicates an expected call of VolumeInspectWithRaw.\nfunc (mr *MockAPIClientMockRecorder) VolumeInspectWithRaw(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"VolumeInspectWithRaw\", reflect.TypeOf((*MockAPIClient)(nil).VolumeInspectWithRaw), arg0, arg1)\n}\n\n// VolumeList mocks base method.\nfunc (m *MockAPIClient) VolumeList(arg0 context.Context, arg1 volume.ListOptions) (volume.ListResponse, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"VolumeList\", arg0, arg1)\n\tret0, _ := ret[0].(volume.ListResponse)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// VolumeList indicates an expected call of VolumeList.\nfunc (mr *MockAPIClientMockRecorder) VolumeList(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"VolumeList\", reflect.TypeOf((*MockAPIClient)(nil).VolumeList), arg0, arg1)\n}\n\n// VolumeRemove mocks base method.\nfunc (m *MockAPIClient) VolumeRemove(arg0 context.Context, arg1 string, arg2 bool) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"VolumeRemove\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// VolumeRemove indicates an expected call of VolumeRemove.\nfunc (mr *MockAPIClientMockRecorder) VolumeRemove(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"VolumeRemove\", reflect.TypeOf((*MockAPIClient)(nil).VolumeRemove), arg0, arg1, arg2)\n}\n\n// VolumeUpdate mocks base method.\nfunc (m *MockAPIClient) VolumeUpdate(arg0 context.Context, arg1 string, arg2 swarm.Version, arg3 volume.UpdateOptions) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"VolumeUpdate\", arg0, arg1, arg2, arg3)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// VolumeUpdate indicates an expected call of VolumeUpdate.\nfunc (mr *MockAPIClientMockRecorder) VolumeUpdate(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"VolumeUpdate\", reflect.TypeOf((*MockAPIClient)(nil).VolumeUpdate), arg0, arg1, arg2, arg3)\n}\n\n// VolumesPrune mocks base method.\nfunc (m *MockAPIClient) VolumesPrune(arg0 context.Context, arg1 filters.Args) (volume.PruneReport, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"VolumesPrune\", arg0, arg1)\n\tret0, _ := ret[0].(volume.PruneReport)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// VolumesPrune indicates an expected call of VolumesPrune.\nfunc (mr *MockAPIClientMockRecorder) VolumesPrune(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"VolumesPrune\", reflect.TypeOf((*MockAPIClient)(nil).VolumesPrune), arg0, arg1)\n}\n"
  },
  {
    "path": "mocks/mock_drone.go",
    "content": "// Code generated by MockGen. DO NOT EDIT.\n// Source: github.com/drone/drone-go/drone (interfaces: Client)\n\n// Package mocks is a generated GoMock package.\npackage mocks\n\nimport (\n\thttp \"net/http\"\n\treflect \"reflect\"\n\n\tdrone \"github.com/drone/drone-go/drone\"\n\tgomock \"github.com/golang/mock/gomock\"\n)\n\n// MockClient is a mock of Client interface.\ntype MockClient struct {\n\tctrl     *gomock.Controller\n\trecorder *MockClientMockRecorder\n}\n\n// MockClientMockRecorder is the mock recorder for MockClient.\ntype MockClientMockRecorder struct {\n\tmock *MockClient\n}\n\n// NewMockClient creates a new mock instance.\nfunc NewMockClient(ctrl *gomock.Controller) *MockClient {\n\tmock := &MockClient{ctrl: ctrl}\n\tmock.recorder = &MockClientMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *MockClient) EXPECT() *MockClientMockRecorder {\n\treturn m.recorder\n}\n\n// Approve mocks base method.\nfunc (m *MockClient) Approve(arg0, arg1 string, arg2, arg3 int) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Approve\", arg0, arg1, arg2, arg3)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// Approve indicates an expected call of Approve.\nfunc (mr *MockClientMockRecorder) Approve(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Approve\", reflect.TypeOf((*MockClient)(nil).Approve), arg0, arg1, arg2, arg3)\n}\n\n// AutoscalePause mocks base method.\nfunc (m *MockClient) AutoscalePause() error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"AutoscalePause\")\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// AutoscalePause indicates an expected call of AutoscalePause.\nfunc (mr *MockClientMockRecorder) AutoscalePause() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"AutoscalePause\", reflect.TypeOf((*MockClient)(nil).AutoscalePause))\n}\n\n// AutoscaleResume mocks base method.\nfunc (m *MockClient) AutoscaleResume() error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"AutoscaleResume\")\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// AutoscaleResume indicates an expected call of AutoscaleResume.\nfunc (mr *MockClientMockRecorder) AutoscaleResume() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"AutoscaleResume\", reflect.TypeOf((*MockClient)(nil).AutoscaleResume))\n}\n\n// AutoscaleVersion mocks base method.\nfunc (m *MockClient) AutoscaleVersion() (*drone.Version, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"AutoscaleVersion\")\n\tret0, _ := ret[0].(*drone.Version)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// AutoscaleVersion indicates an expected call of AutoscaleVersion.\nfunc (mr *MockClientMockRecorder) AutoscaleVersion() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"AutoscaleVersion\", reflect.TypeOf((*MockClient)(nil).AutoscaleVersion))\n}\n\n// Build mocks base method.\nfunc (m *MockClient) Build(arg0, arg1 string, arg2 int) (*drone.Build, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Build\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(*drone.Build)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// Build indicates an expected call of Build.\nfunc (mr *MockClientMockRecorder) Build(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Build\", reflect.TypeOf((*MockClient)(nil).Build), arg0, arg1, arg2)\n}\n\n// BuildCancel mocks base method.\nfunc (m *MockClient) BuildCancel(arg0, arg1 string, arg2 int) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"BuildCancel\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// BuildCancel indicates an expected call of BuildCancel.\nfunc (mr *MockClientMockRecorder) BuildCancel(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"BuildCancel\", reflect.TypeOf((*MockClient)(nil).BuildCancel), arg0, arg1, arg2)\n}\n\n// BuildLast mocks base method.\nfunc (m *MockClient) BuildLast(arg0, arg1, arg2 string) (*drone.Build, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"BuildLast\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(*drone.Build)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// BuildLast indicates an expected call of BuildLast.\nfunc (mr *MockClientMockRecorder) BuildLast(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"BuildLast\", reflect.TypeOf((*MockClient)(nil).BuildLast), arg0, arg1, arg2)\n}\n\n// BuildList mocks base method.\nfunc (m *MockClient) BuildList(arg0, arg1 string, arg2 drone.ListOptions) ([]*drone.Build, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"BuildList\", arg0, arg1, arg2)\n\tret0, _ := ret[0].([]*drone.Build)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// BuildList indicates an expected call of BuildList.\nfunc (mr *MockClientMockRecorder) BuildList(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"BuildList\", reflect.TypeOf((*MockClient)(nil).BuildList), arg0, arg1, arg2)\n}\n\n// BuildPurge mocks base method.\nfunc (m *MockClient) BuildPurge(arg0, arg1 string, arg2 int) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"BuildPurge\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// BuildPurge indicates an expected call of BuildPurge.\nfunc (mr *MockClientMockRecorder) BuildPurge(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"BuildPurge\", reflect.TypeOf((*MockClient)(nil).BuildPurge), arg0, arg1, arg2)\n}\n\n// BuildRestart mocks base method.\nfunc (m *MockClient) BuildRestart(arg0, arg1 string, arg2 int, arg3 map[string]string) (*drone.Build, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"BuildRestart\", arg0, arg1, arg2, arg3)\n\tret0, _ := ret[0].(*drone.Build)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// BuildRestart indicates an expected call of BuildRestart.\nfunc (mr *MockClientMockRecorder) BuildRestart(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"BuildRestart\", reflect.TypeOf((*MockClient)(nil).BuildRestart), arg0, arg1, arg2, arg3)\n}\n\n// Cron mocks base method.\nfunc (m *MockClient) Cron(arg0, arg1, arg2 string) (*drone.Cron, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Cron\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(*drone.Cron)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// Cron indicates an expected call of Cron.\nfunc (mr *MockClientMockRecorder) Cron(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Cron\", reflect.TypeOf((*MockClient)(nil).Cron), arg0, arg1, arg2)\n}\n\n// CronCreate mocks base method.\nfunc (m *MockClient) CronCreate(arg0, arg1 string, arg2 *drone.Cron) (*drone.Cron, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"CronCreate\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(*drone.Cron)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// CronCreate indicates an expected call of CronCreate.\nfunc (mr *MockClientMockRecorder) CronCreate(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"CronCreate\", reflect.TypeOf((*MockClient)(nil).CronCreate), arg0, arg1, arg2)\n}\n\n// CronDelete mocks base method.\nfunc (m *MockClient) CronDelete(arg0, arg1, arg2 string) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"CronDelete\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// CronDelete indicates an expected call of CronDelete.\nfunc (mr *MockClientMockRecorder) CronDelete(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"CronDelete\", reflect.TypeOf((*MockClient)(nil).CronDelete), arg0, arg1, arg2)\n}\n\n// CronList mocks base method.\nfunc (m *MockClient) CronList(arg0, arg1 string) ([]*drone.Cron, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"CronList\", arg0, arg1)\n\tret0, _ := ret[0].([]*drone.Cron)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// CronList indicates an expected call of CronList.\nfunc (mr *MockClientMockRecorder) CronList(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"CronList\", reflect.TypeOf((*MockClient)(nil).CronList), arg0, arg1)\n}\n\n// CronUpdate mocks base method.\nfunc (m *MockClient) CronUpdate(arg0, arg1, arg2 string, arg3 *drone.CronPatch) (*drone.Cron, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"CronUpdate\", arg0, arg1, arg2, arg3)\n\tret0, _ := ret[0].(*drone.Cron)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// CronUpdate indicates an expected call of CronUpdate.\nfunc (mr *MockClientMockRecorder) CronUpdate(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"CronUpdate\", reflect.TypeOf((*MockClient)(nil).CronUpdate), arg0, arg1, arg2, arg3)\n}\n\n// Decline mocks base method.\nfunc (m *MockClient) Decline(arg0, arg1 string, arg2, arg3 int) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Decline\", arg0, arg1, arg2, arg3)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// Decline indicates an expected call of Decline.\nfunc (mr *MockClientMockRecorder) Decline(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Decline\", reflect.TypeOf((*MockClient)(nil).Decline), arg0, arg1, arg2, arg3)\n}\n\n// Encrypt mocks base method.\nfunc (m *MockClient) Encrypt(arg0, arg1 string, arg2 *drone.Secret) (string, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Encrypt\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(string)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// Encrypt indicates an expected call of Encrypt.\nfunc (mr *MockClientMockRecorder) Encrypt(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Encrypt\", reflect.TypeOf((*MockClient)(nil).Encrypt), arg0, arg1, arg2)\n}\n\n// Logs mocks base method.\nfunc (m *MockClient) Logs(arg0, arg1 string, arg2, arg3, arg4 int) ([]*drone.Line, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Logs\", arg0, arg1, arg2, arg3, arg4)\n\tret0, _ := ret[0].([]*drone.Line)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// Logs indicates an expected call of Logs.\nfunc (mr *MockClientMockRecorder) Logs(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Logs\", reflect.TypeOf((*MockClient)(nil).Logs), arg0, arg1, arg2, arg3, arg4)\n}\n\n// LogsPurge mocks base method.\nfunc (m *MockClient) LogsPurge(arg0, arg1 string, arg2, arg3, arg4 int) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"LogsPurge\", arg0, arg1, arg2, arg3, arg4)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// LogsPurge indicates an expected call of LogsPurge.\nfunc (mr *MockClientMockRecorder) LogsPurge(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"LogsPurge\", reflect.TypeOf((*MockClient)(nil).LogsPurge), arg0, arg1, arg2, arg3, arg4)\n}\n\n// Node mocks base method.\nfunc (m *MockClient) Node(arg0 string) (*drone.Node, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Node\", arg0)\n\tret0, _ := ret[0].(*drone.Node)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// Node indicates an expected call of Node.\nfunc (mr *MockClientMockRecorder) Node(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Node\", reflect.TypeOf((*MockClient)(nil).Node), arg0)\n}\n\n// NodeCreate mocks base method.\nfunc (m *MockClient) NodeCreate(arg0 *drone.Node) (*drone.Node, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"NodeCreate\", arg0)\n\tret0, _ := ret[0].(*drone.Node)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// NodeCreate indicates an expected call of NodeCreate.\nfunc (mr *MockClientMockRecorder) NodeCreate(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"NodeCreate\", reflect.TypeOf((*MockClient)(nil).NodeCreate), arg0)\n}\n\n// NodeDelete mocks base method.\nfunc (m *MockClient) NodeDelete(arg0 string) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"NodeDelete\", arg0)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// NodeDelete indicates an expected call of NodeDelete.\nfunc (mr *MockClientMockRecorder) NodeDelete(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"NodeDelete\", reflect.TypeOf((*MockClient)(nil).NodeDelete), arg0)\n}\n\n// NodeList mocks base method.\nfunc (m *MockClient) NodeList() ([]*drone.Node, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"NodeList\")\n\tret0, _ := ret[0].([]*drone.Node)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// NodeList indicates an expected call of NodeList.\nfunc (mr *MockClientMockRecorder) NodeList() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"NodeList\", reflect.TypeOf((*MockClient)(nil).NodeList))\n}\n\n// NodeUpdate mocks base method.\nfunc (m *MockClient) NodeUpdate(arg0 string, arg1 *drone.NodePatch) (*drone.Node, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"NodeUpdate\", arg0, arg1)\n\tret0, _ := ret[0].(*drone.Node)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// NodeUpdate indicates an expected call of NodeUpdate.\nfunc (mr *MockClientMockRecorder) NodeUpdate(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"NodeUpdate\", reflect.TypeOf((*MockClient)(nil).NodeUpdate), arg0, arg1)\n}\n\n// OrgSecret mocks base method.\nfunc (m *MockClient) OrgSecret(arg0, arg1 string) (*drone.Secret, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"OrgSecret\", arg0, arg1)\n\tret0, _ := ret[0].(*drone.Secret)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// OrgSecret indicates an expected call of OrgSecret.\nfunc (mr *MockClientMockRecorder) OrgSecret(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"OrgSecret\", reflect.TypeOf((*MockClient)(nil).OrgSecret), arg0, arg1)\n}\n\n// OrgSecretCreate mocks base method.\nfunc (m *MockClient) OrgSecretCreate(arg0 string, arg1 *drone.Secret) (*drone.Secret, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"OrgSecretCreate\", arg0, arg1)\n\tret0, _ := ret[0].(*drone.Secret)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// OrgSecretCreate indicates an expected call of OrgSecretCreate.\nfunc (mr *MockClientMockRecorder) OrgSecretCreate(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"OrgSecretCreate\", reflect.TypeOf((*MockClient)(nil).OrgSecretCreate), arg0, arg1)\n}\n\n// OrgSecretDelete mocks base method.\nfunc (m *MockClient) OrgSecretDelete(arg0, arg1 string) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"OrgSecretDelete\", arg0, arg1)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// OrgSecretDelete indicates an expected call of OrgSecretDelete.\nfunc (mr *MockClientMockRecorder) OrgSecretDelete(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"OrgSecretDelete\", reflect.TypeOf((*MockClient)(nil).OrgSecretDelete), arg0, arg1)\n}\n\n// OrgSecretList mocks base method.\nfunc (m *MockClient) OrgSecretList(arg0 string) ([]*drone.Secret, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"OrgSecretList\", arg0)\n\tret0, _ := ret[0].([]*drone.Secret)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// OrgSecretList indicates an expected call of OrgSecretList.\nfunc (mr *MockClientMockRecorder) OrgSecretList(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"OrgSecretList\", reflect.TypeOf((*MockClient)(nil).OrgSecretList), arg0)\n}\n\n// OrgSecretListAll mocks base method.\nfunc (m *MockClient) OrgSecretListAll() ([]*drone.Secret, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"OrgSecretListAll\")\n\tret0, _ := ret[0].([]*drone.Secret)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// OrgSecretListAll indicates an expected call of OrgSecretListAll.\nfunc (mr *MockClientMockRecorder) OrgSecretListAll() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"OrgSecretListAll\", reflect.TypeOf((*MockClient)(nil).OrgSecretListAll))\n}\n\n// OrgSecretUpdate mocks base method.\nfunc (m *MockClient) OrgSecretUpdate(arg0 string, arg1 *drone.Secret) (*drone.Secret, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"OrgSecretUpdate\", arg0, arg1)\n\tret0, _ := ret[0].(*drone.Secret)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// OrgSecretUpdate indicates an expected call of OrgSecretUpdate.\nfunc (mr *MockClientMockRecorder) OrgSecretUpdate(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"OrgSecretUpdate\", reflect.TypeOf((*MockClient)(nil).OrgSecretUpdate), arg0, arg1)\n}\n\n// Promote mocks base method.\nfunc (m *MockClient) Promote(arg0, arg1 string, arg2 int, arg3 string, arg4 map[string]string) (*drone.Build, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Promote\", arg0, arg1, arg2, arg3, arg4)\n\tret0, _ := ret[0].(*drone.Build)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// Promote indicates an expected call of Promote.\nfunc (mr *MockClientMockRecorder) Promote(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Promote\", reflect.TypeOf((*MockClient)(nil).Promote), arg0, arg1, arg2, arg3, arg4)\n}\n\n// Queue mocks base method.\nfunc (m *MockClient) Queue() ([]*drone.Stage, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Queue\")\n\tret0, _ := ret[0].([]*drone.Stage)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// Queue indicates an expected call of Queue.\nfunc (mr *MockClientMockRecorder) Queue() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Queue\", reflect.TypeOf((*MockClient)(nil).Queue))\n}\n\n// QueuePause mocks base method.\nfunc (m *MockClient) QueuePause() error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"QueuePause\")\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// QueuePause indicates an expected call of QueuePause.\nfunc (mr *MockClientMockRecorder) QueuePause() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"QueuePause\", reflect.TypeOf((*MockClient)(nil).QueuePause))\n}\n\n// QueueResume mocks base method.\nfunc (m *MockClient) QueueResume() error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"QueueResume\")\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// QueueResume indicates an expected call of QueueResume.\nfunc (mr *MockClientMockRecorder) QueueResume() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"QueueResume\", reflect.TypeOf((*MockClient)(nil).QueueResume))\n}\n\n// Repo mocks base method.\nfunc (m *MockClient) Repo(arg0, arg1 string) (*drone.Repo, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Repo\", arg0, arg1)\n\tret0, _ := ret[0].(*drone.Repo)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// Repo indicates an expected call of Repo.\nfunc (mr *MockClientMockRecorder) Repo(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Repo\", reflect.TypeOf((*MockClient)(nil).Repo), arg0, arg1)\n}\n\n// RepoChown mocks base method.\nfunc (m *MockClient) RepoChown(arg0, arg1 string) (*drone.Repo, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"RepoChown\", arg0, arg1)\n\tret0, _ := ret[0].(*drone.Repo)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// RepoChown indicates an expected call of RepoChown.\nfunc (mr *MockClientMockRecorder) RepoChown(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"RepoChown\", reflect.TypeOf((*MockClient)(nil).RepoChown), arg0, arg1)\n}\n\n// RepoDelete mocks base method.\nfunc (m *MockClient) RepoDelete(arg0, arg1 string) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"RepoDelete\", arg0, arg1)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// RepoDelete indicates an expected call of RepoDelete.\nfunc (mr *MockClientMockRecorder) RepoDelete(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"RepoDelete\", reflect.TypeOf((*MockClient)(nil).RepoDelete), arg0, arg1)\n}\n\n// RepoDisable mocks base method.\nfunc (m *MockClient) RepoDisable(arg0, arg1 string) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"RepoDisable\", arg0, arg1)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// RepoDisable indicates an expected call of RepoDisable.\nfunc (mr *MockClientMockRecorder) RepoDisable(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"RepoDisable\", reflect.TypeOf((*MockClient)(nil).RepoDisable), arg0, arg1)\n}\n\n// RepoEnable mocks base method.\nfunc (m *MockClient) RepoEnable(arg0, arg1 string) (*drone.Repo, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"RepoEnable\", arg0, arg1)\n\tret0, _ := ret[0].(*drone.Repo)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// RepoEnable indicates an expected call of RepoEnable.\nfunc (mr *MockClientMockRecorder) RepoEnable(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"RepoEnable\", reflect.TypeOf((*MockClient)(nil).RepoEnable), arg0, arg1)\n}\n\n// RepoList mocks base method.\nfunc (m *MockClient) RepoList() ([]*drone.Repo, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"RepoList\")\n\tret0, _ := ret[0].([]*drone.Repo)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// RepoList indicates an expected call of RepoList.\nfunc (mr *MockClientMockRecorder) RepoList() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"RepoList\", reflect.TypeOf((*MockClient)(nil).RepoList))\n}\n\n// RepoListSync mocks base method.\nfunc (m *MockClient) RepoListSync() ([]*drone.Repo, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"RepoListSync\")\n\tret0, _ := ret[0].([]*drone.Repo)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// RepoListSync indicates an expected call of RepoListSync.\nfunc (mr *MockClientMockRecorder) RepoListSync() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"RepoListSync\", reflect.TypeOf((*MockClient)(nil).RepoListSync))\n}\n\n// RepoRepair mocks base method.\nfunc (m *MockClient) RepoRepair(arg0, arg1 string) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"RepoRepair\", arg0, arg1)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// RepoRepair indicates an expected call of RepoRepair.\nfunc (mr *MockClientMockRecorder) RepoRepair(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"RepoRepair\", reflect.TypeOf((*MockClient)(nil).RepoRepair), arg0, arg1)\n}\n\n// RepoUpdate mocks base method.\nfunc (m *MockClient) RepoUpdate(arg0, arg1 string, arg2 *drone.RepoPatch) (*drone.Repo, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"RepoUpdate\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(*drone.Repo)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// RepoUpdate indicates an expected call of RepoUpdate.\nfunc (mr *MockClientMockRecorder) RepoUpdate(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"RepoUpdate\", reflect.TypeOf((*MockClient)(nil).RepoUpdate), arg0, arg1, arg2)\n}\n\n// Rollback mocks base method.\nfunc (m *MockClient) Rollback(arg0, arg1 string, arg2 int, arg3 string, arg4 map[string]string) (*drone.Build, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Rollback\", arg0, arg1, arg2, arg3, arg4)\n\tret0, _ := ret[0].(*drone.Build)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// Rollback indicates an expected call of Rollback.\nfunc (mr *MockClientMockRecorder) Rollback(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Rollback\", reflect.TypeOf((*MockClient)(nil).Rollback), arg0, arg1, arg2, arg3, arg4)\n}\n\n// Secret mocks base method.\nfunc (m *MockClient) Secret(arg0, arg1, arg2 string) (*drone.Secret, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Secret\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(*drone.Secret)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// Secret indicates an expected call of Secret.\nfunc (mr *MockClientMockRecorder) Secret(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Secret\", reflect.TypeOf((*MockClient)(nil).Secret), arg0, arg1, arg2)\n}\n\n// SecretCreate mocks base method.\nfunc (m *MockClient) SecretCreate(arg0, arg1 string, arg2 *drone.Secret) (*drone.Secret, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"SecretCreate\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(*drone.Secret)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// SecretCreate indicates an expected call of SecretCreate.\nfunc (mr *MockClientMockRecorder) SecretCreate(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"SecretCreate\", reflect.TypeOf((*MockClient)(nil).SecretCreate), arg0, arg1, arg2)\n}\n\n// SecretDelete mocks base method.\nfunc (m *MockClient) SecretDelete(arg0, arg1, arg2 string) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"SecretDelete\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// SecretDelete indicates an expected call of SecretDelete.\nfunc (mr *MockClientMockRecorder) SecretDelete(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"SecretDelete\", reflect.TypeOf((*MockClient)(nil).SecretDelete), arg0, arg1, arg2)\n}\n\n// SecretList mocks base method.\nfunc (m *MockClient) SecretList(arg0, arg1 string) ([]*drone.Secret, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"SecretList\", arg0, arg1)\n\tret0, _ := ret[0].([]*drone.Secret)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// SecretList indicates an expected call of SecretList.\nfunc (mr *MockClientMockRecorder) SecretList(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"SecretList\", reflect.TypeOf((*MockClient)(nil).SecretList), arg0, arg1)\n}\n\n// SecretUpdate mocks base method.\nfunc (m *MockClient) SecretUpdate(arg0, arg1 string, arg2 *drone.Secret) (*drone.Secret, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"SecretUpdate\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(*drone.Secret)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// SecretUpdate indicates an expected call of SecretUpdate.\nfunc (mr *MockClientMockRecorder) SecretUpdate(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"SecretUpdate\", reflect.TypeOf((*MockClient)(nil).SecretUpdate), arg0, arg1, arg2)\n}\n\n// Self mocks base method.\nfunc (m *MockClient) Self() (*drone.User, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Self\")\n\tret0, _ := ret[0].(*drone.User)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// Self indicates an expected call of Self.\nfunc (mr *MockClientMockRecorder) Self() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Self\", reflect.TypeOf((*MockClient)(nil).Self))\n}\n\n// Server mocks base method.\nfunc (m *MockClient) Server(arg0 string) (*drone.Server, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Server\", arg0)\n\tret0, _ := ret[0].(*drone.Server)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// Server indicates an expected call of Server.\nfunc (mr *MockClientMockRecorder) Server(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Server\", reflect.TypeOf((*MockClient)(nil).Server), arg0)\n}\n\n// ServerCreate mocks base method.\nfunc (m *MockClient) ServerCreate() (*drone.Server, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ServerCreate\")\n\tret0, _ := ret[0].(*drone.Server)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ServerCreate indicates an expected call of ServerCreate.\nfunc (mr *MockClientMockRecorder) ServerCreate() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ServerCreate\", reflect.TypeOf((*MockClient)(nil).ServerCreate))\n}\n\n// ServerDelete mocks base method.\nfunc (m *MockClient) ServerDelete(arg0 string) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ServerDelete\", arg0)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// ServerDelete indicates an expected call of ServerDelete.\nfunc (mr *MockClientMockRecorder) ServerDelete(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ServerDelete\", reflect.TypeOf((*MockClient)(nil).ServerDelete), arg0)\n}\n\n// ServerList mocks base method.\nfunc (m *MockClient) ServerList() ([]*drone.Server, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ServerList\")\n\tret0, _ := ret[0].([]*drone.Server)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ServerList indicates an expected call of ServerList.\nfunc (mr *MockClientMockRecorder) ServerList() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ServerList\", reflect.TypeOf((*MockClient)(nil).ServerList))\n}\n\n// SetAddress mocks base method.\nfunc (m *MockClient) SetAddress(arg0 string) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"SetAddress\", arg0)\n}\n\n// SetAddress indicates an expected call of SetAddress.\nfunc (mr *MockClientMockRecorder) SetAddress(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"SetAddress\", reflect.TypeOf((*MockClient)(nil).SetAddress), arg0)\n}\n\n// SetClient mocks base method.\nfunc (m *MockClient) SetClient(arg0 *http.Client) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"SetClient\", arg0)\n}\n\n// SetClient indicates an expected call of SetClient.\nfunc (mr *MockClientMockRecorder) SetClient(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"SetClient\", reflect.TypeOf((*MockClient)(nil).SetClient), arg0)\n}\n\n// Sign mocks base method.\nfunc (m *MockClient) Sign(arg0, arg1, arg2 string) (string, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Sign\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(string)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// Sign indicates an expected call of Sign.\nfunc (mr *MockClientMockRecorder) Sign(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Sign\", reflect.TypeOf((*MockClient)(nil).Sign), arg0, arg1, arg2)\n}\n\n// User mocks base method.\nfunc (m *MockClient) User(arg0 string) (*drone.User, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"User\", arg0)\n\tret0, _ := ret[0].(*drone.User)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// User indicates an expected call of User.\nfunc (mr *MockClientMockRecorder) User(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"User\", reflect.TypeOf((*MockClient)(nil).User), arg0)\n}\n\n// UserCreate mocks base method.\nfunc (m *MockClient) UserCreate(arg0 *drone.User) (*drone.User, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"UserCreate\", arg0)\n\tret0, _ := ret[0].(*drone.User)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// UserCreate indicates an expected call of UserCreate.\nfunc (mr *MockClientMockRecorder) UserCreate(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"UserCreate\", reflect.TypeOf((*MockClient)(nil).UserCreate), arg0)\n}\n\n// UserDelete mocks base method.\nfunc (m *MockClient) UserDelete(arg0 string) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"UserDelete\", arg0)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// UserDelete indicates an expected call of UserDelete.\nfunc (mr *MockClientMockRecorder) UserDelete(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"UserDelete\", reflect.TypeOf((*MockClient)(nil).UserDelete), arg0)\n}\n\n// UserList mocks base method.\nfunc (m *MockClient) UserList() ([]*drone.User, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"UserList\")\n\tret0, _ := ret[0].([]*drone.User)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// UserList indicates an expected call of UserList.\nfunc (mr *MockClientMockRecorder) UserList() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"UserList\", reflect.TypeOf((*MockClient)(nil).UserList))\n}\n\n// UserUpdate mocks base method.\nfunc (m *MockClient) UserUpdate(arg0 string, arg1 *drone.UserPatch) (*drone.User, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"UserUpdate\", arg0, arg1)\n\tret0, _ := ret[0].(*drone.User)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// UserUpdate indicates an expected call of UserUpdate.\nfunc (mr *MockClientMockRecorder) UserUpdate(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"UserUpdate\", reflect.TypeOf((*MockClient)(nil).UserUpdate), arg0, arg1)\n}\n\n// Verify mocks base method.\nfunc (m *MockClient) Verify(arg0, arg1, arg2 string) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Verify\", arg0, arg1, arg2)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// Verify indicates an expected call of Verify.\nfunc (mr *MockClientMockRecorder) Verify(arg0, arg1, arg2 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Verify\", reflect.TypeOf((*MockClient)(nil).Verify), arg0, arg1, arg2)\n}\n"
  },
  {
    "path": "mocks/mock_engine.go",
    "content": "// Code generated by MockGen. DO NOT EDIT.\n// Source: github.com/drone/autoscaler (interfaces: Engine)\n\n// Package mocks is a generated GoMock package.\npackage mocks\n\nimport (\n\tcontext \"context\"\n\treflect \"reflect\"\n\n\tgomock \"github.com/golang/mock/gomock\"\n)\n\n// MockEngine is a mock of Engine interface.\ntype MockEngine struct {\n\tctrl     *gomock.Controller\n\trecorder *MockEngineMockRecorder\n}\n\n// MockEngineMockRecorder is the mock recorder for MockEngine.\ntype MockEngineMockRecorder struct {\n\tmock *MockEngine\n}\n\n// NewMockEngine creates a new mock instance.\nfunc NewMockEngine(ctrl *gomock.Controller) *MockEngine {\n\tmock := &MockEngine{ctrl: ctrl}\n\tmock.recorder = &MockEngineMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *MockEngine) EXPECT() *MockEngineMockRecorder {\n\treturn m.recorder\n}\n\n// Pause mocks base method.\nfunc (m *MockEngine) Pause() {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"Pause\")\n}\n\n// Pause indicates an expected call of Pause.\nfunc (mr *MockEngineMockRecorder) Pause() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Pause\", reflect.TypeOf((*MockEngine)(nil).Pause))\n}\n\n// Paused mocks base method.\nfunc (m *MockEngine) Paused() bool {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Paused\")\n\tret0, _ := ret[0].(bool)\n\treturn ret0\n}\n\n// Paused indicates an expected call of Paused.\nfunc (mr *MockEngineMockRecorder) Paused() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Paused\", reflect.TypeOf((*MockEngine)(nil).Paused))\n}\n\n// Resume mocks base method.\nfunc (m *MockEngine) Resume() {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"Resume\")\n}\n\n// Resume indicates an expected call of Resume.\nfunc (mr *MockEngineMockRecorder) Resume() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Resume\", reflect.TypeOf((*MockEngine)(nil).Resume))\n}\n\n// Start mocks base method.\nfunc (m *MockEngine) Start(arg0 context.Context) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"Start\", arg0)\n}\n\n// Start indicates an expected call of Start.\nfunc (mr *MockEngineMockRecorder) Start(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Start\", reflect.TypeOf((*MockEngine)(nil).Start), arg0)\n}\n"
  },
  {
    "path": "mocks/mock_metrics.go",
    "content": "// Code generated by MockGen. DO NOT EDIT.\n// Source: github.com/drone/autoscaler/metrics (interfaces: Collector)\n\n// Package mocks is a generated GoMock package.\npackage mocks\n\nimport (\n\treflect \"reflect\"\n\ttime \"time\"\n\n\tgomock \"github.com/golang/mock/gomock\"\n)\n\n// MockCollector is a mock of Collector interface.\ntype MockCollector struct {\n\tctrl     *gomock.Controller\n\trecorder *MockCollectorMockRecorder\n}\n\n// MockCollectorMockRecorder is the mock recorder for MockCollector.\ntype MockCollectorMockRecorder struct {\n\tmock *MockCollector\n}\n\n// NewMockCollector creates a new mock instance.\nfunc NewMockCollector(ctrl *gomock.Controller) *MockCollector {\n\tmock := &MockCollector{ctrl: ctrl}\n\tmock.recorder = &MockCollectorMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *MockCollector) EXPECT() *MockCollectorMockRecorder {\n\treturn m.recorder\n}\n\n// IncrServerCreateError mocks base method.\nfunc (m *MockCollector) IncrServerCreateError() {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"IncrServerCreateError\")\n}\n\n// IncrServerCreateError indicates an expected call of IncrServerCreateError.\nfunc (mr *MockCollectorMockRecorder) IncrServerCreateError() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"IncrServerCreateError\", reflect.TypeOf((*MockCollector)(nil).IncrServerCreateError))\n}\n\n// IncrServerInitError mocks base method.\nfunc (m *MockCollector) IncrServerInitError() {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"IncrServerInitError\")\n}\n\n// IncrServerInitError indicates an expected call of IncrServerInitError.\nfunc (mr *MockCollectorMockRecorder) IncrServerInitError() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"IncrServerInitError\", reflect.TypeOf((*MockCollector)(nil).IncrServerInitError))\n}\n\n// IncrServerSetupError mocks base method.\nfunc (m *MockCollector) IncrServerSetupError() {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"IncrServerSetupError\")\n}\n\n// IncrServerSetupError indicates an expected call of IncrServerSetupError.\nfunc (mr *MockCollectorMockRecorder) IncrServerSetupError() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"IncrServerSetupError\", reflect.TypeOf((*MockCollector)(nil).IncrServerSetupError))\n}\n\n// TrackServerCreateTime mocks base method.\nfunc (m *MockCollector) TrackServerCreateTime(arg0 time.Time) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"TrackServerCreateTime\", arg0)\n}\n\n// TrackServerCreateTime indicates an expected call of TrackServerCreateTime.\nfunc (mr *MockCollectorMockRecorder) TrackServerCreateTime(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"TrackServerCreateTime\", reflect.TypeOf((*MockCollector)(nil).TrackServerCreateTime), arg0)\n}\n\n// TrackServerInitTime mocks base method.\nfunc (m *MockCollector) TrackServerInitTime(arg0 time.Time) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"TrackServerInitTime\", arg0)\n}\n\n// TrackServerInitTime indicates an expected call of TrackServerInitTime.\nfunc (mr *MockCollectorMockRecorder) TrackServerInitTime(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"TrackServerInitTime\", reflect.TypeOf((*MockCollector)(nil).TrackServerInitTime), arg0)\n}\n\n// TrackServerSetupTime mocks base method.\nfunc (m *MockCollector) TrackServerSetupTime(arg0 time.Time) {\n\tm.ctrl.T.Helper()\n\tm.ctrl.Call(m, \"TrackServerSetupTime\", arg0)\n}\n\n// TrackServerSetupTime indicates an expected call of TrackServerSetupTime.\nfunc (mr *MockCollectorMockRecorder) TrackServerSetupTime(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"TrackServerSetupTime\", reflect.TypeOf((*MockCollector)(nil).TrackServerSetupTime), arg0)\n}\n"
  },
  {
    "path": "mocks/mock_provider.go",
    "content": "// Code generated by MockGen. DO NOT EDIT.\n// Source: github.com/drone/autoscaler (interfaces: Provider)\n\n// Package mocks is a generated GoMock package.\npackage mocks\n\nimport (\n\tcontext \"context\"\n\treflect \"reflect\"\n\n\tautoscaler \"github.com/drone/autoscaler\"\n\tgomock \"github.com/golang/mock/gomock\"\n)\n\n// MockProvider is a mock of Provider interface.\ntype MockProvider struct {\n\tctrl     *gomock.Controller\n\trecorder *MockProviderMockRecorder\n}\n\n// MockProviderMockRecorder is the mock recorder for MockProvider.\ntype MockProviderMockRecorder struct {\n\tmock *MockProvider\n}\n\n// NewMockProvider creates a new mock instance.\nfunc NewMockProvider(ctrl *gomock.Controller) *MockProvider {\n\tmock := &MockProvider{ctrl: ctrl}\n\tmock.recorder = &MockProviderMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *MockProvider) EXPECT() *MockProviderMockRecorder {\n\treturn m.recorder\n}\n\n// Create mocks base method.\nfunc (m *MockProvider) Create(arg0 context.Context, arg1 autoscaler.InstanceCreateOpts) (*autoscaler.Instance, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Create\", arg0, arg1)\n\tret0, _ := ret[0].(*autoscaler.Instance)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// Create indicates an expected call of Create.\nfunc (mr *MockProviderMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Create\", reflect.TypeOf((*MockProvider)(nil).Create), arg0, arg1)\n}\n\n// Destroy mocks base method.\nfunc (m *MockProvider) Destroy(arg0 context.Context, arg1 *autoscaler.Instance) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Destroy\", arg0, arg1)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// Destroy indicates an expected call of Destroy.\nfunc (mr *MockProviderMockRecorder) Destroy(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Destroy\", reflect.TypeOf((*MockProvider)(nil).Destroy), arg0, arg1)\n}\n"
  },
  {
    "path": "mocks/mock_server.go",
    "content": "// Code generated by MockGen. DO NOT EDIT.\n// Source: github.com/drone/autoscaler (interfaces: ServerStore)\n\n// Package mocks is a generated GoMock package.\npackage mocks\n\nimport (\n\tcontext \"context\"\n\treflect \"reflect\"\n\n\tautoscaler \"github.com/drone/autoscaler\"\n\tgomock \"github.com/golang/mock/gomock\"\n)\n\n// MockServerStore is a mock of ServerStore interface.\ntype MockServerStore struct {\n\tctrl     *gomock.Controller\n\trecorder *MockServerStoreMockRecorder\n}\n\n// MockServerStoreMockRecorder is the mock recorder for MockServerStore.\ntype MockServerStoreMockRecorder struct {\n\tmock *MockServerStore\n}\n\n// NewMockServerStore creates a new mock instance.\nfunc NewMockServerStore(ctrl *gomock.Controller) *MockServerStore {\n\tmock := &MockServerStore{ctrl: ctrl}\n\tmock.recorder = &MockServerStoreMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *MockServerStore) EXPECT() *MockServerStoreMockRecorder {\n\treturn m.recorder\n}\n\n// Create mocks base method.\nfunc (m *MockServerStore) Create(arg0 context.Context, arg1 *autoscaler.Server) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Create\", arg0, arg1)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// Create indicates an expected call of Create.\nfunc (mr *MockServerStoreMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Create\", reflect.TypeOf((*MockServerStore)(nil).Create), arg0, arg1)\n}\n\n// Delete mocks base method.\nfunc (m *MockServerStore) Delete(arg0 context.Context, arg1 *autoscaler.Server) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Delete\", arg0, arg1)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// Delete indicates an expected call of Delete.\nfunc (mr *MockServerStoreMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Delete\", reflect.TypeOf((*MockServerStore)(nil).Delete), arg0, arg1)\n}\n\n// Find mocks base method.\nfunc (m *MockServerStore) Find(arg0 context.Context, arg1 string) (*autoscaler.Server, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Find\", arg0, arg1)\n\tret0, _ := ret[0].(*autoscaler.Server)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// Find indicates an expected call of Find.\nfunc (mr *MockServerStoreMockRecorder) Find(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Find\", reflect.TypeOf((*MockServerStore)(nil).Find), arg0, arg1)\n}\n\n// List mocks base method.\nfunc (m *MockServerStore) List(arg0 context.Context) ([]*autoscaler.Server, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"List\", arg0)\n\tret0, _ := ret[0].([]*autoscaler.Server)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// List indicates an expected call of List.\nfunc (mr *MockServerStoreMockRecorder) List(arg0 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"List\", reflect.TypeOf((*MockServerStore)(nil).List), arg0)\n}\n\n// ListState mocks base method.\nfunc (m *MockServerStore) ListState(arg0 context.Context, arg1 autoscaler.ServerState) ([]*autoscaler.Server, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ListState\", arg0, arg1)\n\tret0, _ := ret[0].([]*autoscaler.Server)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ListState indicates an expected call of ListState.\nfunc (mr *MockServerStoreMockRecorder) ListState(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ListState\", reflect.TypeOf((*MockServerStore)(nil).ListState), arg0, arg1)\n}\n\n// Purge mocks base method.\nfunc (m *MockServerStore) Purge(arg0 context.Context, arg1 int64) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Purge\", arg0, arg1)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// Purge indicates an expected call of Purge.\nfunc (mr *MockServerStoreMockRecorder) Purge(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Purge\", reflect.TypeOf((*MockServerStore)(nil).Purge), arg0, arg1)\n}\n\n// Update mocks base method.\nfunc (m *MockServerStore) Update(arg0 context.Context, arg1 *autoscaler.Server) error {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Update\", arg0, arg1)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// Update indicates an expected call of Update.\nfunc (mr *MockServerStoreMockRecorder) Update(arg0, arg1 interface{}) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Update\", reflect.TypeOf((*MockServerStore)(nil).Update), arg0, arg1)\n}\n"
  },
  {
    "path": "mocks/mocks.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage mocks\n\n//go:generate mockgen -package=mocks -destination=mock_engine.go   github.com/drone/autoscaler Engine\n//go:generate mockgen -package=mocks -destination=mock_server.go   github.com/drone/autoscaler ServerStore\n//go:generate mockgen -package=mocks -destination=mock_provider.go github.com/drone/autoscaler Provider\n//go:generate mockgen -package=mocks -destination=mock_metrics.go  github.com/drone/autoscaler/metrics Collector\n//go:generate mockgen -package=mocks -destination=mock_drone.go    github.com/drone/drone-go/drone Client\n//go:generate mockgen -package=mocks -destination=mock_docker.go   github.com/docker/docker/client APIClient\n"
  },
  {
    "path": "provider.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage autoscaler\n\nimport (\n\t\"context\"\n\t\"database/sql/driver\"\n\t\"errors\"\n)\n\n// ProviderType specifies the hosting provider.\ntype ProviderType string\n\n// Value converts the value to a sql string.\nfunc (s ProviderType) Value() (driver.Value, error) {\n\treturn string(s), nil\n}\n\n// Provider type enumeration.\nconst (\n\tProviderAmazon       = ProviderType(\"amazon\")\n\tProviderAzure        = ProviderType(\"azure\")\n\tProviderDigitalOcean = ProviderType(\"digitalocean\")\n\tProviderGoogle       = ProviderType(\"google\")\n\tProviderHetznerCloud = ProviderType(\"hetznercloud\")\n\tProviderLinode       = ProviderType(\"linode\")\n\tProviderOpenStack    = ProviderType(\"openstack\")\n\tProviderPacket       = ProviderType(\"packet\")\n\tProviderScaleway     = ProviderType(\"scaleway\")\n\tProviderVultr        = ProviderType(\"vultr\")\n)\n\n// ErrInstanceNotFound is returned when the requested\n// instance does not exist in the cloud provider.\nvar ErrInstanceNotFound = errors.New(\"Not Found\")\n\n// A Provider represents a hosting provider, such as\n// Digital Ocean and is responsible for server management.\ntype Provider interface {\n\t// Create creates a new server.\n\tCreate(context.Context, InstanceCreateOpts) (*Instance, error)\n\t// Destroy destroys an existing server.\n\tDestroy(context.Context, *Instance) error\n}\n\n// An Instance represents a server instance\n// (e.g Digital Ocean Droplet).\ntype Instance struct {\n\tProvider            ProviderType\n\tID                  string\n\tName                string\n\tAddress             string\n\tRegion              string\n\tImage               string\n\tSize                string\n\tServiceAccountEmail string\n\tScopes              []string\n}\n\n// InstanceCreateOpts define soptional instructions for\n// creating server instances.\ntype InstanceCreateOpts struct {\n\tName    string\n\tCAKey   []byte\n\tCACert  []byte\n\tTLSKey  []byte\n\tTLSCert []byte\n}\n\n// InstanceError snapshots an error creating an instance\n// with server logs.\ntype InstanceError struct {\n\tErr  error\n\tLogs []byte\n}\n\n// Error implements the error interface.\nfunc (e *InstanceError) Error() string {\n\treturn e.Err.Error()\n}\n"
  },
  {
    "path": "server/auth.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage server\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/drone/autoscaler/config\"\n\t\"github.com/drone/autoscaler/logger\"\n\t\"github.com/drone/drone-go/drone\"\n\n\t\"golang.org/x/oauth2\"\n)\n\n// CheckDrone returns a middleware function that authorizes\n// the incoming http.Request using the Drone API.\nfunc CheckDrone(conf config.Config) func(http.Handler) http.Handler {\n\treturn func(next http.Handler) http.Handler {\n\t\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tctx := r.Context()\n\t\t\tlog := logger.FromContext(ctx)\n\n\t\t\t// the user can authenticate with a global authorization\n\t\t\t// token provied in the Authorization header.\n\t\t\ttoken := r.Header.Get(\"Authorization\")\n\t\t\ttoken = strings.TrimPrefix(token, \"Bearer \")\n\t\t\ttoken = strings.TrimSpace(token)\n\t\t\tif token == \"\" {\n\t\t\t\tlog.Debugln(\"missing authorization header\")\n\t\t\t\twriteUnauthorized(w, errInvalidToken)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// creates a new drone client using the bearer token\n\t\t\t// in the incoming request to authenticate with drone.\n\t\t\tconfig := new(oauth2.Config)\n\t\t\tauther := config.Client(\n\t\t\t\toauth2.NoContext,\n\t\t\t\t&oauth2.Token{\n\t\t\t\t\tAccessToken: token,\n\t\t\t\t},\n\t\t\t)\n\t\t\tserver := conf.Server.Proto + \"://\" + conf.Server.Host\n\t\t\tclient := drone.NewClient(server, auther)\n\n\t\t\t// fetch the user account associated with the currently\n\t\t\t// authenticated bearer token. This user must exist in\n\t\t\t// drone and must be an administrator.\n\t\t\tuser, err := client.Self()\n\t\t\tif err != nil {\n\t\t\t\tlog.WithError(err).\n\t\t\t\t\tErrorln(\"cannot authenticate user\")\n\t\t\t\twriteUnauthorized(w, errUnauthorized)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif !user.Admin {\n\t\t\t\tlog.WithError(err).\n\t\t\t\t\tWithField(\"username\", user.Login).\n\t\t\t\t\tErrorln(\"insufficient privileges\")\n\t\t\t\twriteForbidden(w, errForbidden)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tlog = log.WithField(\"username\", user.Login)\n\t\t\tlog.Debugln(\"user authorized\")\n\n\t\t\tnext.ServeHTTP(w, r.WithContext(\n\t\t\t\tlogger.WithContext(ctx, log),\n\t\t\t))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/auth_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage server\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/drone/autoscaler/config\"\n\t\"github.com/drone/drone-go/drone\"\n\n\t\"github.com/h2non/gock\"\n)\n\nfunc TestAuthorize(t *testing.T) {\n\tdefer gock.Off()\n\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"GET\", \"/\", nil)\n\tr.Header.Set(\"Authorization\", \"Bearer NTE2M2MwMWRlYToxNGM3MWEyYTIx\")\n\n\tuser := &drone.User{\n\t\tLogin: \"octocat\",\n\t\tAdmin: true,\n\t}\n\n\tc := config.Config{}\n\tc.Server.Host = \"company.drone.com\"\n\tc.Server.Proto = \"https\"\n\n\tgock.New(\"https://company.drone.com\").\n\t\tGet(\"/api/user\").\n\t\tMatchHeader(\"Authorization\", \"Bearer NTE2M2MwMWRlYToxNGM3MWEyYTIx\").\n\t\tReply(200).\n\t\tJSON(user)\n\n\tCheckDrone(c)(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.WriteHeader(http.StatusTeapot)\n\t\t}),\n\t).ServeHTTP(w, r)\n\n\tif got, want := w.Code, http.StatusTeapot; got != want {\n\t\tt.Errorf(\"Want status code %d, got %d\", want, got)\n\t}\n}\n\nfunc TestAuthorizeMissingToken(t *testing.T) {\n\tdefer gock.Off()\n\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"GET\", \"/\", nil)\n\n\tc := config.Config{}\n\tc.Server.Host = \"company.drone.com\"\n\tc.Server.Proto = \"https\"\n\n\tCheckDrone(c)(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tt.Errorf(\"Expect access to handler is restricted\")\n\t\t}),\n\t).ServeHTTP(w, r)\n\n\tif got, want := w.Code, 401; got != want {\n\t\tt.Errorf(\"Want status code %d, got %d\", want, got)\n\t}\n\n\terrjson := &Error{}\n\tjson.NewDecoder(w.Body).Decode(errjson)\n\tif got, want := errjson.Message, errInvalidToken.Error(); got != want {\n\t\tt.Errorf(\"Want error message %s, got %s\", want, got)\n\t}\n}\n\nfunc TestAuthorizeNotFound(t *testing.T) {\n\tdefer gock.Off()\n\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"GET\", \"/\", nil)\n\tr.Header.Set(\"Authorization\", \"Bearer NTE2M2MwMWRlYToxNGM3MWEyYTIx\")\n\n\tc := config.Config{}\n\tc.Server.Host = \"company.drone.com\"\n\tc.Server.Proto = \"https\"\n\n\tgock.New(\"https://company.drone.com\").\n\t\tGet(\"/api/user\").\n\t\tMatchHeader(\"Authorization\", \"Bearer NTE2M2MwMWRlYToxNGM3MWEyYTIx\").\n\t\tReply(404)\n\n\tCheckDrone(c)(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tt.Errorf(\"Expect access to handler is restricted\")\n\t\t}),\n\t).ServeHTTP(w, r)\n\n\tif got, want := w.Code, 401; got != want {\n\t\tt.Errorf(\"Want status code %d, got %d\", want, got)\n\t}\n\n\terrjson := &Error{}\n\tjson.NewDecoder(w.Body).Decode(errjson)\n\tif got, want := errjson.Message, errUnauthorized.Error(); got != want {\n\t\tt.Errorf(\"Want error message %s, got %s\", want, got)\n\t}\n}\n\nfunc TestAuthorizeNonAdmin(t *testing.T) {\n\tdefer gock.Off()\n\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"GET\", \"/\", nil)\n\tr.Header.Set(\"Authorization\", \"Bearer NTE2M2MwMWRlYToxNGM3MWEyYTIx\")\n\n\tuser := &drone.User{\n\t\tLogin: \"octocat\",\n\t\tAdmin: false,\n\t}\n\n\tc := config.Config{}\n\tc.Server.Host = \"company.drone.com\"\n\tc.Server.Proto = \"https\"\n\n\tgock.New(\"https://company.drone.com\").\n\t\tGet(\"/api/user\").\n\t\tMatchHeader(\"Authorization\", \"Bearer NTE2M2MwMWRlYToxNGM3MWEyYTIx\").\n\t\tReply(200).\n\t\tJSON(user)\n\n\tCheckDrone(c)(\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tt.Errorf(\"Expect access to handler is restricted\")\n\t\t}),\n\t).ServeHTTP(w, r)\n\n\tif got, want := w.Code, 403; got != want {\n\t\tt.Errorf(\"Want status code %d, got %d\", want, got)\n\t}\n\n\terrjson := &Error{}\n\tjson.NewDecoder(w.Body).Decode(errjson)\n\tif got, want := errjson.Message, errForbidden.Error(); got != want {\n\t\tt.Errorf(\"Want error message %s, got %s\", want, got)\n\t}\n}\n"
  },
  {
    "path": "server/engine.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage server\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/drone/autoscaler\"\n)\n\n// HandleEnginePause returns an http.HandlerFunc that pauses\n// scaling engine.\nfunc HandleEnginePause(engine autoscaler.Engine) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tengine.Pause()\n\t\tw.WriteHeader(204)\n\t}\n}\n\n// HandleEngineResume returns an http.HandlerFunc that resumes\n// scaling engine.\nfunc HandleEngineResume(engine autoscaler.Engine) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tengine.Resume()\n\t\tw.WriteHeader(204)\n\t}\n}\n"
  },
  {
    "path": "server/engine_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage server\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/drone/autoscaler/mocks\"\n\t\"github.com/golang/mock/gomock\"\n)\n\nfunc TestHandleEnginePause(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"POST\", \"/api/pause\", nil)\n\n\te := mocks.NewMockEngine(controller)\n\te.EXPECT().Pause()\n\n\tHandleEnginePause(e).ServeHTTP(w, r)\n\n\tif got, want := w.Code, 204; want != got {\n\t\tt.Errorf(\"Want response code %d, got %d\", want, got)\n\t}\n}\n\nfunc TestHandleEngineResume(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"POST\", \"/api/resume\", nil)\n\n\te := mocks.NewMockEngine(controller)\n\te.EXPECT().Resume()\n\n\tHandleEngineResume(e).ServeHTTP(w, r)\n\n\tif got, want := w.Code, 204; want != got {\n\t\tt.Errorf(\"Want response code %d, got %d\", want, got)\n\t}\n}\n"
  },
  {
    "path": "server/healthz.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage server\n\nimport (\n\t\"io\"\n\t\"net/http\"\n)\n\n// HandleHealthz creates an http.HandlerFunc that returns performs system\n// healthchecks and returns 500 if the system is in an unhealthy state.\nfunc HandleHealthz() http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(200)\n\t\tw.Header().Set(\"Content-Type\", \"text/plain\")\n\t\tio.WriteString(w, \"OK\")\n\t}\n}\n"
  },
  {
    "path": "server/healthz_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage server\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/golang/mock/gomock\"\n)\n\nfunc TestHandleHealthz(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"GET\", \"/healthz\", nil)\n\n\tHandleHealthz().ServeHTTP(w, r)\n\n\tif got, want := w.Code, 200; want != got {\n\t\tt.Errorf(\"Want response code %d, got %d\", want, got)\n\t}\n}\n"
  },
  {
    "path": "server/metrics.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage server\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n)\n\n// HandleMetrics returns an http.HandlerFunc that writes\n// metrics to the response body in plain text format.\nfunc HandleMetrics(token string) http.HandlerFunc {\n\thandler := promhttp.Handler()\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\t// if a bearer token is not configured we should\n\t\t// just server the http request.\n\t\tif token == \"\" {\n\t\t\thandler.ServeHTTP(w, r)\n\t\t\treturn\n\t\t}\n\t\theader := r.Header.Get(\"Authorization\")\n\t\tif header == \"\" {\n\t\t\thttp.Error(w, errInvalidToken.Error(), 401)\n\t\t\treturn\n\t\t}\n\t\tif header != \"Bearer \"+token {\n\t\t\thttp.Error(w, errInvalidToken.Error(), 401)\n\t\t\treturn\n\t\t}\n\t\thandler.ServeHTTP(w, r)\n\t}\n}\n"
  },
  {
    "path": "server/metrics_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage server\n\nimport (\n\t\"net/http/httptest\"\n\t\"testing\"\n)\n\nfunc TestHandleMetrics(t *testing.T) {\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"GET\", \"/\", nil)\n\tr.Header.Set(\"Authorization\", \"Bearer correct-horse-batter-staple\")\n\n\tHandleMetrics(\"correct-horse-batter-staple\").ServeHTTP(w, r)\n\n\tif got, want := w.Code, 200; got != want {\n\t\tt.Errorf(\"Want status code %d, got %d\", want, got)\n\t}\n\n\tif got, want := w.HeaderMap.Get(\"Content-Type\"), \"text/plain; version=0.0.4; charset=utf-8\"; got != want {\n\t\tt.Errorf(\"Want prometheus header %q, got %q\", want, got)\n\t}\n}\n\nfunc TestHandleMetricsUnprotected(t *testing.T) {\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"GET\", \"/\", nil)\n\n\tHandleMetrics(\"\").ServeHTTP(w, r)\n\n\tif got, want := w.Code, 200; got != want {\n\t\tt.Errorf(\"Want status code %d, got %d\", want, got)\n\t}\n\n\tif got, want := w.HeaderMap.Get(\"Content-Type\"), \"text/plain; version=0.0.4; charset=utf-8\"; got != want {\n\t\tt.Errorf(\"Want prometheus header %q, got %q\", want, got)\n\t}\n}\n\nfunc TestHandleMetricsMissingToken(t *testing.T) {\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"GET\", \"/\", nil)\n\n\tHandleMetrics(\"correct-horse-batter-staple\").ServeHTTP(w, r)\n\n\tif got, want := w.Code, 401; got != want {\n\t\tt.Errorf(\"Want status code %d, got %d\", want, got)\n\t}\n}\n\nfunc TestHandleMetricsInvalidToken(t *testing.T) {\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"GET\", \"/\", nil)\n\tr.Header.Set(\"Authorization\", \"correct-horse-batter-staple\")\n\n\tHandleMetrics(\"correct-horse-batter-staple\").ServeHTTP(w, r)\n\n\tif got, want := w.Code, 401; got != want {\n\t\tt.Errorf(\"Want status code %d, got %d\", want, got)\n\t}\n}\n"
  },
  {
    "path": "server/servers.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage server\n\nimport (\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/dchest/uniuri\"\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/config\"\n\t\"github.com/drone/autoscaler/logger\"\n\n\t\"github.com/go-chi/chi\"\n)\n\n// HandleServerList returns an http.HandlerFunc that writes\n// the json-encoded server list to the the response body.\nfunc HandleServerList(servers autoscaler.ServerStore) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tctx := r.Context()\n\t\tlist, err := servers.List(ctx)\n\t\tif err != nil {\n\t\t\tlogger.FromContext(ctx).\n\t\t\t\tWithError(err).\n\t\t\t\tErrorln(\"cannot get server list\")\n\t\t\twriteError(w, err)\n\t\t\treturn\n\t\t}\n\t\twriteJSON(w, list, 200)\n\t}\n}\n\n// HandleServerFind returns an http.HandlerFunc that finds\n// and writes the json-encoded server to the the response body.\nfunc HandleServerFind(servers autoscaler.ServerStore) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tctx := r.Context()\n\t\tname := chi.URLParam(r, \"name\")\n\t\tserver, err := servers.Find(ctx, name)\n\t\tif err != nil {\n\t\t\tlogger.FromContext(ctx).\n\t\t\t\tWithError(err).\n\t\t\t\tWithField(\"server\", name).\n\t\t\t\tErrorln(\"cannot get server\")\n\t\t\twriteNotFound(w, err)\n\t\t\treturn\n\t\t}\n\t\twriteJSON(w, server, 200)\n\t}\n}\n\n// HandleServerDelete returns an http.HandlerFunc that destroys\n// and then deletes the named server.\nfunc HandleServerDelete(\n\tservers autoscaler.ServerStore,\n) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tctx := r.Context()\n\t\tname := chi.URLParam(r, \"name\")\n\t\tforce, _ := strconv.ParseBool(r.FormValue(\"force\"))\n\n\t\tserver, err := servers.Find(ctx, name)\n\t\tif err != nil {\n\t\t\tlogger.FromContext(ctx).\n\t\t\t\tWithError(err).\n\t\t\t\tWithField(\"server\", name).\n\t\t\t\tErrorln(\"cannot get server\")\n\t\t\twriteNotFound(w, err)\n\t\t\treturn\n\t\t}\n\n\t\t// in some cases the server fails to create and is stuck\n\t\t// in an error state. In this case we force-delete from\n\t\t// the database.\n\t\tif server.State == autoscaler.StateError && (server.ID == \"\" || force) {\n\t\t\tlogger.FromContext(ctx).\n\t\t\t\tWithField(\"server\", server.Name).\n\t\t\t\tWithField(\"state\", string(server.State)).\n\t\t\t\tWithField(\"force\", force).\n\t\t\t\tInfoln(\"force delete server from database\")\n\n\t\t\terr = servers.Delete(ctx, server)\n\t\t\tif err != nil {\n\t\t\t\tlogger.FromContext(ctx).\n\t\t\t\t\tWithError(err).\n\t\t\t\t\tWithField(\"server\", server.Name).\n\t\t\t\t\tErrorln(\"cannot delete instance\")\n\t\t\t\twriteError(w, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tw.WriteHeader(204)\n\t\t\treturn\n\t\t}\n\n\t\tlogger.FromContext(ctx).\n\t\t\tWithField(\"server\", server.Name).\n\t\t\tWithField(\"state\", string(server.State)).\n\t\t\tWithField(\"force\", force).\n\t\t\tInfoln(\"schedule server shutdown\")\n\n\t\tserver.State = autoscaler.StateShutdown\n\t\terr = servers.Update(ctx, server)\n\t\tif err != nil {\n\t\t\tlogger.FromContext(ctx).\n\t\t\t\tWithError(err).\n\t\t\t\tWithField(\"server\", server.Name).\n\t\t\t\tWithField(\"state\", \"shutdown\").\n\t\t\t\tErrorln(\"cannot update server\")\n\t\t\twriteError(w, err)\n\t\t\treturn\n\t\t}\n\t\twriteJSON(w, server, 200)\n\t}\n}\n\n// HandleServerCreate returns an http.HandlerFunc that creates\n// and a new server.\nfunc HandleServerCreate(\n\tservers autoscaler.ServerStore,\n\tconfig config.Config,\n) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tctx := r.Context()\n\t\tserver := &autoscaler.Server{\n\t\t\tName:     config.Agent.NamePrefix + uniuri.NewLen(8),\n\t\t\tState:    autoscaler.StatePending,\n\t\t\tCapacity: config.Agent.Concurrency,\n\t\t}\n\t\terr := servers.Create(ctx, server)\n\t\tif err != nil {\n\t\t\tlogger.FromContext(ctx).\n\t\t\t\tWithError(err).\n\t\t\t\tErrorln(\"cannot persist server\")\n\t\t\twriteError(w, err)\n\t\t\treturn\n\t\t}\n\t\twriteJSON(w, server, 200)\n\t}\n}\n"
  },
  {
    "path": "server/servers_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage server\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http/httptest\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/config\"\n\t\"github.com/drone/autoscaler/mocks\"\n\t\"github.com/go-chi/chi\"\n\t\"github.com/golang/mock/gomock\"\n\t\"github.com/kr/pretty\"\n)\n\nfunc TestHandleServerList(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"GET\", \"/api/servers\", nil)\n\n\tservers := []*autoscaler.Server{\n\t\t{Name: \"server1\", Capacity: 1},\n\t\t{Name: \"server2\", Capacity: 1},\n\t}\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().List(gomock.Any()).Return(servers, nil)\n\n\tHandleServerList(store).ServeHTTP(w, r)\n\n\tif got, want := w.Code, 200; want != got {\n\t\tt.Errorf(\"Want response code %d, got %d\", want, got)\n\t}\n\n\tgot, want := []*autoscaler.Server{}, servers\n\tjson.NewDecoder(w.Body).Decode(&got)\n\tif !reflect.DeepEqual(got, want) {\n\t\tt.Errorf(\"response body does match expected result\")\n\t\tpretty.Ldiff(t, got, want)\n\t}\n}\n\nfunc TestHandleServerListErr(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"GET\", \"/api/servers\", nil)\n\n\terr := errors.New(\"not found\")\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().List(gomock.Any()).Return(nil, err)\n\n\tHandleServerList(store).ServeHTTP(w, r)\n\n\tif got, want := w.Code, 500; want != got {\n\t\tt.Errorf(\"Want response code %d, got %d\", want, got)\n\t}\n\n\terrjson := &Error{}\n\tjson.NewDecoder(w.Body).Decode(errjson)\n\tif got, want := errjson.Message, err.Error(); got != want {\n\t\tt.Errorf(\"Want error message %s, got %s\", want, got)\n\t}\n}\n\nfunc TestHandleServerFind(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"GET\", \"/api/servers/server1\", nil)\n\n\tserver := &autoscaler.Server{Name: \"server1\", Capacity: 1}\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().Find(gomock.Any(), \"server1\").Return(server, nil)\n\n\trouter := chi.NewRouter()\n\trouter.Get(\"/api/servers/{name}\", HandleServerFind(store))\n\trouter.ServeHTTP(w, r)\n\n\tif got, want := w.Code, 200; want != got {\n\t\tt.Errorf(\"Want response code %d, got %d\", want, got)\n\t}\n\n\tgot, want := &autoscaler.Server{}, server\n\tjson.NewDecoder(w.Body).Decode(got)\n\tif !reflect.DeepEqual(got, want) {\n\t\tt.Errorf(\"response body does match expected result\")\n\t\tpretty.Ldiff(t, got, want)\n\t}\n}\n\nfunc TestHandleServerFindErr(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"GET\", \"/api/servers/server1\", nil)\n\n\terr := errors.New(\"not found\")\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().Find(gomock.Any(), \"server1\").Return(nil, err)\n\n\trouter := chi.NewRouter()\n\trouter.Get(\"/api/servers/{name}\", HandleServerFind(store))\n\trouter.ServeHTTP(w, r)\n\n\tif got, want := w.Code, 404; want != got {\n\t\tt.Errorf(\"Want response code %d, got %d\", want, got)\n\t}\n\n\terrjson := &Error{}\n\tjson.NewDecoder(w.Body).Decode(errjson)\n\tif got, want := errjson.Message, err.Error(); got != want {\n\t\tt.Errorf(\"Want error message %s, got %s\", want, got)\n\t}\n}\n\nfunc TestHandleServerCreate(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"POST\", \"/api/servers\", nil)\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil)\n\n\tHandleServerCreate(store, config.Config{}).ServeHTTP(w, r)\n\n\tif got, want := w.Code, 200; want != got {\n\t\tt.Errorf(\"Want response code %d, got %d\", want, got)\n\t}\n}\n\nfunc TestHandleServerCreateFailure(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"POST\", \"/api/servers\", nil)\n\n\terr := errors.New(\"oops\")\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().Create(gomock.Any(), gomock.Any()).Return(err)\n\n\th := HandleServerCreate(store, config.Config{})\n\th.ServeHTTP(w, r)\n\n\tif got, want := w.Code, 500; want != got {\n\t\tt.Errorf(\"Want response code %d, got %d\", want, got)\n\t}\n\n\terrjson := &Error{}\n\tjson.NewDecoder(w.Body).Decode(errjson)\n\tif got, want := errjson.Message, err.Error(); got != want {\n\t\tt.Errorf(\"Want error message %s, got %s\", want, got)\n\t}\n}\n\nfunc TestHandleServerDelete(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"DELETE\", \"/api/servers/i-5203422c\", nil)\n\n\tserver := &autoscaler.Server{\n\t\tName:   \"i-5203422c\",\n\t\tImage:  \"docker-18-04\",\n\t\tRegion: \"nyc1\",\n\t\tSize:   \"s-1vcpu-1gb\",\n\t}\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().Find(gomock.Any(), server.Name).Return(server, nil)\n\tstore.EXPECT().Update(gomock.Any(), server).Return(nil)\n\n\trouter := chi.NewRouter()\n\trouter.Delete(\"/api/servers/{name}\", HandleServerDelete(store))\n\trouter.ServeHTTP(w, r)\n\n\tif got, want := w.Code, 200; want != got {\n\t\tt.Errorf(\"Want response code %d, got %d\", want, got)\n\t}\n\tif got, want := server.State, autoscaler.StateShutdown; got != want {\n\t\tt.Errorf(\"Want server state Shutdown, got %s\", got)\n\t}\n}\n\nfunc TestHandleServerDeleteNotFound(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"DELETE\", \"/api/servers/i-5203422c\", nil)\n\n\terr := errors.New(\"not found\")\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().Find(gomock.Any(), \"i-5203422c\").Return(nil, err)\n\n\trouter := chi.NewRouter()\n\trouter.Delete(\"/api/servers/{name}\", HandleServerDelete(store))\n\trouter.ServeHTTP(w, r)\n\n\tif got, want := w.Code, 404; want != got {\n\t\tt.Errorf(\"Want response code %d, got %d\", want, got)\n\t}\n\n\terrjson := &Error{}\n\tjson.NewDecoder(w.Body).Decode(errjson)\n\tif got, want := errjson.Message, err.Error(); got != want {\n\t\tt.Errorf(\"Want error message %s, got %s\", want, got)\n\t}\n}\n\nfunc TestHandleServerDeleteFailure(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"DELETE\", \"/api/servers/i-5203422c\", nil)\n\n\tserver := &autoscaler.Server{\n\t\tName:   \"i-5203422c\",\n\t\tImage:  \"docker-18-04\",\n\t\tRegion: \"nyc1\",\n\t\tSize:   \"s-1vcpu-1gb\",\n\t}\n\n\terr := errors.New(\"bad request\")\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().Find(gomock.Any(), server.Name).Return(server, nil)\n\tstore.EXPECT().Update(gomock.Any(), server).Return(err)\n\n\trouter := chi.NewRouter()\n\trouter.Delete(\"/api/servers/{name}\", HandleServerDelete(store))\n\trouter.ServeHTTP(w, r)\n\n\tif got, want := w.Code, 500; want != got {\n\t\tt.Errorf(\"Want response code %d, got %d\", want, got)\n\t}\n\n\terrjson := &Error{}\n\tjson.NewDecoder(w.Body).Decode(errjson)\n\tif got, want := errjson.Message, err.Error(); got != want {\n\t\tt.Errorf(\"Want error message %s, got %s\", want, got)\n\t}\n}\n\nfunc TestHandleServerDeleteErrorState(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"DELETE\", \"/api/servers/i-5203422c\", nil)\n\n\tserver := &autoscaler.Server{\n\t\tID:     \"\",\n\t\tState:  autoscaler.StateError,\n\t\tName:   \"i-5203422c\",\n\t\tImage:  \"docker-18-04\",\n\t\tRegion: \"nyc1\",\n\t\tSize:   \"s-1vcpu-1gb\",\n\t}\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().Find(gomock.Any(), server.Name).Return(server, nil)\n\tstore.EXPECT().Delete(gomock.Any(), server).Return(nil)\n\n\trouter := chi.NewRouter()\n\trouter.Delete(\"/api/servers/{name}\", HandleServerDelete(store))\n\trouter.ServeHTTP(w, r)\n\n\tif got, want := w.Code, 204; want != got {\n\t\tt.Errorf(\"Want response code %d, got %d\", want, got)\n\t}\n}\n\nfunc TestHandleServerForceDeleteErrorState(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"DELETE\", \"/api/servers/i-5203422c?force=true\", nil)\n\n\tserver := &autoscaler.Server{\n\t\tID:     \"i-5203422c\",\n\t\tState:  autoscaler.StateError,\n\t\tName:   \"i-5203422c\",\n\t\tImage:  \"docker-18-04\",\n\t\tRegion: \"nyc1\",\n\t\tSize:   \"s-1vcpu-1gb\",\n\t}\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().Find(gomock.Any(), server.Name).Return(server, nil)\n\tstore.EXPECT().Delete(gomock.Any(), server).Return(nil)\n\n\trouter := chi.NewRouter()\n\trouter.Delete(\"/api/servers/{name}\", HandleServerDelete(store))\n\trouter.ServeHTTP(w, r)\n\n\tif got, want := w.Code, 204; want != got {\n\t\tt.Errorf(\"Want response code %d, got %d\", want, got)\n\t}\n}\n"
  },
  {
    "path": "server/varz.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage server\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/drone/autoscaler\"\n)\n\ntype varz struct {\n\tPaused bool `json:\"paused\"`\n}\n\n// HandleVarz creates an http.HandlerFunc that returns system\n// configuration and runtime information.\nfunc HandleVarz(engine autoscaler.Engine) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tdata := varz{\n\t\t\tPaused: engine.Paused(),\n\t\t}\n\t\twriteJSON(w, &data, 200)\n\t}\n}\n"
  },
  {
    "path": "server/varz_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage server\n\nimport (\n\t\"encoding/json\"\n\t\"net/http/httptest\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/drone/autoscaler/mocks\"\n\t\"github.com/go-chi/chi\"\n\t\"github.com/golang/mock/gomock\"\n\t\"github.com/kr/pretty\"\n)\n\nfunc TestHandleVarz(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tmockVarz := &varz{\n\t\tPaused: true,\n\t}\n\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"POST\", \"/varz\", nil)\n\n\tengine := mocks.NewMockEngine(controller)\n\tengine.EXPECT().Paused().Return(true)\n\n\trouter := chi.NewRouter()\n\trouter.Post(\"/varz\", HandleVarz(engine))\n\trouter.ServeHTTP(w, r)\n\n\tif got, want := w.Code, 200; want != got {\n\t\tt.Errorf(\"Want response code %d, got %d\", want, got)\n\t}\n\n\tgot, want := &varz{}, mockVarz\n\tjson.NewDecoder(w.Body).Decode(got)\n\tif !reflect.DeepEqual(got, want) {\n\t\tt.Errorf(\"response body does match expected result\")\n\t\tpretty.Ldiff(t, got, want)\n\t}\n}\n"
  },
  {
    "path": "server/version.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage server\n\nimport (\n\t\"net/http\"\n)\n\n// version information, loosely based on\n// https://github.com/mozilla-services/Dockerflow\ntype versionInfo struct {\n\tSource  string `json:\"source,omitempty\"`\n\tVersion string `json:\"version,omitempty\"`\n\tCommit  string `json:\"commit,omitempty\"`\n}\n\n// HandleVersion creates an http.HandlerFunc that returns the\n// version number and build details.\nfunc HandleVersion(source, version, commit string) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tdata := versionInfo{\n\t\t\tSource:  source,\n\t\t\tVersion: version,\n\t\t\tCommit:  commit,\n\t\t}\n\t\twriteJSON(w, &data, 200)\n\t}\n}\n"
  },
  {
    "path": "server/version_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage server\n\nimport (\n\t\"encoding/json\"\n\t\"net/http/httptest\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/golang/mock/gomock\"\n\t\"github.com/kr/pretty\"\n)\n\nfunc TestHandleVersion(t *testing.T) {\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tw := httptest.NewRecorder()\n\tr := httptest.NewRequest(\"GET\", \"/version\", nil)\n\n\tmockVersion := &versionInfo{\n\t\tSource:  \"github.com/octocat/hello-world\",\n\t\tVersion: \"1.0.0\",\n\t\tCommit:  \"ad2aec\",\n\t}\n\n\th := HandleVersion(mockVersion.Source, mockVersion.Version, mockVersion.Commit)\n\th.ServeHTTP(w, r)\n\n\tif got, want := w.Code, 200; want != got {\n\t\tt.Errorf(\"Want response code %d, got %d\", want, got)\n\t}\n\n\tgot, want := &versionInfo{}, mockVersion\n\tjson.NewDecoder(w.Body).Decode(got)\n\tif !reflect.DeepEqual(got, want) {\n\t\tt.Errorf(\"response body does match expected result\")\n\t\tpretty.Ldiff(t, got, want)\n\t}\n}\n"
  },
  {
    "path": "server/web/handler.go",
    "content": "// Copyright 2019 Drone.IO Inc. All rights reserved.\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\n// Package web provides HTTP handlers that expose pipeline\n// state and status.\npackage web\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/logger/history\"\n)\n\n// HandleServers returns a http.HandlerFunc that displays a\n// list of activate servers.\nfunc HandleServers(servers autoscaler.ServerStore) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tnocache(w)\n\t\titems, _ := servers.List(r.Context())\n\t\tfiltered := []*autoscaler.Server{}\n\t\tfor _, item := range items {\n\t\t\tif item.State != autoscaler.StateStopped {\n\t\t\t\tfiltered = append(filtered, item)\n\t\t\t}\n\t\t}\n\t\trender(w, \"index.tmpl\", struct {\n\t\t\tItems []*autoscaler.Server\n\t\t}{filtered})\n\t}\n}\n\n// HandleLogging returns a http.HandlerFunc that displays a\n// list recent log entries.\nfunc HandleLogging(t *history.Hook) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tnocache(w)\n\t\trender(w, \"logs.tmpl\", struct {\n\t\t\tEntries []*history.Entry\n\t\t}{t.Entries()})\n\t}\n}\n"
  },
  {
    "path": "server/web/nocache.go",
    "content": "// Copyright 2019 Drone.IO Inc. All rights reserved.\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage web\n\nimport (\n\t\"net/http\"\n\t\"time\"\n)\n\n// unix epoch time\nvar epoch = time.Unix(0, 0).Format(time.RFC1123)\n\n// http headers to disable caching.\nvar noCacheHeaders = map[string]string{\n\t\"Expires\":         epoch,\n\t\"Cache-Control\":   \"no-cache, private, max-age=0\",\n\t\"Pragma\":          \"no-cache\",\n\t\"X-Accel-Expires\": \"0\",\n}\n\n// helper function to prevent http response caching.\nfunc nocache(w http.ResponseWriter) {\n\tfor k, v := range noCacheHeaders {\n\t\tw.Header().Set(k, v)\n\t}\n}\n"
  },
  {
    "path": "server/web/nocache_test.go",
    "content": "// Copyright 2019 Drone.IO Inc. All rights reserved.\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage web\n"
  },
  {
    "path": "server/web/render.go",
    "content": "// Copyright 2019 Drone.IO Inc. All rights reserved.\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage web\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/drone/autoscaler/server/web/template\"\n)\n\n// render writes the template to the response body.\nfunc render(w http.ResponseWriter, t string, v interface{}) {\n\tw.Header().Set(\"Content-Type\", \"text/html\")\n\ttemplate.T.ExecuteTemplate(w, t, v)\n}\n"
  },
  {
    "path": "server/web/render_test.go",
    "content": "// Copyright 2019 Drone.IO Inc. All rights reserved.\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage web\n"
  },
  {
    "path": "server/web/static/files/reset.css",
    "content": "html, body, div, span, applet, object, iframe,\nh1, h2, h3, h4, h5, h6, p, blockquote, pre,\na, abbr, acronym, address, big, cite, code,\ndel, dfn, em, img, ins, kbd, q, s, samp,\nsmall, strike, strong, sub, sup, tt, var,\nb, u, i, center,\ndl, dt, dd, ol, ul, li,\nfieldset, form, label, legend,\ntable, caption, tbody, tfoot, thead, tr, th, td,\narticle, aside, canvas, details, embed, \nfigure, figcaption, footer, header, hgroup, \nmenu, nav, output, ruby, section, summary,\ntime, mark, audio, video {\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 0;\n\tfont-size: 100%;\n\tfont: inherit;\n\tvertical-align: baseline;\n}\n/* HTML5 display-role reset for older browsers */\narticle, aside, details, figcaption, figure, \nfooter, header, hgroup, menu, nav, section {\n\tdisplay: block;\n}\nbody {\n\tline-height: 1;\n}\nol, ul {\n\tlist-style: none;\n}\nblockquote, q {\n\tquotes: none;\n}\nblockquote:before, blockquote:after,\nq:before, q:after {\n\tcontent: '';\n\tcontent: none;\n}\ntable {\n\tborder-collapse: collapse;\n\tborder-spacing: 0;\n}"
  },
  {
    "path": "server/web/static/files/style.css",
    "content": ":root {\n    --font-sans: -apple-system,BlinkMacSystemFont,\"Segoe UI\",\"Roboto\",\"Oxygen\",\"Ubuntu\",\"Cantarell\",\"Fira Sans\",\"Droid Sans\",\"Helvetica Neue\",sans-serif;\n    --font-mono: Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace;\n\n    --font-size-1: 12px;\n    --font-size-2: 14px;\n    --font-size-3: 16px;\n    --font-size-4: 18px;\n    --font-size-5: 20px;\n    --font-size-6: 24px;\n    --font-size-7: 30px;\n    --font-size-8: 38px;\n    --font-size-9: 48px;\n\n    --spacing-1: 4px;\n    --spacing-2: 8px;\n    --spacing-3: 12px;\n    --spacing-4: 16px;\n    --spacing-5: 24px;\n    --spacing-6: 32px;\n    --spacing-7: 48px;\n    --spacing-8: 64px;\n    --spacing-9: 96px;\n\n    --height-1: 16px;\n    --height-2: 20px;\n    --height-3: 24px;\n    --height-4: 32px;\n    --height-5: 40px;\n    --height-6: 48px;\n    --height-7: 64px;\n    --height-8: 80px;\n    --height-9: 96px;\n\n    --box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);\n    --box-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);\n    --box-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);\n    --box-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);\n    --box-shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);\n    --box-shadow-outline: 0 0 0 3px #F6B9FF;\n\n    /* --background: #edf2f7;\n\n    --text-color: #1a202c;\n    --text-color-pimary: #1a202c;\n    --text-color-secondary: #1a202c;\n\n    --card-text-color-primary: #2D3748;\n    --card-text-color-secondary: #829ab1;\n    --card-background-color: #FFF;\n    --card-border-radius: 8px;\n\n    --badge-border-radius: 20px;\n    --badge-font-size: 11px;\n    --badge-font-weight: 500;\n\n    --badge-pending-background: #FFEA7F;\n    --badge-pending-color: #960F18;\n\n    --badge-running-background: #D0FFED;\n    --badge-running-color: #083F37;\n\n    --badge-stopped-background: #FFEA7F;\n    --badge-stopped-color: #960F18;\n\n    --badge-error-background: #FFE0E8;\n    --badge-error-color: #800220;\n\n    --nav-link-color: #718096;\n    --nav-link-color-active: #E6DFFF;\n\n    --header-background-color: #4A5468;\n    --header-height: 72px; */\n\n    --background: #1A202C;\n\n    --text-color: #829ab1;\n\n    --card-text-color-title: #FFFFFF;\n    --card-text-color-primary: #E2E8F0;\n    --card-text-color-secondary: #829ab1;\n    --card-background-color: #2D3748;\n    --card-border-radius: 8px;\n    --card-icon-background: #718096;\n\n    --badge-border-radius: 8px;\n    --badge-font-size: var(--font-size-1);\n    --badge-font-weight: 500;\n\n    --badge-pending-background: rgba(255,255,255,0.075);\n    --badge-pending-color: #FFD567;\n\n    --badge-running-background: rgba(255,255,255,0.075);\n    --badge-running-color: #53FEBE;\n\n    --badge-stopped-background: rgba(255,255,255,0.075);\n    --badge-stopped-color: #FFE0E8;\n\n    --badge-error-background: #3c4656;\n    --badge-error-color: #FFE0E8;\n\n    --nav-link-color: #718096;\n    --nav-link-color-active: #EDF2F7;\n\n    --header-background-color: #2D3748;\n    --header-height: 72px;\n}\n\nhtml, body {\n    background: var(--background);\n    color: var(--text-color);\n    width: 100%;\n    height: 100%;\n    font-family: var(--font-sans);\n    font-size: var(--font-size-2);\n}\n\nmain {\n    box-sizing: border-box;\n    max-width: 800px;\n    margin: 0 auto;\n    margin-bottom: var(--spacing-6);\n}\n\nmain section > header h1 {\n    height: var(--height-5);\n    font-size: var(--font-size-7);\n    font-weight: 400;\n    font-style: normal;\n    font-stretch: normal;\n    line-height: normal;\n    letter-spacing: normal;\n    color: var(--text-color);\n    margin-top: var(--spacing-6);\n    margin-bottom: var(--spacing-3);\n}\n\nbody > header {\n    background-color: var(--header-background-color);\n    height: var(--header-height);\n    box-shadow: var(--box-shadow-lg);\n    box-sizing: border-box;\n    padding: 0 var(--spacing-4);\n    display: flex;\n    align-items: center;\n}\n\nheader .logo {\n    width: 30px;\n    height: 30px;\n}\n\n.navbar .inline-nav {\n    display: flex;\n    flex: 1 1 auto;\n    justify-content: flex-end;\n}\n\n.navbar .inline-nav li {\n    display: inline-block;\n    margin-left: var(--spacing-4);\n}\n\n.navbar .inline-nav a,\n.navbar .inline-nav a:active,\n.navbar .inline-nav a:visited {\n    color: var(--nav-link-color);\n    text-decoration: none;\n    font-size: var(--font-size-3);\n}\n\n.navbar .inline-nav a.active,\n.navbar .inline-nav a:hover {\n    color: var(--nav-link-color-active);\n}\n\n/*\n * cards\n */\n\n.card {\n    padding: var(--spacing-5);\n    color: var(--card-text-color-primary);\n    box-shadow: var(--box-shadow-md);\n    box-sizing: border-box;\n    border-radius: var(--card-border-radius);\n    background-color: var(--card-background-color);\n    margin-bottom: var(--spacing-3);\n}\n\n/*\n * instance card component\n */\n\n.instance {\n    display: grid;\n    grid-gap: var(--spacing-1) var(--spacing-2);\n    grid-template-columns: 40px 210px 130px 110px 110px 110px;\n}\n\n.instance:hover {\n    cursor: pointer;\n}\n\n/* .instance:hover {\n    box-shadow: var(--box-shadow-outline);\n} */\n\n.instance .icon {\n    grid-column: 1;\n    grid-row: 1 / span 2;\n    user-select: none; \n}\n\n.instance .addr {\n    grid-column: 2;\n    grid-row: 1;\n    font-weight: 500;\n    line-height: var(--height-3);\n    color: var(--card-text-color-title);\n}\n\n.instance .id {\n    grid-column: 2;\n    grid-row: 2;\n    color: var(--card-text-color-secondary);\n    font-weight: 300;\n}\n\n.instance .state {\n    grid-column: 3;\n    grid-row: 1 / span 2;\n    line-height: var(--height-3);\n\n    /* display: flex;\n    align-items: center; */\n}\n\n.instance .region {\n    grid-column: 4;\n    grid-row: 1;\n    line-height: var(--height-3);\n}\n\n.instance .image {\n    grid-column: 4;\n    grid-row: 2;\n    color: var(--card-text-color-secondary);\n    font-weight: 300;\n}\n\n.instance .size {\n    grid-column: 5;\n    grid-row: 1 / span 2;\n    line-height: var(--height-3);\n}\n\n.instance .time {\n    grid-column: 6;\n    grid-row: 1 / span 2;\n    text-align: right;\n    line-height: var(--height-3);\n}\n\n/*\n * instance icon server\n */\n\n.instance .icon-server {\n    width: 22px;\n    height: 22px;\n}\n\n.instance .icon .primary {\n    fill: var(--card-icon-background);\n}\n\n.instance .icon .secondary {\n    fill: var(--card-background-color);\n}\n\n/*\n * badge components\n */\n\n.badge {\n    border-radius: var(--badge-border-radius);\n    text-align: center;\n    text-transform: uppercase;\n    font-size: var(--badge-font-size);\n    font-weight: var(--badge-font-weight);\n    display: inline-block;\n    height: var(--height-3);\n    padding-right: var(--spacing-4);\n    padding-left: var(--spacing-4);\n    cursor: default;\n    user-select: none;\n    display: inline-flex;\n    align-content: center;\n}\n\n/**\n * badge for state\n */\n\n.state .badge {\n    padding-left: 0px;\n}\n\n.badge-creating,\n.badge-created,\n.badge-staging,\n.badge-starting,\n.badge-pending {\n    background: var(--badge-pending-background);\n    color: var(--badge-pending-color);\n}\n\n.badge-running {\n    background: var(--badge-running-background);\n    color: var(--badge-running-color);\n}\n\n.badge-stopping,\n.badge-stopped,\n.badge-shutdown {\n    background: var(--badge-stopped-background);\n    color: var(--badge-stopped-color);\n}\n\n.state .badge-error {\n    background: var(--badge-error-background);\n    color: var(--badge-error-color);\n}\n\n.badge svg {\n    width: 24px;\n    height: 24px;\n    fill: var(--badge-pending-color);\n    animation: blink 2s linear infinite;\n}\n\n.badge .icon-close-circle .secondary {\n    fill: var(--badge-error-color);\n}\n.badge .icon-close-circle .primary {\n    fill: var(--badge-error-background);\n}\n\n.badge-running svg {\n    fill: var(--badge-running-color);\n}\n\n.badge-stopped svg,\n.badge-shutdown svg,\n.badge-error svg {\n    animation: none;\n}\n\n/**\n * badge for trace logging\n */\n\n.badge-error,\n.badge-panic {\n    color: #FF9AA2;\n}\n\n.badge-warn {\n    color: var(--badge-pending-color);\n}\n\n.badge-info {\n    color: #00DDFF;\n}\n\n.badge-debug {\n    color: #F564FF;\n}\n\n.badge-trace {\n    color: #B3FFE3;\n}\n\n/**\n * log entry componenets\n */\n\n.entry {\n    display: grid;\n    grid-gap: var(--spacing-1) var(--spacing-2);\n    grid-template-columns: 85px 200px 1fr;\n\n    border-radius: 0px;\n    margin-bottom: 0px;\n    border-bottom: 2px solid var(--background);\n}\n\n.entry:first-child {\n    border-top-left-radius: var(--card-border-radius);\n    border-top-right-radius: var(--card-border-radius);\n}\n\n.entry:last-child {\n    border-bottom-left-radius: var(--card-border-radius);\n    border-bottom-right-radius: var(--card-border-radius);\n    border-bottom: none;\n}\n\n.entry .level {\n    grid-column: 1;\n    grid-row: 1;\n\n    display: flex;\n    align-items: center;\n    align-content: center;\n    font-size: var(--font-size-2);\n}\n\n.entry .level .badge {\n    display: flex;\n    align-items: center;\n    align-content: center;\n    font-family: var(--font-mono);\n    font-size: var(--font-size-2);\n}\n\n.entry .message {\n    grid-column: 3;\n    grid-row: 1;\n    /* padding: 0px var(--spacing-2); */\n    border-radius: var(--card-border-radius);\n    font-family: var(--font-mono);\n    font-size: var(--font-size-2);\n\n    display: flex;\n    align-items: center;\n    align-content: center;\n}\n\n.entry .fields {\n    grid-column: 3;\n    grid-row: 2;\n\n    /* padding: var(--spacing-1) var(--spacing-2); */\n    /* border-radius: var(--card-border-radius); */\n    line-height: var(--height-1);\n    font-family: var(--font-mono);\n    font-size: var(--font-size-2);\n}\n\n.entry .time {\n    grid-column: 2;\n    text-align: left;\n    font-family: var(--font-mono);\n    font-size: var(--font-size-2);\n    display: flex;\n    align-items: center;\n    align-content: center;\n}\n\n.fields span {\n    display: block;\n    color: #718096;\n}\n\n.fields span em:after {\n    content: \"=\";\n    color: #A0AEC0;\n    margin: 0px 3px;\n}\n\n/**\n * alerts\n */\n\n.alert {\n    display: grid;\n    grid-template-columns: 1fr 80px;\n}\n\n.alert span:first-child {\n    display: flex;\n    flex-direction: column;\n    justify-content: flex-end;\n}\n\n.alert h1 {\n    color: var(--card-text-color-primary);\n    font-size: var(--font-size-6);\n    margin-bottom: var(--spacing-3);\n}\n\n.alert p {\n    color: var(--card-text-color-secondary);\n    font-size: var(--font-size-2);\n}\n\n/*\n * animations\n */\n\n@keyframes spin {\n    0% { transform: rotate(0deg); }\n    100% { transform: rotate(359deg); }\n}\n\n@keyframes wrench {\n    0%  { transform: rotate(-12deg); }\n    8%  { transform: rotate(12deg); }\n    10% { transform: rotate(24deg); }\n    18% { transform: rotate(-24deg); }\n    20% { transform: rotate(-24deg); }\n    28% { transform: rotate(24deg); }\n    30% { transform: rotate(24deg); }\n    38% { transform: rotate(-24deg); }\n    40% { transform: rotate(-24deg); }\n    48% { transform: rotate(24deg); }\n    50% { transform: rotate(24deg); }\n    58% { transform: rotate(-24deg); }\n    60% { transform: rotate(-24deg); }\n    68% { transform: rotate(24deg); }\n    75%, 100% { transform: rotate(0deg); }\n}\n\n@keyframes blink {\n    50% { opacity: 0.0; }\n}\n"
  },
  {
    "path": "server/web/static/files/timeago.js",
    "content": "!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<s&&(e+=\"s\"),[s+\" \"+e+\" ago\",\"in \"+s+\" \"+e]}var t=[\"秒\",\"分钟\",\"小时\",\"天\",\"周\",\"个月\",\"年\"];function e(s,n){if(0===n)return[\"刚刚\",\"片刻后\"];var e=t[~~(n/2)];return[s+\" \"+e+\"前\",s+\" \"+e+\"后\"]}function u(s,n){i[s]=n}function r(s){return i[s]||i.en_US}var i={},o=[60,60,24,7,365/7/12,12];function m(s){return s instanceof Date?s:!isNaN(s)||/^\\d+$/.test(s)?new Date(parseInt(s)):(s=(s||\"\").trim().replace(/\\.\\d+/,\"\").replace(/-/,\"/\").replace(/-/,\"/\").replace(/(\\d)T(\\d)/,\"$1 $2\").replace(/Z/,\" UTC\").replace(/([+-]\\d\\d):?(\\d\\d)/,\" $1$2\"),new Date(s))}function d(s,n){for(var e=s<0?1:0,a=s=Math.abs(s),t=0;s>=o[t]&&t<o.length;t++)s/=o[t];return(0===(t*=2)?9:1)<(s=~~s)&&(t+=1),n(s,t,a)[e].replace(\"%s\",s)}function c(s,n){return(+(n=n?m(n):new Date)-+m(s))/1e3}var l=\"timeago-id\";function h(s){return parseInt(s.getAttribute(l))}var g={},f=function(s){clearTimeout(s),delete g[s]};function p(s,n,e,a){f(h(s));var t=a.relativeDate,u=a.minInterval,r=c(n,t);s.innerText=d(r,e);var i=setTimeout(function(){p(s,n,e,a)},Math.min(1e3*Math.max(function(s){for(var n=1,e=0,a=Math.abs(s);s>=o[e]&&e<o.length;e++)s/=o[e],n*=o[e];return a=(a%=n)?n-a:n,Math.ceil(a)}(r),u||1),2147483647));g[i]=0,function(s,n){s.setAttribute(l,n)}(s,i)}u(\"en_US\",n),u(\"zh_CN\",e);var b=[[\"ثانية\",\"ثانيتين\",\"%s ثوان\",\"%s ثانية\"],[\"دقيقة\",\"دقيقتين\",\"%s دقائق\",\"%s دقيقة\"],[\"ساعة\",\"ساعتين\",\"%s ساعات\",\"%s ساعة\"],[\"يوم\",\"يومين\",\"%s أيام\",\"%s يوماً\"],[\"أسبوع\",\"أسبوعين\",\"%s أسابيع\",\"%s أسبوعاً\"],[\"شهر\",\"شهرين\",\"%s أشهر\",\"%s شهراً\"],[\"عام\",\"عامين\",\"%s أعوام\",\"%s عاماً\"]];function v(s,n,e,a,t){var u=t%10,r=a;return 1===t?r=s:1==u&&20<t?r=n:1<u&&u<5&&(20<t||t<10)&&(r=e),r}var y=v.bind(null,\"секунду\",\"%s секунду\",\"%s секунды\",\"%s секунд\"),k=v.bind(null,\"хвіліну\",\"%s хвіліну\",\"%s хвіліны\",\"%s хвілін\"),j=v.bind(null,\"гадзіну\",\"%s гадзіну\",\"%s гадзіны\",\"%s гадзін\"),z=v.bind(null,\"дзень\",\"%s дзень\",\"%s дні\",\"%s дзён\"),w=v.bind(null,\"тыдзень\",\"%s тыдзень\",\"%s тыдні\",\"%s тыдняў\"),_=v.bind(null,\"месяц\",\"%s месяц\",\"%s месяцы\",\"%s месяцаў\"),q=v.bind(null,\"год\",\"%s год\",\"%s гады\",\"%s гадоў\");function M(s){var n=[\"۰\",\"۱\",\"۲\",\"۳\",\"۴\",\"۵\",\"۶\",\"۷\",\"۸\",\"۹\"];return s.toString().replace(/\\d/g,function(s){return n[s]})}var S=[[\"w tej chwili\",\"za chwilę\"],[\"%s sekund temu\",\"za %s sekund\"],[\"1 minutę temu\",\"za 1 minutę\"],[\"%s minut temu\",\"za %s minut\"],[\"1 godzinę temu\",\"za 1 godzinę\"],[\"%s godzin temu\",\"za %s godzin\"],[\"1 dzień temu\",\"za 1 dzień\"],[\"%s dni temu\",\"za %s dni\"],[\"1 tydzień temu\",\"za 1 tydzień\"],[\"%s tygodni temu\",\"za %s tygodni\"],[\"1 miesiąc temu\",\"za 1 miesiąc\"],[\"%s miesięcy temu\",\"za %s miesięcy\"],[\"1 rok temu\",\"za 1 rok\"],[\"%s lat temu\",\"za %s lat\"],[\"%s sekundy temu\",\"za %s sekundy\"],[\"%s minuty temu\",\"za %s minuty\"],[\"%s godziny temu\",\"za %s godziny\"],[\"%s dni temu\",\"za %s dni\"],[\"%s tygodnie temu\",\"za %s tygodnie\"],[\"%s miesiące temu\",\"za %s miesiące\"],[\"%s lata temu\",\"za %s lata\"]];function T(s,n,e,a,t){var u=t%10,r=a;return 1===t?r=s:1==u&&20<t?r=n:1<u&&u<5&&(20<t||t<10)&&(r=e),r}var N=T.bind(null,\"секунду\",\"%s секунду\",\"%s секунды\",\"%s секунд\"),x=T.bind(null,\"минуту\",\"%s минуту\",\"%s минуты\",\"%s минут\"),D=T.bind(null,\"час\",\"%s час\",\"%s часа\",\"%s часов\"),I=T.bind(null,\"день\",\"%s день\",\"%s дня\",\"%s дней\"),O=T.bind(null,\"неделю\",\"%s неделю\",\"%s недели\",\"%s недель\"),W=T.bind(null,\"месяц\",\"%s месяц\",\"%s месяца\",\"%s месяцев\"),$=T.bind(null,\"год\",\"%s год\",\"%s года\",\"%s лет\");function J(s,n,e,a,t){var u=t%10,r=t%100;return 1==t?s:1==u&&11!=r?n:2<=u&&u<=4&&!(12<=r&&r<=14)?e:a}var U=J.bind(null,\"1 секунд\",\"%s секунд\",\"%s секунде\",\"%s секунди\"),A=J.bind(null,\"1 минут\",\"%s минут\",\"%s минуте\",\"%s минута\"),C=J.bind(null,\"сат времена\",\"%s сат\",\"%s сата\",\"%s сати\"),E=J.bind(null,\"1 дан\",\"%s дан\",\"%s дана\",\"%s дана\"),B=J.bind(null,\"недељу дана\",\"%s недељу\",\"%s недеље\",\"%s недеља\"),P=J.bind(null,\"месец дана\",\"%s месец\",\"%s месеца\",\"%s месеци\"),R=J.bind(null,\"годину дана\",\"%s годину\",\"%s године\",\"%s година\");function Z(s,n,e,a,t){var u=t%10,r=a;return 1===t?r=s:1==u&&20<t?r=n:1<u&&u<5&&(20<t||t<10)&&(r=e),r}var F=Z.bind(null,\"секунду\",\"%s секунду\",\"%s секунди\",\"%s секунд\"),G=Z.bind(null,\"хвилину\",\"%s хвилину\",\"%s хвилини\",\"%s хвилин\"),H=Z.bind(null,\"годину\",\"%s годину\",\"%s години\",\"%s годин\"),K=Z.bind(null,\"день\",\"%s день\",\"%s дні\",\"%s днів\"),L=Z.bind(null,\"тиждень\",\"%s тиждень\",\"%s тиждні\",\"%s тижднів\"),Q=Z.bind(null,\"місяць\",\"%s місяць\",\"%s місяці\",\"%s місяців\"),V=Z.bind(null,\"рік\",\"%s рік\",\"%s роки\",\"%s років\");var X=Object.freeze({__proto__:null,ar:function(s,n){if(0===n)return[\"منذ لحظات\",\"بعد لحظات\"];var e=function(s,n){return n<3?b[s][n-1]:3<=n&&n<=10?b[s][2]:b[s][3]}(Math.floor(n/2),s);return[\"منذ \"+e,\"بعد \"+e]},be:function(s,n){switch(n){case 0:return[\"толькі што\",\"праз некалькі секунд\"];case 1:return[y(s)+\" таму\",\"праз \"+y(s)];case 2:case 3:return[k(s)+\" таму\",\"праз \"+k(s)];case 4:case 5:return[j(s)+\" таму\",\"праз \"+j(s)];case 6:case 7:return[z(s)+\" таму\",\"праз \"+z(s)];case 8:case 9:return[w(s)+\" таму\",\"праз \"+w(s)];case 10:case 11:return[_(s)+\" таму\",\"праз \"+_(s)];case 12:case 13:return[q(s)+\" таму\",\"праз \"+q(s)];default:return[\"\",\"\"]}},bg:function(s,n){return[[\"току що\",\"съвсем скоро\"],[\"преди %s секунди\",\"след %s секунди\"],[\"преди 1 минута\",\"след 1 минута\"],[\"преди %s минути\",\"след %s минути\"],[\"преди 1 час\",\"след 1 час\"],[\"преди %s часа\",\"след %s часа\"],[\"преди 1 ден\",\"след 1 ден\"],[\"преди %s дни\",\"след %s дни\"],[\"преди 1 седмица\",\"след 1 седмица\"],[\"преди %s седмици\",\"след %s седмици\"],[\"преди 1 месец\",\"след 1 месец\"],[\"преди %s месеца\",\"след %s месеца\"],[\"преди 1 година\",\"след 1 година\"],[\"преди %s години\",\"след %s години\"]][n]},bn_IN:function(s,n){return[[\"এইমাত্র\",\"একটা সময়\"],[\"%s সেকেন্ড আগে\",\"%s এর সেকেন্ডের মধ্যে\"],[\"1 মিনিট আগে\",\"1 মিনিটে\"],[\"%s এর মিনিট আগে\",\"%s এর মিনিটের মধ্যে\"],[\"1 ঘন্টা আগে\",\"1 ঘন্টা\"],[\"%s ঘণ্টা আগে\",\"%s এর ঘন্টার মধ্যে\"],[\"1 দিন আগে\",\"1 দিনের মধ্যে\"],[\"%s এর দিন আগে\",\"%s এর দিন\"],[\"1 সপ্তাহ আগে\",\"1 সপ্তাহের মধ্যে\"],[\"%s এর সপ্তাহ আগে\",\"%s সপ্তাহের মধ্যে\"],[\"1 মাস আগে\",\"1 মাসে\"],[\"%s মাস আগে\",\"%s মাসে\"],[\"1 বছর আগে\",\"1 বছরের মধ্যে\"],[\"%s বছর আগে\",\"%s বছরে\"]][n]},ca:function(s,n){return[[\"fa un moment\",\"d'aquí un moment\"],[\"fa %s segons\",\"d'aquí %s segons\"],[\"fa 1 minut\",\"d'aquí 1 minut\"],[\"fa %s minuts\",\"d'aquí %s minuts\"],[\"fa 1 hora\",\"d'aquí 1 hora\"],[\"fa %s hores\",\"d'aquí %s hores\"],[\"fa 1 dia\",\"d'aquí 1 dia\"],[\"fa %s dies\",\"d'aquí %s dies\"],[\"fa 1 setmana\",\"d'aquí 1 setmana\"],[\"fa %s setmanes\",\"d'aquí %s setmanes\"],[\"fa 1 mes\",\"d'aquí 1 mes\"],[\"fa %s mesos\",\"d'aquí %s mesos\"],[\"fa 1 any\",\"d'aquí 1 any\"],[\"fa %s anys\",\"d'aquí %s anys\"]][n]},de:function(s,n){return[[\"gerade eben\",\"vor einer Weile\"],[\"vor %s Sekunden\",\"in %s Sekunden\"],[\"vor 1 Minute\",\"in 1 Minute\"],[\"vor %s Minuten\",\"in %s Minuten\"],[\"vor 1 Stunde\",\"in 1 Stunde\"],[\"vor %s Stunden\",\"in %s Stunden\"],[\"vor 1 Tag\",\"in 1 Tag\"],[\"vor %s Tagen\",\"in %s Tagen\"],[\"vor 1 Woche\",\"in 1 Woche\"],[\"vor %s Wochen\",\"in %s Wochen\"],[\"vor 1 Monat\",\"in 1 Monat\"],[\"vor %s Monaten\",\"in %s Monaten\"],[\"vor 1 Jahr\",\"in 1 Jahr\"],[\"vor %s Jahren\",\"in %s Jahren\"]][n]},el:function(s,n){return[[\"μόλις τώρα\",\"σε λίγο\"],[\"%s δευτερόλεπτα πριν\",\"σε %s δευτερόλεπτα\"],[\"1 λεπτό πριν\",\"σε 1 λεπτό\"],[\"%s λεπτά πριν\",\"σε %s λεπτά\"],[\"1 ώρα πριν\",\"σε 1 ώρα\"],[\"%s ώρες πριν\",\"σε %s ώρες\"],[\"1 μέρα πριν\",\"σε 1 μέρα\"],[\"%s μέρες πριν\",\"σε %s μέρες\"],[\"1 εβδομάδα πριν\",\"σε 1 εβδομάδα\"],[\"%s εβδομάδες πριν\",\"σε %s εβδομάδες\"],[\"1 μήνα πριν\",\"σε 1 μήνα\"],[\"%s μήνες πριν\",\"σε %s μήνες\"],[\"1 χρόνο πριν\",\"σε 1 χρόνο\"],[\"%s χρόνια πριν\",\"σε %s χρόνια\"]][n]},en_short:function(s,n){return[[\"just now\",\"right now\"],[\"%ss ago\",\"in %ss\"],[\"1m ago\",\"in 1m\"],[\"%sm ago\",\"in %sm\"],[\"1h ago\",\"in 1h\"],[\"%sh ago\",\"in %sh\"],[\"1d ago\",\"in 1d\"],[\"%sd ago\",\"in %sd\"],[\"1w ago\",\"in 1w\"],[\"%sw ago\",\"in %sw\"],[\"1mo ago\",\"in 1mo\"],[\"%smo ago\",\"in %smo\"],[\"1yr ago\",\"in 1yr\"],[\"%syr ago\",\"in %syr\"]][n]},en_US:n,es:function(s,n){return[[\"justo ahora\",\"en un rato\"],[\"hace %s segundos\",\"en %s segundos\"],[\"hace 1 minuto\",\"en 1 minuto\"],[\"hace %s minutos\",\"en %s minutos\"],[\"hace 1 hora\",\"en 1 hora\"],[\"hace %s horas\",\"en %s horas\"],[\"hace 1 día\",\"en 1 día\"],[\"hace %s días\",\"en %s días\"],[\"hace 1 semana\",\"en 1 semana\"],[\"hace %s semanas\",\"en %s semanas\"],[\"hace 1 mes\",\"en 1 mes\"],[\"hace %s meses\",\"en %s meses\"],[\"hace 1 año\",\"en 1 año\"],[\"hace %s años\",\"en %s años\"]][n]},eu:function(s,n){return[[\"orain\",\"denbora bat barru\"],[\"duela %s segundu\",\"%s segundu barru\"],[\"duela minutu 1\",\"minutu 1 barru\"],[\"duela %s minutu\",\"%s minutu barru\"],[\"duela ordu 1\",\"ordu 1 barru\"],[\"duela %s ordu\",\"%s ordu barru\"],[\"duela egun 1\",\"egun 1 barru\"],[\"duela %s egun\",\"%s egun barru\"],[\"duela aste 1\",\"aste 1 barru\"],[\"duela %s aste\",\"%s aste barru\"],[\"duela hillabete 1\",\"hillabete 1 barru\"],[\"duela %s hillabete\",\"%s hillabete barru\"],[\"duela urte 1\",\"urte 1 barru\"],[\"duela %s urte\",\"%s urte barru\"]][n]},fa:function(s,n){var e=[[\"لحظاتی پیش\",\"همین حالا\"],[\"%s ثانیه پیش\",\"%s ثانیه دیگر\"],[\"۱ دقیقه پیش\",\"۱ دقیقه دیگر\"],[\"%s دقیقه پیش\",\"%s دقیقه دیگر\"],[\"۱ ساعت پیش\",\"۱ ساعت دیگر\"],[\"%s ساعت پیش\",\"%s ساعت دیگر\"],[\"۱ روز پیش\",\"۱ روز دیگر\"],[\"%s روز پیش\",\"%s روز دیگر\"],[\"۱ هفته پیش\",\"۱ هفته دیگر\"],[\"%s هفته پیش\",\"%s هفته دیگر\"],[\"۱ ماه پیش\",\"۱ ماه دیگر\"],[\"%s ماه پیش\",\"%s ماه دیگر\"],[\"۱ سال پیش\",\"۱ سال دیگر\"],[\"%s سال پیش\",\"%s سال دیگر\"]][n];return[e[0].replace(\"%s\",M(s)),e[1].replace(\"%s\",M(s))]},fi:function(s,n){return[[\"juuri äsken\",\"juuri nyt\"],[\"%s sekuntia sitten\",\"%s sekunnin päästä\"],[\"minuutti sitten\",\"minuutin päästä\"],[\"%s minuuttia sitten\",\"%s minuutin päästä\"],[\"tunti sitten\",\"tunnin päästä\"],[\"%s tuntia sitten\",\"%s tunnin päästä\"],[\"päivä sitten\",\"päivän päästä\"],[\"%s päivää sitten\",\"%s päivän päästä\"],[\"viikko sitten\",\"viikon päästä\"],[\"%s viikkoa sitten\",\"%s viikon päästä\"],[\"kuukausi sitten\",\"kuukauden päästä\"],[\"%s kuukautta sitten\",\"%s kuukauden päästä\"],[\"vuosi sitten\",\"vuoden päästä\"],[\"%s vuotta sitten\",\"%s vuoden päästä\"]][n]},fr:function(s,n){return[[\"à l'instant\",\"dans un instant\"],[\"il y a %s secondes\",\"dans %s secondes\"],[\"il y a 1 minute\",\"dans 1 minute\"],[\"il y a %s minutes\",\"dans %s minutes\"],[\"il y a 1 heure\",\"dans 1 heure\"],[\"il y a %s heures\",\"dans %s heures\"],[\"il y a 1 jour\",\"dans 1 jour\"],[\"il y a %s jours\",\"dans %s jours\"],[\"il y a 1 semaine\",\"dans 1 semaine\"],[\"il y a %s semaines\",\"dans %s semaines\"],[\"il y a 1 mois\",\"dans 1 mois\"],[\"il y a %s mois\",\"dans %s mois\"],[\"il y a 1 an\",\"dans 1 an\"],[\"il y a %s ans\",\"dans %s ans\"]][n]},gl:function(s,n){return[[\"xusto agora\",\"daquí a un pouco\"],[\"hai %s segundos\",\"en %s segundos\"],[\"hai 1 minuto\",\"nun minuto\"],[\"hai %s minutos\",\"en %s minutos\"],[\"hai 1 hora\",\"nunha hora\"],[\"hai %s horas\",\"en %s horas\"],[\"hai 1 día\",\"nun día\"],[\"hai %s días\",\"en %s días\"],[\"hai 1 semana\",\"nunha semana\"],[\"hai %s semanas\",\"en %s semanas\"],[\"hai 1 mes\",\"nun mes\"],[\"hai %s meses\",\"en %s meses\"],[\"hai 1 ano\",\"nun ano\"],[\"hai %s anos\",\"en %s anos\"]][n]},he:function(s,n){return[[\"זה עתה\",\"עכשיו\"],[\"לפני %s שניות\",\"בעוד %s שניות\"],[\"לפני דקה\",\"בעוד דקה\"],[\"לפני %s דקות\",\"בעוד %s דקות\"],[\"לפני שעה\",\"בעוד שעה\"],2===s?[\"לפני שעתיים\",\"בעוד שעתיים\"]:[\"לפני %s שעות\",\"בעוד %s שעות\"],[\"אתמול\",\"מחר\"],2===s?[\"לפני יומיים\",\"בעוד יומיים\"]:[\"לפני %s ימים\",\"בעוד %s ימים\"],[\"לפני שבוע\",\"בעוד שבוע\"],2===s?[\"לפני שבועיים\",\"בעוד שבועיים\"]:[\"לפני %s שבועות\",\"בעוד %s שבועות\"],[\"לפני חודש\",\"בעוד חודש\"],2===s?[\"לפני חודשיים\",\"בעוד חודשיים\"]:[\"לפני %s חודשים\",\"בעוד %s חודשים\"],[\"לפני שנה\",\"בעוד שנה\"],2===s?[\"לפני שנתיים\",\"בעוד שנתיים\"]:[\"לפני %s שנים\",\"בעוד %s שנים\"]][n]},hi_IN:function(s,n){return[[\"अभी\",\"कुछ समय\"],[\"%s सेकंड पहले\",\"%s सेकंड में\"],[\"1 मिनट पहले\",\"1 मिनट में\"],[\"%s मिनट पहले\",\"%s मिनट में\"],[\"1 घंटे पहले\",\"1 घंटे में\"],[\"%s घंटे पहले\",\"%s घंटे में\"],[\"1 दिन पहले\",\"1 दिन में\"],[\"%s दिन पहले\",\"%s दिनों में\"],[\"1 सप्ताह पहले\",\"1 सप्ताह में\"],[\"%s हफ्ते पहले\",\"%s हफ्तों में\"],[\"1 महीने पहले\",\"1 महीने में\"],[\"%s महीने पहले\",\"%s महीनों में\"],[\"1 साल पहले\",\"1 साल में\"],[\"%s साल पहले\",\"%s साल में\"]][n]},hu:function(s,n){return[[\"éppen most\",\"éppen most\"],[\"%s másodperce\",\"%s másodpercen belül\"],[\"1 perce\",\"1 percen belül\"],[\"%s perce\",\"%s percen belül\"],[\"1 órája\",\"1 órán belül\"],[\"%s órája\",\"%s órán belül\"],[\"1 napja\",\"1 napon belül\"],[\"%s napja\",\"%s napon belül\"],[\"1 hete\",\"1 héten belül\"],[\"%s hete\",\"%s héten belül\"],[\"1 hónapja\",\"1 hónapon belül\"],[\"%s hónapja\",\"%s hónapon belül\"],[\"1 éve\",\"1 éven belül\"],[\"%s éve\",\"%s éven belül\"]][n]},id_ID:function(s,n){return[[\"baru saja\",\"sebentar\"],[\"%s detik yang lalu\",\"dalam %s detik\"],[\"1 menit yang lalu\",\"dalam 1 menit\"],[\"%s menit yang lalu\",\"dalam %s menit\"],[\"1 jam yang lalu\",\"dalam 1 jam\"],[\"%s jam yang lalu\",\"dalam %s jam\"],[\"1 hari yang lalu\",\"dalam 1 hari\"],[\"%s hari yang lalu\",\"dalam %s hari\"],[\"1 minggu yang lalu\",\"dalam 1 minggu\"],[\"%s minggu yang lalu\",\"dalam %s minggu\"],[\"1 bulan yang lalu\",\"dalam 1 bulan\"],[\"%s bulan yang lalu\",\"dalam %s bulan\"],[\"1 tahun yang lalu\",\"dalam 1 tahun\"],[\"%s tahun yang lalu\",\"dalam %s tahun\"]][n]},it:function(s,n){return[[\"poco fa\",\"fra poco\"],[\"%s secondi fa\",\"fra %s secondi\"],[\"un minuto fa\",\"fra un minuto\"],[\"%s minuti fa\",\"fra %s minuti\"],[\"un'ora fa\",\"fra un'ora\"],[\"%s ore fa\",\"fra %s ore\"],[\"un giorno fa\",\"fra un giorno\"],[\"%s giorni fa\",\"fra %s giorni\"],[\"una settimana fa\",\"fra una settimana\"],[\"%s settimane fa\",\"fra %s settimane\"],[\"un mese fa\",\"fra un mese\"],[\"%s mesi fa\",\"fra %s mesi\"],[\"un anno fa\",\"fra un anno\"],[\"%s anni fa\",\"fra %s anni\"]][n]},ja:function(s,n){return[[\"すこし前\",\"すぐに\"],[\"%s秒前\",\"%s秒以内\"],[\"1分前\",\"1分以内\"],[\"%s分前\",\"%s分以内\"],[\"1時間前\",\"1時間以内\"],[\"%s時間前\",\"%s時間以内\"],[\"1日前\",\"1日以内\"],[\"%s日前\",\"%s日以内\"],[\"1週間前\",\"1週間以内\"],[\"%s週間前\",\"%s週間以内\"],[\"1ヶ月前\",\"1ヶ月以内\"],[\"%sヶ月前\",\"%sヶ月以内\"],[\"1年前\",\"1年以内\"],[\"%s年前\",\"%s年以内\"]][n]},ko:function(s,n){return[[\"방금\",\"곧\"],[\"%s초 전\",\"%s초 후\"],[\"1분 전\",\"1분 후\"],[\"%s분 전\",\"%s분 후\"],[\"1시간 전\",\"1시간 후\"],[\"%s시간 전\",\"%s시간 후\"],[\"1일 전\",\"1일 후\"],[\"%s일 전\",\"%s일 후\"],[\"1주일 전\",\"1주일 후\"],[\"%s주일 전\",\"%s주일 후\"],[\"1개월 전\",\"1개월 후\"],[\"%s개월 전\",\"%s개월 후\"],[\"1년 전\",\"1년 후\"],[\"%s년 전\",\"%s년 후\"]][n]},ml:function(s,n){return[[\"ഇപ്പോള്‍\",\"കുറച്ചു മുന്‍പ്\"],[\"%s സെക്കന്റ്‌കള്‍ക്ക് മുന്‍പ്\",\"%s സെക്കന്റില്‍\"],[\"1 മിനിറ്റിനു മുന്‍പ്\",\"1 മിനിറ്റില്‍\"],[\"%s മിനിറ്റുകള്‍ക്ക് മുന്‍പ\",\"%s മിനിറ്റില്‍\"],[\"1 മണിക്കൂറിനു മുന്‍പ്\",\"1 മണിക്കൂറില്‍\"],[\"%s മണിക്കൂറുകള്‍ക്കു മുന്‍പ്\",\"%s മണിക്കൂറില്‍\"],[\"1 ഒരു ദിവസം മുന്‍പ്\",\"1 ദിവസത്തില്‍\"],[\"%s ദിവസങ്ങള്‍ക് മുന്‍പ്\",\"%s ദിവസങ്ങള്‍ക്കുള്ളില്‍\"],[\"1 ആഴ്ച മുന്‍പ്\",\"1 ആഴ്ചയില്‍\"],[\"%s ആഴ്ചകള്‍ക്ക് മുന്‍പ്\",\"%s ആഴ്ചകള്‍ക്കുള്ളില്‍\"],[\"1 മാസത്തിനു മുന്‍പ്\",\"1 മാസത്തിനുള്ളില്‍\"],[\"%s മാസങ്ങള്‍ക്ക് മുന്‍പ്\",\"%s മാസങ്ങള്‍ക്കുള്ളില്‍\"],[\"1 വര്‍ഷത്തിനു  മുന്‍പ്\",\"1 വര്‍ഷത്തിനുള്ളില്‍\"],[\"%s വര്‍ഷങ്ങള്‍ക്കു മുന്‍പ്\",\"%s വര്‍ഷങ്ങള്‍ക്കുല്ല്ളില്‍\"]][n]},my:function(s,n){return[[\"ယခုအတွင်း\",\"ယခု\"],[\"%s စက္ကန့် အကြာက\",\"%s စက္ကန့်အတွင်း\"],[\"1 မိနစ် အကြာက\",\"1 မိနစ်အတွင်း\"],[\"%s မိနစ် အကြာက\",\"%s မိနစ်အတွင်း\"],[\"1 နာရီ အကြာက\",\"1 နာရီအတွင်း\"],[\"%s နာရီ အကြာက\",\"%s နာရီအတွင်း\"],[\"1 ရက် အကြာက\",\"1 ရက်အတွင်း\"],[\"%s ရက် အကြာက\",\"%s ရက်အတွင်း\"],[\"1 ပတ် အကြာက\",\"1 ပတ်အတွင်း\"],[\"%s ပတ် အကြာက\",\"%s ပတ်အတွင်း\"],[\"1 လ အကြာက\",\"1 လအတွင်း\"],[\"%s လ အကြာက\",\"%s လအတွင်း\"],[\"1 နှစ် အကြာက\",\"1 နှစ်အတွင်း\"],[\"%s နှစ် အကြာက\",\"%s နှစ်အတွင်း\"]][n]},nb_NO:function(s,n){return[[\"akkurat nå\",\"om litt\"],[\"%s sekunder siden\",\"om %s sekunder\"],[\"1 minutt siden\",\"om 1 minutt\"],[\"%s minutter siden\",\"om %s minutter\"],[\"1 time siden\",\"om 1 time\"],[\"%s timer siden\",\"om %s timer\"],[\"1 dag siden\",\"om 1 dag\"],[\"%s dager siden\",\"om %s dager\"],[\"1 uke siden\",\"om 1 uke\"],[\"%s uker siden\",\"om %s uker\"],[\"1 måned siden\",\"om 1 måned\"],[\"%s måneder siden\",\"om %s måneder\"],[\"1 år siden\",\"om 1 år\"],[\"%s år siden\",\"om %s år\"]][n]},nl:function(s,n){return[[\"recent\",\"binnenkort\"],[\"%s seconden geleden\",\"binnen %s seconden\"],[\"1 minuut geleden\",\"binnen 1 minuut\"],[\"%s minuten geleden\",\"binnen %s minuten\"],[\"1 uur geleden\",\"binnen 1 uur\"],[\"%s uur geleden\",\"binnen %s uur\"],[\"1 dag geleden\",\"binnen 1 dag\"],[\"%s dagen geleden\",\"binnen %s dagen\"],[\"1 week geleden\",\"binnen 1 week\"],[\"%s weken geleden\",\"binnen %s weken\"],[\"1 maand geleden\",\"binnen 1 maand\"],[\"%s maanden geleden\",\"binnen %s maanden\"],[\"1 jaar geleden\",\"binnen 1 jaar\"],[\"%s jaar geleden\",\"binnen %s jaar\"]][n]},nn_NO:function(s,n){return[[\"nett no\",\"om litt\"],[\"%s sekund sidan\",\"om %s sekund\"],[\"1 minutt sidan\",\"om 1 minutt\"],[\"%s minutt sidan\",\"om %s minutt\"],[\"1 time sidan\",\"om 1 time\"],[\"%s timar sidan\",\"om %s timar\"],[\"1 dag sidan\",\"om 1 dag\"],[\"%s dagar sidan\",\"om %s dagar\"],[\"1 veke sidan\",\"om 1 veke\"],[\"%s veker sidan\",\"om %s veker\"],[\"1 månad sidan\",\"om 1 månad\"],[\"%s månadar sidan\",\"om %s månadar\"],[\"1 år sidan\",\"om 1 år\"],[\"%s år sidan\",\"om %s år\"]][n]},pl:function(s,n){return S[1&n?4<s%10||s%10<2||1==~~(s/10)%10?n:++n/2+13:n]},pt_BR:function(s,n){return[[\"agora mesmo\",\"agora\"],[\"há %s segundos\",\"em %s segundos\"],[\"há um minuto\",\"em um minuto\"],[\"há %s minutos\",\"em %s minutos\"],[\"há uma hora\",\"em uma hora\"],[\"há %s horas\",\"em %s horas\"],[\"há um dia\",\"em um dia\"],[\"há %s dias\",\"em %s dias\"],[\"há uma semana\",\"em uma semana\"],[\"há %s semanas\",\"em %s semanas\"],[\"há um mês\",\"em um mês\"],[\"há %s meses\",\"em %s meses\"],[\"há um ano\",\"em um ano\"],[\"há %s anos\",\"em %s anos\"]][n]},ro:function(s,n){var e=[[\"chiar acum\",\"chiar acum\"],[\"acum %s secunde\",\"peste %s secunde\"],[\"acum un minut\",\"peste un minut\"],[\"acum %s minute\",\"peste %s minute\"],[\"acum o oră\",\"peste o oră\"],[\"acum %s ore\",\"peste %s ore\"],[\"acum o zi\",\"peste o zi\"],[\"acum %s zile\",\"peste %s zile\"],[\"acum o săptămână\",\"peste o săptămână\"],[\"acum %s săptămâni\",\"peste %s săptămâni\"],[\"acum o lună\",\"peste o lună\"],[\"acum %s luni\",\"peste %s luni\"],[\"acum un an\",\"peste un an\"],[\"acum %s ani\",\"peste %s ani\"]];return s<20?e[n]:[e[n][0].replace(\"%s\",\"%s de\"),e[n][1].replace(\"%s\",\"%s de\")]},ru:function(s,n){switch(n){case 0:return[\"только что\",\"через несколько секунд\"];case 1:return[N(s)+\" назад\",\"через \"+N(s)];case 2:case 3:return[x(s)+\" назад\",\"через \"+x(s)];case 4:case 5:return[D(s)+\" назад\",\"через \"+D(s)];case 6:return[\"вчера\",\"завтра\"];case 7:return[I(s)+\" назад\",\"через \"+I(s)];case 8:case 9:return[O(s)+\" назад\",\"через \"+O(s)];case 10:case 11:return[W(s)+\" назад\",\"через \"+W(s)];case 12:case 13:return[$(s)+\" назад\",\"через \"+$(s)];default:return[\"\",\"\"]}},sq:function(s,n){return[[\"pak më parë\",\"pas pak\"],[\"para %s sekondash\",\"pas %s sekondash\"],[\"para një minute\",\"pas një minute\"],[\"para %s minutash\",\"pas %s minutash\"],[\"para një ore\",\"pas një ore\"],[\"para %s orësh\",\"pas %s orësh\"],[\"dje\",\"nesër\"],[\"para %s ditësh\",\"pas %s ditësh\"],[\"para një jave\",\"pas një jave\"],[\"para %s javësh\",\"pas %s javësh\"],[\"para një muaji\",\"pas një muaji\"],[\"para %s muajsh\",\"pas %s muajsh\"],[\"para një viti\",\"pas një viti\"],[\"para %s vjetësh\",\"pas %s vjetësh\"]][n]},sr:function(s,n){switch(n){case 0:return[\"малопре\",\"управо сад\"];case 1:return[\"пре \"+U(s),\"за \"+U(s)];case 2:case 3:return[\"пре \"+A(s),\"за \"+A(s)];case 4:case 5:return[\"пре \"+C(s),\"за \"+C(s)];case 6:case 7:return[\"пре \"+E(s),\"за \"+E(s)];case 8:case 9:return[\"пре \"+B(s),\"за \"+B(s)];case 10:case 11:return[\"пре \"+P(s),\"за \"+P(s)];case 12:case 13:return[\"пре \"+R(s),\"за \"+R(s)];default:return[\"\",\"\"]}},sv:function(s,n){return[[\"just nu\",\"om en stund\"],[\"%s sekunder sedan\",\"om %s sekunder\"],[\"1 minut sedan\",\"om 1 minut\"],[\"%s minuter sedan\",\"om %s minuter\"],[\"1 timme sedan\",\"om 1 timme\"],[\"%s timmar sedan\",\"om %s timmar\"],[\"1 dag sedan\",\"om 1 dag\"],[\"%s dagar sedan\",\"om %s dagar\"],[\"1 vecka sedan\",\"om 1 vecka\"],[\"%s veckor sedan\",\"om %s veckor\"],[\"1 månad sedan\",\"om 1 månad\"],[\"%s månader sedan\",\"om %s månader\"],[\"1 år sedan\",\"om 1 år\"],[\"%s år sedan\",\"om %s år\"]][n]},ta:function(s,n){return[[\"இப்போது\",\"சற்று நேரம் முன்பு\"],[\"%s நொடிக்கு முன்\",\"%s நொடிகளில்\"],[\"1 நிமிடத்திற்க்கு முன்\",\"1 நிமிடத்தில்\"],[\"%s நிமிடத்திற்க்கு முன்\",\"%s நிமிடங்களில்\"],[\"1 மணி நேரத்திற்கு முன்\",\"1 மணி நேரத்திற்குள்\"],[\"%s மணி நேரத்திற்கு முன்\",\"%s மணி நேரத்திற்குள்\"],[\"1 நாளுக்கு முன்\",\"1 நாளில்\"],[\"%s நாட்களுக்கு முன்\",\"%s நாட்களில்\"],[\"1 வாரத்திற்கு முன்\",\"1 வாரத்தில்\"],[\"%s வாரங்களுக்கு முன்\",\"%s வாரங்களில்\"],[\"1 மாதத்திற்கு முன்\",\"1 மாதத்தில்\"],[\"%s மாதங்களுக்கு முன்\",\"%s மாதங்களில்\"],[\"1 வருடத்திற்கு முன்\",\"1 வருடத்தில்\"],[\"%s வருடங்களுக்கு முன்\",\"%s வருடங்களில்\"]][n]},th:function(s,n){return[[\"เมื่อสักครู่นี้\",\"อีกสักครู่\"],[\"%s วินาทีที่แล้ว\",\"ใน %s วินาที\"],[\"1 นาทีที่แล้ว\",\"ใน 1 นาที\"],[\"%s นาทีที่แล้ว\",\"ใน %s นาที\"],[\"1 ชั่วโมงที่แล้ว\",\"ใน 1 ชั่วโมง\"],[\"%s ชั่วโมงที่แล้ว\",\"ใน %s ชั่วโมง\"],[\"1 วันที่แล้ว\",\"ใน 1 วัน\"],[\"%s วันที่แล้ว\",\"ใน %s วัน\"],[\"1 อาทิตย์ที่แล้ว\",\"ใน 1 อาทิตย์\"],[\"%s อาทิตย์ที่แล้ว\",\"ใน %s อาทิตย์\"],[\"1 เดือนที่แล้ว\",\"ใน 1 เดือน\"],[\"%s เดือนที่แล้ว\",\"ใน %s เดือน\"],[\"1 ปีที่แล้ว\",\"ใน 1 ปี\"],[\"%s ปีที่แล้ว\",\"ใน %s ปี\"]][n]},tr:function(s,n){return[[\"az önce\",\"şimdi\"],[\"%s saniye önce\",\"%s saniye içinde\"],[\"1 dakika önce\",\"1 dakika içinde\"],[\"%s dakika önce\",\"%s dakika içinde\"],[\"1 saat önce\",\"1 saat içinde\"],[\"%s saat önce\",\"%s saat içinde\"],[\"1 gün önce\",\"1 gün içinde\"],[\"%s gün önce\",\"%s gün içinde\"],[\"1 hafta önce\",\"1 hafta içinde\"],[\"%s hafta önce\",\"%s hafta içinde\"],[\"1 ay önce\",\"1 ay içinde\"],[\"%s ay önce\",\"%s ay içinde\"],[\"1 yıl önce\",\"1 yıl içinde\"],[\"%s yıl önce\",\"%s yıl içinde\"]][n]},uk:function(s,n){switch(n){case 0:return[\"щойно\",\"через декілька секунд\"];case 1:return[F(s)+\" тому\",\"через \"+F(s)];case 2:case 3:return[G(s)+\" тому\",\"через \"+G(s)];case 4:case 5:return[H(s)+\" тому\",\"через \"+H(s)];case 6:case 7:return[K(s)+\" тому\",\"через \"+K(s)];case 8:case 9:return[L(s)+\" тому\",\"через \"+L(s)];case 10:case 11:return[Q(s)+\" тому\",\"через \"+Q(s)];case 12:case 13:return[V(s)+\" тому\",\"через \"+V(s)];default:return[\"\",\"\"]}},vi:function(s,n){return[[\"vừa xong\",\"một lúc\"],[\"%s giây trước\",\"trong %s giây\"],[\"1 phút trước\",\"trong 1 phút\"],[\"%s phút trước\",\"trong %s phút\"],[\"1 giờ trước\",\"trong 1 giờ\"],[\"%s giờ trước\",\"trong %s giờ\"],[\"1 ngày trước\",\"trong 1 ngày\"],[\"%s ngày trước\",\"trong %s ngày\"],[\"1 tuần trước\",\"trong 1 tuần\"],[\"%s tuần trước\",\"trong %s tuần\"],[\"1 tháng trước\",\"trong 1 tháng\"],[\"%s tháng trước\",\"trong %s tháng\"],[\"1 năm trước\",\"trong 1 năm\"],[\"%s năm trước\",\"trong %s năm\"]][n]},zh_CN:e,zh_TW:function(s,n){return[[\"剛剛\",\"片刻後\"],[\"%s 秒前\",\"%s 秒後\"],[\"1 分鐘前\",\"1 分鐘後\"],[\"%s 分鐘前\",\"%s 分鐘後\"],[\"1 小時前\",\"1 小時後\"],[\"%s 小時前\",\"%s 小時後\"],[\"1 天前\",\"1 天後\"],[\"%s 天前\",\"%s 天後\"],[\"1 週前\",\"1 週後\"],[\"%s 週前\",\"%s 週後\"],[\"1 個月前\",\"1 個月後\"],[\"%s 個月前\",\"%s 個月後\"],[\"1 年前\",\"1 年後\"],[\"%s 年前\",\"%s 年後\"]][n]}});Object.keys(X).forEach(function(s){u(s,X[s])}),s.cancel=function(s){s?f(h(s)):Object.keys(g).forEach(f)},s.format=function(s,n,e){return d(c(s,e&&e.relativeDate),r(n))},s.register=u,s.render=function(s,n,e){var a=s.length?s:[s];return a.forEach(function(s){p(s,function(s){return s.getAttribute(\"datetime\")}(s),r(n),e||{})}),a},Object.defineProperty(s,\"__esModule\",{value:!0})});"
  },
  {
    "path": "server/web/static/static.go",
    "content": "// Copyright 2019 Drone.IO Inc. All rights reserved.\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage static\n\n//go:generate togo http -package static -output static_gen.go\n"
  },
  {
    "path": "server/web/static/static_gen.go",
    "content": "package static\n\nimport (\n\t\"bytes\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n)\n\ntype fileSystem struct {\n\tfiles map[string]file\n}\n\nfunc (fs *fileSystem) Open(name string) (http.File, error) {\n\tname = strings.Replace(name, \"//\", \"/\", -1)\n\tf, ok := fs.files[name]\n\tif ok {\n\t\treturn newHTTPFile(f, false), nil\n\t}\n\tindex := strings.Replace(name+\"/index.html\", \"//\", \"/\", -1)\n\tf, ok = fs.files[index]\n\tif !ok {\n\t\treturn nil, os.ErrNotExist\n\t}\n\treturn newHTTPFile(f, true), nil\n}\n\ntype file struct {\n\tos.FileInfo\n\tdata []byte\n}\n\ntype fileInfo struct {\n\tname    string\n\tsize    int64\n\tmode    os.FileMode\n\tmodTime time.Time\n\tisDir   bool\n\n\tfiles []os.FileInfo\n}\n\nfunc (f *fileInfo) Name() string {\n\treturn f.name\n}\n\nfunc (f *fileInfo) Size() int64 {\n\treturn f.size\n}\n\nfunc (f *fileInfo) Mode() os.FileMode {\n\treturn f.mode\n}\n\nfunc (f *fileInfo) ModTime() time.Time {\n\treturn f.modTime\n}\n\nfunc (f *fileInfo) IsDir() bool {\n\treturn f.isDir\n}\n\nfunc (f *fileInfo) Readdir(count int) ([]os.FileInfo, error) {\n\treturn make([]os.FileInfo, 0), nil\n}\n\nfunc (f *fileInfo) Sys() interface{} {\n\treturn nil\n}\n\nfunc newHTTPFile(file file, isDir bool) *httpFile {\n\treturn &httpFile{\n\t\tfile:   file,\n\t\treader: bytes.NewReader(file.data),\n\t\tisDir:  isDir,\n\t}\n}\n\ntype httpFile struct {\n\tfile\n\n\treader *bytes.Reader\n\tisDir  bool\n}\n\nfunc (f *httpFile) Read(p []byte) (n int, err error) {\n\treturn f.reader.Read(p)\n}\n\nfunc (f *httpFile) Seek(offset int64, whence int) (ret int64, err error) {\n\treturn f.reader.Seek(offset, whence)\n}\n\nfunc (f *httpFile) Stat() (os.FileInfo, error) {\n\treturn f, nil\n}\n\nfunc (f *httpFile) IsDir() bool {\n\treturn f.isDir\n}\n\nfunc (f *httpFile) Readdir(count int) ([]os.FileInfo, error) {\n\treturn make([]os.FileInfo, 0), nil\n}\n\nfunc (f *httpFile) Close() error {\n\treturn nil\n}\n\n// New returns an embedded http.FileSystem\nfunc New() http.FileSystem {\n\treturn &fileSystem{\n\t\tfiles: files,\n\t}\n}\n\n// Lookup returns the file at the specified path\nfunc Lookup(path string) ([]byte, error) {\n\tf, ok := files[path]\n\tif !ok {\n\t\treturn nil, os.ErrNotExist\n\t}\n\treturn f.data, nil\n}\n\n// MustLookup returns the file at the specified path\n// and panics if the file is not found.\nfunc MustLookup(path string) []byte {\n\td, err := Lookup(path)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn d\n}\n\n// Index of all files\nvar files = map[string]file{\n\t\"/reset.css\": {\n\t\tdata: file0,\n\t\tFileInfo: &fileInfo{\n\t\t\tname:    \"reset.css\",\n\t\t\tsize:    990,\n\t\t\tmodTime: time.Unix(1573482321, 0),\n\t\t},\n\t},\n\t\"/timeago.js\": {\n\t\tdata: file1,\n\t\tFileInfo: &fileInfo{\n\t\t\tname:    \"timeago.js\",\n\t\t\tsize:    29512,\n\t\t\tmodTime: time.Unix(1573599821, 0),\n\t\t},\n\t},\n\t\"/favicon.png\": {\n\t\tdata: file2,\n\t\tFileInfo: &fileInfo{\n\t\t\tname:    \"favicon.png\",\n\t\t\tsize:    1804,\n\t\t\tmodTime: time.Unix(1573482321, 0),\n\t\t},\n\t},\n\t\"/icons/server-list-empty.svg\": {\n\t\tdata: file3,\n\t\tFileInfo: &fileInfo{\n\t\t\tname:    \"server-list-empty.svg\",\n\t\t\tsize:    2550,\n\t\t\tmodTime: time.Unix(1574719084, 0),\n\t\t},\n\t},\n\t\"/icons/server-list-empty-mono.svg\": {\n\t\tdata: file4,\n\t\tFileInfo: &fileInfo{\n\t\t\tname:    \"server-list-empty-mono.svg\",\n\t\t\tsize:    2528,\n\t\t\tmodTime: time.Unix(1574719111, 0),\n\t\t},\n\t},\n\t\"/style.css\": {\n\t\tdata: file5,\n\t\tFileInfo: &fileInfo{\n\t\t\tname:    \"style.css\",\n\t\t\tsize:    11015,\n\t\t\tmodTime: time.Unix(1574720127, 0),\n\t\t},\n\t},\n}\n\n//\n// embedded files.\n//\n\n// /reset.css\nvar file0 = []byte(`html, body, div, span, applet, object, iframe,\nh1, h2, h3, h4, h5, h6, p, blockquote, pre,\na, abbr, acronym, address, big, cite, code,\ndel, dfn, em, img, ins, kbd, q, s, samp,\nsmall, strike, strong, sub, sup, tt, var,\nb, u, i, center,\ndl, dt, dd, ol, ul, li,\nfieldset, form, label, legend,\ntable, caption, tbody, tfoot, thead, tr, th, td,\narticle, aside, canvas, details, embed, \nfigure, figcaption, footer, header, hgroup, \nmenu, nav, output, ruby, section, summary,\ntime, mark, audio, video {\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 0;\n\tfont-size: 100%;\n\tfont: inherit;\n\tvertical-align: baseline;\n}\n/* HTML5 display-role reset for older browsers */\narticle, aside, details, figcaption, figure, \nfooter, header, hgroup, menu, nav, section {\n\tdisplay: block;\n}\nbody {\n\tline-height: 1;\n}\nol, ul {\n\tlist-style: none;\n}\nblockquote, q {\n\tquotes: none;\n}\nblockquote:before, blockquote:after,\nq:before, q:after {\n\tcontent: '';\n\tcontent: none;\n}\ntable {\n\tborder-collapse: collapse;\n\tborder-spacing: 0;\n}`)\n\n// /timeago.js\nvar file1 = []byte(`!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<s&&(e+=\"s\"),[s+\" \"+e+\" ago\",\"in \"+s+\" \"+e]}var t=[\"秒\",\"分钟\",\"小时\",\"天\",\"周\",\"个月\",\"年\"];function e(s,n){if(0===n)return[\"刚刚\",\"片刻后\"];var e=t[~~(n/2)];return[s+\" \"+e+\"前\",s+\" \"+e+\"后\"]}function u(s,n){i[s]=n}function r(s){return i[s]||i.en_US}var i={},o=[60,60,24,7,365/7/12,12];function m(s){return s instanceof Date?s:!isNaN(s)||/^\\d+$/.test(s)?new Date(parseInt(s)):(s=(s||\"\").trim().replace(/\\.\\d+/,\"\").replace(/-/,\"/\").replace(/-/,\"/\").replace(/(\\d)T(\\d)/,\"$1 $2\").replace(/Z/,\" UTC\").replace(/([+-]\\d\\d):?(\\d\\d)/,\" $1$2\"),new Date(s))}function d(s,n){for(var e=s<0?1:0,a=s=Math.abs(s),t=0;s>=o[t]&&t<o.length;t++)s/=o[t];return(0===(t*=2)?9:1)<(s=~~s)&&(t+=1),n(s,t,a)[e].replace(\"%s\",s)}function c(s,n){return(+(n=n?m(n):new Date)-+m(s))/1e3}var l=\"timeago-id\";function h(s){return parseInt(s.getAttribute(l))}var g={},f=function(s){clearTimeout(s),delete g[s]};function p(s,n,e,a){f(h(s));var t=a.relativeDate,u=a.minInterval,r=c(n,t);s.innerText=d(r,e);var i=setTimeout(function(){p(s,n,e,a)},Math.min(1e3*Math.max(function(s){for(var n=1,e=0,a=Math.abs(s);s>=o[e]&&e<o.length;e++)s/=o[e],n*=o[e];return a=(a%=n)?n-a:n,Math.ceil(a)}(r),u||1),2147483647));g[i]=0,function(s,n){s.setAttribute(l,n)}(s,i)}u(\"en_US\",n),u(\"zh_CN\",e);var b=[[\"ثانية\",\"ثانيتين\",\"%s ثوان\",\"%s ثانية\"],[\"دقيقة\",\"دقيقتين\",\"%s دقائق\",\"%s دقيقة\"],[\"ساعة\",\"ساعتين\",\"%s ساعات\",\"%s ساعة\"],[\"يوم\",\"يومين\",\"%s أيام\",\"%s يوماً\"],[\"أسبوع\",\"أسبوعين\",\"%s أسابيع\",\"%s أسبوعاً\"],[\"شهر\",\"شهرين\",\"%s أشهر\",\"%s شهراً\"],[\"عام\",\"عامين\",\"%s أعوام\",\"%s عاماً\"]];function v(s,n,e,a,t){var u=t%10,r=a;return 1===t?r=s:1==u&&20<t?r=n:1<u&&u<5&&(20<t||t<10)&&(r=e),r}var y=v.bind(null,\"секунду\",\"%s секунду\",\"%s секунды\",\"%s секунд\"),k=v.bind(null,\"хвіліну\",\"%s хвіліну\",\"%s хвіліны\",\"%s хвілін\"),j=v.bind(null,\"гадзіну\",\"%s гадзіну\",\"%s гадзіны\",\"%s гадзін\"),z=v.bind(null,\"дзень\",\"%s дзень\",\"%s дні\",\"%s дзён\"),w=v.bind(null,\"тыдзень\",\"%s тыдзень\",\"%s тыдні\",\"%s тыдняў\"),_=v.bind(null,\"месяц\",\"%s месяц\",\"%s месяцы\",\"%s месяцаў\"),q=v.bind(null,\"год\",\"%s год\",\"%s гады\",\"%s гадоў\");function M(s){var n=[\"۰\",\"۱\",\"۲\",\"۳\",\"۴\",\"۵\",\"۶\",\"۷\",\"۸\",\"۹\"];return s.toString().replace(/\\d/g,function(s){return n[s]})}var S=[[\"w tej chwili\",\"za chwilę\"],[\"%s sekund temu\",\"za %s sekund\"],[\"1 minutę temu\",\"za 1 minutę\"],[\"%s minut temu\",\"za %s minut\"],[\"1 godzinę temu\",\"za 1 godzinę\"],[\"%s godzin temu\",\"za %s godzin\"],[\"1 dzień temu\",\"za 1 dzień\"],[\"%s dni temu\",\"za %s dni\"],[\"1 tydzień temu\",\"za 1 tydzień\"],[\"%s tygodni temu\",\"za %s tygodni\"],[\"1 miesiąc temu\",\"za 1 miesiąc\"],[\"%s miesięcy temu\",\"za %s miesięcy\"],[\"1 rok temu\",\"za 1 rok\"],[\"%s lat temu\",\"za %s lat\"],[\"%s sekundy temu\",\"za %s sekundy\"],[\"%s minuty temu\",\"za %s minuty\"],[\"%s godziny temu\",\"za %s godziny\"],[\"%s dni temu\",\"za %s dni\"],[\"%s tygodnie temu\",\"za %s tygodnie\"],[\"%s miesiące temu\",\"za %s miesiące\"],[\"%s lata temu\",\"za %s lata\"]];function T(s,n,e,a,t){var u=t%10,r=a;return 1===t?r=s:1==u&&20<t?r=n:1<u&&u<5&&(20<t||t<10)&&(r=e),r}var N=T.bind(null,\"секунду\",\"%s секунду\",\"%s секунды\",\"%s секунд\"),x=T.bind(null,\"минуту\",\"%s минуту\",\"%s минуты\",\"%s минут\"),D=T.bind(null,\"час\",\"%s час\",\"%s часа\",\"%s часов\"),I=T.bind(null,\"день\",\"%s день\",\"%s дня\",\"%s дней\"),O=T.bind(null,\"неделю\",\"%s неделю\",\"%s недели\",\"%s недель\"),W=T.bind(null,\"месяц\",\"%s месяц\",\"%s месяца\",\"%s месяцев\"),$=T.bind(null,\"год\",\"%s год\",\"%s года\",\"%s лет\");function J(s,n,e,a,t){var u=t%10,r=t%100;return 1==t?s:1==u&&11!=r?n:2<=u&&u<=4&&!(12<=r&&r<=14)?e:a}var U=J.bind(null,\"1 секунд\",\"%s секунд\",\"%s секунде\",\"%s секунди\"),A=J.bind(null,\"1 минут\",\"%s минут\",\"%s минуте\",\"%s минута\"),C=J.bind(null,\"сат времена\",\"%s сат\",\"%s сата\",\"%s сати\"),E=J.bind(null,\"1 дан\",\"%s дан\",\"%s дана\",\"%s дана\"),B=J.bind(null,\"недељу дана\",\"%s недељу\",\"%s недеље\",\"%s недеља\"),P=J.bind(null,\"месец дана\",\"%s месец\",\"%s месеца\",\"%s месеци\"),R=J.bind(null,\"годину дана\",\"%s годину\",\"%s године\",\"%s година\");function Z(s,n,e,a,t){var u=t%10,r=a;return 1===t?r=s:1==u&&20<t?r=n:1<u&&u<5&&(20<t||t<10)&&(r=e),r}var F=Z.bind(null,\"секунду\",\"%s секунду\",\"%s секунди\",\"%s секунд\"),G=Z.bind(null,\"хвилину\",\"%s хвилину\",\"%s хвилини\",\"%s хвилин\"),H=Z.bind(null,\"годину\",\"%s годину\",\"%s години\",\"%s годин\"),K=Z.bind(null,\"день\",\"%s день\",\"%s дні\",\"%s днів\"),L=Z.bind(null,\"тиждень\",\"%s тиждень\",\"%s тиждні\",\"%s тижднів\"),Q=Z.bind(null,\"місяць\",\"%s місяць\",\"%s місяці\",\"%s місяців\"),V=Z.bind(null,\"рік\",\"%s рік\",\"%s роки\",\"%s років\");var X=Object.freeze({__proto__:null,ar:function(s,n){if(0===n)return[\"منذ لحظات\",\"بعد لحظات\"];var e=function(s,n){return n<3?b[s][n-1]:3<=n&&n<=10?b[s][2]:b[s][3]}(Math.floor(n/2),s);return[\"منذ \"+e,\"بعد \"+e]},be:function(s,n){switch(n){case 0:return[\"толькі што\",\"праз некалькі секунд\"];case 1:return[y(s)+\" таму\",\"праз \"+y(s)];case 2:case 3:return[k(s)+\" таму\",\"праз \"+k(s)];case 4:case 5:return[j(s)+\" таму\",\"праз \"+j(s)];case 6:case 7:return[z(s)+\" таму\",\"праз \"+z(s)];case 8:case 9:return[w(s)+\" таму\",\"праз \"+w(s)];case 10:case 11:return[_(s)+\" таму\",\"праз \"+_(s)];case 12:case 13:return[q(s)+\" таму\",\"праз \"+q(s)];default:return[\"\",\"\"]}},bg:function(s,n){return[[\"току що\",\"съвсем скоро\"],[\"преди %s секунди\",\"след %s секунди\"],[\"преди 1 минута\",\"след 1 минута\"],[\"преди %s минути\",\"след %s минути\"],[\"преди 1 час\",\"след 1 час\"],[\"преди %s часа\",\"след %s часа\"],[\"преди 1 ден\",\"след 1 ден\"],[\"преди %s дни\",\"след %s дни\"],[\"преди 1 седмица\",\"след 1 седмица\"],[\"преди %s седмици\",\"след %s седмици\"],[\"преди 1 месец\",\"след 1 месец\"],[\"преди %s месеца\",\"след %s месеца\"],[\"преди 1 година\",\"след 1 година\"],[\"преди %s години\",\"след %s години\"]][n]},bn_IN:function(s,n){return[[\"এইমাত্র\",\"একটা সময়\"],[\"%s সেকেন্ড আগে\",\"%s এর সেকেন্ডের মধ্যে\"],[\"1 মিনিট আগে\",\"1 মিনিটে\"],[\"%s এর মিনিট আগে\",\"%s এর মিনিটের মধ্যে\"],[\"1 ঘন্টা আগে\",\"1 ঘন্টা\"],[\"%s ঘণ্টা আগে\",\"%s এর ঘন্টার মধ্যে\"],[\"1 দিন আগে\",\"1 দিনের মধ্যে\"],[\"%s এর দিন আগে\",\"%s এর দিন\"],[\"1 সপ্তাহ আগে\",\"1 সপ্তাহের মধ্যে\"],[\"%s এর সপ্তাহ আগে\",\"%s সপ্তাহের মধ্যে\"],[\"1 মাস আগে\",\"1 মাসে\"],[\"%s মাস আগে\",\"%s মাসে\"],[\"1 বছর আগে\",\"1 বছরের মধ্যে\"],[\"%s বছর আগে\",\"%s বছরে\"]][n]},ca:function(s,n){return[[\"fa un moment\",\"d'aquí un moment\"],[\"fa %s segons\",\"d'aquí %s segons\"],[\"fa 1 minut\",\"d'aquí 1 minut\"],[\"fa %s minuts\",\"d'aquí %s minuts\"],[\"fa 1 hora\",\"d'aquí 1 hora\"],[\"fa %s hores\",\"d'aquí %s hores\"],[\"fa 1 dia\",\"d'aquí 1 dia\"],[\"fa %s dies\",\"d'aquí %s dies\"],[\"fa 1 setmana\",\"d'aquí 1 setmana\"],[\"fa %s setmanes\",\"d'aquí %s setmanes\"],[\"fa 1 mes\",\"d'aquí 1 mes\"],[\"fa %s mesos\",\"d'aquí %s mesos\"],[\"fa 1 any\",\"d'aquí 1 any\"],[\"fa %s anys\",\"d'aquí %s anys\"]][n]},de:function(s,n){return[[\"gerade eben\",\"vor einer Weile\"],[\"vor %s Sekunden\",\"in %s Sekunden\"],[\"vor 1 Minute\",\"in 1 Minute\"],[\"vor %s Minuten\",\"in %s Minuten\"],[\"vor 1 Stunde\",\"in 1 Stunde\"],[\"vor %s Stunden\",\"in %s Stunden\"],[\"vor 1 Tag\",\"in 1 Tag\"],[\"vor %s Tagen\",\"in %s Tagen\"],[\"vor 1 Woche\",\"in 1 Woche\"],[\"vor %s Wochen\",\"in %s Wochen\"],[\"vor 1 Monat\",\"in 1 Monat\"],[\"vor %s Monaten\",\"in %s Monaten\"],[\"vor 1 Jahr\",\"in 1 Jahr\"],[\"vor %s Jahren\",\"in %s Jahren\"]][n]},el:function(s,n){return[[\"μόλις τώρα\",\"σε λίγο\"],[\"%s δευτερόλεπτα πριν\",\"σε %s δευτερόλεπτα\"],[\"1 λεπτό πριν\",\"σε 1 λεπτό\"],[\"%s λεπτά πριν\",\"σε %s λεπτά\"],[\"1 ώρα πριν\",\"σε 1 ώρα\"],[\"%s ώρες πριν\",\"σε %s ώρες\"],[\"1 μέρα πριν\",\"σε 1 μέρα\"],[\"%s μέρες πριν\",\"σε %s μέρες\"],[\"1 εβδομάδα πριν\",\"σε 1 εβδομάδα\"],[\"%s εβδομάδες πριν\",\"σε %s εβδομάδες\"],[\"1 μήνα πριν\",\"σε 1 μήνα\"],[\"%s μήνες πριν\",\"σε %s μήνες\"],[\"1 χρόνο πριν\",\"σε 1 χρόνο\"],[\"%s χρόνια πριν\",\"σε %s χρόνια\"]][n]},en_short:function(s,n){return[[\"just now\",\"right now\"],[\"%ss ago\",\"in %ss\"],[\"1m ago\",\"in 1m\"],[\"%sm ago\",\"in %sm\"],[\"1h ago\",\"in 1h\"],[\"%sh ago\",\"in %sh\"],[\"1d ago\",\"in 1d\"],[\"%sd ago\",\"in %sd\"],[\"1w ago\",\"in 1w\"],[\"%sw ago\",\"in %sw\"],[\"1mo ago\",\"in 1mo\"],[\"%smo ago\",\"in %smo\"],[\"1yr ago\",\"in 1yr\"],[\"%syr ago\",\"in %syr\"]][n]},en_US:n,es:function(s,n){return[[\"justo ahora\",\"en un rato\"],[\"hace %s segundos\",\"en %s segundos\"],[\"hace 1 minuto\",\"en 1 minuto\"],[\"hace %s minutos\",\"en %s minutos\"],[\"hace 1 hora\",\"en 1 hora\"],[\"hace %s horas\",\"en %s horas\"],[\"hace 1 día\",\"en 1 día\"],[\"hace %s días\",\"en %s días\"],[\"hace 1 semana\",\"en 1 semana\"],[\"hace %s semanas\",\"en %s semanas\"],[\"hace 1 mes\",\"en 1 mes\"],[\"hace %s meses\",\"en %s meses\"],[\"hace 1 año\",\"en 1 año\"],[\"hace %s años\",\"en %s años\"]][n]},eu:function(s,n){return[[\"orain\",\"denbora bat barru\"],[\"duela %s segundu\",\"%s segundu barru\"],[\"duela minutu 1\",\"minutu 1 barru\"],[\"duela %s minutu\",\"%s minutu barru\"],[\"duela ordu 1\",\"ordu 1 barru\"],[\"duela %s ordu\",\"%s ordu barru\"],[\"duela egun 1\",\"egun 1 barru\"],[\"duela %s egun\",\"%s egun barru\"],[\"duela aste 1\",\"aste 1 barru\"],[\"duela %s aste\",\"%s aste barru\"],[\"duela hillabete 1\",\"hillabete 1 barru\"],[\"duela %s hillabete\",\"%s hillabete barru\"],[\"duela urte 1\",\"urte 1 barru\"],[\"duela %s urte\",\"%s urte barru\"]][n]},fa:function(s,n){var e=[[\"لحظاتی پیش\",\"همین حالا\"],[\"%s ثانیه پیش\",\"%s ثانیه دیگر\"],[\"۱ دقیقه پیش\",\"۱ دقیقه دیگر\"],[\"%s دقیقه پیش\",\"%s دقیقه دیگر\"],[\"۱ ساعت پیش\",\"۱ ساعت دیگر\"],[\"%s ساعت پیش\",\"%s ساعت دیگر\"],[\"۱ روز پیش\",\"۱ روز دیگر\"],[\"%s روز پیش\",\"%s روز دیگر\"],[\"۱ هفته پیش\",\"۱ هفته دیگر\"],[\"%s هفته پیش\",\"%s هفته دیگر\"],[\"۱ ماه پیش\",\"۱ ماه دیگر\"],[\"%s ماه پیش\",\"%s ماه دیگر\"],[\"۱ سال پیش\",\"۱ سال دیگر\"],[\"%s سال پیش\",\"%s سال دیگر\"]][n];return[e[0].replace(\"%s\",M(s)),e[1].replace(\"%s\",M(s))]},fi:function(s,n){return[[\"juuri äsken\",\"juuri nyt\"],[\"%s sekuntia sitten\",\"%s sekunnin päästä\"],[\"minuutti sitten\",\"minuutin päästä\"],[\"%s minuuttia sitten\",\"%s minuutin päästä\"],[\"tunti sitten\",\"tunnin päästä\"],[\"%s tuntia sitten\",\"%s tunnin päästä\"],[\"päivä sitten\",\"päivän päästä\"],[\"%s päivää sitten\",\"%s päivän päästä\"],[\"viikko sitten\",\"viikon päästä\"],[\"%s viikkoa sitten\",\"%s viikon päästä\"],[\"kuukausi sitten\",\"kuukauden päästä\"],[\"%s kuukautta sitten\",\"%s kuukauden päästä\"],[\"vuosi sitten\",\"vuoden päästä\"],[\"%s vuotta sitten\",\"%s vuoden päästä\"]][n]},fr:function(s,n){return[[\"à l'instant\",\"dans un instant\"],[\"il y a %s secondes\",\"dans %s secondes\"],[\"il y a 1 minute\",\"dans 1 minute\"],[\"il y a %s minutes\",\"dans %s minutes\"],[\"il y a 1 heure\",\"dans 1 heure\"],[\"il y a %s heures\",\"dans %s heures\"],[\"il y a 1 jour\",\"dans 1 jour\"],[\"il y a %s jours\",\"dans %s jours\"],[\"il y a 1 semaine\",\"dans 1 semaine\"],[\"il y a %s semaines\",\"dans %s semaines\"],[\"il y a 1 mois\",\"dans 1 mois\"],[\"il y a %s mois\",\"dans %s mois\"],[\"il y a 1 an\",\"dans 1 an\"],[\"il y a %s ans\",\"dans %s ans\"]][n]},gl:function(s,n){return[[\"xusto agora\",\"daquí a un pouco\"],[\"hai %s segundos\",\"en %s segundos\"],[\"hai 1 minuto\",\"nun minuto\"],[\"hai %s minutos\",\"en %s minutos\"],[\"hai 1 hora\",\"nunha hora\"],[\"hai %s horas\",\"en %s horas\"],[\"hai 1 día\",\"nun día\"],[\"hai %s días\",\"en %s días\"],[\"hai 1 semana\",\"nunha semana\"],[\"hai %s semanas\",\"en %s semanas\"],[\"hai 1 mes\",\"nun mes\"],[\"hai %s meses\",\"en %s meses\"],[\"hai 1 ano\",\"nun ano\"],[\"hai %s anos\",\"en %s anos\"]][n]},he:function(s,n){return[[\"זה עתה\",\"עכשיו\"],[\"לפני %s שניות\",\"בעוד %s שניות\"],[\"לפני דקה\",\"בעוד דקה\"],[\"לפני %s דקות\",\"בעוד %s דקות\"],[\"לפני שעה\",\"בעוד שעה\"],2===s?[\"לפני שעתיים\",\"בעוד שעתיים\"]:[\"לפני %s שעות\",\"בעוד %s שעות\"],[\"אתמול\",\"מחר\"],2===s?[\"לפני יומיים\",\"בעוד יומיים\"]:[\"לפני %s ימים\",\"בעוד %s ימים\"],[\"לפני שבוע\",\"בעוד שבוע\"],2===s?[\"לפני שבועיים\",\"בעוד שבועיים\"]:[\"לפני %s שבועות\",\"בעוד %s שבועות\"],[\"לפני חודש\",\"בעוד חודש\"],2===s?[\"לפני חודשיים\",\"בעוד חודשיים\"]:[\"לפני %s חודשים\",\"בעוד %s חודשים\"],[\"לפני שנה\",\"בעוד שנה\"],2===s?[\"לפני שנתיים\",\"בעוד שנתיים\"]:[\"לפני %s שנים\",\"בעוד %s שנים\"]][n]},hi_IN:function(s,n){return[[\"अभी\",\"कुछ समय\"],[\"%s सेकंड पहले\",\"%s सेकंड में\"],[\"1 मिनट पहले\",\"1 मिनट में\"],[\"%s मिनट पहले\",\"%s मिनट में\"],[\"1 घंटे पहले\",\"1 घंटे में\"],[\"%s घंटे पहले\",\"%s घंटे में\"],[\"1 दिन पहले\",\"1 दिन में\"],[\"%s दिन पहले\",\"%s दिनों में\"],[\"1 सप्ताह पहले\",\"1 सप्ताह में\"],[\"%s हफ्ते पहले\",\"%s हफ्तों में\"],[\"1 महीने पहले\",\"1 महीने में\"],[\"%s महीने पहले\",\"%s महीनों में\"],[\"1 साल पहले\",\"1 साल में\"],[\"%s साल पहले\",\"%s साल में\"]][n]},hu:function(s,n){return[[\"éppen most\",\"éppen most\"],[\"%s másodperce\",\"%s másodpercen belül\"],[\"1 perce\",\"1 percen belül\"],[\"%s perce\",\"%s percen belül\"],[\"1 órája\",\"1 órán belül\"],[\"%s órája\",\"%s órán belül\"],[\"1 napja\",\"1 napon belül\"],[\"%s napja\",\"%s napon belül\"],[\"1 hete\",\"1 héten belül\"],[\"%s hete\",\"%s héten belül\"],[\"1 hónapja\",\"1 hónapon belül\"],[\"%s hónapja\",\"%s hónapon belül\"],[\"1 éve\",\"1 éven belül\"],[\"%s éve\",\"%s éven belül\"]][n]},id_ID:function(s,n){return[[\"baru saja\",\"sebentar\"],[\"%s detik yang lalu\",\"dalam %s detik\"],[\"1 menit yang lalu\",\"dalam 1 menit\"],[\"%s menit yang lalu\",\"dalam %s menit\"],[\"1 jam yang lalu\",\"dalam 1 jam\"],[\"%s jam yang lalu\",\"dalam %s jam\"],[\"1 hari yang lalu\",\"dalam 1 hari\"],[\"%s hari yang lalu\",\"dalam %s hari\"],[\"1 minggu yang lalu\",\"dalam 1 minggu\"],[\"%s minggu yang lalu\",\"dalam %s minggu\"],[\"1 bulan yang lalu\",\"dalam 1 bulan\"],[\"%s bulan yang lalu\",\"dalam %s bulan\"],[\"1 tahun yang lalu\",\"dalam 1 tahun\"],[\"%s tahun yang lalu\",\"dalam %s tahun\"]][n]},it:function(s,n){return[[\"poco fa\",\"fra poco\"],[\"%s secondi fa\",\"fra %s secondi\"],[\"un minuto fa\",\"fra un minuto\"],[\"%s minuti fa\",\"fra %s minuti\"],[\"un'ora fa\",\"fra un'ora\"],[\"%s ore fa\",\"fra %s ore\"],[\"un giorno fa\",\"fra un giorno\"],[\"%s giorni fa\",\"fra %s giorni\"],[\"una settimana fa\",\"fra una settimana\"],[\"%s settimane fa\",\"fra %s settimane\"],[\"un mese fa\",\"fra un mese\"],[\"%s mesi fa\",\"fra %s mesi\"],[\"un anno fa\",\"fra un anno\"],[\"%s anni fa\",\"fra %s anni\"]][n]},ja:function(s,n){return[[\"すこし前\",\"すぐに\"],[\"%s秒前\",\"%s秒以内\"],[\"1分前\",\"1分以内\"],[\"%s分前\",\"%s分以内\"],[\"1時間前\",\"1時間以内\"],[\"%s時間前\",\"%s時間以内\"],[\"1日前\",\"1日以内\"],[\"%s日前\",\"%s日以内\"],[\"1週間前\",\"1週間以内\"],[\"%s週間前\",\"%s週間以内\"],[\"1ヶ月前\",\"1ヶ月以内\"],[\"%sヶ月前\",\"%sヶ月以内\"],[\"1年前\",\"1年以内\"],[\"%s年前\",\"%s年以内\"]][n]},ko:function(s,n){return[[\"방금\",\"곧\"],[\"%s초 전\",\"%s초 후\"],[\"1분 전\",\"1분 후\"],[\"%s분 전\",\"%s분 후\"],[\"1시간 전\",\"1시간 후\"],[\"%s시간 전\",\"%s시간 후\"],[\"1일 전\",\"1일 후\"],[\"%s일 전\",\"%s일 후\"],[\"1주일 전\",\"1주일 후\"],[\"%s주일 전\",\"%s주일 후\"],[\"1개월 전\",\"1개월 후\"],[\"%s개월 전\",\"%s개월 후\"],[\"1년 전\",\"1년 후\"],[\"%s년 전\",\"%s년 후\"]][n]},ml:function(s,n){return[[\"ഇപ്പോള്‍\",\"കുറച്ചു മുന്‍പ്\"],[\"%s സെക്കന്റ്‌കള്‍ക്ക് മുന്‍പ്\",\"%s സെക്കന്റില്‍\"],[\"1 മിനിറ്റിനു മുന്‍പ്\",\"1 മിനിറ്റില്‍\"],[\"%s മിനിറ്റുകള്‍ക്ക് മുന്‍പ\",\"%s മിനിറ്റില്‍\"],[\"1 മണിക്കൂറിനു മുന്‍പ്\",\"1 മണിക്കൂറില്‍\"],[\"%s മണിക്കൂറുകള്‍ക്കു മുന്‍പ്\",\"%s മണിക്കൂറില്‍\"],[\"1 ഒരു ദിവസം മുന്‍പ്\",\"1 ദിവസത്തില്‍\"],[\"%s ദിവസങ്ങള്‍ക് മുന്‍പ്\",\"%s ദിവസങ്ങള്‍ക്കുള്ളില്‍\"],[\"1 ആഴ്ച മുന്‍പ്\",\"1 ആഴ്ചയില്‍\"],[\"%s ആഴ്ചകള്‍ക്ക് മുന്‍പ്\",\"%s ആഴ്ചകള്‍ക്കുള്ളില്‍\"],[\"1 മാസത്തിനു മുന്‍പ്\",\"1 മാസത്തിനുള്ളില്‍\"],[\"%s മാസങ്ങള്‍ക്ക് മുന്‍പ്\",\"%s മാസങ്ങള്‍ക്കുള്ളില്‍\"],[\"1 വര്‍ഷത്തിനു  മുന്‍പ്\",\"1 വര്‍ഷത്തിനുള്ളില്‍\"],[\"%s വര്‍ഷങ്ങള്‍ക്കു മുന്‍പ്\",\"%s വര്‍ഷങ്ങള്‍ക്കുല്ല്ളില്‍\"]][n]},my:function(s,n){return[[\"ယခုအတွင်း\",\"ယခု\"],[\"%s စက္ကန့် အကြာက\",\"%s စက္ကန့်အတွင်း\"],[\"1 မိနစ် အကြာက\",\"1 မိနစ်အတွင်း\"],[\"%s မိနစ် အကြာက\",\"%s မိနစ်အတွင်း\"],[\"1 နာရီ အကြာက\",\"1 နာရီအတွင်း\"],[\"%s နာရီ အကြာက\",\"%s နာရီအတွင်း\"],[\"1 ရက် အကြာက\",\"1 ရက်အတွင်း\"],[\"%s ရက် အကြာက\",\"%s ရက်အတွင်း\"],[\"1 ပတ် အကြာက\",\"1 ပတ်အတွင်း\"],[\"%s ပတ် အကြာက\",\"%s ပတ်အတွင်း\"],[\"1 လ အကြာက\",\"1 လအတွင်း\"],[\"%s လ အကြာက\",\"%s လအတွင်း\"],[\"1 နှစ် အကြာက\",\"1 နှစ်အတွင်း\"],[\"%s နှစ် အကြာက\",\"%s နှစ်အတွင်း\"]][n]},nb_NO:function(s,n){return[[\"akkurat nå\",\"om litt\"],[\"%s sekunder siden\",\"om %s sekunder\"],[\"1 minutt siden\",\"om 1 minutt\"],[\"%s minutter siden\",\"om %s minutter\"],[\"1 time siden\",\"om 1 time\"],[\"%s timer siden\",\"om %s timer\"],[\"1 dag siden\",\"om 1 dag\"],[\"%s dager siden\",\"om %s dager\"],[\"1 uke siden\",\"om 1 uke\"],[\"%s uker siden\",\"om %s uker\"],[\"1 måned siden\",\"om 1 måned\"],[\"%s måneder siden\",\"om %s måneder\"],[\"1 år siden\",\"om 1 år\"],[\"%s år siden\",\"om %s år\"]][n]},nl:function(s,n){return[[\"recent\",\"binnenkort\"],[\"%s seconden geleden\",\"binnen %s seconden\"],[\"1 minuut geleden\",\"binnen 1 minuut\"],[\"%s minuten geleden\",\"binnen %s minuten\"],[\"1 uur geleden\",\"binnen 1 uur\"],[\"%s uur geleden\",\"binnen %s uur\"],[\"1 dag geleden\",\"binnen 1 dag\"],[\"%s dagen geleden\",\"binnen %s dagen\"],[\"1 week geleden\",\"binnen 1 week\"],[\"%s weken geleden\",\"binnen %s weken\"],[\"1 maand geleden\",\"binnen 1 maand\"],[\"%s maanden geleden\",\"binnen %s maanden\"],[\"1 jaar geleden\",\"binnen 1 jaar\"],[\"%s jaar geleden\",\"binnen %s jaar\"]][n]},nn_NO:function(s,n){return[[\"nett no\",\"om litt\"],[\"%s sekund sidan\",\"om %s sekund\"],[\"1 minutt sidan\",\"om 1 minutt\"],[\"%s minutt sidan\",\"om %s minutt\"],[\"1 time sidan\",\"om 1 time\"],[\"%s timar sidan\",\"om %s timar\"],[\"1 dag sidan\",\"om 1 dag\"],[\"%s dagar sidan\",\"om %s dagar\"],[\"1 veke sidan\",\"om 1 veke\"],[\"%s veker sidan\",\"om %s veker\"],[\"1 månad sidan\",\"om 1 månad\"],[\"%s månadar sidan\",\"om %s månadar\"],[\"1 år sidan\",\"om 1 år\"],[\"%s år sidan\",\"om %s år\"]][n]},pl:function(s,n){return S[1&n?4<s%10||s%10<2||1==~~(s/10)%10?n:++n/2+13:n]},pt_BR:function(s,n){return[[\"agora mesmo\",\"agora\"],[\"há %s segundos\",\"em %s segundos\"],[\"há um minuto\",\"em um minuto\"],[\"há %s minutos\",\"em %s minutos\"],[\"há uma hora\",\"em uma hora\"],[\"há %s horas\",\"em %s horas\"],[\"há um dia\",\"em um dia\"],[\"há %s dias\",\"em %s dias\"],[\"há uma semana\",\"em uma semana\"],[\"há %s semanas\",\"em %s semanas\"],[\"há um mês\",\"em um mês\"],[\"há %s meses\",\"em %s meses\"],[\"há um ano\",\"em um ano\"],[\"há %s anos\",\"em %s anos\"]][n]},ro:function(s,n){var e=[[\"chiar acum\",\"chiar acum\"],[\"acum %s secunde\",\"peste %s secunde\"],[\"acum un minut\",\"peste un minut\"],[\"acum %s minute\",\"peste %s minute\"],[\"acum o oră\",\"peste o oră\"],[\"acum %s ore\",\"peste %s ore\"],[\"acum o zi\",\"peste o zi\"],[\"acum %s zile\",\"peste %s zile\"],[\"acum o săptămână\",\"peste o săptămână\"],[\"acum %s săptămâni\",\"peste %s săptămâni\"],[\"acum o lună\",\"peste o lună\"],[\"acum %s luni\",\"peste %s luni\"],[\"acum un an\",\"peste un an\"],[\"acum %s ani\",\"peste %s ani\"]];return s<20?e[n]:[e[n][0].replace(\"%s\",\"%s de\"),e[n][1].replace(\"%s\",\"%s de\")]},ru:function(s,n){switch(n){case 0:return[\"только что\",\"через несколько секунд\"];case 1:return[N(s)+\" назад\",\"через \"+N(s)];case 2:case 3:return[x(s)+\" назад\",\"через \"+x(s)];case 4:case 5:return[D(s)+\" назад\",\"через \"+D(s)];case 6:return[\"вчера\",\"завтра\"];case 7:return[I(s)+\" назад\",\"через \"+I(s)];case 8:case 9:return[O(s)+\" назад\",\"через \"+O(s)];case 10:case 11:return[W(s)+\" назад\",\"через \"+W(s)];case 12:case 13:return[$(s)+\" назад\",\"через \"+$(s)];default:return[\"\",\"\"]}},sq:function(s,n){return[[\"pak më parë\",\"pas pak\"],[\"para %s sekondash\",\"pas %s sekondash\"],[\"para një minute\",\"pas një minute\"],[\"para %s minutash\",\"pas %s minutash\"],[\"para një ore\",\"pas një ore\"],[\"para %s orësh\",\"pas %s orësh\"],[\"dje\",\"nesër\"],[\"para %s ditësh\",\"pas %s ditësh\"],[\"para një jave\",\"pas një jave\"],[\"para %s javësh\",\"pas %s javësh\"],[\"para një muaji\",\"pas një muaji\"],[\"para %s muajsh\",\"pas %s muajsh\"],[\"para një viti\",\"pas një viti\"],[\"para %s vjetësh\",\"pas %s vjetësh\"]][n]},sr:function(s,n){switch(n){case 0:return[\"малопре\",\"управо сад\"];case 1:return[\"пре \"+U(s),\"за \"+U(s)];case 2:case 3:return[\"пре \"+A(s),\"за \"+A(s)];case 4:case 5:return[\"пре \"+C(s),\"за \"+C(s)];case 6:case 7:return[\"пре \"+E(s),\"за \"+E(s)];case 8:case 9:return[\"пре \"+B(s),\"за \"+B(s)];case 10:case 11:return[\"пре \"+P(s),\"за \"+P(s)];case 12:case 13:return[\"пре \"+R(s),\"за \"+R(s)];default:return[\"\",\"\"]}},sv:function(s,n){return[[\"just nu\",\"om en stund\"],[\"%s sekunder sedan\",\"om %s sekunder\"],[\"1 minut sedan\",\"om 1 minut\"],[\"%s minuter sedan\",\"om %s minuter\"],[\"1 timme sedan\",\"om 1 timme\"],[\"%s timmar sedan\",\"om %s timmar\"],[\"1 dag sedan\",\"om 1 dag\"],[\"%s dagar sedan\",\"om %s dagar\"],[\"1 vecka sedan\",\"om 1 vecka\"],[\"%s veckor sedan\",\"om %s veckor\"],[\"1 månad sedan\",\"om 1 månad\"],[\"%s månader sedan\",\"om %s månader\"],[\"1 år sedan\",\"om 1 år\"],[\"%s år sedan\",\"om %s år\"]][n]},ta:function(s,n){return[[\"இப்போது\",\"சற்று நேரம் முன்பு\"],[\"%s நொடிக்கு முன்\",\"%s நொடிகளில்\"],[\"1 நிமிடத்திற்க்கு முன்\",\"1 நிமிடத்தில்\"],[\"%s நிமிடத்திற்க்கு முன்\",\"%s நிமிடங்களில்\"],[\"1 மணி நேரத்திற்கு முன்\",\"1 மணி நேரத்திற்குள்\"],[\"%s மணி நேரத்திற்கு முன்\",\"%s மணி நேரத்திற்குள்\"],[\"1 நாளுக்கு முன்\",\"1 நாளில்\"],[\"%s நாட்களுக்கு முன்\",\"%s நாட்களில்\"],[\"1 வாரத்திற்கு முன்\",\"1 வாரத்தில்\"],[\"%s வாரங்களுக்கு முன்\",\"%s வாரங்களில்\"],[\"1 மாதத்திற்கு முன்\",\"1 மாதத்தில்\"],[\"%s மாதங்களுக்கு முன்\",\"%s மாதங்களில்\"],[\"1 வருடத்திற்கு முன்\",\"1 வருடத்தில்\"],[\"%s வருடங்களுக்கு முன்\",\"%s வருடங்களில்\"]][n]},th:function(s,n){return[[\"เมื่อสักครู่นี้\",\"อีกสักครู่\"],[\"%s วินาทีที่แล้ว\",\"ใน %s วินาที\"],[\"1 นาทีที่แล้ว\",\"ใน 1 นาที\"],[\"%s นาทีที่แล้ว\",\"ใน %s นาที\"],[\"1 ชั่วโมงที่แล้ว\",\"ใน 1 ชั่วโมง\"],[\"%s ชั่วโมงที่แล้ว\",\"ใน %s ชั่วโมง\"],[\"1 วันที่แล้ว\",\"ใน 1 วัน\"],[\"%s วันที่แล้ว\",\"ใน %s วัน\"],[\"1 อาทิตย์ที่แล้ว\",\"ใน 1 อาทิตย์\"],[\"%s อาทิตย์ที่แล้ว\",\"ใน %s อาทิตย์\"],[\"1 เดือนที่แล้ว\",\"ใน 1 เดือน\"],[\"%s เดือนที่แล้ว\",\"ใน %s เดือน\"],[\"1 ปีที่แล้ว\",\"ใน 1 ปี\"],[\"%s ปีที่แล้ว\",\"ใน %s ปี\"]][n]},tr:function(s,n){return[[\"az önce\",\"şimdi\"],[\"%s saniye önce\",\"%s saniye içinde\"],[\"1 dakika önce\",\"1 dakika içinde\"],[\"%s dakika önce\",\"%s dakika içinde\"],[\"1 saat önce\",\"1 saat içinde\"],[\"%s saat önce\",\"%s saat içinde\"],[\"1 gün önce\",\"1 gün içinde\"],[\"%s gün önce\",\"%s gün içinde\"],[\"1 hafta önce\",\"1 hafta içinde\"],[\"%s hafta önce\",\"%s hafta içinde\"],[\"1 ay önce\",\"1 ay içinde\"],[\"%s ay önce\",\"%s ay içinde\"],[\"1 yıl önce\",\"1 yıl içinde\"],[\"%s yıl önce\",\"%s yıl içinde\"]][n]},uk:function(s,n){switch(n){case 0:return[\"щойно\",\"через декілька секунд\"];case 1:return[F(s)+\" тому\",\"через \"+F(s)];case 2:case 3:return[G(s)+\" тому\",\"через \"+G(s)];case 4:case 5:return[H(s)+\" тому\",\"через \"+H(s)];case 6:case 7:return[K(s)+\" тому\",\"через \"+K(s)];case 8:case 9:return[L(s)+\" тому\",\"через \"+L(s)];case 10:case 11:return[Q(s)+\" тому\",\"через \"+Q(s)];case 12:case 13:return[V(s)+\" тому\",\"через \"+V(s)];default:return[\"\",\"\"]}},vi:function(s,n){return[[\"vừa xong\",\"một lúc\"],[\"%s giây trước\",\"trong %s giây\"],[\"1 phút trước\",\"trong 1 phút\"],[\"%s phút trước\",\"trong %s phút\"],[\"1 giờ trước\",\"trong 1 giờ\"],[\"%s giờ trước\",\"trong %s giờ\"],[\"1 ngày trước\",\"trong 1 ngày\"],[\"%s ngày trước\",\"trong %s ngày\"],[\"1 tuần trước\",\"trong 1 tuần\"],[\"%s tuần trước\",\"trong %s tuần\"],[\"1 tháng trước\",\"trong 1 tháng\"],[\"%s tháng trước\",\"trong %s tháng\"],[\"1 năm trước\",\"trong 1 năm\"],[\"%s năm trước\",\"trong %s năm\"]][n]},zh_CN:e,zh_TW:function(s,n){return[[\"剛剛\",\"片刻後\"],[\"%s 秒前\",\"%s 秒後\"],[\"1 分鐘前\",\"1 分鐘後\"],[\"%s 分鐘前\",\"%s 分鐘後\"],[\"1 小時前\",\"1 小時後\"],[\"%s 小時前\",\"%s 小時後\"],[\"1 天前\",\"1 天後\"],[\"%s 天前\",\"%s 天後\"],[\"1 週前\",\"1 週後\"],[\"%s 週前\",\"%s 週後\"],[\"1 個月前\",\"1 個月後\"],[\"%s 個月前\",\"%s 個月後\"],[\"1 年前\",\"1 年後\"],[\"%s 年前\",\"%s 年後\"]][n]}});Object.keys(X).forEach(function(s){u(s,X[s])}),s.cancel=function(s){s?f(h(s)):Object.keys(g).forEach(f)},s.format=function(s,n,e){return d(c(s,e&&e.relativeDate),r(n))},s.register=u,s.render=function(s,n,e){var a=s.length?s:[s];return a.forEach(function(s){p(s,function(s){return s.getAttribute(\"datetime\")}(s),r(n),e||{})}),a},Object.defineProperty(s,\"__esModule\",{value:!0})});`)\n\n// /favicon.png\nvar file2 = []byte{\n\t0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,\n\t0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20,\n\t0x08, 0x03, 0x00, 0x00, 0x00, 0x44, 0xa4, 0x8a, 0xc6, 0x00, 0x00, 0x00,\n\t0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61,\n\t0x05, 0x00, 0x00, 0x00, 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce,\n\t0x1c, 0xe9, 0x00, 0x00, 0x00, 0x20, 0x63, 0x48, 0x52, 0x4d, 0x00, 0x00,\n\t0x7a, 0x26, 0x00, 0x00, 0x80, 0x84, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x00,\n\t0x80, 0xe8, 0x00, 0x00, 0x75, 0x30, 0x00, 0x00, 0xea, 0x60, 0x00, 0x00,\n\t0x3a, 0x98, 0x00, 0x00, 0x17, 0x70, 0x9c, 0xba, 0x51, 0x3c, 0x00, 0x00,\n\t0x01, 0xd1, 0x50, 0x4c, 0x54, 0x45, 0x00, 0x00, 0x00, 0x19, 0x2d, 0x46,\n\t0x0e, 0x21, 0x47, 0x1a, 0x2e, 0x47, 0x19, 0x2d, 0x45, 0x14, 0x28, 0x41,\n\t0x19, 0x2c, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x47, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46, 0x19, 0x2d, 0x46,\n\t0xff, 0xff, 0xff, 0x7a, 0xc6, 0x05, 0xa1, 0x00, 0x00, 0x00, 0x99, 0x74,\n\t0x52, 0x4e, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x33,\n\t0x82, 0xb1, 0xd1, 0xf0, 0xfd, 0xef, 0xd0, 0xb0, 0x7f, 0x31, 0x04, 0x68,\n\t0xe4, 0xe0, 0x9f, 0x44, 0x01, 0x2c, 0xc1, 0xec, 0x96, 0x23, 0x4b, 0x75,\n\t0x0e, 0x29, 0xbf, 0xca, 0x42, 0x54, 0xe2, 0xfb, 0x8d, 0x0c, 0xde, 0x4d,\n\t0x49, 0xe1, 0xfa, 0x0d, 0xc0, 0xcd, 0x87, 0x64, 0x66, 0x79, 0xb8, 0xf4,\n\t0x41, 0x28, 0xce, 0x2a, 0x5b, 0x1a, 0xbb, 0xfe, 0xc9, 0x25, 0x99, 0xa4,\n\t0x92, 0x4a, 0x81, 0x02, 0x76, 0x39, 0xbd, 0xee, 0x43, 0xa2, 0x84, 0xd9,\n\t0x17, 0x4f, 0xf2, 0x97, 0x03, 0x35, 0xed, 0x3a, 0x8f, 0x0b, 0xb7, 0x34,\n\t0x83, 0xb9, 0x3b, 0x70, 0xa3, 0x02, 0x2b, 0xe8, 0x65, 0x73, 0xa1, 0x9c,\n\t0x37, 0x69, 0xaa, 0x20, 0xdc, 0x5d, 0x7d, 0xd2, 0x18, 0x9e, 0xd4, 0x9a,\n\t0xf8, 0x50, 0x1b, 0xcf, 0xa8, 0x30, 0xdd, 0xf1, 0xe7, 0xfc, 0xf9, 0x71,\n\t0x0a, 0x74, 0x80, 0x57, 0x3f, 0x94, 0x6f, 0xac, 0xa6, 0xc4, 0x32, 0xa0,\n\t0x2d, 0x46, 0x24, 0x3e, 0x48, 0x40, 0xc5, 0x91, 0x8c, 0x21, 0x3d, 0x6e,\n\t0x35, 0x08, 0xac, 0x2a, 0x00, 0x00, 0x00, 0x01, 0x62, 0x4b, 0x47, 0x44,\n\t0x9a, 0x98, 0xdf, 0x67, 0x12, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59,\n\t0x73, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x48, 0x00, 0x46, 0xc9,\n\t0x6b, 0x3e, 0x00, 0x00, 0x02, 0x02, 0x49, 0x44, 0x41, 0x54, 0x38, 0xcb,\n\t0x7d, 0x93, 0xf9, 0x43, 0x12, 0x41, 0x14, 0xc7, 0xe7, 0x9b, 0xd5, 0x22,\n\t0xb2, 0xa2, 0x0e, 0x66, 0x05, 0x86, 0x96, 0xb9, 0x88, 0x66, 0x98, 0x99,\n\t0x69, 0x42, 0x68, 0xd1, 0x25, 0xe6, 0x11, 0x66, 0x1e, 0xa5, 0xa1, 0x1d,\n\t0x9a, 0x78, 0x90, 0x28, 0x9d, 0xda, 0x69, 0x96, 0x65, 0xa9, 0x1d, 0xf3,\n\t0xdf, 0x3a, 0xb3, 0x17, 0x8b, 0x62, 0xef, 0x97, 0x99, 0x79, 0xef, 0xb3,\n\t0xf3, 0xde, 0xce, 0xf7, 0x3d, 0x42, 0x34, 0x03, 0x24, 0x5b, 0xbe, 0xbd,\n\t0xc0, 0x21, 0xcb, 0x85, 0xce, 0xa2, 0xe2, 0x12, 0x0a, 0xb2, 0xcb, 0x70,\n\t0xc0, 0x55, 0x2a, 0x33, 0xdd, 0xe4, 0x23, 0x65, 0x47, 0xb3, 0x10, 0xe0,\n\t0x18, 0x70, 0xdc, 0xcd, 0x2c, 0xe6, 0x29, 0x3f, 0x01, 0x64, 0xe2, 0xde,\n\t0x8a, 0x4a, 0xe0, 0xe4, 0x29, 0x2b, 0xc1, 0xaa, 0x4e, 0x1b, 0x04, 0x50,\n\t0xad, 0xf8, 0x6a, 0xfc, 0x7b, 0x88, 0xda, 0x3a, 0x8d, 0x00, 0xce, 0xd4,\n\t0x33, 0x76, 0xb6, 0x26, 0xc0, 0x89, 0x06, 0xf9, 0x5c, 0xa3, 0xeb, 0x7c,\n\t0xd3, 0x85, 0xe6, 0x8b, 0x2a, 0xd1, 0xa2, 0x12, 0x68, 0xbd, 0x24, 0x4e,\n\t0x2a, 0xd1, 0x16, 0x0c, 0xe5, 0x81, 0x7f, 0xe2, 0xf7, 0x5e, 0x0e, 0x73,\n\t0x5f, 0x7b, 0x07, 0x07, 0x40, 0xaf, 0x68, 0x37, 0x0a, 0x22, 0x53, 0x76,\n\t0xe8, 0xaa, 0xf0, 0x45, 0xf8, 0xbf, 0xe0, 0x5a, 0xa1, 0x9e, 0xd3, 0x77,\n\t0xfd, 0x06, 0x80, 0x0e, 0x57, 0xe3, 0xcd, 0x5b, 0x12, 0x10, 0xea, 0xe4,\n\t0xae, 0x68, 0x17, 0x08, 0xbd, 0x6d, 0x56, 0xd5, 0x7d, 0x10, 0xb4, 0xa9,\n\t0x87, 0xdf, 0xed, 0x89, 0xf4, 0x02, 0x7d, 0x77, 0xb8, 0x2b, 0xd6, 0x4f,\n\t0xee, 0x2a, 0x46, 0x7c, 0xe0, 0x1e, 0x10, 0xd4, 0xaf, 0x1b, 0x94, 0x30,\n\t0x34, 0xcc, 0x57, 0x65, 0x84, 0xdc, 0x0f, 0x1b, 0x80, 0x3b, 0x80, 0xa1,\n\t0x07, 0xc6, 0x3b, 0x8d, 0x02, 0x83, 0xe2, 0x4d, 0xc7, 0x88, 0xdd, 0xcc,\n\t0xf0, 0x30, 0x8e, 0xf1, 0x09, 0xe3, 0xf0, 0x08, 0x78, 0x2c, 0xd6, 0x27,\n\t0xa4, 0xc0, 0x04, 0x9e, 0x02, 0x93, 0x0e, 0xe3, 0x30, 0x05, 0x4c, 0x89,\n\t0xd5, 0x49, 0x4c, 0x17, 0x7b, 0x46, 0x31, 0x9d, 0x30, 0xe4, 0x9a, 0x01,\n\t0x66, 0xc5, 0xc6, 0x41, 0x4c, 0x09, 0xd9, 0xdc, 0x3c, 0xfa, 0x93, 0xfa,\n\t0xfe, 0x39, 0x97, 0x6a, 0x41, 0x25, 0x2d, 0x40, 0x6a, 0x11, 0x28, 0x51,\n\t0x5f, 0x95, 0x45, 0x83, 0xc0, 0x52, 0x5a, 0x03, 0x32, 0x29, 0xd8, 0x0b,\n\t0x2e, 0xd8, 0xcb, 0xd8, 0xab, 0xd7, 0x6f, 0xec, 0x5e, 0x0a, 0xb8, 0xde,\n\t0x6a, 0x29, 0x9c, 0x19, 0x60, 0x79, 0x25, 0x0e, 0xd0, 0xe9, 0xc9, 0xb6,\n\t0x77, 0x5c, 0x24, 0x8c, 0x37, 0x08, 0x5f, 0x9a, 0x14, 0x59, 0x04, 0x8e,\n\t0xbe, 0xf7, 0x6b, 0x72, 0x20, 0x4e, 0x75, 0xf5, 0x93, 0xe4, 0x43, 0xd8,\n\t0x42, 0xa4, 0xec, 0x1f, 0xe7, 0x29, 0xe2, 0x95, 0x9f, 0x3e, 0xe7, 0x07,\n\t0x54, 0x42, 0x5e, 0x25, 0xb6, 0xfa, 0xac, 0x2e, 0x49, 0xcd, 0x7d, 0x49,\n\t0xae, 0x7d, 0x75, 0xe8, 0xfd, 0xe1, 0x56, 0xbe, 0x11, 0xba, 0xce, 0x72,\n\t0x9a, 0x4a, 0x7c, 0x9f, 0x91, 0x08, 0x7e, 0x0c, 0xfc, 0x87, 0x10, 0xd5,\n\t0xd2, 0x18, 0xdb, 0x9f, 0x38, 0x24, 0x2a, 0xde, 0xa8, 0xda, 0x87, 0xf0,\n\t0x8d, 0x69, 0x3d, 0x89, 0xae, 0xda, 0xdc, 0x40, 0xe2, 0xa7, 0xd1, 0xd6,\n\t0x75, 0x39, 0x89, 0xc4, 0x2f, 0x73, 0x30, 0x0e, 0x6f, 0xb6, 0xef, 0x8d,\n\t0x6f, 0x8d, 0x5a, 0x47, 0x6b, 0x63, 0x3b, 0x9a, 0x1d, 0xf6, 0xfc, 0xfe,\n\t0x83, 0xac, 0xe9, 0x94, 0x5a, 0xca, 0x15, 0xcb, 0xf0, 0x46, 0xfe, 0x4a,\n\t0xbb, 0xe7, 0x1b, 0xd4, 0xb6, 0xba, 0xee, 0x14, 0xe3, 0x9f, 0x4e, 0xfe,\n\t0xb3, 0x8c, 0xff, 0x0e, 0x8e, 0xb4, 0x63, 0x2b, 0x88, 0xb0, 0x0f, 0x8b,\n\t0x00, 0x00, 0x00, 0x25, 0x74, 0x45, 0x58, 0x74, 0x64, 0x61, 0x74, 0x65,\n\t0x3a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x00, 0x32, 0x30, 0x31, 0x39,\n\t0x2d, 0x30, 0x32, 0x2d, 0x32, 0x37, 0x54, 0x31, 0x31, 0x3a, 0x35, 0x31,\n\t0x3a, 0x34, 0x34, 0x2b, 0x30, 0x30, 0x3a, 0x30, 0x30, 0x27, 0x6a, 0xa6,\n\t0x2a, 0x00, 0x00, 0x00, 0x25, 0x74, 0x45, 0x58, 0x74, 0x64, 0x61, 0x74,\n\t0x65, 0x3a, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x79, 0x00, 0x32, 0x30, 0x31,\n\t0x39, 0x2d, 0x30, 0x32, 0x2d, 0x32, 0x37, 0x54, 0x31, 0x31, 0x3a, 0x35,\n\t0x31, 0x3a, 0x34, 0x34, 0x2b, 0x30, 0x30, 0x3a, 0x30, 0x30, 0x56, 0x37,\n\t0x1e, 0x96, 0x00, 0x00, 0x00, 0x46, 0x74, 0x45, 0x58, 0x74, 0x73, 0x6f,\n\t0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x00, 0x49, 0x6d, 0x61, 0x67, 0x65,\n\t0x4d, 0x61, 0x67, 0x69, 0x63, 0x6b, 0x20, 0x36, 0x2e, 0x37, 0x2e, 0x38,\n\t0x2d, 0x39, 0x20, 0x32, 0x30, 0x31, 0x34, 0x2d, 0x30, 0x35, 0x2d, 0x31,\n\t0x32, 0x20, 0x51, 0x31, 0x36, 0x20, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f,\n\t0x2f, 0x77, 0x77, 0x77, 0x2e, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x6d, 0x61,\n\t0x67, 0x69, 0x63, 0x6b, 0x2e, 0x6f, 0x72, 0x67, 0xdc, 0x86, 0xed, 0x00,\n\t0x00, 0x00, 0x00, 0x18, 0x74, 0x45, 0x58, 0x74, 0x54, 0x68, 0x75, 0x6d,\n\t0x62, 0x3a, 0x3a, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x3a,\n\t0x3a, 0x50, 0x61, 0x67, 0x65, 0x73, 0x00, 0x31, 0xa7, 0xff, 0xbb, 0x2f,\n\t0x00, 0x00, 0x00, 0x18, 0x74, 0x45, 0x58, 0x74, 0x54, 0x68, 0x75, 0x6d,\n\t0x62, 0x3a, 0x3a, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x3a, 0x3a, 0x68, 0x65,\n\t0x69, 0x67, 0x68, 0x74, 0x00, 0x31, 0x39, 0x32, 0x0f, 0x00, 0x72, 0x85,\n\t0x00, 0x00, 0x00, 0x17, 0x74, 0x45, 0x58, 0x74, 0x54, 0x68, 0x75, 0x6d,\n\t0x62, 0x3a, 0x3a, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x3a, 0x3a, 0x57, 0x69,\n\t0x64, 0x74, 0x68, 0x00, 0x31, 0x39, 0x32, 0xd3, 0xac, 0x21, 0x08, 0x00,\n\t0x00, 0x00, 0x19, 0x74, 0x45, 0x58, 0x74, 0x54, 0x68, 0x75, 0x6d, 0x62,\n\t0x3a, 0x3a, 0x4d, 0x69, 0x6d, 0x65, 0x74, 0x79, 0x70, 0x65, 0x00, 0x69,\n\t0x6d, 0x61, 0x67, 0x65, 0x2f, 0x70, 0x6e, 0x67, 0x3f, 0xb2, 0x56, 0x4e,\n\t0x00, 0x00, 0x00, 0x17, 0x74, 0x45, 0x58, 0x74, 0x54, 0x68, 0x75, 0x6d,\n\t0x62, 0x3a, 0x3a, 0x4d, 0x54, 0x69, 0x6d, 0x65, 0x00, 0x31, 0x35, 0x35,\n\t0x31, 0x32, 0x36, 0x38, 0x33, 0x30, 0x34, 0xd7, 0x02, 0x40, 0x72, 0x00,\n\t0x00, 0x00, 0x0f, 0x74, 0x45, 0x58, 0x74, 0x54, 0x68, 0x75, 0x6d, 0x62,\n\t0x3a, 0x3a, 0x53, 0x69, 0x7a, 0x65, 0x00, 0x30, 0x42, 0x42, 0x94, 0xa2,\n\t0x3e, 0xec, 0x00, 0x00, 0x00, 0x56, 0x74, 0x45, 0x58, 0x74, 0x54, 0x68,\n\t0x75, 0x6d, 0x62, 0x3a, 0x3a, 0x55, 0x52, 0x49, 0x00, 0x66, 0x69, 0x6c,\n\t0x65, 0x3a, 0x2f, 0x2f, 0x2f, 0x6d, 0x6e, 0x74, 0x6c, 0x6f, 0x67, 0x2f,\n\t0x66, 0x61, 0x76, 0x69, 0x63, 0x6f, 0x6e, 0x73, 0x2f, 0x32, 0x30, 0x31,\n\t0x39, 0x2d, 0x30, 0x32, 0x2d, 0x32, 0x37, 0x2f, 0x36, 0x34, 0x32, 0x39,\n\t0x36, 0x31, 0x34, 0x63, 0x37, 0x61, 0x38, 0x30, 0x66, 0x37, 0x62, 0x35,\n\t0x63, 0x39, 0x62, 0x62, 0x36, 0x64, 0x30, 0x35, 0x35, 0x61, 0x64, 0x61,\n\t0x62, 0x61, 0x65, 0x65, 0x2e, 0x69, 0x63, 0x6f, 0x2e, 0x70, 0x6e, 0x67,\n\t0x2f, 0x36, 0x62, 0x9b, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44,\n\t0xae, 0x42, 0x60, 0x82,\n}\n\n// /icons/server-list-empty.svg\nvar file3 = []byte{\n\t0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n\t0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x73,\n\t0x76, 0x67, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3d, 0x22, 0x68, 0x74,\n\t0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x77, 0x33, 0x2e,\n\t0x6f, 0x72, 0x67, 0x2f, 0x32, 0x30, 0x30, 0x30, 0x2f, 0x73, 0x76, 0x67,\n\t0x22, 0x20, 0x76, 0x69, 0x65, 0x77, 0x42, 0x6f, 0x78, 0x3d, 0x22, 0x30,\n\t0x20, 0x30, 0x20, 0x38, 0x30, 0x20, 0x38, 0x30, 0x22, 0x20, 0x63, 0x6c,\n\t0x61, 0x73, 0x73, 0x3d, 0x22, 0x69, 0x63, 0x6f, 0x6e, 0x2d, 0x64, 0x72,\n\t0x6f, 0x6e, 0x65, 0x2d, 0x73, 0x6c, 0x65, 0x65, 0x70, 0x22, 0x20, 0x77,\n\t0x69, 0x64, 0x74, 0x68, 0x3d, 0x22, 0x38, 0x30, 0x70, 0x78, 0x22, 0x20,\n\t0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x3d, 0x22, 0x38, 0x30, 0x70, 0x78,\n\t0x22, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x6c, 0x69, 0x6e, 0x65,\n\t0x61, 0x72, 0x47, 0x72, 0x61, 0x64, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x69,\n\t0x64, 0x3d, 0x22, 0x53, 0x56, 0x47, 0x49, 0x44, 0x5f, 0x31, 0x5f, 0x22,\n\t0x20, 0x67, 0x72, 0x61, 0x64, 0x69, 0x65, 0x6e, 0x74, 0x55, 0x6e, 0x69,\n\t0x74, 0x73, 0x3d, 0x22, 0x75, 0x73, 0x65, 0x72, 0x53, 0x70, 0x61, 0x63,\n\t0x65, 0x4f, 0x6e, 0x55, 0x73, 0x65, 0x22, 0x20, 0x78, 0x31, 0x3d, 0x22,\n\t0x31, 0x32, 0x2e, 0x36, 0x30, 0x34, 0x22, 0x20, 0x79, 0x31, 0x3d, 0x22,\n\t0x36, 0x39, 0x2e, 0x38, 0x34, 0x35, 0x22, 0x20, 0x78, 0x32, 0x3d, 0x22,\n\t0x36, 0x39, 0x2e, 0x33, 0x39, 0x37, 0x22, 0x20, 0x79, 0x32, 0x3d, 0x22,\n\t0x31, 0x33, 0x2e, 0x30, 0x35, 0x32, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20,\n\t0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n\t0x20, 0x20, 0x20, 0x20, 0x67, 0x72, 0x61, 0x64, 0x69, 0x65, 0x6e, 0x74,\n\t0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x3d, 0x22, 0x6d,\n\t0x61, 0x74, 0x72, 0x69, 0x78, 0x28, 0x31, 0x20, 0x30, 0x20, 0x30, 0x20,\n\t0x2d, 0x31, 0x20, 0x30, 0x20, 0x38, 0x32, 0x2e, 0x32, 0x29, 0x22, 0x3e,\n\t0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x73, 0x74, 0x6f, 0x70,\n\t0x20, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x3d, 0x22, 0x30, 0x22, 0x20,\n\t0x73, 0x74, 0x6f, 0x70, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3d, 0x22,\n\t0x23, 0x33, 0x64, 0x38, 0x32, 0x64, 0x65, 0x22, 0x2f, 0x3e, 0x0a, 0x20,\n\t0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x73, 0x74, 0x6f, 0x70, 0x20, 0x6f,\n\t0x66, 0x66, 0x73, 0x65, 0x74, 0x3d, 0x22, 0x31, 0x22, 0x20, 0x73, 0x74,\n\t0x6f, 0x70, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3d, 0x22, 0x23, 0x62,\n\t0x62, 0x34, 0x30, 0x65, 0x31, 0x22, 0x2f, 0x3e, 0x0a, 0x20, 0x20, 0x20,\n\t0x20, 0x3c, 0x2f, 0x6c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x47, 0x72, 0x61,\n\t0x64, 0x69, 0x65, 0x6e, 0x74, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c,\n\t0x70, 0x61, 0x74, 0x68, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x64,\n\t0x3d, 0x22, 0x4d, 0x38, 0x30, 0x2e, 0x32, 0x20, 0x33, 0x39, 0x2e, 0x39,\n\t0x76, 0x2e, 0x38, 0x63, 0x30, 0x20, 0x31, 0x2e, 0x34, 0x2d, 0x2e, 0x31,\n\t0x20, 0x32, 0x2e, 0x39, 0x2d, 0x2e, 0x33, 0x20, 0x34, 0x2e, 0x31, 0x2d,\n\t0x2e, 0x34, 0x20, 0x33, 0x2e, 0x34, 0x2d, 0x31, 0x2e, 0x32, 0x20, 0x36,\n\t0x2e, 0x36, 0x2d, 0x32, 0x2e, 0x34, 0x20, 0x39, 0x2e, 0x37, 0x43, 0x37,\n\t0x31, 0x2e, 0x37, 0x20, 0x36, 0x39, 0x2e, 0x34, 0x20, 0x35, 0x37, 0x2e,\n\t0x32, 0x20, 0x38, 0x30, 0x20, 0x34, 0x30, 0x2e, 0x32, 0x20, 0x38, 0x30,\n\t0x20, 0x32, 0x32, 0x2e, 0x38, 0x20, 0x38, 0x30, 0x20, 0x38, 0x2e, 0x31,\n\t0x20, 0x36, 0x39, 0x20, 0x32, 0x2e, 0x36, 0x20, 0x35, 0x33, 0x2e, 0x36,\n\t0x63, 0x2d, 0x31, 0x2d, 0x32, 0x2e, 0x37, 0x2d, 0x31, 0x2e, 0x36, 0x2d,\n\t0x35, 0x2e, 0x37, 0x2d, 0x32, 0x2e, 0x31, 0x2d, 0x38, 0x2e, 0x37, 0x2d,\n\t0x2e, 0x31, 0x2d, 0x31, 0x2e, 0x31, 0x2d, 0x2e, 0x32, 0x2d, 0x32, 0x2e,\n\t0x33, 0x2d, 0x2e, 0x33, 0x2d, 0x33, 0x2e, 0x35, 0x56, 0x34, 0x30, 0x63,\n\t0x30, 0x2d, 0x32, 0x2e, 0x31, 0x2e, 0x31, 0x2d, 0x34, 0x2e, 0x32, 0x2e,\n\t0x34, 0x2d, 0x36, 0x2e, 0x32, 0x43, 0x33, 0x2e, 0x36, 0x20, 0x31, 0x34,\n\t0x2e, 0x37, 0x20, 0x32, 0x30, 0x2e, 0x32, 0x20, 0x30, 0x20, 0x34, 0x30,\n\t0x2e, 0x32, 0x20, 0x30, 0x73, 0x33, 0x36, 0x2e, 0x36, 0x20, 0x31, 0x34,\n\t0x2e, 0x36, 0x20, 0x33, 0x39, 0x2e, 0x35, 0x20, 0x33, 0x33, 0x2e, 0x38,\n\t0x63, 0x2e, 0x32, 0x20, 0x31, 0x2e, 0x39, 0x2e, 0x35, 0x20, 0x34, 0x20,\n\t0x2e, 0x35, 0x20, 0x36, 0x2e, 0x31, 0x7a, 0x22, 0x0a, 0x20, 0x20, 0x20,\n\t0x20, 0x20, 0x20, 0x66, 0x69, 0x6c, 0x6c, 0x3d, 0x22, 0x75, 0x72, 0x6c,\n\t0x28, 0x23, 0x53, 0x56, 0x47, 0x49, 0x44, 0x5f, 0x31, 0x5f, 0x29, 0x22,\n\t0x2f, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x70, 0x61, 0x74, 0x68,\n\t0x20, 0x66, 0x69, 0x6c, 0x6c, 0x3d, 0x22, 0x23, 0x32, 0x44, 0x33, 0x37,\n\t0x34, 0x38, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n\t0x20, 0x20, 0x64, 0x3d, 0x22, 0x4d, 0x38, 0x30, 0x2e, 0x32, 0x20, 0x34,\n\t0x30, 0x2e, 0x37, 0x63, 0x30, 0x20, 0x31, 0x2e, 0x34, 0x2d, 0x2e, 0x31,\n\t0x20, 0x32, 0x2e, 0x39, 0x2d, 0x2e, 0x33, 0x20, 0x34, 0x2e, 0x31, 0x2d,\n\t0x2e, 0x34, 0x20, 0x33, 0x2e, 0x34, 0x2d, 0x31, 0x2e, 0x32, 0x20, 0x36,\n\t0x2e, 0x36, 0x2d, 0x32, 0x2e, 0x34, 0x20, 0x39, 0x2e, 0x37, 0x2d, 0x33,\n\t0x2e, 0x33, 0x20, 0x32, 0x2e, 0x31, 0x2d, 0x37, 0x2e, 0x33, 0x20, 0x34,\n\t0x2d, 0x31, 0x30, 0x2e, 0x39, 0x20, 0x35, 0x2e, 0x33, 0x2d, 0x38, 0x2e,\n\t0x36, 0x20, 0x33, 0x2d, 0x31, 0x37, 0x2e, 0x35, 0x20, 0x34, 0x2e, 0x32,\n\t0x2d, 0x32, 0x36, 0x2e, 0x33, 0x20, 0x34, 0x2e, 0x32, 0x2d, 0x38, 0x2e,\n\t0x39, 0x2d, 0x2e, 0x31, 0x2d, 0x31, 0x37, 0x2e, 0x38, 0x2d, 0x31, 0x2e,\n\t0x35, 0x2d, 0x32, 0x36, 0x2e, 0x33, 0x2d, 0x34, 0x2e, 0x37, 0x2d, 0x34,\n\t0x2d, 0x31, 0x2e, 0x34, 0x2d, 0x37, 0x2e, 0x37, 0x2d, 0x33, 0x2e, 0x34,\n\t0x2d, 0x31, 0x31, 0x2e, 0x34, 0x2d, 0x35, 0x2e, 0x37, 0x68, 0x2d, 0x2e,\n\t0x31, 0x63, 0x2d, 0x31, 0x2d, 0x32, 0x2e, 0x37, 0x2d, 0x31, 0x2e, 0x36,\n\t0x2d, 0x35, 0x2e, 0x37, 0x2d, 0x32, 0x2e, 0x31, 0x2d, 0x38, 0x2e, 0x37,\n\t0x2d, 0x2e, 0x31, 0x2d, 0x31, 0x2e, 0x31, 0x2d, 0x2e, 0x32, 0x2d, 0x32,\n\t0x2e, 0x32, 0x2d, 0x2e, 0x32, 0x2d, 0x33, 0x2e, 0x34, 0x76, 0x2d, 0x31,\n\t0x2e, 0x32, 0x43, 0x35, 0x20, 0x34, 0x34, 0x2e, 0x36, 0x20, 0x31, 0x31,\n\t0x20, 0x34, 0x37, 0x2e, 0x38, 0x20, 0x31, 0x37, 0x2e, 0x34, 0x20, 0x34,\n\t0x39, 0x2e, 0x39, 0x63, 0x37, 0x2e, 0x33, 0x20, 0x32, 0x2e, 0x34, 0x20,\n\t0x31, 0x35, 0x2e, 0x32, 0x20, 0x33, 0x2e, 0x36, 0x20, 0x32, 0x33, 0x20,\n\t0x33, 0x2e, 0x36, 0x20, 0x37, 0x2e, 0x38, 0x2e, 0x31, 0x20, 0x31, 0x35,\n\t0x2e, 0x37, 0x2d, 0x2e, 0x39, 0x20, 0x32, 0x33, 0x2d, 0x33, 0x2e, 0x33,\n\t0x20, 0x36, 0x2e, 0x32, 0x2d, 0x31, 0x2e, 0x39, 0x20, 0x31, 0x32, 0x2e,\n\t0x35, 0x2d, 0x35, 0x2e, 0x32, 0x20, 0x31, 0x36, 0x2e, 0x38, 0x2d, 0x39,\n\t0x2e, 0x35, 0x7a, 0x22, 0x2f, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c,\n\t0x65, 0x6c, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x20, 0x66, 0x69, 0x6c, 0x6c,\n\t0x3d, 0x22, 0x23, 0x32, 0x44, 0x33, 0x37, 0x34, 0x38, 0x22, 0x20, 0x63,\n\t0x78, 0x3d, 0x22, 0x34, 0x30, 0x2e, 0x32, 0x22, 0x20, 0x63, 0x79, 0x3d,\n\t0x22, 0x35, 0x36, 0x2e, 0x38, 0x22, 0x20, 0x72, 0x78, 0x3d, 0x22, 0x32,\n\t0x31, 0x2e, 0x33, 0x22, 0x20, 0x72, 0x79, 0x3d, 0x22, 0x31, 0x35, 0x2e,\n\t0x34, 0x22, 0x2f, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x6c, 0x69,\n\t0x6e, 0x65, 0x61, 0x72, 0x47, 0x72, 0x61, 0x64, 0x69, 0x65, 0x6e, 0x74,\n\t0x20, 0x69, 0x64, 0x3d, 0x22, 0x53, 0x56, 0x47, 0x49, 0x44, 0x5f, 0x32,\n\t0x5f, 0x22, 0x20, 0x67, 0x72, 0x61, 0x64, 0x69, 0x65, 0x6e, 0x74, 0x55,\n\t0x6e, 0x69, 0x74, 0x73, 0x3d, 0x22, 0x75, 0x73, 0x65, 0x72, 0x53, 0x70,\n\t0x61, 0x63, 0x65, 0x4f, 0x6e, 0x55, 0x73, 0x65, 0x22, 0x20, 0x78, 0x31,\n\t0x3d, 0x22, 0x33, 0x32, 0x2e, 0x32, 0x37, 0x35, 0x22, 0x20, 0x79, 0x31,\n\t0x3d, 0x22, 0x37, 0x33, 0x35, 0x2e, 0x36, 0x30, 0x37, 0x22, 0x20, 0x78,\n\t0x32, 0x3d, 0x22, 0x34, 0x37, 0x2e, 0x35, 0x33, 0x36, 0x22, 0x20, 0x79,\n\t0x32, 0x3d, 0x22, 0x37, 0x35, 0x30, 0x2e, 0x38, 0x36, 0x38, 0x22, 0x0a,\n\t0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n\t0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x67, 0x72, 0x61, 0x64,\n\t0x69, 0x65, 0x6e, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72,\n\t0x6d, 0x3d, 0x22, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65,\n\t0x28, 0x30, 0x20, 0x2d, 0x36, 0x38, 0x36, 0x29, 0x22, 0x3e, 0x0a, 0x20,\n\t0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x73, 0x74, 0x6f, 0x70, 0x20, 0x6f,\n\t0x66, 0x66, 0x73, 0x65, 0x74, 0x3d, 0x22, 0x30, 0x22, 0x20, 0x73, 0x74,\n\t0x6f, 0x70, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3d, 0x22, 0x23, 0x33,\n\t0x64, 0x38, 0x32, 0x64, 0x65, 0x22, 0x2f, 0x3e, 0x0a, 0x20, 0x20, 0x20,\n\t0x20, 0x20, 0x20, 0x3c, 0x73, 0x74, 0x6f, 0x70, 0x20, 0x6f, 0x66, 0x66,\n\t0x73, 0x65, 0x74, 0x3d, 0x22, 0x31, 0x22, 0x20, 0x73, 0x74, 0x6f, 0x70,\n\t0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3d, 0x22, 0x23, 0x62, 0x62, 0x34,\n\t0x30, 0x65, 0x31, 0x22, 0x2f, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c,\n\t0x2f, 0x6c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x47, 0x72, 0x61, 0x64, 0x69,\n\t0x65, 0x6e, 0x74, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x65, 0x6c,\n\t0x6c, 0x69, 0x70, 0x73, 0x65, 0x20, 0x63, 0x78, 0x3d, 0x22, 0x34, 0x30,\n\t0x2e, 0x32, 0x22, 0x20, 0x63, 0x79, 0x3d, 0x22, 0x35, 0x37, 0x2e, 0x35,\n\t0x22, 0x20, 0x72, 0x78, 0x3d, 0x22, 0x31, 0x32, 0x2e, 0x34, 0x22, 0x20,\n\t0x72, 0x79, 0x3d, 0x22, 0x38, 0x2e, 0x39, 0x22, 0x20, 0x66, 0x69, 0x6c,\n\t0x6c, 0x3d, 0x22, 0x75, 0x72, 0x6c, 0x28, 0x23, 0x53, 0x56, 0x47, 0x49,\n\t0x44, 0x5f, 0x32, 0x5f, 0x29, 0x22, 0x2f, 0x3e, 0x0a, 0x20, 0x20, 0x20,\n\t0x20, 0x3c, 0x6c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x47, 0x72, 0x61, 0x64,\n\t0x69, 0x65, 0x6e, 0x74, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x53, 0x56, 0x47,\n\t0x49, 0x44, 0x5f, 0x33, 0x5f, 0x22, 0x20, 0x67, 0x72, 0x61, 0x64, 0x69,\n\t0x65, 0x6e, 0x74, 0x55, 0x6e, 0x69, 0x74, 0x73, 0x3d, 0x22, 0x75, 0x73,\n\t0x65, 0x72, 0x53, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x6e, 0x55, 0x73, 0x65,\n\t0x22, 0x20, 0x78, 0x31, 0x3d, 0x22, 0x33, 0x31, 0x2e, 0x30, 0x34, 0x22,\n\t0x20, 0x79, 0x31, 0x3d, 0x22, 0x37, 0x30, 0x30, 0x2e, 0x36, 0x33, 0x36,\n\t0x22, 0x20, 0x78, 0x32, 0x3d, 0x22, 0x35, 0x38, 0x2e, 0x38, 0x39, 0x34,\n\t0x22, 0x20, 0x79, 0x32, 0x3d, 0x22, 0x37, 0x32, 0x38, 0x2e, 0x34, 0x39,\n\t0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n\t0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x67, 0x72,\n\t0x61, 0x64, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66,\n\t0x6f, 0x72, 0x6d, 0x3d, 0x22, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61,\n\t0x74, 0x65, 0x28, 0x30, 0x20, 0x2d, 0x36, 0x38, 0x36, 0x29, 0x22, 0x3e,\n\t0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x73, 0x74, 0x6f, 0x70,\n\t0x20, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x3d, 0x22, 0x30, 0x22, 0x20,\n\t0x73, 0x74, 0x6f, 0x70, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3d, 0x22,\n\t0x23, 0x33, 0x32, 0x31, 0x39, 0x35, 0x36, 0x22, 0x2f, 0x3e, 0x0a, 0x20,\n\t0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x73, 0x74, 0x6f, 0x70, 0x20, 0x6f,\n\t0x66, 0x66, 0x73, 0x65, 0x74, 0x3d, 0x22, 0x31, 0x22, 0x20, 0x73, 0x74,\n\t0x6f, 0x70, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3d, 0x22, 0x23, 0x38,\n\t0x33, 0x34, 0x62, 0x64, 0x39, 0x22, 0x2f, 0x3e, 0x0a, 0x20, 0x20, 0x20,\n\t0x20, 0x3c, 0x2f, 0x6c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x47, 0x72, 0x61,\n\t0x64, 0x69, 0x65, 0x6e, 0x74, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c,\n\t0x70, 0x61, 0x74, 0x68, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x64,\n\t0x3d, 0x22, 0x4d, 0x35, 0x35, 0x2e, 0x38, 0x20, 0x34, 0x30, 0x2e, 0x39,\n\t0x48, 0x35, 0x30, 0x63, 0x2d, 0x2e, 0x33, 0x20, 0x30, 0x2d, 0x2e, 0x37,\n\t0x2d, 0x2e, 0x32, 0x2d, 0x2e, 0x38, 0x2d, 0x2e, 0x34, 0x2d, 0x2e, 0x31,\n\t0x2d, 0x2e, 0x33, 0x2d, 0x2e, 0x31, 0x2d, 0x2e, 0x37, 0x2e, 0x31, 0x2d,\n\t0x2e, 0x39, 0x6c, 0x34, 0x2e, 0x37, 0x2d, 0x35, 0x2e, 0x38, 0x68, 0x2d,\n\t0x34, 0x2e, 0x31, 0x63, 0x2d, 0x2e, 0x34, 0x20, 0x30, 0x2d, 0x2e, 0x39,\n\t0x2d, 0x2e, 0x33, 0x2d, 0x2e, 0x39, 0x2d, 0x2e, 0x39, 0x20, 0x30, 0x2d,\n\t0x2e, 0x35, 0x2e, 0x33, 0x2d, 0x2e, 0x39, 0x2e, 0x39, 0x2d, 0x2e, 0x39,\n\t0x68, 0x35, 0x2e, 0x38, 0x63, 0x2e, 0x33, 0x20, 0x30, 0x20, 0x2e, 0x37,\n\t0x2e, 0x32, 0x2e, 0x38, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x33, 0x2e, 0x31,\n\t0x2e, 0x37, 0x2d, 0x2e, 0x31, 0x2e, 0x39, 0x6c, 0x2d, 0x34, 0x2e, 0x37,\n\t0x20, 0x35, 0x2e, 0x38, 0x68, 0x34, 0x2e, 0x31, 0x63, 0x2e, 0x34, 0x20,\n\t0x30, 0x20, 0x2e, 0x39, 0x2e, 0x33, 0x2e, 0x39, 0x2e, 0x39, 0x2d, 0x2e,\n\t0x31, 0x2e, 0x36, 0x2d, 0x2e, 0x35, 0x2e, 0x39, 0x2d, 0x2e, 0x39, 0x2e,\n\t0x39, 0x7a, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x66, 0x69,\n\t0x6c, 0x6c, 0x3d, 0x22, 0x75, 0x72, 0x6c, 0x28, 0x23, 0x53, 0x56, 0x47,\n\t0x49, 0x44, 0x5f, 0x33, 0x5f, 0x29, 0x22, 0x2f, 0x3e, 0x0a, 0x20, 0x20,\n\t0x20, 0x20, 0x3c, 0x67, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n\t0x3c, 0x6c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x47, 0x72, 0x61, 0x64, 0x69,\n\t0x65, 0x6e, 0x74, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x53, 0x56, 0x47, 0x49,\n\t0x44, 0x5f, 0x34, 0x5f, 0x22, 0x20, 0x67, 0x72, 0x61, 0x64, 0x69, 0x65,\n\t0x6e, 0x74, 0x55, 0x6e, 0x69, 0x74, 0x73, 0x3d, 0x22, 0x75, 0x73, 0x65,\n\t0x72, 0x53, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x6e, 0x55, 0x73, 0x65, 0x22,\n\t0x20, 0x78, 0x31, 0x3d, 0x22, 0x34, 0x33, 0x2e, 0x33, 0x32, 0x22, 0x20,\n\t0x79, 0x31, 0x3d, 0x22, 0x36, 0x38, 0x38, 0x2e, 0x33, 0x35, 0x35, 0x22,\n\t0x20, 0x78, 0x32, 0x3d, 0x22, 0x37, 0x31, 0x2e, 0x31, 0x37, 0x34, 0x22,\n\t0x20, 0x79, 0x32, 0x3d, 0x22, 0x37, 0x31, 0x36, 0x2e, 0x32, 0x31, 0x22,\n\t0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n\t0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x67,\n\t0x72, 0x61, 0x64, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73,\n\t0x66, 0x6f, 0x72, 0x6d, 0x3d, 0x22, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c,\n\t0x61, 0x74, 0x65, 0x28, 0x30, 0x20, 0x2d, 0x36, 0x38, 0x36, 0x29, 0x22,\n\t0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x73,\n\t0x74, 0x6f, 0x70, 0x20, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x3d, 0x22,\n\t0x30, 0x22, 0x20, 0x73, 0x74, 0x6f, 0x70, 0x2d, 0x63, 0x6f, 0x6c, 0x6f,\n\t0x72, 0x3d, 0x22, 0x23, 0x33, 0x32, 0x31, 0x39, 0x35, 0x36, 0x22, 0x2f,\n\t0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x73,\n\t0x74, 0x6f, 0x70, 0x20, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x3d, 0x22,\n\t0x31, 0x22, 0x20, 0x73, 0x74, 0x6f, 0x70, 0x2d, 0x63, 0x6f, 0x6c, 0x6f,\n\t0x72, 0x3d, 0x22, 0x23, 0x38, 0x33, 0x34, 0x62, 0x64, 0x39, 0x22, 0x2f,\n\t0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x2f, 0x6c, 0x69,\n\t0x6e, 0x65, 0x61, 0x72, 0x47, 0x72, 0x61, 0x64, 0x69, 0x65, 0x6e, 0x74,\n\t0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x70, 0x61, 0x74,\n\t0x68, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x64, 0x3d,\n\t0x22, 0x4d, 0x36, 0x37, 0x2e, 0x34, 0x20, 0x32, 0x38, 0x2e, 0x37, 0x68,\n\t0x2d, 0x38, 0x2e, 0x38, 0x63, 0x2d, 0x2e, 0x34, 0x20, 0x30, 0x2d, 0x2e,\n\t0x39, 0x2d, 0x2e, 0x32, 0x2d, 0x31, 0x2e, 0x31, 0x2d, 0x2e, 0x37, 0x2d,\n\t0x2e, 0x32, 0x2d, 0x2e, 0x34, 0x2d, 0x2e, 0x31, 0x2d, 0x2e, 0x39, 0x2e,\n\t0x31, 0x2d, 0x31, 0x2e, 0x33, 0x6c, 0x37, 0x2e, 0x33, 0x2d, 0x39, 0x68,\n\t0x2d, 0x36, 0x2e, 0x34, 0x63, 0x2d, 0x2e, 0x37, 0x20, 0x30, 0x2d, 0x31,\n\t0x2e, 0x32, 0x2d, 0x2e, 0x35, 0x2d, 0x31, 0x2e, 0x32, 0x2d, 0x31, 0x2e,\n\t0x32, 0x73, 0x2e, 0x35, 0x2d, 0x31, 0x2e, 0x32, 0x20, 0x31, 0x2e, 0x32,\n\t0x2d, 0x31, 0x2e, 0x32, 0x68, 0x38, 0x2e, 0x38, 0x63, 0x2e, 0x34, 0x20,\n\t0x30, 0x20, 0x2e, 0x39, 0x2e, 0x32, 0x20, 0x31, 0x2e, 0x31, 0x2e, 0x37,\n\t0x73, 0x2e, 0x31, 0x2e, 0x39, 0x2d, 0x2e, 0x31, 0x20, 0x31, 0x2e, 0x33,\n\t0x6c, 0x2d, 0x37, 0x2e, 0x33, 0x20, 0x39, 0x68, 0x36, 0x2e, 0x33, 0x63,\n\t0x2e, 0x37, 0x20, 0x30, 0x20, 0x31, 0x2e, 0x32, 0x2e, 0x35, 0x20, 0x31,\n\t0x2e, 0x32, 0x20, 0x31, 0x2e, 0x32, 0x20, 0x30, 0x20, 0x2e, 0x36, 0x2d,\n\t0x2e, 0x34, 0x20, 0x31, 0x2e, 0x32, 0x2d, 0x31, 0x2e, 0x31, 0x20, 0x31,\n\t0x2e, 0x32, 0x7a, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n\t0x20, 0x66, 0x69, 0x6c, 0x6c, 0x3d, 0x22, 0x75, 0x72, 0x6c, 0x28, 0x23,\n\t0x53, 0x56, 0x47, 0x49, 0x44, 0x5f, 0x34, 0x5f, 0x29, 0x22, 0x2f, 0x3e,\n\t0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x2f, 0x67, 0x3e, 0x0a, 0x20, 0x20,\n\t0x3c, 0x2f, 0x73, 0x76, 0x67, 0x3e,\n}\n\n// /icons/server-list-empty-mono.svg\nvar file4 = []byte{\n\t0x3c, 0x73, 0x76, 0x67, 0x20, 0x78, 0x6d, 0x6c, 0x6e, 0x73, 0x3d, 0x22,\n\t0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x77,\n\t0x33, 0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x32, 0x30, 0x30, 0x30, 0x2f, 0x73,\n\t0x76, 0x67, 0x22, 0x20, 0x76, 0x69, 0x65, 0x77, 0x42, 0x6f, 0x78, 0x3d,\n\t0x22, 0x30, 0x20, 0x30, 0x20, 0x38, 0x30, 0x20, 0x38, 0x30, 0x22, 0x20,\n\t0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x22, 0x69, 0x63, 0x6f, 0x6e, 0x2d,\n\t0x64, 0x72, 0x6f, 0x6e, 0x65, 0x2d, 0x73, 0x6c, 0x65, 0x65, 0x70, 0x22,\n\t0x20, 0x77, 0x69, 0x64, 0x74, 0x68, 0x3d, 0x22, 0x38, 0x30, 0x70, 0x78,\n\t0x22, 0x20, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x3d, 0x22, 0x38, 0x30,\n\t0x70, 0x78, 0x22, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x6c, 0x69,\n\t0x6e, 0x65, 0x61, 0x72, 0x47, 0x72, 0x61, 0x64, 0x69, 0x65, 0x6e, 0x74,\n\t0x20, 0x69, 0x64, 0x3d, 0x22, 0x53, 0x56, 0x47, 0x49, 0x44, 0x5f, 0x31,\n\t0x5f, 0x22, 0x20, 0x67, 0x72, 0x61, 0x64, 0x69, 0x65, 0x6e, 0x74, 0x55,\n\t0x6e, 0x69, 0x74, 0x73, 0x3d, 0x22, 0x75, 0x73, 0x65, 0x72, 0x53, 0x70,\n\t0x61, 0x63, 0x65, 0x4f, 0x6e, 0x55, 0x73, 0x65, 0x22, 0x20, 0x78, 0x31,\n\t0x3d, 0x22, 0x31, 0x32, 0x2e, 0x36, 0x30, 0x34, 0x22, 0x20, 0x79, 0x31,\n\t0x3d, 0x22, 0x36, 0x39, 0x2e, 0x38, 0x34, 0x35, 0x22, 0x20, 0x78, 0x32,\n\t0x3d, 0x22, 0x36, 0x39, 0x2e, 0x33, 0x39, 0x37, 0x22, 0x20, 0x79, 0x32,\n\t0x3d, 0x22, 0x31, 0x33, 0x2e, 0x30, 0x35, 0x32, 0x22, 0x0a, 0x20, 0x20,\n\t0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n\t0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x67, 0x72, 0x61, 0x64, 0x69, 0x65,\n\t0x6e, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x3d,\n\t0x22, 0x6d, 0x61, 0x74, 0x72, 0x69, 0x78, 0x28, 0x31, 0x20, 0x30, 0x20,\n\t0x30, 0x20, 0x2d, 0x31, 0x20, 0x30, 0x20, 0x38, 0x32, 0x2e, 0x32, 0x29,\n\t0x22, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x73, 0x74,\n\t0x6f, 0x70, 0x20, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x3d, 0x22, 0x30,\n\t0x22, 0x20, 0x73, 0x74, 0x6f, 0x70, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72,\n\t0x3d, 0x22, 0x23, 0x37, 0x31, 0x38, 0x30, 0x39, 0x36, 0x22, 0x2f, 0x3e,\n\t0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x73, 0x74, 0x6f, 0x70,\n\t0x20, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x3d, 0x22, 0x31, 0x22, 0x20,\n\t0x73, 0x74, 0x6f, 0x70, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3d, 0x22,\n\t0x23, 0x32, 0x44, 0x33, 0x37, 0x34, 0x38, 0x22, 0x2f, 0x3e, 0x0a, 0x20,\n\t0x20, 0x20, 0x20, 0x3c, 0x2f, 0x6c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x47,\n\t0x72, 0x61, 0x64, 0x69, 0x65, 0x6e, 0x74, 0x3e, 0x0a, 0x20, 0x20, 0x20,\n\t0x20, 0x3c, 0x70, 0x61, 0x74, 0x68, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20,\n\t0x20, 0x64, 0x3d, 0x22, 0x4d, 0x38, 0x30, 0x2e, 0x32, 0x20, 0x33, 0x39,\n\t0x2e, 0x39, 0x76, 0x2e, 0x38, 0x63, 0x30, 0x20, 0x31, 0x2e, 0x34, 0x2d,\n\t0x2e, 0x31, 0x20, 0x32, 0x2e, 0x39, 0x2d, 0x2e, 0x33, 0x20, 0x34, 0x2e,\n\t0x31, 0x2d, 0x2e, 0x34, 0x20, 0x33, 0x2e, 0x34, 0x2d, 0x31, 0x2e, 0x32,\n\t0x20, 0x36, 0x2e, 0x36, 0x2d, 0x32, 0x2e, 0x34, 0x20, 0x39, 0x2e, 0x37,\n\t0x43, 0x37, 0x31, 0x2e, 0x37, 0x20, 0x36, 0x39, 0x2e, 0x34, 0x20, 0x35,\n\t0x37, 0x2e, 0x32, 0x20, 0x38, 0x30, 0x20, 0x34, 0x30, 0x2e, 0x32, 0x20,\n\t0x38, 0x30, 0x20, 0x32, 0x32, 0x2e, 0x38, 0x20, 0x38, 0x30, 0x20, 0x38,\n\t0x2e, 0x31, 0x20, 0x36, 0x39, 0x20, 0x32, 0x2e, 0x36, 0x20, 0x35, 0x33,\n\t0x2e, 0x36, 0x63, 0x2d, 0x31, 0x2d, 0x32, 0x2e, 0x37, 0x2d, 0x31, 0x2e,\n\t0x36, 0x2d, 0x35, 0x2e, 0x37, 0x2d, 0x32, 0x2e, 0x31, 0x2d, 0x38, 0x2e,\n\t0x37, 0x2d, 0x2e, 0x31, 0x2d, 0x31, 0x2e, 0x31, 0x2d, 0x2e, 0x32, 0x2d,\n\t0x32, 0x2e, 0x33, 0x2d, 0x2e, 0x33, 0x2d, 0x33, 0x2e, 0x35, 0x56, 0x34,\n\t0x30, 0x63, 0x30, 0x2d, 0x32, 0x2e, 0x31, 0x2e, 0x31, 0x2d, 0x34, 0x2e,\n\t0x32, 0x2e, 0x34, 0x2d, 0x36, 0x2e, 0x32, 0x43, 0x33, 0x2e, 0x36, 0x20,\n\t0x31, 0x34, 0x2e, 0x37, 0x20, 0x32, 0x30, 0x2e, 0x32, 0x20, 0x30, 0x20,\n\t0x34, 0x30, 0x2e, 0x32, 0x20, 0x30, 0x73, 0x33, 0x36, 0x2e, 0x36, 0x20,\n\t0x31, 0x34, 0x2e, 0x36, 0x20, 0x33, 0x39, 0x2e, 0x35, 0x20, 0x33, 0x33,\n\t0x2e, 0x38, 0x63, 0x2e, 0x32, 0x20, 0x31, 0x2e, 0x39, 0x2e, 0x35, 0x20,\n\t0x34, 0x20, 0x2e, 0x35, 0x20, 0x36, 0x2e, 0x31, 0x7a, 0x22, 0x0a, 0x20,\n\t0x20, 0x20, 0x20, 0x20, 0x20, 0x66, 0x69, 0x6c, 0x6c, 0x3d, 0x22, 0x75,\n\t0x72, 0x6c, 0x28, 0x23, 0x53, 0x56, 0x47, 0x49, 0x44, 0x5f, 0x31, 0x5f,\n\t0x29, 0x22, 0x2f, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x70, 0x61,\n\t0x74, 0x68, 0x20, 0x66, 0x69, 0x6c, 0x6c, 0x3d, 0x22, 0x23, 0x32, 0x44,\n\t0x33, 0x37, 0x34, 0x38, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n\t0x20, 0x20, 0x20, 0x20, 0x64, 0x3d, 0x22, 0x4d, 0x38, 0x30, 0x2e, 0x32,\n\t0x20, 0x34, 0x30, 0x2e, 0x37, 0x63, 0x30, 0x20, 0x31, 0x2e, 0x34, 0x2d,\n\t0x2e, 0x31, 0x20, 0x32, 0x2e, 0x39, 0x2d, 0x2e, 0x33, 0x20, 0x34, 0x2e,\n\t0x31, 0x2d, 0x2e, 0x34, 0x20, 0x33, 0x2e, 0x34, 0x2d, 0x31, 0x2e, 0x32,\n\t0x20, 0x36, 0x2e, 0x36, 0x2d, 0x32, 0x2e, 0x34, 0x20, 0x39, 0x2e, 0x37,\n\t0x2d, 0x33, 0x2e, 0x33, 0x20, 0x32, 0x2e, 0x31, 0x2d, 0x37, 0x2e, 0x33,\n\t0x20, 0x34, 0x2d, 0x31, 0x30, 0x2e, 0x39, 0x20, 0x35, 0x2e, 0x33, 0x2d,\n\t0x38, 0x2e, 0x36, 0x20, 0x33, 0x2d, 0x31, 0x37, 0x2e, 0x35, 0x20, 0x34,\n\t0x2e, 0x32, 0x2d, 0x32, 0x36, 0x2e, 0x33, 0x20, 0x34, 0x2e, 0x32, 0x2d,\n\t0x38, 0x2e, 0x39, 0x2d, 0x2e, 0x31, 0x2d, 0x31, 0x37, 0x2e, 0x38, 0x2d,\n\t0x31, 0x2e, 0x35, 0x2d, 0x32, 0x36, 0x2e, 0x33, 0x2d, 0x34, 0x2e, 0x37,\n\t0x2d, 0x34, 0x2d, 0x31, 0x2e, 0x34, 0x2d, 0x37, 0x2e, 0x37, 0x2d, 0x33,\n\t0x2e, 0x34, 0x2d, 0x31, 0x31, 0x2e, 0x34, 0x2d, 0x35, 0x2e, 0x37, 0x68,\n\t0x2d, 0x2e, 0x31, 0x63, 0x2d, 0x31, 0x2d, 0x32, 0x2e, 0x37, 0x2d, 0x31,\n\t0x2e, 0x36, 0x2d, 0x35, 0x2e, 0x37, 0x2d, 0x32, 0x2e, 0x31, 0x2d, 0x38,\n\t0x2e, 0x37, 0x2d, 0x2e, 0x31, 0x2d, 0x31, 0x2e, 0x31, 0x2d, 0x2e, 0x32,\n\t0x2d, 0x32, 0x2e, 0x32, 0x2d, 0x2e, 0x32, 0x2d, 0x33, 0x2e, 0x34, 0x76,\n\t0x2d, 0x31, 0x2e, 0x32, 0x43, 0x35, 0x20, 0x34, 0x34, 0x2e, 0x36, 0x20,\n\t0x31, 0x31, 0x20, 0x34, 0x37, 0x2e, 0x38, 0x20, 0x31, 0x37, 0x2e, 0x34,\n\t0x20, 0x34, 0x39, 0x2e, 0x39, 0x63, 0x37, 0x2e, 0x33, 0x20, 0x32, 0x2e,\n\t0x34, 0x20, 0x31, 0x35, 0x2e, 0x32, 0x20, 0x33, 0x2e, 0x36, 0x20, 0x32,\n\t0x33, 0x20, 0x33, 0x2e, 0x36, 0x20, 0x37, 0x2e, 0x38, 0x2e, 0x31, 0x20,\n\t0x31, 0x35, 0x2e, 0x37, 0x2d, 0x2e, 0x39, 0x20, 0x32, 0x33, 0x2d, 0x33,\n\t0x2e, 0x33, 0x20, 0x36, 0x2e, 0x32, 0x2d, 0x31, 0x2e, 0x39, 0x20, 0x31,\n\t0x32, 0x2e, 0x35, 0x2d, 0x35, 0x2e, 0x32, 0x20, 0x31, 0x36, 0x2e, 0x38,\n\t0x2d, 0x39, 0x2e, 0x35, 0x7a, 0x22, 0x2f, 0x3e, 0x0a, 0x20, 0x20, 0x20,\n\t0x20, 0x3c, 0x65, 0x6c, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x20, 0x66, 0x69,\n\t0x6c, 0x6c, 0x3d, 0x22, 0x23, 0x32, 0x44, 0x33, 0x37, 0x34, 0x38, 0x22,\n\t0x20, 0x63, 0x78, 0x3d, 0x22, 0x34, 0x30, 0x2e, 0x32, 0x22, 0x20, 0x63,\n\t0x79, 0x3d, 0x22, 0x35, 0x36, 0x2e, 0x38, 0x22, 0x20, 0x72, 0x78, 0x3d,\n\t0x22, 0x32, 0x31, 0x2e, 0x33, 0x22, 0x20, 0x72, 0x79, 0x3d, 0x22, 0x31,\n\t0x35, 0x2e, 0x34, 0x22, 0x2f, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c,\n\t0x6c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x47, 0x72, 0x61, 0x64, 0x69, 0x65,\n\t0x6e, 0x74, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x53, 0x56, 0x47, 0x49, 0x44,\n\t0x5f, 0x32, 0x5f, 0x22, 0x20, 0x67, 0x72, 0x61, 0x64, 0x69, 0x65, 0x6e,\n\t0x74, 0x55, 0x6e, 0x69, 0x74, 0x73, 0x3d, 0x22, 0x75, 0x73, 0x65, 0x72,\n\t0x53, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x6e, 0x55, 0x73, 0x65, 0x22, 0x20,\n\t0x78, 0x31, 0x3d, 0x22, 0x33, 0x32, 0x2e, 0x32, 0x37, 0x35, 0x22, 0x20,\n\t0x79, 0x31, 0x3d, 0x22, 0x37, 0x33, 0x35, 0x2e, 0x36, 0x30, 0x37, 0x22,\n\t0x20, 0x78, 0x32, 0x3d, 0x22, 0x34, 0x37, 0x2e, 0x35, 0x33, 0x36, 0x22,\n\t0x20, 0x79, 0x32, 0x3d, 0x22, 0x37, 0x35, 0x30, 0x2e, 0x38, 0x36, 0x38,\n\t0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n\t0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x67, 0x72,\n\t0x61, 0x64, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66,\n\t0x6f, 0x72, 0x6d, 0x3d, 0x22, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61,\n\t0x74, 0x65, 0x28, 0x30, 0x20, 0x2d, 0x36, 0x38, 0x36, 0x29, 0x22, 0x3e,\n\t0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x73, 0x74, 0x6f, 0x70,\n\t0x20, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x3d, 0x22, 0x30, 0x22, 0x20,\n\t0x73, 0x74, 0x6f, 0x70, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3d, 0x22,\n\t0x23, 0x37, 0x31, 0x38, 0x30, 0x39, 0x36, 0x22, 0x2f, 0x3e, 0x0a, 0x20,\n\t0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x73, 0x74, 0x6f, 0x70, 0x20, 0x6f,\n\t0x66, 0x66, 0x73, 0x65, 0x74, 0x3d, 0x22, 0x31, 0x22, 0x20, 0x73, 0x74,\n\t0x6f, 0x70, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3d, 0x22, 0x23, 0x32,\n\t0x44, 0x33, 0x37, 0x34, 0x38, 0x22, 0x2f, 0x3e, 0x0a, 0x20, 0x20, 0x20,\n\t0x20, 0x3c, 0x2f, 0x6c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x47, 0x72, 0x61,\n\t0x64, 0x69, 0x65, 0x6e, 0x74, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c,\n\t0x65, 0x6c, 0x6c, 0x69, 0x70, 0x73, 0x65, 0x20, 0x63, 0x78, 0x3d, 0x22,\n\t0x34, 0x30, 0x2e, 0x32, 0x22, 0x20, 0x63, 0x79, 0x3d, 0x22, 0x35, 0x37,\n\t0x2e, 0x35, 0x22, 0x20, 0x72, 0x78, 0x3d, 0x22, 0x31, 0x32, 0x2e, 0x34,\n\t0x22, 0x20, 0x72, 0x79, 0x3d, 0x22, 0x38, 0x2e, 0x39, 0x22, 0x20, 0x66,\n\t0x69, 0x6c, 0x6c, 0x3d, 0x22, 0x75, 0x72, 0x6c, 0x28, 0x23, 0x53, 0x56,\n\t0x47, 0x49, 0x44, 0x5f, 0x32, 0x5f, 0x29, 0x22, 0x2f, 0x3e, 0x0a, 0x20,\n\t0x20, 0x20, 0x20, 0x3c, 0x6c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x47, 0x72,\n\t0x61, 0x64, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x53,\n\t0x56, 0x47, 0x49, 0x44, 0x5f, 0x33, 0x5f, 0x22, 0x20, 0x67, 0x72, 0x61,\n\t0x64, 0x69, 0x65, 0x6e, 0x74, 0x55, 0x6e, 0x69, 0x74, 0x73, 0x3d, 0x22,\n\t0x75, 0x73, 0x65, 0x72, 0x53, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x6e, 0x55,\n\t0x73, 0x65, 0x22, 0x20, 0x78, 0x31, 0x3d, 0x22, 0x33, 0x31, 0x2e, 0x30,\n\t0x34, 0x22, 0x20, 0x79, 0x31, 0x3d, 0x22, 0x37, 0x30, 0x30, 0x2e, 0x36,\n\t0x33, 0x36, 0x22, 0x20, 0x78, 0x32, 0x3d, 0x22, 0x35, 0x38, 0x2e, 0x38,\n\t0x39, 0x34, 0x22, 0x20, 0x79, 0x32, 0x3d, 0x22, 0x37, 0x32, 0x38, 0x2e,\n\t0x34, 0x39, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n\t0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n\t0x67, 0x72, 0x61, 0x64, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x72, 0x61, 0x6e,\n\t0x73, 0x66, 0x6f, 0x72, 0x6d, 0x3d, 0x22, 0x74, 0x72, 0x61, 0x6e, 0x73,\n\t0x6c, 0x61, 0x74, 0x65, 0x28, 0x30, 0x20, 0x2d, 0x36, 0x38, 0x36, 0x29,\n\t0x22, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x73, 0x74,\n\t0x6f, 0x70, 0x20, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x3d, 0x22, 0x30,\n\t0x22, 0x20, 0x73, 0x74, 0x6f, 0x70, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72,\n\t0x3d, 0x22, 0x23, 0x41, 0x30, 0x41, 0x45, 0x43, 0x30, 0x22, 0x2f, 0x3e,\n\t0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x73, 0x74, 0x6f, 0x70,\n\t0x20, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x3d, 0x22, 0x31, 0x22, 0x20,\n\t0x73, 0x74, 0x6f, 0x70, 0x2d, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x3d, 0x22,\n\t0x23, 0x37, 0x31, 0x38, 0x30, 0x39, 0x36, 0x22, 0x2f, 0x3e, 0x0a, 0x20,\n\t0x20, 0x20, 0x20, 0x3c, 0x2f, 0x6c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x47,\n\t0x72, 0x61, 0x64, 0x69, 0x65, 0x6e, 0x74, 0x3e, 0x0a, 0x20, 0x20, 0x20,\n\t0x20, 0x3c, 0x70, 0x61, 0x74, 0x68, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20,\n\t0x20, 0x64, 0x3d, 0x22, 0x4d, 0x35, 0x35, 0x2e, 0x38, 0x20, 0x34, 0x30,\n\t0x2e, 0x39, 0x48, 0x35, 0x30, 0x63, 0x2d, 0x2e, 0x33, 0x20, 0x30, 0x2d,\n\t0x2e, 0x37, 0x2d, 0x2e, 0x32, 0x2d, 0x2e, 0x38, 0x2d, 0x2e, 0x34, 0x2d,\n\t0x2e, 0x31, 0x2d, 0x2e, 0x33, 0x2d, 0x2e, 0x31, 0x2d, 0x2e, 0x37, 0x2e,\n\t0x31, 0x2d, 0x2e, 0x39, 0x6c, 0x34, 0x2e, 0x37, 0x2d, 0x35, 0x2e, 0x38,\n\t0x68, 0x2d, 0x34, 0x2e, 0x31, 0x63, 0x2d, 0x2e, 0x34, 0x20, 0x30, 0x2d,\n\t0x2e, 0x39, 0x2d, 0x2e, 0x33, 0x2d, 0x2e, 0x39, 0x2d, 0x2e, 0x39, 0x20,\n\t0x30, 0x2d, 0x2e, 0x35, 0x2e, 0x33, 0x2d, 0x2e, 0x39, 0x2e, 0x39, 0x2d,\n\t0x2e, 0x39, 0x68, 0x35, 0x2e, 0x38, 0x63, 0x2e, 0x33, 0x20, 0x30, 0x20,\n\t0x2e, 0x37, 0x2e, 0x32, 0x2e, 0x38, 0x2e, 0x34, 0x2e, 0x31, 0x2e, 0x33,\n\t0x2e, 0x31, 0x2e, 0x37, 0x2d, 0x2e, 0x31, 0x2e, 0x39, 0x6c, 0x2d, 0x34,\n\t0x2e, 0x37, 0x20, 0x35, 0x2e, 0x38, 0x68, 0x34, 0x2e, 0x31, 0x63, 0x2e,\n\t0x34, 0x20, 0x30, 0x20, 0x2e, 0x39, 0x2e, 0x33, 0x2e, 0x39, 0x2e, 0x39,\n\t0x2d, 0x2e, 0x31, 0x2e, 0x36, 0x2d, 0x2e, 0x35, 0x2e, 0x39, 0x2d, 0x2e,\n\t0x39, 0x2e, 0x39, 0x7a, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n\t0x66, 0x69, 0x6c, 0x6c, 0x3d, 0x22, 0x75, 0x72, 0x6c, 0x28, 0x23, 0x53,\n\t0x56, 0x47, 0x49, 0x44, 0x5f, 0x33, 0x5f, 0x29, 0x22, 0x2f, 0x3e, 0x0a,\n\t0x20, 0x20, 0x20, 0x20, 0x3c, 0x67, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20,\n\t0x20, 0x20, 0x3c, 0x6c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x47, 0x72, 0x61,\n\t0x64, 0x69, 0x65, 0x6e, 0x74, 0x20, 0x69, 0x64, 0x3d, 0x22, 0x53, 0x56,\n\t0x47, 0x49, 0x44, 0x5f, 0x34, 0x5f, 0x22, 0x20, 0x67, 0x72, 0x61, 0x64,\n\t0x69, 0x65, 0x6e, 0x74, 0x55, 0x6e, 0x69, 0x74, 0x73, 0x3d, 0x22, 0x75,\n\t0x73, 0x65, 0x72, 0x53, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x6e, 0x55, 0x73,\n\t0x65, 0x22, 0x20, 0x78, 0x31, 0x3d, 0x22, 0x34, 0x33, 0x2e, 0x33, 0x32,\n\t0x22, 0x20, 0x79, 0x31, 0x3d, 0x22, 0x36, 0x38, 0x38, 0x2e, 0x33, 0x35,\n\t0x35, 0x22, 0x20, 0x78, 0x32, 0x3d, 0x22, 0x37, 0x31, 0x2e, 0x31, 0x37,\n\t0x34, 0x22, 0x20, 0x79, 0x32, 0x3d, 0x22, 0x37, 0x31, 0x36, 0x2e, 0x32,\n\t0x31, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n\t0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n\t0x20, 0x67, 0x72, 0x61, 0x64, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x72, 0x61,\n\t0x6e, 0x73, 0x66, 0x6f, 0x72, 0x6d, 0x3d, 0x22, 0x74, 0x72, 0x61, 0x6e,\n\t0x73, 0x6c, 0x61, 0x74, 0x65, 0x28, 0x30, 0x20, 0x2d, 0x36, 0x38, 0x36,\n\t0x29, 0x22, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n\t0x3c, 0x73, 0x74, 0x6f, 0x70, 0x20, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74,\n\t0x3d, 0x22, 0x30, 0x22, 0x20, 0x73, 0x74, 0x6f, 0x70, 0x2d, 0x63, 0x6f,\n\t0x6c, 0x6f, 0x72, 0x3d, 0x22, 0x23, 0x41, 0x30, 0x41, 0x45, 0x43, 0x30,\n\t0x22, 0x2f, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n\t0x3c, 0x73, 0x74, 0x6f, 0x70, 0x20, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74,\n\t0x3d, 0x22, 0x31, 0x22, 0x20, 0x73, 0x74, 0x6f, 0x70, 0x2d, 0x63, 0x6f,\n\t0x6c, 0x6f, 0x72, 0x3d, 0x22, 0x23, 0x37, 0x31, 0x38, 0x30, 0x39, 0x36,\n\t0x22, 0x2f, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x2f,\n\t0x6c, 0x69, 0x6e, 0x65, 0x61, 0x72, 0x47, 0x72, 0x61, 0x64, 0x69, 0x65,\n\t0x6e, 0x74, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x70,\n\t0x61, 0x74, 0x68, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,\n\t0x64, 0x3d, 0x22, 0x4d, 0x36, 0x37, 0x2e, 0x34, 0x20, 0x32, 0x38, 0x2e,\n\t0x37, 0x68, 0x2d, 0x38, 0x2e, 0x38, 0x63, 0x2d, 0x2e, 0x34, 0x20, 0x30,\n\t0x2d, 0x2e, 0x39, 0x2d, 0x2e, 0x32, 0x2d, 0x31, 0x2e, 0x31, 0x2d, 0x2e,\n\t0x37, 0x2d, 0x2e, 0x32, 0x2d, 0x2e, 0x34, 0x2d, 0x2e, 0x31, 0x2d, 0x2e,\n\t0x39, 0x2e, 0x31, 0x2d, 0x31, 0x2e, 0x33, 0x6c, 0x37, 0x2e, 0x33, 0x2d,\n\t0x39, 0x68, 0x2d, 0x36, 0x2e, 0x34, 0x63, 0x2d, 0x2e, 0x37, 0x20, 0x30,\n\t0x2d, 0x31, 0x2e, 0x32, 0x2d, 0x2e, 0x35, 0x2d, 0x31, 0x2e, 0x32, 0x2d,\n\t0x31, 0x2e, 0x32, 0x73, 0x2e, 0x35, 0x2d, 0x31, 0x2e, 0x32, 0x20, 0x31,\n\t0x2e, 0x32, 0x2d, 0x31, 0x2e, 0x32, 0x68, 0x38, 0x2e, 0x38, 0x63, 0x2e,\n\t0x34, 0x20, 0x30, 0x20, 0x2e, 0x39, 0x2e, 0x32, 0x20, 0x31, 0x2e, 0x31,\n\t0x2e, 0x37, 0x73, 0x2e, 0x31, 0x2e, 0x39, 0x2d, 0x2e, 0x31, 0x20, 0x31,\n\t0x2e, 0x33, 0x6c, 0x2d, 0x37, 0x2e, 0x33, 0x20, 0x39, 0x68, 0x36, 0x2e,\n\t0x33, 0x63, 0x2e, 0x37, 0x20, 0x30, 0x20, 0x31, 0x2e, 0x32, 0x2e, 0x35,\n\t0x20, 0x31, 0x2e, 0x32, 0x20, 0x31, 0x2e, 0x32, 0x20, 0x30, 0x20, 0x2e,\n\t0x36, 0x2d, 0x2e, 0x34, 0x20, 0x31, 0x2e, 0x32, 0x2d, 0x31, 0x2e, 0x31,\n\t0x20, 0x31, 0x2e, 0x32, 0x7a, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20,\n\t0x20, 0x20, 0x20, 0x66, 0x69, 0x6c, 0x6c, 0x3d, 0x22, 0x75, 0x72, 0x6c,\n\t0x28, 0x23, 0x53, 0x56, 0x47, 0x49, 0x44, 0x5f, 0x34, 0x5f, 0x29, 0x22,\n\t0x2f, 0x3e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x3c, 0x2f, 0x67, 0x3e, 0x0a,\n\t0x20, 0x20, 0x3c, 0x2f, 0x73, 0x76, 0x67, 0x3e,\n}\n\n// /style.css\nvar file5 = []byte(`:root {\n    --font-sans: -apple-system,BlinkMacSystemFont,\"Segoe UI\",\"Roboto\",\"Oxygen\",\"Ubuntu\",\"Cantarell\",\"Fira Sans\",\"Droid Sans\",\"Helvetica Neue\",sans-serif;\n    --font-mono: Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace;\n\n    --font-size-1: 12px;\n    --font-size-2: 14px;\n    --font-size-3: 16px;\n    --font-size-4: 18px;\n    --font-size-5: 20px;\n    --font-size-6: 24px;\n    --font-size-7: 30px;\n    --font-size-8: 38px;\n    --font-size-9: 48px;\n\n    --spacing-1: 4px;\n    --spacing-2: 8px;\n    --spacing-3: 12px;\n    --spacing-4: 16px;\n    --spacing-5: 24px;\n    --spacing-6: 32px;\n    --spacing-7: 48px;\n    --spacing-8: 64px;\n    --spacing-9: 96px;\n\n    --height-1: 16px;\n    --height-2: 20px;\n    --height-3: 24px;\n    --height-4: 32px;\n    --height-5: 40px;\n    --height-6: 48px;\n    --height-7: 64px;\n    --height-8: 80px;\n    --height-9: 96px;\n\n    --box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);\n    --box-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);\n    --box-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);\n    --box-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);\n    --box-shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);\n    --box-shadow-outline: 0 0 0 3px #F6B9FF;\n\n    /* --background: #edf2f7;\n\n    --text-color: #1a202c;\n    --text-color-pimary: #1a202c;\n    --text-color-secondary: #1a202c;\n\n    --card-text-color-primary: #2D3748;\n    --card-text-color-secondary: #829ab1;\n    --card-background-color: #FFF;\n    --card-border-radius: 8px;\n\n    --badge-border-radius: 20px;\n    --badge-font-size: 11px;\n    --badge-font-weight: 500;\n\n    --badge-pending-background: #FFEA7F;\n    --badge-pending-color: #960F18;\n\n    --badge-running-background: #D0FFED;\n    --badge-running-color: #083F37;\n\n    --badge-stopped-background: #FFEA7F;\n    --badge-stopped-color: #960F18;\n\n    --badge-error-background: #FFE0E8;\n    --badge-error-color: #800220;\n\n    --nav-link-color: #718096;\n    --nav-link-color-active: #E6DFFF;\n\n    --header-background-color: #4A5468;\n    --header-height: 72px; */\n\n    --background: #1A202C;\n\n    --text-color: #829ab1;\n\n    --card-text-color-title: #FFFFFF;\n    --card-text-color-primary: #E2E8F0;\n    --card-text-color-secondary: #829ab1;\n    --card-background-color: #2D3748;\n    --card-border-radius: 8px;\n    --card-icon-background: #718096;\n\n    --badge-border-radius: 8px;\n    --badge-font-size: var(--font-size-1);\n    --badge-font-weight: 500;\n\n    --badge-pending-background: rgba(255,255,255,0.075);\n    --badge-pending-color: #FFD567;\n\n    --badge-running-background: rgba(255,255,255,0.075);\n    --badge-running-color: #53FEBE;\n\n    --badge-stopped-background: rgba(255,255,255,0.075);\n    --badge-stopped-color: #FFE0E8;\n\n    --badge-error-background: #3c4656;\n    --badge-error-color: #FFE0E8;\n\n    --nav-link-color: #718096;\n    --nav-link-color-active: #EDF2F7;\n\n    --header-background-color: #2D3748;\n    --header-height: 72px;\n}\n\nhtml, body {\n    background: var(--background);\n    color: var(--text-color);\n    width: 100%;\n    height: 100%;\n    font-family: var(--font-sans);\n    font-size: var(--font-size-2);\n}\n\nmain {\n    box-sizing: border-box;\n    max-width: 800px;\n    margin: 0 auto;\n    margin-bottom: var(--spacing-6);\n}\n\nmain section > header h1 {\n    height: var(--height-5);\n    font-size: var(--font-size-7);\n    font-weight: 400;\n    font-style: normal;\n    font-stretch: normal;\n    line-height: normal;\n    letter-spacing: normal;\n    color: var(--text-color);\n    margin-top: var(--spacing-6);\n    margin-bottom: var(--spacing-3);\n}\n\nbody > header {\n    background-color: var(--header-background-color);\n    height: var(--header-height);\n    box-shadow: var(--box-shadow-lg);\n    box-sizing: border-box;\n    padding: 0 var(--spacing-4);\n    display: flex;\n    align-items: center;\n}\n\nheader .logo {\n    width: 30px;\n    height: 30px;\n}\n\n.navbar .inline-nav {\n    display: flex;\n    flex: 1 1 auto;\n    justify-content: flex-end;\n}\n\n.navbar .inline-nav li {\n    display: inline-block;\n    margin-left: var(--spacing-4);\n}\n\n.navbar .inline-nav a,\n.navbar .inline-nav a:active,\n.navbar .inline-nav a:visited {\n    color: var(--nav-link-color);\n    text-decoration: none;\n    font-size: var(--font-size-3);\n}\n\n.navbar .inline-nav a.active,\n.navbar .inline-nav a:hover {\n    color: var(--nav-link-color-active);\n}\n\n/*\n * cards\n */\n\n.card {\n    padding: var(--spacing-5);\n    color: var(--card-text-color-primary);\n    box-shadow: var(--box-shadow-md);\n    box-sizing: border-box;\n    border-radius: var(--card-border-radius);\n    background-color: var(--card-background-color);\n    margin-bottom: var(--spacing-3);\n}\n\n/*\n * instance card component\n */\n\n.instance {\n    display: grid;\n    grid-gap: var(--spacing-1) var(--spacing-2);\n    grid-template-columns: 40px 210px 130px 110px 110px 110px;\n}\n\n.instance:hover {\n    cursor: pointer;\n}\n\n/* .instance:hover {\n    box-shadow: var(--box-shadow-outline);\n} */\n\n.instance .icon {\n    grid-column: 1;\n    grid-row: 1 / span 2;\n    user-select: none; \n}\n\n.instance .addr {\n    grid-column: 2;\n    grid-row: 1;\n    font-weight: 500;\n    line-height: var(--height-3);\n    color: var(--card-text-color-title);\n}\n\n.instance .id {\n    grid-column: 2;\n    grid-row: 2;\n    color: var(--card-text-color-secondary);\n    font-weight: 300;\n}\n\n.instance .state {\n    grid-column: 3;\n    grid-row: 1 / span 2;\n    line-height: var(--height-3);\n\n    /* display: flex;\n    align-items: center; */\n}\n\n.instance .region {\n    grid-column: 4;\n    grid-row: 1;\n    line-height: var(--height-3);\n}\n\n.instance .image {\n    grid-column: 4;\n    grid-row: 2;\n    color: var(--card-text-color-secondary);\n    font-weight: 300;\n}\n\n.instance .size {\n    grid-column: 5;\n    grid-row: 1 / span 2;\n    line-height: var(--height-3);\n}\n\n.instance .time {\n    grid-column: 6;\n    grid-row: 1 / span 2;\n    text-align: right;\n    line-height: var(--height-3);\n}\n\n/*\n * instance icon server\n */\n\n.instance .icon-server {\n    width: 22px;\n    height: 22px;\n}\n\n.instance .icon .primary {\n    fill: var(--card-icon-background);\n}\n\n.instance .icon .secondary {\n    fill: var(--card-background-color);\n}\n\n/*\n * badge components\n */\n\n.badge {\n    border-radius: var(--badge-border-radius);\n    text-align: center;\n    text-transform: uppercase;\n    font-size: var(--badge-font-size);\n    font-weight: var(--badge-font-weight);\n    display: inline-block;\n    height: var(--height-3);\n    padding-right: var(--spacing-4);\n    padding-left: var(--spacing-4);\n    cursor: default;\n    user-select: none;\n    display: inline-flex;\n    align-content: center;\n}\n\n/**\n * badge for state\n */\n\n.state .badge {\n    padding-left: 0px;\n}\n\n.badge-creating,\n.badge-created,\n.badge-staging,\n.badge-starting,\n.badge-pending {\n    background: var(--badge-pending-background);\n    color: var(--badge-pending-color);\n}\n\n.badge-running {\n    background: var(--badge-running-background);\n    color: var(--badge-running-color);\n}\n\n.badge-stopping,\n.badge-stopped,\n.badge-shutdown {\n    background: var(--badge-stopped-background);\n    color: var(--badge-stopped-color);\n}\n\n.state .badge-error {\n    background: var(--badge-error-background);\n    color: var(--badge-error-color);\n}\n\n.badge svg {\n    width: 24px;\n    height: 24px;\n    fill: var(--badge-pending-color);\n    animation: blink 2s linear infinite;\n}\n\n.badge .icon-close-circle .secondary {\n    fill: var(--badge-error-color);\n}\n.badge .icon-close-circle .primary {\n    fill: var(--badge-error-background);\n}\n\n.badge-running svg {\n    fill: var(--badge-running-color);\n}\n\n.badge-stopped svg,\n.badge-shutdown svg,\n.badge-error svg {\n    animation: none;\n}\n\n/**\n * badge for trace logging\n */\n\n.badge-error,\n.badge-panic {\n    color: #FF9AA2;\n}\n\n.badge-warn {\n    color: var(--badge-pending-color);\n}\n\n.badge-info {\n    color: #00DDFF;\n}\n\n.badge-debug {\n    color: #F564FF;\n}\n\n.badge-trace {\n    color: #B3FFE3;\n}\n\n/**\n * log entry componenets\n */\n\n.entry {\n    display: grid;\n    grid-gap: var(--spacing-1) var(--spacing-2);\n    grid-template-columns: 85px 200px 1fr;\n\n    border-radius: 0px;\n    margin-bottom: 0px;\n    border-bottom: 2px solid var(--background);\n}\n\n.entry:first-child {\n    border-top-left-radius: var(--card-border-radius);\n    border-top-right-radius: var(--card-border-radius);\n}\n\n.entry:last-child {\n    border-bottom-left-radius: var(--card-border-radius);\n    border-bottom-right-radius: var(--card-border-radius);\n    border-bottom: none;\n}\n\n.entry .level {\n    grid-column: 1;\n    grid-row: 1;\n\n    display: flex;\n    align-items: center;\n    align-content: center;\n    font-size: var(--font-size-2);\n}\n\n.entry .level .badge {\n    display: flex;\n    align-items: center;\n    align-content: center;\n    font-family: var(--font-mono);\n    font-size: var(--font-size-2);\n}\n\n.entry .message {\n    grid-column: 3;\n    grid-row: 1;\n    /* padding: 0px var(--spacing-2); */\n    border-radius: var(--card-border-radius);\n    font-family: var(--font-mono);\n    font-size: var(--font-size-2);\n\n    display: flex;\n    align-items: center;\n    align-content: center;\n}\n\n.entry .fields {\n    grid-column: 3;\n    grid-row: 2;\n\n    /* padding: var(--spacing-1) var(--spacing-2); */\n    /* border-radius: var(--card-border-radius); */\n    line-height: var(--height-1);\n    font-family: var(--font-mono);\n    font-size: var(--font-size-2);\n}\n\n.entry .time {\n    grid-column: 2;\n    text-align: left;\n    font-family: var(--font-mono);\n    font-size: var(--font-size-2);\n    display: flex;\n    align-items: center;\n    align-content: center;\n}\n\n.fields span {\n    display: block;\n    color: #718096;\n}\n\n.fields span em:after {\n    content: \"=\";\n    color: #A0AEC0;\n    margin: 0px 3px;\n}\n\n/**\n * alerts\n */\n\n.alert {\n    display: grid;\n    grid-template-columns: 1fr 80px;\n}\n\n.alert span:first-child {\n    display: flex;\n    flex-direction: column;\n    justify-content: flex-end;\n}\n\n.alert h1 {\n    color: var(--card-text-color-primary);\n    font-size: var(--font-size-6);\n    margin-bottom: var(--spacing-3);\n}\n\n.alert p {\n    color: var(--card-text-color-secondary);\n    font-size: var(--font-size-2);\n}\n\n/*\n * animations\n */\n\n@keyframes spin {\n    0% { transform: rotate(0deg); }\n    100% { transform: rotate(359deg); }\n}\n\n@keyframes wrench {\n    0%  { transform: rotate(-12deg); }\n    8%  { transform: rotate(12deg); }\n    10% { transform: rotate(24deg); }\n    18% { transform: rotate(-24deg); }\n    20% { transform: rotate(-24deg); }\n    28% { transform: rotate(24deg); }\n    30% { transform: rotate(24deg); }\n    38% { transform: rotate(-24deg); }\n    40% { transform: rotate(-24deg); }\n    48% { transform: rotate(24deg); }\n    50% { transform: rotate(24deg); }\n    58% { transform: rotate(-24deg); }\n    60% { transform: rotate(-24deg); }\n    68% { transform: rotate(24deg); }\n    75%, 100% { transform: rotate(0deg); }\n}\n\n@keyframes blink {\n    50% { opacity: 0.0; }\n}\n`)\n"
  },
  {
    "path": "server/web/template/files/index.tmpl",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<meta http-equiv=\"refresh\" content=\"30\">\n<title>Dashboard</title>\n<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/reset.css\">\n<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/style.css\">\n<link rel=\"icon\" type=\"image/png\" id=\"favicon\" href=\"/static/favicon.png\">\n<script src=\"/static/timeago.js\" type=\"text/javascript\"></script>\n</head>\n<body>\n\n<header class=\"navbar\">\n    <div class=\"logo\">\n        <svg viewBox=\"0 0 60 60\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"><defs><path d=\"M12.086 5.814l-.257.258 10.514 10.514C20.856 18.906 20 21.757 20 25c0 9.014 6.618 15 15 15 3.132 0 6.018-.836 8.404-2.353l10.568 10.568C48.497 55.447 39.796 60 30 60 13.434 60 0 46.978 0 30 0 19.903 4.751 11.206 12.086 5.814zm5.002-2.97C20.998 1.015 25.378 0 30 0c16.566 0 30 13.022 30 30 0 4.67-1.016 9.04-2.835 12.923l-9.508-9.509C49.144 31.094 50 28.243 50 25c0-9.014-6.618-15-15-15-3.132 0-6.018.836-8.404 2.353l-9.508-9.508zM35 34c-5.03 0-9-3.591-9-9s3.97-9 9-9c5.03 0 9 3.591 9 9s-3.97 9-9 9z\" id=\"a\"></path></defs><use fill=\"#fff\" xlink:href=\"#a\" fill-rule=\"evenodd\"></use></svg>\n    </div>\n    <nav class=\"inline-nav\">\n        <ul>\n            <li><a href=\"#\" class=\"active\">Servers</a></li>\n            <li><a href=\"ui/logs\">Logging</a></li>\n        </ul>\n    </nav>\n</header>\n\n<main>\n    <section>\n        <header>\n            <h1>Servers</h1>\n        </header>\n        <article class=\"cards stages\">\n            {{ if not .Items }}\n            <div class=\"card alert sleeping\">\n                <span>\n                    <h1>There are no active servers</h1>\n                    <p>The system will not provision instances when the queue is empty.</p>\n                </span>\n                <img src=\"/static/icons/server-list-empty.svg\" />\n            </div>\n            {{ else }}\n                {{ range .Items }}\n                <div class=\"card instance\">\n                    <div class=\"icon\">\n                        <svg viewBox=\"0 0 24 24\" class=\"icon-server icon-server-{{ .State }}\"><path class=\"primary\" d=\"M5 3h14a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5c0-1.1.9-2 2-2zm0 10h14a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4c0-1.1.9-2 2-2zm2 3a1 1 0 0 0 0 2h3a1 1 0 0 0 0-2H7z\"/><rect width=\"5\" height=\"2\" x=\"6\" y=\"6\" class=\"secondary\" rx=\"1\"/></svg>\n                    </div>\n                    <div class=\"addr\">{{ .Name }}</div>\n                    <div class=\"id\">{{ if .Address }}{{ .Address }}{{ else }}0.0.0.0{{ end }}</div>\n                    <div class=\"state\">\n                        <span class=\"badge badge-{{ .State }}\">\n                            {{ if eq .State \"error\" }}\n                            <svg viewBox=\"0 0 24 24\" class=\"icon-close-circle\"><path class=\"primary\" d=\"M12 2a10 10 0 1 1 0 20 10 10 0 0 1 0-20z\"/><path class=\"secondary\" d=\"M12 18a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm1-5.9c-.13 1.2-1.88 1.2-2 0l-.5-5a1 1 0 0 1 1-1.1h1a1 1 0 0 1 1 1.1l-.5 5z\"/></svg>\n                            {{ else }}\n                            <svg viewBox=\"0 0 24 24\" class=\"icon-dot\"><path d=\"M12,10A2,2 0 0,0 10,12C10,13.11 10.9,14 12,14C13.11,14 14,13.11 14,12A2,2 0 0,0 12,10Z\" /></svg>\n                            {{ end }}\n                            {{ .State }}\n                        </span>\n                    </div>\n                    <div class=\"region\">{{ .Region }}</div>\n                    <div class=\"image\">{{ .Image }}</div>\n                    <div class=\"size\">{{ .Size }}</div>\n                    <div class=\"time\" datetime=\"{{ timestamp .Created }}\"></div>\n                </div>\n                {{ end }}\n            {{ end }}\n        </article>\n    </section>\n</main>\n\n<footer></footer>\n\n<script>\ntimeago.render(document.querySelectorAll('.time'));\n</script>\n</body>\n</html>"
  },
  {
    "path": "server/web/template/files/logs.tmpl",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<title>Dashboard</title>\n<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/reset.css\">\n<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/style.css\">\n<link rel=\"icon\" type=\"image/png\" id=\"favicon\" href=\"/static/favicon.png\">\n</head>\n<body>\n\n<header class=\"navbar\">\n    <div class=\"logo\">\n        <svg viewBox=\"0 0 60 60\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"><defs><path d=\"M12.086 5.814l-.257.258 10.514 10.514C20.856 18.906 20 21.757 20 25c0 9.014 6.618 15 15 15 3.132 0 6.018-.836 8.404-2.353l10.568 10.568C48.497 55.447 39.796 60 30 60 13.434 60 0 46.978 0 30 0 19.903 4.751 11.206 12.086 5.814zm5.002-2.97C20.998 1.015 25.378 0 30 0c16.566 0 30 13.022 30 30 0 4.67-1.016 9.04-2.835 12.923l-9.508-9.509C49.144 31.094 50 28.243 50 25c0-9.014-6.618-15-15-15-3.132 0-6.018.836-8.404 2.353l-9.508-9.508zM35 34c-5.03 0-9-3.591-9-9s3.97-9 9-9c5.03 0 9 3.591 9 9s-3.97 9-9 9z\" id=\"a\"></path></defs><use fill=\"#FFF\" xlink:href=\"#a\" fill-rule=\"evenodd\"></use></svg>\n    </div>\n    <nav class=\"inline-nav\">\n        <ul>\n            <li><a href=\"../\">Servers</a></li>\n            <li><a href=\"#\" class=\"active\">Logging</a></li>\n        </ul>\n    </nav>\n</header>\n\n<main>\n    <section>\n        <header>\n            <h1>Recent Logs</h1>\n        </header>\n\n        <div class=\"cards\">\n            {{ range .Entries }}\n            <div class=\"card entry\">\n                <div class=\"level\">\n                    <span class=\"badge badge-{{ .Level }}\">{{ .Level }}</span>\n                </div>\n                <div class=\"message\">{{ .Message }}</div>\n                <div class=\"fields\">\n                    {{ range $key, $val := .Data }}\n                    <span><em>{{ $key }}</em>{{ $val }}</span>\n                    {{ end }}\n                </div>\n                <div class=\"time\" datetime=\"{{ timestamp .Unix }}\">{{ timestamp .Unix }}</div>\n            </div>\n            {{ end }}\n        </div>\n\n    </section>\n</main>\n\n<footer></footer>\n</body>\n</html>"
  },
  {
    "path": "server/web/template/server.go",
    "content": "// Copyright 2019 Drone.IO Inc. All rights reserved.\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\n// +build ignore\n\npackage main\n\nimport (\n\t\"encoding/json\"\n\t\"html/template\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"time\"\n)\n\nfunc main() {\n\taddr := \":3333\"\n\n\t// serve templates with dummy data\n\thttp.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) {\n\t\tpath := r.FormValue(\"data\")\n\t\tif path == \"\" {\n\t\t\thttp.Error(w, \"missing data parameter\", 500)\n\t\t\treturn\n\t\t}\n\n\t\ttmpl := r.FormValue(\"template\")\n\t\tif path == \"\" {\n\t\t\thttp.Error(w, \"missing template parameter\", 500)\n\t\t\treturn\n\t\t}\n\n\t\t// read the json data from file.\n\t\trawjson, err := ioutil.ReadFile(filepath.Join(\"testdata\", path))\n\t\tif err != nil {\n\t\t\thttp.Error(w, \"cannot open json file\", 500)\n\t\t\treturn\n\t\t}\n\n\t\t// unmarshal the json data\n\t\tdata := map[string]interface{}{}\n\t\terr = json.Unmarshal(rawjson, &data)\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), 500)\n\t\t\treturn\n\t\t}\n\n\t\t// load the templates\n\t\tT := template.New(\"_\").Funcs(funcMap)\n\t\tmatches, _ := filepath.Glob(\"files/*.tmpl\")\n\t\tfor _, match := range matches {\n\t\t\traw, _ := ioutil.ReadFile(match)\n\t\t\tbase := filepath.Base(match)\n\t\t\tT = template.Must(\n\t\t\t\tT.New(base).Parse(string(raw)),\n\t\t\t)\n\t\t}\n\n\t\t// render the template\n\t\tw.Header().Set(\"Content-Type\", \"text/html\")\n\t\terr = T.ExecuteTemplate(w, tmpl, data)\n\t\tif err != nil {\n\t\t\tlog.Println(err)\n\t\t}\n\t})\n\n\t// serve static content.\n\thttp.Handle(\"/static/\",\n\t\thttp.StripPrefix(\"/static/\",\n\t\t\thttp.FileServer(\n\t\t\t\thttp.Dir(\"../static/files\"),\n\t\t\t),\n\t\t),\n\t)\n\n\tlog.Printf(\"listening at %s\", addr)\n\tlog.Fatalln(http.ListenAndServe(addr, nil))\n}\n\n// mirros the func map in template.go\nvar funcMap = map[string]interface{}{\n\t\"substr\": func(v string, i int) string {\n\t\treturn v[0:i]\n\t},\n\t\"timestamp\": func(v float64) string {\n\t\treturn time.Unix(int64(v), 0).UTC().Format(\"2006-01-02T15:04:05Z\")\n\t},\n}\n"
  },
  {
    "path": "server/web/template/template.go",
    "content": "// Copyright 2019 Drone.IO Inc. All rights reserved.\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage template\n\nimport \"time\"\n\n//go:generate togo tmpl -func funcMap -format html\n\n// mirros the func map in template.go\nvar funcMap = map[string]interface{}{\n\t\"timestamp\": func(v int64) string {\n\t\treturn time.Unix(v, 0).UTC().Format(\"2006-01-02T15:04:05Z\")\n\t},\n}\n"
  },
  {
    "path": "server/web/template/template_gen.go",
    "content": "package template\n\nimport \"html/template\"\n\n// list of embedded template files.\nvar files = []struct {\n\tname string\n\tdata string\n}{\n\t{\n\t\tname: \"index.tmpl\",\n\t\tdata: index,\n\t}, {\n\t\tname: \"logs.tmpl\",\n\t\tdata: logs,\n\t},\n}\n\n// T exposes the embedded templates.\nvar T *template.Template\n\nfunc init() {\n\tT = template.New(\"_\").Funcs(funcMap)\n\tfor _, file := range files {\n\t\tT = template.Must(\n\t\t\tT.New(file.name).Parse(file.data),\n\t\t)\n\t}\n}\n\n//\n// embedded template files.\n//\n\n// files/index.tmpl\nvar index = `<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<meta http-equiv=\"refresh\" content=\"30\">\n<title>Dashboard</title>\n<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/reset.css\">\n<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/style.css\">\n<link rel=\"icon\" type=\"image/png\" id=\"favicon\" href=\"/static/favicon.png\">\n<script src=\"/static/timeago.js\" type=\"text/javascript\"></script>\n</head>\n<body>\n\n<header class=\"navbar\">\n    <div class=\"logo\">\n        <svg viewBox=\"0 0 60 60\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"><defs><path d=\"M12.086 5.814l-.257.258 10.514 10.514C20.856 18.906 20 21.757 20 25c0 9.014 6.618 15 15 15 3.132 0 6.018-.836 8.404-2.353l10.568 10.568C48.497 55.447 39.796 60 30 60 13.434 60 0 46.978 0 30 0 19.903 4.751 11.206 12.086 5.814zm5.002-2.97C20.998 1.015 25.378 0 30 0c16.566 0 30 13.022 30 30 0 4.67-1.016 9.04-2.835 12.923l-9.508-9.509C49.144 31.094 50 28.243 50 25c0-9.014-6.618-15-15-15-3.132 0-6.018.836-8.404 2.353l-9.508-9.508zM35 34c-5.03 0-9-3.591-9-9s3.97-9 9-9c5.03 0 9 3.591 9 9s-3.97 9-9 9z\" id=\"a\"></path></defs><use fill=\"#fff\" xlink:href=\"#a\" fill-rule=\"evenodd\"></use></svg>\n    </div>\n    <nav class=\"inline-nav\">\n        <ul>\n            <li><a href=\"#\" class=\"active\">Servers</a></li>\n            <li><a href=\"ui/logs\">Logging</a></li>\n        </ul>\n    </nav>\n</header>\n\n<main>\n    <section>\n        <header>\n            <h1>Servers</h1>\n        </header>\n        <article class=\"cards stages\">\n            {{ if not .Items }}\n            <div class=\"card alert sleeping\">\n                <span>\n                    <h1>There are no active servers</h1>\n                    <p>The system will not provision instances when the queue is empty.</p>\n                </span>\n                <img src=\"/static/icons/server-list-empty.svg\" />\n            </div>\n            {{ else }}\n                {{ range .Items }}\n                <div class=\"card instance\">\n                    <div class=\"icon\">\n                        <svg viewBox=\"0 0 24 24\" class=\"icon-server icon-server-{{ .State }}\"><path class=\"primary\" d=\"M5 3h14a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5c0-1.1.9-2 2-2zm0 10h14a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4c0-1.1.9-2 2-2zm2 3a1 1 0 0 0 0 2h3a1 1 0 0 0 0-2H7z\"/><rect width=\"5\" height=\"2\" x=\"6\" y=\"6\" class=\"secondary\" rx=\"1\"/></svg>\n                    </div>\n                    <div class=\"addr\">{{ .Name }}</div>\n                    <div class=\"id\">{{ if .Address }}{{ .Address }}{{ else }}0.0.0.0{{ end }}</div>\n                    <div class=\"state\">\n                        <span class=\"badge badge-{{ .State }}\">\n                            {{ if eq .State \"error\" }}\n                            <svg viewBox=\"0 0 24 24\" class=\"icon-close-circle\"><path class=\"primary\" d=\"M12 2a10 10 0 1 1 0 20 10 10 0 0 1 0-20z\"/><path class=\"secondary\" d=\"M12 18a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm1-5.9c-.13 1.2-1.88 1.2-2 0l-.5-5a1 1 0 0 1 1-1.1h1a1 1 0 0 1 1 1.1l-.5 5z\"/></svg>\n                            {{ else }}\n                            <svg viewBox=\"0 0 24 24\" class=\"icon-dot\"><path d=\"M12,10A2,2 0 0,0 10,12C10,13.11 10.9,14 12,14C13.11,14 14,13.11 14,12A2,2 0 0,0 12,10Z\" /></svg>\n                            {{ end }}\n                            {{ .State }}\n                        </span>\n                    </div>\n                    <div class=\"region\">{{ .Region }}</div>\n                    <div class=\"image\">{{ .Image }}</div>\n                    <div class=\"size\">{{ .Size }}</div>\n                    <div class=\"time\" datetime=\"{{ timestamp .Created }}\"></div>\n                </div>\n                {{ end }}\n            {{ end }}\n        </article>\n    </section>\n</main>\n\n<footer></footer>\n\n<script>\ntimeago.render(document.querySelectorAll('.time'));\n</script>\n</body>\n</html>`\n\n// files/logs.tmpl\nvar logs = `<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<title>Dashboard</title>\n<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/reset.css\">\n<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/style.css\">\n<link rel=\"icon\" type=\"image/png\" id=\"favicon\" href=\"/static/favicon.png\">\n</head>\n<body>\n\n<header class=\"navbar\">\n    <div class=\"logo\">\n        <svg viewBox=\"0 0 60 60\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"><defs><path d=\"M12.086 5.814l-.257.258 10.514 10.514C20.856 18.906 20 21.757 20 25c0 9.014 6.618 15 15 15 3.132 0 6.018-.836 8.404-2.353l10.568 10.568C48.497 55.447 39.796 60 30 60 13.434 60 0 46.978 0 30 0 19.903 4.751 11.206 12.086 5.814zm5.002-2.97C20.998 1.015 25.378 0 30 0c16.566 0 30 13.022 30 30 0 4.67-1.016 9.04-2.835 12.923l-9.508-9.509C49.144 31.094 50 28.243 50 25c0-9.014-6.618-15-15-15-3.132 0-6.018.836-8.404 2.353l-9.508-9.508zM35 34c-5.03 0-9-3.591-9-9s3.97-9 9-9c5.03 0 9 3.591 9 9s-3.97 9-9 9z\" id=\"a\"></path></defs><use fill=\"#FFF\" xlink:href=\"#a\" fill-rule=\"evenodd\"></use></svg>\n    </div>\n    <nav class=\"inline-nav\">\n        <ul>\n            <li><a href=\"../\">Servers</a></li>\n            <li><a href=\"#\" class=\"active\">Logging</a></li>\n        </ul>\n    </nav>\n</header>\n\n<main>\n    <section>\n        <header>\n            <h1>Recent Logs</h1>\n        </header>\n\n        <div class=\"cards\">\n            {{ range .Entries }}\n            <div class=\"card entry\">\n                <div class=\"level\">\n                    <span class=\"badge badge-{{ .Level }}\">{{ .Level }}</span>\n                </div>\n                <div class=\"message\">{{ .Message }}</div>\n                <div class=\"fields\">\n                    {{ range $key, $val := .Data }}\n                    <span><em>{{ $key }}</em>{{ $val }}</span>\n                    {{ end }}\n                </div>\n                <div class=\"time\" datetime=\"{{ timestamp .Unix }}\">{{ timestamp .Unix }}</div>\n            </div>\n            {{ end }}\n        </div>\n\n    </section>\n</main>\n\n<footer></footer>\n</body>\n</html>`\n"
  },
  {
    "path": "server/web/template/testdata/logs.json",
    "content": "{\n    \"Entries\": [\n        {\n            \"Level\": \"trace\",\n            \"Message\": \"this is a test trace message\",\n            \"Data\": { \"foo\": \"bar\", \"baz\": \"boo\" },\n            \"Unix\": 1563058875\n        },\n        {\n            \"Level\": \"debug\",\n            \"Message\": \"this is a test debug message\",\n            \"Data\": { \"foo\": \"bar\", \"baz\": \"boo\" },\n            \"Unix\": 1563058875\n        },\n        {\n            \"Level\": \"info\",\n            \"Message\": \"this is an info trace message\",\n            \"Data\": { \"foo\": \"bar\", \"baz\": \"boo\" },\n            \"Unix\": 1563058975\n        },\n        {\n            \"Level\": \"warn\",\n            \"Message\": \"this is a test warning message\",\n            \"Data\": { \"foo\": \"bar\", \"baz\": \"boo\" },\n            \"Unix\": 1563058977\n        },\n        {\n            \"Level\": \"error\",\n            \"Message\": \"this is a test error message\",\n            \"Data\": { \"foo\": \"bar\", \"baz\": \"boo\" },\n            \"Unix\": 1563059000\n        }\n    ]\n}"
  },
  {
    "path": "server/web/template/testdata/logs_empty.json",
    "content": "{\n    \"Entries\": []\n}"
  },
  {
    "path": "server/web/template/testdata/servers.json",
    "content": "{\n    \"Items\": [\n        {\n            \"ID\": \"agent-123456789\",\n            \"Name\": \"i-5203422c\",\n            \"Provider\": \"amazon\",\n            \"State\": \"starting\",\n            \"Address\": \"54.194.252.215\",\n            \"Capacity\": 2,\n            \"Error\": \"\",\n            \"Image\": \"ami-0070c5311b7677678\",\n            \"Size\": \"t3.medium\",\n            \"Region\": \"us-east-1\",\n            \"Platform\": \"linux/amd64\",\n            \"Created\": 1573575703,\n            \"Updated\": 1573575719,\n            \"Started\": 1573575703,\n            \"Stopped\": 1573575719\n        },\n        {\n            \"ID\": \"agent-123456789\",\n            \"Name\": \"i-5203422c\",\n            \"Provider\": \"amazon\",\n            \"State\": \"running\",\n            \"Address\": \"54.194.252.215\",\n            \"Capacity\": 2,\n            \"Error\": \"\",\n            \"Image\": \"ami-0070c5311b7677678\",\n            \"Size\": \"t3.medium\",\n            \"Region\": \"us-east-1\",\n            \"Platform\": \"linux/amd64\",\n            \"Created\": 1573575703,\n            \"Updated\": 1573575719,\n            \"Started\": 1573575703,\n            \"Stopped\": 1573575719\n        },\n        {\n            \"ID\": \"agent-123456789\",\n            \"Name\": \"i-5203422c\",\n            \"Provider\": \"amazon\",\n            \"State\": \"running\",\n            \"Address\": \"54.194.252.215\",\n            \"Capacity\": 2,\n            \"Error\": \"\",\n            \"Image\": \"ami-0070c5311b7677678\",\n            \"Size\": \"t3.medium\",\n            \"Region\": \"us-east-1\",\n            \"Platform\": \"linux/amd64\",\n            \"Created\": 1573575703,\n            \"Updated\": 1573575719,\n            \"Started\": 1573575703,\n            \"Stopped\": 1573575719\n        },\n        {\n            \"ID\": \"agent-123456789\",\n            \"Name\": \"i-5203422c\",\n            \"Provider\": \"amazon\",\n            \"State\": \"running\",\n            \"Address\": \"54.194.252.215\",\n            \"Capacity\": 2,\n            \"Error\": \"\",\n            \"Image\": \"ami-0070c5311b7677678\",\n            \"Size\": \"t3.medium\",\n            \"Region\": \"us-east-1\",\n            \"Platform\": \"linux/amd64\",\n            \"Created\": 1573600782,\n            \"Updated\": 1573575719,\n            \"Started\": 1573575703,\n            \"Stopped\": 1573575719\n        },\n        {\n            \"ID\": \"agent-123456789\",\n            \"Name\": \"i-5203422c\",\n            \"Provider\": \"amazon\",\n            \"State\": \"error\",\n            \"Address\": \"54.194.252.215\",\n            \"Capacity\": 2,\n            \"Error\": \"\",\n            \"Image\": \"ami-0070c5311b7677678\",\n            \"Size\": \"t3.medium\",\n            \"Region\": \"us-east-1\",\n            \"Platform\": \"linux/amd64\",\n            \"Created\": 1573600782,\n            \"Updated\": 1573575719,\n            \"Started\": 1573575703,\n            \"Stopped\": 1573575719\n        }\n    ]\n}\n"
  },
  {
    "path": "server/web/template/testdata/servers_empty.json",
    "content": "{\n    \"Items\": []\n}"
  },
  {
    "path": "server/writer.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage server\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n)\n\n// indent the json-encoded API responses\nvar indent bool\n\nfunc init() {\n\tindent, _ = strconv.ParseBool(\n\t\tos.Getenv(\"HTTP_JSON_INDENT\"),\n\t)\n}\n\nvar (\n\t// errInvalidToken is returned when the api request token is invalid.\n\terrInvalidToken = errors.New(\"Invalid or missing token\")\n\n\t// errUnauthorized is returned when the user is not authorized.\n\terrUnauthorized = errors.New(\"Unauthorized\")\n\n\t// errForbidden is returned when user access is forbidden.\n\terrForbidden = errors.New(\"Forbidden\")\n\n\t// errNotFound is returned when a resource is not found.\n\terrNotFound = errors.New(\"Not Found\")\n)\n\n// Error represents a json-encoded API error.\ntype Error struct {\n\tMessage string `json:\"message\"`\n}\n\n// writeErrorCode writes the json-encoded error message to the response.\nfunc writeErrorCode(w http.ResponseWriter, err error, status int) {\n\twriteJSON(w, &Error{Message: err.Error()}, status)\n}\n\n// writeError writes the json-encoded error message to the response\n// with a 500 internal server error.\nfunc writeError(w http.ResponseWriter, err error) {\n\twriteErrorCode(w, err, 500)\n}\n\n// writeNotFound writes the json-encoded error message to the response\n// with a 404 not found status code.\nfunc writeNotFound(w http.ResponseWriter, err error) {\n\twriteErrorCode(w, err, 404)\n}\n\n// writeUnauthorized writes the json-encoded error message to the response\n// with a 401 unauthorized status code.\nfunc writeUnauthorized(w http.ResponseWriter, err error) {\n\twriteErrorCode(w, err, 401)\n}\n\n// writeForbidden writes the json-encoded error message to the response\n// with a 403 forbidden status code.\nfunc writeForbidden(w http.ResponseWriter, err error) {\n\twriteErrorCode(w, err, 403)\n}\n\n// writeBadRequest writes the json-encoded error message to the response\n// with a 400 bad request status code.\nfunc writeBadRequest(w http.ResponseWriter, err error) {\n\twriteErrorCode(w, err, 400)\n}\n\n// writeJSON writes the json-encoded error message to the response\n// with a 400 bad request status code.\nfunc writeJSON(w http.ResponseWriter, v interface{}, status int) {\n\tw.Header().Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\tw.Header().Set(\"X-Content-Type-Options\", \"nosniff\")\n\tw.WriteHeader(status)\n\tenc := json.NewEncoder(w)\n\tif indent {\n\t\tenc.SetIndent(\"\", \"  \")\n\t}\n\tenc.Encode(v)\n}\n"
  },
  {
    "path": "server/writer_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage server\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n)\n\nfunc TestWriteError(t *testing.T) {\n\tw := httptest.NewRecorder()\n\n\terr := errors.New(\"pc load letter\")\n\twriteError(w, err)\n\n\tif got, want := w.Code, 500; want != got {\n\t\tt.Errorf(\"Want response code %d, got %d\", want, got)\n\t}\n\n\terrjson := &Error{}\n\tjson.NewDecoder(w.Body).Decode(errjson)\n\tif got, want := errjson.Message, err.Error(); got != want {\n\t\tt.Errorf(\"Want error message %s, got %s\", want, got)\n\t}\n}\n\nfunc TestWriteErrorCode(t *testing.T) {\n\tw := httptest.NewRecorder()\n\n\terr := errors.New(\"pc load letter\")\n\twriteErrorCode(w, err, 418)\n\n\tif got, want := w.Code, 418; want != got {\n\t\tt.Errorf(\"Want response code %d, got %d\", want, got)\n\t}\n\n\terrjson := &Error{}\n\tjson.NewDecoder(w.Body).Decode(errjson)\n\tif got, want := errjson.Message, err.Error(); got != want {\n\t\tt.Errorf(\"Want error message %s, got %s\", want, got)\n\t}\n}\n\nfunc TestWriteNotFound(t *testing.T) {\n\tw := httptest.NewRecorder()\n\n\terr := errors.New(\"pc load letter\")\n\twriteNotFound(w, err)\n\n\tif got, want := w.Code, 404; want != got {\n\t\tt.Errorf(\"Want response code %d, got %d\", want, got)\n\t}\n\n\terrjson := &Error{}\n\tjson.NewDecoder(w.Body).Decode(errjson)\n\tif got, want := errjson.Message, err.Error(); got != want {\n\t\tt.Errorf(\"Want error message %s, got %s\", want, got)\n\t}\n}\n\nfunc TestWriteUnauthorized(t *testing.T) {\n\tw := httptest.NewRecorder()\n\n\terr := errors.New(\"pc load letter\")\n\twriteUnauthorized(w, err)\n\n\tif got, want := w.Code, 401; want != got {\n\t\tt.Errorf(\"Want response code %d, got %d\", want, got)\n\t}\n\n\terrjson := &Error{}\n\tjson.NewDecoder(w.Body).Decode(errjson)\n\tif got, want := errjson.Message, err.Error(); got != want {\n\t\tt.Errorf(\"Want error message %s, got %s\", want, got)\n\t}\n}\n\nfunc TestWriteForbidden(t *testing.T) {\n\tw := httptest.NewRecorder()\n\n\terr := errors.New(\"pc load letter\")\n\twriteForbidden(w, err)\n\n\tif got, want := w.Code, 403; want != got {\n\t\tt.Errorf(\"Want response code %d, got %d\", want, got)\n\t}\n\n\terrjson := &Error{}\n\tjson.NewDecoder(w.Body).Decode(errjson)\n\tif got, want := errjson.Message, err.Error(); got != want {\n\t\tt.Errorf(\"Want error message %s, got %s\", want, got)\n\t}\n}\n\nfunc TestWriteBadRequest(t *testing.T) {\n\tw := httptest.NewRecorder()\n\n\terr := errors.New(\"pc load letter\")\n\twriteBadRequest(w, err)\n\n\tif got, want := w.Code, 400; want != got {\n\t\tt.Errorf(\"Want response code %d, got %d\", want, got)\n\t}\n\n\terrjson := &Error{}\n\tjson.NewDecoder(w.Body).Decode(errjson)\n\tif got, want := errjson.Message, err.Error(); got != want {\n\t\tt.Errorf(\"Want error message %s, got %s\", want, got)\n\t}\n}\n\nfunc TestWriteJSON(t *testing.T) {\n\t// without indent\n\t{\n\t\tw := httptest.NewRecorder()\n\t\twriteJSON(w, map[string]string{\"hello\": \"world\"}, http.StatusTeapot)\n\t\tif got, want := w.Body.String(), \"{\\\"hello\\\":\\\"world\\\"}\\n\"; got != want {\n\t\t\tt.Errorf(\"Want JSON body %q, got %q\", want, got)\n\t\t}\n\t\tif got, want := w.HeaderMap.Get(\"Content-Type\"), \"application/json; charset=utf-8\"; got != want {\n\t\t\tt.Errorf(\"Want Content-Type %q, got %q\", want, got)\n\t\t}\n\t\tif got, want := w.Code, http.StatusTeapot; got != want {\n\t\t\tt.Errorf(\"Want status code %d, got %d\", want, got)\n\t\t}\n\t}\n\t// with indent\n\t{\n\t\tindent = true\n\t\tdefer func() {\n\t\t\tindent = false\n\t\t}()\n\t\tw := httptest.NewRecorder()\n\t\twriteJSON(w, map[string]string{\"hello\": \"world\"}, http.StatusTeapot)\n\t\tif got, want := w.Body.String(), \"{\\n  \\\"hello\\\": \\\"world\\\"\\n}\\n\"; got != want {\n\t\t\tt.Errorf(\"Want JSON body %q, got %q\", want, got)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "server.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage autoscaler\n\nimport (\n\t\"context\"\n\t\"database/sql/driver\"\n\t\"errors\"\n)\n\n// ServerState specifies the server state.\ntype ServerState string\n\n// Value converts the value to a sql string.\nfunc (s ServerState) Value() (driver.Value, error) {\n\treturn string(s), nil\n}\n\n// ServerState type enumeration.\nconst (\n\tStatePending  = ServerState(\"pending\")\n\tStateCreating = ServerState(\"creating\")\n\tStateCreated  = ServerState(\"created\")\n\tStateStaging  = ServerState(\"staging\") // starting\n\tStateRunning  = ServerState(\"running\")\n\tStateShutdown = ServerState(\"shutdown\")\n\tStateStopping = ServerState(\"stopping\")\n\tStateStopped  = ServerState(\"stopped\")\n\tStateError    = ServerState(\"error\")\n)\n\n// ErrServerNotFound is returned when the requested server\n// does not exist in the store.\nvar ErrServerNotFound = errors.New(\"Not Found\")\n\n// A ServerStore persists server information.\ntype ServerStore interface {\n\t// Find a server by unique name.\n\tFind(context.Context, string) (*Server, error)\n\n\t// List returns all registered servers\n\tList(context.Context) ([]*Server, error)\n\n\t// ListState returns all servers with the given state.\n\tListState(context.Context, ServerState) ([]*Server, error)\n\n\t// Create the server record in the store.\n\tCreate(context.Context, *Server) error\n\n\t// Update the server record in the store.\n\tUpdate(context.Context, *Server) error\n\n\t// Delete the server record from the store.\n\tDelete(context.Context, *Server) error\n\n\t// Purge old server records from the store.\n\tPurge(context.Context, int64) error\n}\n\n// Server stores the server details.\ntype Server struct {\n\tID       string       `db:\"server_id\"       json:\"id\"`\n\tProvider ProviderType `db:\"server_provider\" json:\"provider\"`\n\tState    ServerState  `db:\"server_state\"    json:\"state\"`\n\tName     string       `db:\"server_name\"     json:\"name\"`\n\tImage    string       `db:\"server_image\"    json:\"image\"`\n\tRegion   string       `db:\"server_region\"   json:\"region\"`\n\tSize     string       `db:\"server_size\"     json:\"size\"`\n\tPlatform string       `db:\"server_platform\" json:\"platform\"`\n\tAddress  string       `db:\"server_address\"  json:\"address\"`\n\tCapacity int          `db:\"server_capacity\" json:\"capacity\"`\n\tSecret   string       `db:\"server_secret\"   json:\"secret\"`\n\tError    string       `db:\"server_error\"    json:\"error\"`\n\tCAKey    []byte       `db:\"server_ca_key\"   json:\"ca_key\"`\n\tCACert   []byte       `db:\"server_ca_cert\"  json:\"ca_cert\"`\n\tTLSKey   []byte       `db:\"server_tls_key\"  json:\"tls_key\"`\n\tTLSCert  []byte       `db:\"server_tls_cert\" json:\"tls_cert\"`\n\tCreated  int64        `db:\"server_created\"  json:\"created\"`\n\tUpdated  int64        `db:\"server_updated\"  json:\"updated\"`\n\tStarted  int64        `db:\"server_started\"  json:\"started\"`\n\tStopped  int64        `db:\"server_stopped\"  json:\"stopped\"`\n}\n"
  },
  {
    "path": "slack/slack.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage slack\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/config\"\n\n\t\"github.com/bluele/slack\"\n\t\"github.com/dustin/go-humanize\"\n)\n\n// New returns a new provider that is instrumented to send\n// Slack notifications when server instances are provisioned\n// or terminated.\nfunc New(config config.Config, base autoscaler.ServerStore) autoscaler.ServerStore {\n\treturn &notifier{\n\t\tServerStore: base,\n\t\tclient:      slack.NewWebHook(config.Slack.Webhook),\n\t\tcreate:      config.Slack.Create,\n\t\tdestroy:     config.Slack.Destroy,\n\t\terror:       config.Slack.Error,\n\t}\n}\n\ntype notifier struct {\n\tautoscaler.ServerStore\n\tclient  *slack.WebHook\n\tchannel string\n\tcreate  bool\n\tdestroy bool\n\terror   bool\n}\n\nfunc (n *notifier) Update(ctx context.Context, server *autoscaler.Server) error {\n\terr := n.ServerStore.Update(ctx, server)\n\tswitch {\n\tcase server.State == autoscaler.StateRunning && n.create:\n\t\tn.notifyCreate(server)\n\tcase server.State == autoscaler.StateStopped && n.destroy:\n\t\tn.notifyDestroy(server)\n\tcase server.State == autoscaler.StateError && n.error:\n\t\tn.notifyError(server)\n\t}\n\treturn err\n}\n\nfunc (n *notifier) notifyCreate(server *autoscaler.Server) error {\n\topts := &slack.WebHookPostPayload{\n\t\tText: fmt.Sprintf(\"Provisioned server instance %s\", server.Name),\n\t\tAttachments: []*slack.Attachment{\n\t\t\t{\n\t\t\t\tColor: \"#00BFA5\",\n\t\t\t\tFields: []*slack.AttachmentField{\n\t\t\t\t\t{\n\t\t\t\t\t\tTitle: \"Name\",\n\t\t\t\t\t\tValue: server.Name,\n\t\t\t\t\t\tShort: false,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tTitle: \"Size\",\n\t\t\t\t\t\tValue: server.Size,\n\t\t\t\t\t\tShort: false,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tTitle: \"Region\",\n\t\t\t\t\t\tValue: server.Region,\n\t\t\t\t\t\tShort: false,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\treturn n.client.PostMessage(opts)\n}\n\nfunc (n *notifier) notifyDestroy(server *autoscaler.Server) error {\n\topts := &slack.WebHookPostPayload{\n\t\tText: fmt.Sprintf(\"Terminated server instance %s\", server.Name),\n\t\tAttachments: []*slack.Attachment{\n\t\t\t{\n\t\t\t\tColor: \"#CFD8DC\",\n\t\t\t\tFields: []*slack.AttachmentField{\n\t\t\t\t\t{\n\t\t\t\t\t\tTitle: \"Name\",\n\t\t\t\t\t\tValue: server.Name,\n\t\t\t\t\t\tShort: false,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tTitle: \"Size\",\n\t\t\t\t\t\tValue: server.Size,\n\t\t\t\t\t\tShort: false,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tTitle: \"Region\",\n\t\t\t\t\t\tValue: server.Region,\n\t\t\t\t\t\tShort: false,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tTitle: \"Uptime\",\n\t\t\t\t\t\tValue: humanizeTime(server.Created),\n\t\t\t\t\t\tShort: false,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\treturn n.client.PostMessage(opts)\n}\n\nfunc (n *notifier) notifyError(server *autoscaler.Server) error {\n\topts := &slack.WebHookPostPayload{\n\t\tText: fmt.Sprintf(\"Problem with server instance %s\", server.Name),\n\t\tAttachments: []*slack.Attachment{\n\t\t\t{\n\t\t\t\tColor: \"#F44336\",\n\t\t\t\tFields: []*slack.AttachmentField{\n\t\t\t\t\t{\n\t\t\t\t\t\tTitle: \"Name\",\n\t\t\t\t\t\tValue: server.Name,\n\t\t\t\t\t\tShort: false,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tTitle: \"Error\",\n\t\t\t\t\t\tValue: server.Error,\n\t\t\t\t\t\tShort: false,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\treturn n.client.PostMessage(opts)\n}\n\nfunc humanizeTime(unix int64) string {\n\td := time.Unix(unix, 0)\n\ts := humanize.RelTime(d, time.Now(), \"\", \"\")\n\treturn strings.TrimSpace(s)\n}\n"
  },
  {
    "path": "slack/slack_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage slack\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/drone/autoscaler\"\n\t\"github.com/drone/autoscaler/config\"\n\t\"github.com/drone/autoscaler/mocks\"\n\n\t\"github.com/bluele/slack\"\n\t\"github.com/golang/mock/gomock\"\n\t\"github.com/h2non/gock\"\n)\n\nvar noContext = context.TODO()\n\nfunc TestHumanizeTime(t *testing.T) {\n\tunix := time.Now().Add(time.Minute * 60 * -1).Unix()\n\ttext := humanizeTime(unix)\n\tif got, want := text, \"1 hour\"; got != want {\n\t\tt.Errorf(\"Want humanized time %s, got %s\", want, got)\n\t}\n}\n\nfunc TestUpdateRunning(t *testing.T) {\n\tdefer gock.Off()\n\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tserver := &autoscaler.Server{\n\t\tName:   \"this-is-a-test-message\",\n\t\tRegion: \"nyc1\",\n\t\tSize:   \"s-1vcpu-1gb\",\n\t\tState:  autoscaler.StateRunning,\n\t}\n\n\tgock.New(\"https://hooks.slack.com\").\n\t\tPost(\"/services/XXX/YYY/ZZZ\").\n\t\tJSON(createPayload).\n\t\tReply(200)\n\n\tconf := config.Config{}\n\tconf.Slack.Webhook = \"https://hooks.slack.com/services/XXX/YYY/ZZZ\"\n\tconf.Slack.Create = true\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().Update(gomock.Any(), server).Return(nil)\n\n\tslack := New(conf, store)\n\terr := slack.Update(noContext, server)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif !gock.IsDone() {\n\t\tt.Errorf(\"Pending mocks not executed\")\n\t}\n}\n\nfunc TestUpdateStopped(t *testing.T) {\n\tdefer gock.Off()\n\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tserver := &autoscaler.Server{\n\t\tName:   \"this-is-a-test-message\",\n\t\tRegion: \"nyc1\",\n\t\tSize:   \"s-1vcpu-1gb\",\n\t\tState:  autoscaler.StateStopped,\n\t}\n\n\tgock.New(\"https://hooks.slack.com\").\n\t\tPost(\"/services/XXX/YYY/ZZZ\").\n\t\tReply(200)\n\n\tconf := config.Config{}\n\tconf.Slack.Webhook = \"https://hooks.slack.com/services/XXX/YYY/ZZZ\"\n\tconf.Slack.Destroy = true\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().Update(gomock.Any(), server).Return(nil)\n\n\tslack := New(conf, store)\n\terr := slack.Update(noContext, server)\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\n\tif !gock.IsDone() {\n\t\tt.Errorf(\"Pending mocks not executed\")\n\t}\n}\n\nfunc TestUpdateError(t *testing.T) {\n\tdefer gock.Off()\n\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tserver := &autoscaler.Server{\n\t\tName:   \"this-is-a-test-message\",\n\t\tRegion: \"nyc1\",\n\t\tSize:   \"s-1vcpu-1gb\",\n\t\tError:  \"pc load letter\",\n\t\tState:  autoscaler.StateError,\n\t}\n\n\tgock.New(\"https://hooks.slack.com\").\n\t\tPost(\"/services/XXX/YYY/ZZZ\").\n\t\tJSON(errorPayload).\n\t\tReply(200)\n\n\tconf := config.Config{}\n\tconf.Slack.Webhook = \"https://hooks.slack.com/services/XXX/YYY/ZZZ\"\n\tconf.Slack.Error = true\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().Update(gomock.Any(), server).Return(nil)\n\n\tslack := New(conf, store)\n\terr := slack.Update(noContext, server)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\n\tif !gock.IsDone() {\n\t\tt.Errorf(\"Pending mocks not executed\")\n\t}\n}\n\nvar createPayload = slack.WebHookPostPayload{\n\tText: \"Provisioned server instance this-is-a-test-message\",\n\tAttachments: []*slack.Attachment{\n\t\t{\n\t\t\tColor: \"#00BFA5\",\n\t\t\tFields: []*slack.AttachmentField{\n\t\t\t\t{\n\t\t\t\t\tTitle: \"Name\",\n\t\t\t\t\tValue: \"this-is-a-test-message\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tTitle: \"Size\",\n\t\t\t\t\tValue: \"s-1vcpu-1gb\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tTitle: \"Region\",\n\t\t\t\t\tValue: \"nyc1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n}\n\nvar errorPayload = slack.WebHookPostPayload{\n\tText: \"Problem with server instance this-is-a-test-message\",\n\tAttachments: []*slack.Attachment{\n\t\t{\n\t\t\tColor: \"#F44336\",\n\t\t\tFields: []*slack.AttachmentField{\n\t\t\t\t{\n\t\t\t\t\tTitle: \"Name\",\n\t\t\t\t\tValue: \"this-is-a-test-message\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tTitle: \"Error\",\n\t\t\t\t\tValue: \"pc load letter\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n}\n\n// This is an integration test that will send a real\n// message to a Slack channel using a webhook defined\n// in the TEST_SLACK_WEBHOOK environment variable.\nfunc TestIntegration(t *testing.T) {\n\twebhook := os.Getenv(\"TEST_SLACK_WEBHOOK\")\n\tif webhook == \"\" {\n\t\tt.Skipf(\"Skip Slack integration test. No webhook provided.\")\n\t\treturn\n\t}\n\n\tcontroller := gomock.NewController(t)\n\tdefer controller.Finish()\n\n\tserver := &autoscaler.Server{\n\t\tName:     \"i-123789331\",\n\t\tAddress:  \"1.2.3.4\",\n\t\tRegion:   \"nyc1\",\n\t\tSize:     \"s-1vcpu-1gb\",\n\t\tCapacity: 2,\n\t\tState:    autoscaler.StateRunning,\n\t}\n\n\tconf := config.Config{}\n\tconf.Slack.Webhook = webhook\n\n\tstore := mocks.NewMockServerStore(controller)\n\tstore.EXPECT().Update(gomock.Any(), server).Return(nil)\n\n\tslack := New(conf, store)\n\terr := slack.Update(noContext, server)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n}\n"
  },
  {
    "path": "store/db.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage store\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"time\"\n\n\tddl \"github.com/drone/autoscaler/store/migrate\"\n\n\t\"github.com/jmoiron/sqlx\"\n)\n\nvar noContext = context.Background()\n\n// Connect to a database and verify with a ping.\nfunc Connect(driver, datasource string, maxconn int, maxlifetime time.Duration) (*sqlx.DB, error) {\n\tdb, err := sql.Open(driver, datasource)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tswitch driver {\n\tcase \"postgres\":\n\t\tdb.SetMaxIdleConns(maxconn)\n\t\tdb.SetConnMaxLifetime(maxlifetime)\n\tcase \"mysql\":\n\t\tdb.SetMaxIdleConns(0)\n\tcase \"sqlite3\":\n\t\tdb.SetMaxOpenConns(1)\n\t}\n\tdbx := sqlx.NewDb(db, driver)\n\tif err := pingDatabase(dbx); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := setupDatabase(dbx); err != nil {\n\t\treturn nil, err\n\t}\n\treturn dbx, nil\n}\n\n// Must is a helper function that wraps a call to Connect\n// and panics if the error is non-nil.\nfunc Must(db *sqlx.DB, err error) *sqlx.DB {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn db\n}\n\n// helper function to ping the database with backoff to ensure\n// a connection can be established before we proceed with the\n// database setup and migration.\nfunc pingDatabase(db *sqlx.DB) (err error) {\n\tfor i := 0; i < 30; i++ {\n\t\terr = db.Ping()\n\t\tif err == nil {\n\t\t\treturn\n\t\t}\n\t\ttime.Sleep(time.Second)\n\t}\n\treturn\n}\n\n// helper function to setup the databsae by performing automated\n// database migration steps.\nfunc setupDatabase(db *sqlx.DB) error {\n\treturn ddl.Migrate(db)\n}\n"
  },
  {
    "path": "store/db_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage store\n\nimport (\n\t\"os\"\n\t\"sync\"\n\n\t\"github.com/jmoiron/sqlx\"\n\n\t_ \"github.com/go-sql-driver/mysql\"\n\t_ \"github.com/lib/pq\"\n\t_ \"github.com/mattn/go-sqlite3\"\n)\n\n// connect opens a new test database connection.\nfunc connect() (*sqlx.DB, error) {\n\tvar (\n\t\tdriver = \"sqlite3\"\n\t\tconfig = \":memory:\"\n\t)\n\tif os.Getenv(\"DATABASE_DRIVER\") != \"\" {\n\t\tdriver = os.Getenv(\"DATABASE_DRIVER\")\n\t\tconfig = os.Getenv(\"DATABASE_CONFIG\")\n\t}\n\treturn Connect(driver, config, 0, 0)\n}\n\n// locker returns a new text locker.\nfunc locker() sync.Locker {\n\tdriver := \"sqlite3\"\n\tif os.Getenv(\"DATABASE_DRIVER\") != \"\" {\n\t\tdriver = os.Getenv(\"DATABASE_DRIVER\")\n\t}\n\treturn NewLocker(driver)\n}\n"
  },
  {
    "path": "store/lock.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage store\n\nimport \"sync\"\n\n// NewLocker returns a new database mutex. If the driver\n// is mysql or postgres a noop is returned.\nfunc NewLocker(driver string) sync.Locker {\n\tswitch driver {\n\tcase \"sqlite3\":\n\t\treturn new(sync.Mutex)\n\tdefault:\n\t\treturn new(noopLocker)\n\t}\n}\n\ntype noopLocker struct{}\n\nfunc (*noopLocker) Lock()   {}\nfunc (*noopLocker) Unlock() {}\n"
  },
  {
    "path": "store/migrate/migrate.go",
    "content": "package ddl\n\nimport (\n\t\"github.com/drone/autoscaler/store/migrate/mysql\"\n\t\"github.com/drone/autoscaler/store/migrate/postgres\"\n\t\"github.com/drone/autoscaler/store/migrate/sqlite\"\n\n\t\"github.com/jmoiron/sqlx\"\n)\n\n// Migrate performs the database migration.\nfunc Migrate(db *sqlx.DB) error {\n\tswitch db.DriverName() {\n\tcase \"postgres\":\n\t\treturn postgres.Migrate(db.DB)\n\tcase \"mysql\":\n\t\treturn mysql.Migrate(db.DB)\n\tdefault:\n\t\treturn sqlite.Migrate(db.DB)\n\t}\n}\n"
  },
  {
    "path": "store/migrate/mysql/ddl.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage mysql\n\n//go:generate togo ddl -package mysql -dialect mysql\n"
  },
  {
    "path": "store/migrate/mysql/ddl_gen.go",
    "content": "package mysql\n\nimport (\n\t\"database/sql\"\n)\n\nvar migrations = []struct {\n\tname string\n\tstmt string\n}{\n\t{\n\t\tname: \"create-table-servers\",\n\t\tstmt: createTableServers,\n\t},\n\t{\n\t\tname: \"create-index-server-id\",\n\t\tstmt: createIndexServerId,\n\t},\n\t{\n\t\tname: \"create-index-server-state\",\n\t\tstmt: createIndexServerState,\n\t},\n}\n\n// Migrate performs the database migration. If the migration fails\n// and error is returned.\nfunc Migrate(db *sql.DB) error {\n\tif err := createTable(db); err != nil {\n\t\treturn err\n\t}\n\tcompleted, err := selectCompleted(db)\n\tif err != nil && err != sql.ErrNoRows {\n\t\treturn err\n\t}\n\tfor _, migration := range migrations {\n\t\tif _, ok := completed[migration.name]; ok {\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif _, err := db.Exec(migration.stmt); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := insertMigration(db, migration.name); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t}\n\treturn nil\n}\n\nfunc createTable(db *sql.DB) error {\n\t_, err := db.Exec(migrationTableCreate)\n\treturn err\n}\n\nfunc insertMigration(db *sql.DB, name string) error {\n\t_, err := db.Exec(migrationInsert, name)\n\treturn err\n}\n\nfunc selectCompleted(db *sql.DB) (map[string]struct{}, error) {\n\tmigrations := map[string]struct{}{}\n\trows, err := db.Query(migrationSelect)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar name string\n\t\tif err := rows.Scan(&name); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tmigrations[name] = struct{}{}\n\t}\n\treturn migrations, nil\n}\n\n//\n// migration table ddl and sql\n//\n\nvar migrationTableCreate = `\nCREATE TABLE IF NOT EXISTS migrations (\n name VARCHAR(255)\n,UNIQUE(name)\n)\n`\n\nvar migrationInsert = `\nINSERT INTO migrations (name) VALUES (?)\n`\n\nvar migrationSelect = `\nSELECT name FROM migrations\n`\n\n//\n// 001_create_table_servers.sql\n//\n\nvar createTableServers = `\nCREATE TABLE servers (\n server_name      VARCHAR(50) PRIMARY KEY\n,server_id        VARCHAR(250)\n,server_provider  VARCHAR(50)\n,server_state     VARCHAR(50)\n,server_image     VARCHAR(250)\n,server_region    VARCHAR(50)\n,server_size      VARCHAR(50)\n,server_platform  VARCHAR(50)\n,server_address   VARCHAR(250)\n,server_capacity  INTEGER\n,server_secret    VARCHAR(50)\n,server_error     BLOB\n,server_ca_key    BLOB\n,server_ca_cert   BLOB\n,server_tls_key   BLOB\n,server_tls_cert  BLOB\n,server_created   INTEGER\n,server_updated   INTEGER\n,server_started   INTEGER\n,server_stopped   INTEGER\n);\n`\n\nvar createIndexServerId = `\nCREATE INDEX ix_servers_id ON servers (server_id);\n`\n\nvar createIndexServerState = `\nCREATE INDEX ix_servers_state ON servers (server_state);\n`\n"
  },
  {
    "path": "store/migrate/mysql/files/001_create_table_servers.sql",
    "content": "-- name: create-table-servers\n\nCREATE TABLE servers (\n server_name      VARCHAR(50) PRIMARY KEY\n,server_id        VARCHAR(250)\n,server_provider  VARCHAR(50)\n,server_state     VARCHAR(50)\n,server_image     VARCHAR(250)\n,server_region    VARCHAR(50)\n,server_size      VARCHAR(50)\n,server_platform  VARCHAR(50)\n,server_address   VARCHAR(250)\n,server_capacity  INTEGER\n,server_secret    VARCHAR(50)\n,server_error     BLOB\n,server_ca_key    BLOB\n,server_ca_cert   BLOB\n,server_tls_key   BLOB\n,server_tls_cert  BLOB\n,server_created   INTEGER\n,server_updated   INTEGER\n,server_started   INTEGER\n,server_stopped   INTEGER\n);\n\n-- name: create-index-server-id\n\nCREATE INDEX ix_servers_id ON servers (server_id);\n\n-- name: create-index-server-state\n\nCREATE INDEX ix_servers_state ON servers (server_state);\n"
  },
  {
    "path": "store/migrate/postgres/ddl.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage postgres\n\n//go:generate togo ddl -package postgres -dialect postgres\n"
  },
  {
    "path": "store/migrate/postgres/ddl_gen.go",
    "content": "package postgres\n\nimport (\n\t\"database/sql\"\n)\n\nvar migrations = []struct {\n\tname string\n\tstmt string\n}{\n\t{\n\t\tname: \"create-table-servers\",\n\t\tstmt: createTableServers,\n\t},\n\t{\n\t\tname: \"create-index-server-id\",\n\t\tstmt: createIndexServerId,\n\t},\n\t{\n\t\tname: \"create-index-server-state\",\n\t\tstmt: createIndexServerState,\n\t},\n}\n\n// Migrate performs the database migration. If the migration fails\n// and error is returned.\nfunc Migrate(db *sql.DB) error {\n\tif err := createTable(db); err != nil {\n\t\treturn err\n\t}\n\tcompleted, err := selectCompleted(db)\n\tif err != nil && err != sql.ErrNoRows {\n\t\treturn err\n\t}\n\tfor _, migration := range migrations {\n\t\tif _, ok := completed[migration.name]; ok {\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif _, err := db.Exec(migration.stmt); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := insertMigration(db, migration.name); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t}\n\treturn nil\n}\n\nfunc createTable(db *sql.DB) error {\n\t_, err := db.Exec(migrationTableCreate)\n\treturn err\n}\n\nfunc insertMigration(db *sql.DB, name string) error {\n\t_, err := db.Exec(migrationInsert, name)\n\treturn err\n}\n\nfunc selectCompleted(db *sql.DB) (map[string]struct{}, error) {\n\tmigrations := map[string]struct{}{}\n\trows, err := db.Query(migrationSelect)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar name string\n\t\tif err := rows.Scan(&name); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tmigrations[name] = struct{}{}\n\t}\n\treturn migrations, nil\n}\n\n//\n// migration table ddl and sql\n//\n\nvar migrationTableCreate = `\nCREATE TABLE IF NOT EXISTS migrations (\n name VARCHAR(255)\n,UNIQUE(name)\n)\n`\n\nvar migrationInsert = `\nINSERT INTO migrations (name) VALUES ($1)\n`\n\nvar migrationSelect = `\nSELECT name FROM migrations\n`\n\n//\n// 001_create_table_servers.sql\n//\n\nvar createTableServers = `\nCREATE TABLE servers (\n server_name      VARCHAR(50) PRIMARY KEY\n,server_id        VARCHAR(250)\n,server_provider  VARCHAR(50)\n,server_state     VARCHAR(50)\n,server_image     VARCHAR(250)\n,server_region    VARCHAR(50)\n,server_size      VARCHAR(50)\n,server_platform  VARCHAR(50)\n,server_address   VARCHAR(250)\n,server_capacity  INTEGER\n,server_secret    VARCHAR(50)\n,server_error     TEXT\n,server_ca_key    TEXT\n,server_ca_cert   TEXT\n,server_tls_key   TEXT\n,server_tls_cert  TEXT\n,server_created   INTEGER\n,server_updated   INTEGER\n,server_started   INTEGER\n,server_stopped   INTEGER\n);\n`\n\nvar createIndexServerId = `\nCREATE INDEX ix_servers_id ON servers (server_id);\n`\n\nvar createIndexServerState = `\nCREATE INDEX ix_servers_state ON servers (server_state);\n`\n"
  },
  {
    "path": "store/migrate/postgres/files/001_create_table_servers.sql",
    "content": "-- name: create-table-servers\n\nCREATE TABLE servers (\n server_name      VARCHAR(50) PRIMARY KEY\n,server_id        VARCHAR(250)\n,server_provider  VARCHAR(50)\n,server_state     VARCHAR(50)\n,server_image     VARCHAR(250)\n,server_region    VARCHAR(50)\n,server_size      VARCHAR(50)\n,server_platform  VARCHAR(50)\n,server_address   VARCHAR(250)\n,server_capacity  INTEGER\n,server_secret    VARCHAR(50)\n,server_error     TEXT\n,server_ca_key    TEXT\n,server_ca_cert   TEXT\n,server_tls_key   TEXT\n,server_tls_cert  TEXT\n,server_created   INTEGER\n,server_updated   INTEGER\n,server_started   INTEGER\n,server_stopped   INTEGER\n);\n\n-- name: create-index-server-id\n\nCREATE INDEX ix_servers_id ON servers (server_id);\n\n-- name: create-index-server-state\n\nCREATE INDEX ix_servers_state ON servers (server_state);\n"
  },
  {
    "path": "store/migrate/sqlite/ddl.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage sqlite\n\n//go:generate togo ddl -package sqlite -dialect sqlite3\n"
  },
  {
    "path": "store/migrate/sqlite/ddl_gen.go",
    "content": "package sqlite\n\nimport (\n\t\"database/sql\"\n)\n\nvar migrations = []struct {\n\tname string\n\tstmt string\n}{\n\t{\n\t\tname: \"create-table-servers\",\n\t\tstmt: createTableServers,\n\t},\n\t{\n\t\tname: \"create-index-server-id\",\n\t\tstmt: createIndexServerId,\n\t},\n\t{\n\t\tname: \"create-index-server-state\",\n\t\tstmt: createIndexServerState,\n\t},\n}\n\n// Migrate performs the database migration. If the migration fails\n// and error is returned.\nfunc Migrate(db *sql.DB) error {\n\tif err := createTable(db); err != nil {\n\t\treturn err\n\t}\n\tcompleted, err := selectCompleted(db)\n\tif err != nil && err != sql.ErrNoRows {\n\t\treturn err\n\t}\n\tfor _, migration := range migrations {\n\t\tif _, ok := completed[migration.name]; ok {\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif _, err := db.Exec(migration.stmt); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := insertMigration(db, migration.name); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t}\n\treturn nil\n}\n\nfunc createTable(db *sql.DB) error {\n\t_, err := db.Exec(migrationTableCreate)\n\treturn err\n}\n\nfunc insertMigration(db *sql.DB, name string) error {\n\t_, err := db.Exec(migrationInsert, name)\n\treturn err\n}\n\nfunc selectCompleted(db *sql.DB) (map[string]struct{}, error) {\n\tmigrations := map[string]struct{}{}\n\trows, err := db.Query(migrationSelect)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar name string\n\t\tif err := rows.Scan(&name); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tmigrations[name] = struct{}{}\n\t}\n\treturn migrations, nil\n}\n\n//\n// migration table ddl and sql\n//\n\nvar migrationTableCreate = `\nCREATE TABLE IF NOT EXISTS migrations (\n name VARCHAR(255)\n,UNIQUE(name)\n)\n`\n\nvar migrationInsert = `\nINSERT INTO migrations (name) VALUES (?)\n`\n\nvar migrationSelect = `\nSELECT name FROM migrations\n`\n\n//\n// 001_create_table_servers.sql\n//\n\nvar createTableServers = `\nCREATE TABLE IF NOT EXISTS servers (\n server_name      TEXT PRIMARY KEY\n,server_id        TEXT\n,server_provider  TEXT\n,server_state     TEXT\n,server_image     TEXT\n,server_region    TEXT\n,server_size      TEXT\n,server_platform  TEXT\n,server_address   TEXT\n,server_capacity  INTEGER\n,server_secret    TEXT\n,server_error     TEXT\n,server_ca_key    TEXT\n,server_ca_cert   TEXT\n,server_tls_key   TEXT\n,server_tls_cert  TEXT\n,server_created   INTEGER\n,server_updated   INTEGER\n,server_started   INTEGER\n,server_stopped   INTEGER\n);\n`\n\nvar createIndexServerId = `\nCREATE INDEX IF NOT EXISTS ix_servers_id ON servers (server_id);\n`\n\nvar createIndexServerState = `\nCREATE INDEX IF NOT EXISTS ix_servers_state ON servers (server_state);\n`\n"
  },
  {
    "path": "store/migrate/sqlite/files/001_create_table_servers.sql",
    "content": "-- name: create-table-servers\n\nCREATE TABLE IF NOT EXISTS servers (\n server_name      TEXT PRIMARY KEY\n,server_id        TEXT\n,server_provider  TEXT\n,server_state     TEXT\n,server_image     TEXT\n,server_region    TEXT\n,server_size      TEXT\n,server_platform  TEXT\n,server_address   TEXT\n,server_capacity  INTEGER\n,server_secret    TEXT\n,server_error     TEXT\n,server_ca_key    TEXT\n,server_ca_cert   TEXT\n,server_tls_key   TEXT\n,server_tls_cert  TEXT\n,server_created   INTEGER\n,server_updated   INTEGER\n,server_started   INTEGER\n,server_stopped   INTEGER\n);\n\n-- name: create-index-server-id\n\nCREATE INDEX IF NOT EXISTS ix_servers_id ON servers (server_id);\n\n-- name: create-index-server-state\n\nCREATE INDEX IF NOT EXISTS ix_servers_state ON servers (server_state);\n"
  },
  {
    "path": "store/servers.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage store\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/drone/autoscaler\"\n\n\t\"github.com/avast/retry-go\"\n\t\"github.com/jmoiron/sqlx\"\n)\n\n// NewServerStore returns a new server store.\nfunc NewServerStore(db *sqlx.DB, mu sync.Locker) autoscaler.ServerStore {\n\treturn &serverStore{mu, db}\n}\n\ntype serverStore struct {\n\tmu sync.Locker\n\tdb *sqlx.DB\n}\n\nfunc (s *serverStore) Find(_ context.Context, name string) (*autoscaler.Server, error) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tdest := &autoscaler.Server{Name: name}\n\tstmt, args, err := s.db.BindNamed(serverFindStmt, dest)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\terr = s.db.GetContext(noContext, dest, stmt, args...)\n\treturn dest, err\n}\n\nfunc (s *serverStore) List(_ context.Context) ([]*autoscaler.Server, error) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tdest := []*autoscaler.Server{}\n\terr := s.db.SelectContext(noContext, &dest, serverListStmt)\n\treturn dest, err\n}\n\nfunc (s *serverStore) ListState(_ context.Context, state autoscaler.ServerState) ([]*autoscaler.Server, error) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tdest := []*autoscaler.Server{}\n\tstmt, args, err := s.db.BindNamed(serverListStateStmt, map[string]interface{}{\"server_state\": state})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\terr = s.db.SelectContext(noContext, &dest, stmt, args...)\n\tif err == sql.ErrNoRows {\n\t\treturn dest, nil\n\t}\n\treturn dest, err\n}\n\nfunc (s *serverStore) Create(_ context.Context, server *autoscaler.Server) error {\n\treturn retry.Do(\n\t\tfunc() error {\n\t\t\tif err := s.create(server); isConnReset(err) {\n\t\t\t\treturn err\n\t\t\t} else {\n\t\t\t\treturn retry.Unrecoverable(err)\n\t\t\t}\n\t\t},\n\t\tretry.Attempts(5),\n\t\tretry.MaxDelay(time.Second*5),\n\t\tretry.LastErrorOnly(true),\n\t)\n}\n\nfunc (s *serverStore) create(server *autoscaler.Server) error {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tserver.Created = time.Now().Unix()\n\tserver.Updated = time.Now().Unix()\n\tstmt, args, err := s.db.BindNamed(serverInsertStmt, server)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = s.db.ExecContext(noContext, stmt, args...)\n\treturn err\n}\n\nfunc (s *serverStore) Update(_ context.Context, server *autoscaler.Server) error {\n\treturn retry.Do(\n\t\tfunc() error {\n\t\t\tif err := s.update(server); isConnReset(err) {\n\t\t\t\treturn err\n\t\t\t} else {\n\t\t\t\treturn retry.Unrecoverable(err)\n\t\t\t}\n\t\t},\n\t\tretry.Attempts(5),\n\t\tretry.MaxDelay(time.Second*5),\n\t\tretry.LastErrorOnly(true),\n\t)\n}\n\nfunc (s *serverStore) update(server *autoscaler.Server) error {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tserver.Updated = time.Now().Unix()\n\tstmt, args, err := s.db.BindNamed(serverUpdateStmt, server)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = s.db.ExecContext(noContext, stmt, args...)\n\treturn err\n}\n\nfunc (s *serverStore) Delete(_ context.Context, server *autoscaler.Server) error {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tstmt, args, err := s.db.BindNamed(serverDeleteStmt, server)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = s.db.ExecContext(noContext, stmt, args...)\n\treturn err\n}\n\nfunc (s *serverStore) Purge(_ context.Context, before int64) error {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tstmt, args, err := s.db.BindNamed(serverPurgeStmt, &autoscaler.Server{Stopped: before})\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = s.db.ExecContext(noContext, stmt, args...)\n\treturn err\n}\n\nconst serverFindStmt = `\nSELECT\n server_name\n,server_id\n,server_provider\n,server_state\n,server_image\n,server_region\n,server_size\n,server_platform\n,server_address\n,server_capacity\n,server_secret\n,server_error\n,server_ca_key\n,server_ca_cert\n,server_tls_key\n,server_tls_cert\n,server_created\n,server_updated\n,server_started\n,server_stopped\nFROM servers\nWHERE server_name=:server_name\n`\n\nconst serverListStmt = `\nSELECT\n server_name\n,server_id\n,server_provider\n,server_state\n,server_image\n,server_region\n,server_size\n,server_platform\n,server_address\n,server_capacity\n,server_secret\n,server_error\n,server_ca_key\n,server_ca_cert\n,server_tls_key\n,server_tls_cert\n,server_created\n,server_updated\n,server_started\n,server_stopped\nFROM servers\nORDER BY server_created ASC\n`\n\nconst serverListStateStmt = `\nSELECT\n server_name\n,server_id\n,server_provider\n,server_state\n,server_image\n,server_region\n,server_size\n,server_platform\n,server_address\n,server_capacity\n,server_secret\n,server_error\n,server_ca_key\n,server_ca_cert\n,server_tls_key\n,server_tls_cert\n,server_created\n,server_updated\n,server_started\n,server_stopped\nFROM servers\nWHERE server_state=:server_state\nORDER BY server_created ASC\n`\n\nconst serverInsertStmt = `\nINSERT INTO servers (\n server_name\n,server_id\n,server_provider\n,server_state\n,server_image\n,server_region\n,server_size\n,server_platform\n,server_address\n,server_capacity\n,server_secret\n,server_error\n,server_ca_key\n,server_ca_cert\n,server_tls_key\n,server_tls_cert\n,server_created\n,server_updated\n,server_started\n,server_stopped\n) VALUES (\n :server_name\n,:server_id\n,:server_provider\n,:server_state\n,:server_image\n,:server_region\n,:server_size\n,:server_platform\n,:server_address\n,:server_capacity\n,:server_secret\n,:server_error\n,:server_ca_key\n,:server_ca_cert\n,:server_tls_key\n,:server_tls_cert\n,:server_created\n,:server_updated\n,:server_started\n,:server_stopped\n)\n`\n\nconst serverUpdateStmt = `\nUPDATE servers SET\n server_id=:server_id\n,server_provider=:server_provider\n,server_state=:server_state\n,server_image=:server_image\n,server_region=:server_region\n,server_size=:server_size\n,server_platform=:server_platform\n,server_address=:server_address\n,server_capacity=:server_capacity\n,server_secret=:server_secret\n,server_error=:server_error\n,server_ca_key=:server_ca_key\n,server_ca_cert=:server_ca_cert\n,server_tls_key=:server_tls_key\n,server_tls_cert=:server_tls_cert\n,server_updated=:server_updated\n,server_started=:server_started\n,server_stopped=:server_stopped\nWHERE server_name=:server_name\n`\n\nconst serverDeleteStmt = `\nDELETE FROM servers WHERE server_name=:server_name\n`\n\nconst serverPurgeStmt = `\nDELETE FROM servers\nWHERE server_state = 'stopped'\n  AND server_stopped < :server_stopped\n`\n"
  },
  {
    "path": "store/servers_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage store\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/drone/autoscaler\"\n)\n\nfunc TestServer(t *testing.T) {\n\tconn, err := connect()\n\tif err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\tdefer conn.Close()\n\n\tmu := locker()\n\tstore := NewServerStore(conn, mu).(*serverStore)\n\tt.Run(\"Create\", testServerCreate(store))\n\tt.Run(\"Find\", testServerFind(store))\n\tt.Run(\"List\", testServerList(store))\n\tt.Run(\"ListState\", testServerListState(store))\n\tt.Run(\"Update\", testServerUpdate(store))\n\tt.Run(\"Delete\", testServerDelete(store))\n\tt.Run(\"Purge\", testServerPurge(store))\n}\n\nfunc testServerCreate(store *serverStore) func(t *testing.T) {\n\treturn func(t *testing.T) {\n\t\tserver := &autoscaler.Server{\n\t\t\tProvider: autoscaler.ProviderGoogle,\n\t\t\tState:    autoscaler.StateRunning,\n\t\t\tName:     \"i-5203422c\",\n\t\t\tAddress:  \"54.194.252.215\",\n\t\t\tCapacity: 2,\n\t\t\tCreated:  time.Now().Unix(),\n\t\t\tUpdated:  time.Now().Unix(),\n\t\t}\n\t\terr := store.Create(context.TODO(), server)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t}\n}\n\nfunc testServerFind(store *serverStore) func(t *testing.T) {\n\treturn func(t *testing.T) {\n\t\tserver, err := store.Find(context.TODO(), \"i-5203422c\")\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t} else {\n\t\t\tt.Run(\"Fields\", testServer(server))\n\t\t}\n\t}\n}\n\nfunc testServerList(store *serverStore) func(t *testing.T) {\n\treturn func(t *testing.T) {\n\t\tservers, err := store.List(context.TODO())\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn\n\t\t}\n\t\tif got, want := len(servers), 1; got != want {\n\t\t\tt.Errorf(\"Want server count %d, got %d\", want, got)\n\t\t} else {\n\t\t\tt.Run(\"Fields\", testServer(servers[0]))\n\t\t}\n\t}\n}\n\nfunc testServerListState(store *serverStore) func(t *testing.T) {\n\treturn func(t *testing.T) {\n\t\t// seed the database with two servers with shutdown state.\n\t\t// to confirm we can list servers by state. These will be\n\t\t// used in a subsequent purge test.\n\t\tstore.Create(context.TODO(), &autoscaler.Server{\n\t\t\tProvider: autoscaler.ProviderGoogle,\n\t\t\tState:    autoscaler.StateStopped,\n\t\t\tName:     \"agent-123456789\",\n\t\t})\n\t\tstore.Create(context.TODO(), &autoscaler.Server{\n\t\t\tProvider: autoscaler.ProviderGoogle,\n\t\t\tState:    autoscaler.StateStopped,\n\t\t\tName:     \"agent-987654321\",\n\t\t})\n\t\tservers, err := store.ListState(context.TODO(), autoscaler.StateStopped)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn\n\t\t}\n\t\tif got, want := len(servers), 2; got != want {\n\t\t\tt.Errorf(\"Want server count %d, got %d\", want, got)\n\t\t}\n\t}\n}\n\nfunc testServerUpdate(store *serverStore) func(t *testing.T) {\n\treturn func(t *testing.T) {\n\t\tserver := &autoscaler.Server{\n\t\t\tProvider: autoscaler.ProviderGoogle,\n\t\t\tName:     \"i-5203422c\",\n\t\t\tAddress:  \"54.194.252.215\",\n\t\t\tCapacity: 2,\n\t\t\tCreated:  time.Now().Unix(),\n\t\t\tUpdated:  time.Now().Unix(),\n\t\t}\n\t\terr := store.Update(context.TODO(), server)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn\n\t\t}\n\t\tupdated, err := store.Find(context.TODO(), server.Name)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn\n\t\t}\n\t\tif got, want := updated.Capacity, server.Capacity; got != want {\n\t\t\tt.Errorf(\"Want updated capacity %d, got %d\", want, got)\n\t\t}\n\t}\n}\n\nfunc testServerDelete(store *serverStore) func(t *testing.T) {\n\treturn func(t *testing.T) {\n\t\t_, err := store.Find(context.TODO(), \"i-5203422c\")\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn\n\t\t}\n\n\t\terr = store.Delete(context.TODO(), &autoscaler.Server{Name: \"i-5203422c\"})\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn\n\t\t}\n\n\t\t_, err = store.Find(context.TODO(), \"i-5203422c\")\n\t\tif got, want := err, sql.ErrNoRows; got != want {\n\t\t\tt.Errorf(\"Want ErrNoRows, got %s\", got)\n\t\t}\n\t}\n}\n\nfunc testServerPurge(store *serverStore) func(t *testing.T) {\n\treturn func(t *testing.T) {\n\t\t// this test attempts to purge the database of all\n\t\t// servers with a state of stopped. The database was\n\t\t// seeded with stopped servers in testServerListState.\n\t\tbefore, _ := store.List(context.TODO())\n\t\tif got, want := len(before), 2; got != want {\n\t\t\tt.Errorf(\"Want %d servers, got %d\", want, got)\n\t\t\treturn\n\t\t}\n\n\t\terr := store.Purge(context.TODO(), time.Now().Unix()+1)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn\n\t\t}\n\n\t\tafter, err := store.List(context.TODO())\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\treturn\n\t\t}\n\n\t\tif got, want := len(after), 0; got != want {\n\t\t\tt.Errorf(\"Want 0 remaining servers, got %d\", got)\n\t\t}\n\t}\n}\n\nfunc testServer(server *autoscaler.Server) func(t *testing.T) {\n\treturn func(t *testing.T) {\n\t\tif got, want := server.Name, \"i-5203422c\"; got != want {\n\t\t\tt.Errorf(\"Want server Name %q, got %q\", want, got)\n\t\t}\n\t\tif got, want := server.State, autoscaler.StateRunning; got != want {\n\t\t\tt.Errorf(\"Want server State %v, got %v\", want, got)\n\t\t}\n\t\tif got, want := server.Address, \"54.194.252.215\"; got != want {\n\t\t\tt.Errorf(\"Want server Address %q, got %q\", want, got)\n\t\t}\n\t\tif got, want := server.Capacity, 2; got != want {\n\t\t\tt.Errorf(\"Want server Capacity %d, got %d\", want, got)\n\t\t}\n\t\tif got, want := server.Provider, autoscaler.ProviderGoogle; got != want {\n\t\t\tt.Errorf(\"Want server Provider %v, got %v\", want, got)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "store/util.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage store\n\nimport \"strings\"\n\n// helper function returns true if the error message\n// indicates the connection has been reset.\nfunc isConnReset(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\treturn strings.Contains(err.Error(),\n\t\t\"connection reset by peer\")\n}\n"
  },
  {
    "path": "store/util_test.go",
    "content": "// Copyright 2018 Drone.IO Inc\n// Use of this source code is governed by the Polyform License\n// that can be found in the LICENSE file.\n\npackage store\n\nimport (\n\t\"database/sql\"\n\t\"errors\"\n\t\"testing\"\n)\n\nfunc TestConnectionReset(t *testing.T) {\n\tif isConnReset(nil) {\n\t\tt.Errorf(\"Expect nil error returns false\")\n\t}\n\tif isConnReset(sql.ErrNoRows) {\n\t\tt.Errorf(\"Expect ErrNoRows returns false\")\n\t}\n\tif !isConnReset(errors.New(\"read: connection reset by peer\")) {\n\t\tt.Errorf(\"Expect connection reset by peer return true\")\n\t}\n}\n\n// connect: connection timed out\n"
  }
]