Initial version of the Orchard orchestration system (#3)

* Initial version of the Orchard orchestration system

* Update README.md

Co-authored-by: Fedor Korotkov <fedor.korotkov@gmail.com>

Co-authored-by: Fedor Korotkov <fedor.korotkov@gmail.com>
This commit is contained in:
Nikolay Edigaryev 2023-01-26 23:46:23 +04:00 committed by GitHub
parent dcbbb8b8de
commit 92e8732d46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 2746 additions and 0 deletions

View File

@ -3,3 +3,29 @@
Orchard is an orchestration system for [Tart](https://github.com/cirruslabs/tart).
Create a cluster of bare-metal Apple Silicon machines and manage dozens of VMs with ease!
## Installation
```
go install github.com/cirruslabs/orchard/...@latest
```
## Quick start
Start the Orchard Controller and the Worker in a single inocation:
```shell
orchard dev
```
Create a Virtual Machine resource:
```shell
orchard create vm --image ghcr.io/cirruslabs/macos-ventura-base:latest ventura-base
```
Check a list of VM resources to see if the Virtual Machine we've created above is already running:
```shell
orchard list vms
```

148
api/openapi.yaml Normal file
View File

@ -0,0 +1,148 @@
openapi: 3.0.0
info:
title: Orchard
description: Orchard orchestration API
version: 0.1.0
paths:
/workers:
post:
summary: "Create a Worker"
tags:
- workers
responses:
'200':
description: Worker resource was successfully created
content:
application/json:
schema:
$ref: '#components/schemas/Worker'
'409':
description: Worker resource with with the same name already exists
get:
summary: "List Workers"
tags:
- workers
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#components/schemas/Worker'
/workers/{name}:
get:
summary: "Retrieve a Worker"
tags:
- workers
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#components/schemas/Worker'
'404':
description: Worker resource with the given name doesn't exist
put:
summary: "Update a Worker"
tags:
- workers
responses:
'200':
description: Worker object was successfully updated
content:
application/json:
schema:
$ref: '#components/schemas/Worker'
'404':
description: Worker resource with the given name doesn't exist
delete:
summary: "Delete a Worker"
tags:
- workers
responses:
'200':
description: Worker resource was successfully deleted
'404':
description: Worker resource with the given name doesn't exist
/vms:
post:
summary: "Create a VM"
tags:
- vms
responses:
'200':
description: VM resource was successfully created
content:
application/json:
schema:
$ref: '#components/schemas/VM'
'409':
description: VM resource with with the same name already exists
get:
summary: "List VMs"
tags:
- vms
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#components/schemas/VM'
/vms/{name}:
get:
summary: "Retrieve a VM"
tags:
- vms
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#components/schemas/VM'
'404':
description: VM resource with the given name doesn't exist
put:
summary: "Update a VM"
tags:
- vms
responses:
'200':
description: VM object was successfully updated
content:
application/json:
schema:
$ref: '#components/schemas/VM'
'404':
description: VM resource with the given name doesn't exist
delete:
summary: "Delete a VM"
tags:
- vms
responses:
'200':
description: VM resource was successfully deleted
'404':
description: VM resource with the given name doesn't exist
components:
schemas:
Worker:
title: Worker node
type: object
properties:
name:
type: string
description: Node name
VM:
title: Virtual Machine
type: object
properties:
name:
type: string
description: VM name

30
cmd/orchard/main.go Normal file
View File

@ -0,0 +1,30 @@
package main
import (
"context"
"github.com/cirruslabs/orchard/internal/command"
"log"
"os"
"os/signal"
)
func main() {
// Set up a signal-interruptible context
ctx, cancel := context.WithCancel(context.Background())
interruptCh := make(chan os.Signal, 1)
signal.Notify(interruptCh, os.Interrupt)
go func() {
select {
case <-interruptCh:
cancel()
case <-ctx.Done():
}
}()
// Run the command
if err := command.NewRootCmd().ExecuteContext(ctx); err != nil {
log.Fatal(err)
}
}

55
go.mod Normal file
View File

@ -0,0 +1,55 @@
module github.com/cirruslabs/orchard
go 1.19
require (
github.com/dgraph-io/badger/v3 v3.2103.5
github.com/dustin/go-humanize v1.0.0
github.com/gin-gonic/gin v1.8.2
github.com/google/uuid v1.3.0
github.com/gosuri/uitable v0.0.4
github.com/spf13/cobra v1.6.0
go.uber.org/zap v1.24.0
)
require (
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.11.1 // indirect
github.com/goccy/go-json v0.9.11 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.3 // indirect
github.com/google/flatbuffers v1.12.1 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.12.3 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
go.opencensus.io v0.22.5 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
golang.org/x/net v0.5.0 // indirect
golang.org/x/sys v0.4.0 // indirect
golang.org/x/text v0.6.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

254
go.sum Normal file
View File

@ -0,0 +1,254 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg=
github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw=
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY=
github.com/gin-gonic/gin v1.8.2/go.mod h1:qw5AYuDrzRTnhvusDsrov+fDIxp9Dleuu12h8nfB398=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU=
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI=
github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -0,0 +1,29 @@
package controller
import (
"github.com/cirruslabs/orchard/internal/orchardhome"
"github.com/spf13/cobra"
"log"
"path/filepath"
)
var dataDir string
func NewCommand() *cobra.Command {
command := &cobra.Command{
Use: "controller",
Short: "Initialize and run a controller on the local machine",
}
command.AddCommand(newRunCommand())
orchardHome, err := orchardhome.Path()
if err != nil {
log.Fatal(err)
}
command.PersistentFlags().StringVar(&dataDir, "data-dir", filepath.Join(orchardHome, "controller"),
"path to the data directory")
return command
}

View File

@ -0,0 +1,34 @@
package controller
import (
"github.com/cirruslabs/orchard/internal/controller"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
func newRunCommand() *cobra.Command {
return &cobra.Command{
Use: "run",
RunE: runController,
}
}
func runController(cmd *cobra.Command, args []string) (err error) {
// Initialize the logger
logger, err := zap.NewProduction()
if err != nil {
return err
}
defer func() {
if syncErr := logger.Sync(); syncErr != nil && err == nil {
err = syncErr
}
}()
controller, err := controller.New(controller.WithDataDir(dataDir), controller.WithLogger(logger))
if err != nil {
return err
}
return controller.Run(cmd.Context())
}

View File

@ -0,0 +1,16 @@
package create
import (
"github.com/spf13/cobra"
)
func NewCommand() *cobra.Command {
command := &cobra.Command{
Use: "create",
Short: "Create resources on the controller (VMs)",
}
command.AddCommand(newCreateVMCommand())
return command
}

View File

@ -0,0 +1,49 @@
package create
import (
"github.com/cirruslabs/orchard/pkg/client"
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
"github.com/spf13/cobra"
)
var image string
var cpu uint64
var memory uint64
var softnet bool
var headless bool
func newCreateVMCommand() *cobra.Command {
command := &cobra.Command{
Use: "vm",
RunE: runCreateVM,
Args: cobra.ExactArgs(1),
}
command.PersistentFlags().StringVar(&image, "image", "ghcr.io/cirruslabs/macos-ventura-base:latest", "image to use")
command.PersistentFlags().Uint64Var(&cpu, "cpu", 4, "number of CPUs to use")
command.PersistentFlags().Uint64Var(&memory, "memory", 8, "gigabytes of memory to use")
command.PersistentFlags().BoolVar(&softnet, "softnet", false, "whether to use Softnet network isolation")
command.PersistentFlags().BoolVar(&headless, "headless", true, "whether to run without graphics")
return command
}
func runCreateVM(cmd *cobra.Command, args []string) error {
name := args[0]
client, err := client.New()
if err != nil {
return err
}
return client.VMs().Create(cmd.Context(), &v1.VM{
Meta: v1.Meta{
Name: name,
},
Image: image,
CPU: cpu,
Memory: memory,
Softnet: softnet,
Headless: headless,
})
}

View File

@ -0,0 +1,16 @@
package delete
import (
"github.com/spf13/cobra"
)
func NewCommand() *cobra.Command {
command := &cobra.Command{
Use: "delete",
Short: "Delete resources from the controller (VMs)",
}
command.AddCommand(newDeleteVMCommand())
return command
}

View File

@ -0,0 +1,25 @@
package delete
import (
"github.com/cirruslabs/orchard/pkg/client"
"github.com/spf13/cobra"
)
func newDeleteVMCommand() *cobra.Command {
return &cobra.Command{
Use: "vm",
Args: cobra.ExactArgs(1),
RunE: runDeleteVM,
}
}
func runDeleteVM(cmd *cobra.Command, args []string) error {
name := args[0]
client, err := client.New()
if err != nil {
return err
}
return client.VMs().Delete(cmd.Context(), name, false)
}

View File

@ -0,0 +1,63 @@
package dev
import (
"github.com/cirruslabs/orchard/internal/controller"
"github.com/cirruslabs/orchard/internal/worker"
"github.com/spf13/cobra"
"go.uber.org/zap"
"os"
)
func NewCommand() *cobra.Command {
command := &cobra.Command{
Use: "dev",
Short: "Run a controller and a worker for development purposes",
RunE: runDev,
}
return command
}
func runDev(cmd *cobra.Command, args []string) error {
tempDir, err := os.MkdirTemp("", "")
if err != nil {
return err
}
// Initialize the logger
logger, err := zap.NewDevelopment()
if err != nil {
return err
}
defer func() {
if syncErr := logger.Sync(); syncErr != nil && err == nil {
err = syncErr
}
}()
controller, err := controller.New(controller.WithDataDir(tempDir), controller.WithLogger(logger))
if err != nil {
return err
}
worker, err := worker.New(worker.WithDataDir(tempDir), worker.WithLogger(logger))
if err != nil {
return err
}
errChan := make(chan error, 2)
go func() {
if err := controller.Run(cmd.Context()); err != nil {
errChan <- err
}
}()
go func() {
if err := worker.Run(cmd.Context()); err != nil {
errChan <- err
}
}()
return <-errChan
}

View File

@ -0,0 +1,20 @@
package list
import (
"github.com/spf13/cobra"
)
var quiet bool
func NewCommand() *cobra.Command {
command := &cobra.Command{
Use: "list",
Short: "List resources on the controller (workers, VMs)",
}
command.AddCommand(newListWorkersCommand(), newListVMsCommand())
command.PersistentFlags().BoolVarP(&quiet, "", "q", false, "only show resource names")
return command
}

View File

@ -0,0 +1,49 @@
package list
import (
"fmt"
"github.com/cirruslabs/orchard/pkg/client"
"github.com/gosuri/uitable"
"github.com/spf13/cobra"
)
func newListVMsCommand() *cobra.Command {
command := &cobra.Command{
Use: "vms",
RunE: runListVMs,
}
return command
}
func runListVMs(cmd *cobra.Command, args []string) error {
client, err := client.New()
if err != nil {
return err
}
vms, err := client.VMs().List(cmd.Context())
if err != nil {
return err
}
if quiet {
for _, vm := range vms {
fmt.Println(vm.Name)
}
return nil
}
table := uitable.New()
table.AddRow("Name", "Image", "Status")
for _, vm := range vms {
table.AddRow(vm.Name, vm.Image, vm.Status)
}
fmt.Println(table)
return nil
}

View File

@ -0,0 +1,51 @@
package list
import (
"fmt"
"github.com/cirruslabs/orchard/pkg/client"
"github.com/dustin/go-humanize"
"github.com/gosuri/uitable"
"github.com/spf13/cobra"
)
func newListWorkersCommand() *cobra.Command {
command := &cobra.Command{
Use: "workers",
Short: "List workers",
RunE: runListWorkers,
}
return command
}
func runListWorkers(cmd *cobra.Command, args []string) error {
client, err := client.New()
if err != nil {
return err
}
workers, err := client.Workers().List(cmd.Context())
if err != nil {
return err
}
if quiet {
for _, worker := range workers {
fmt.Println(worker.Name)
}
return nil
}
table := uitable.New()
table.AddRow("Name", "Last seen")
for _, worker := range workers {
table.AddRow(worker.Name, humanize.Time(worker.LastSeen))
}
fmt.Println(table)
return nil
}

46
internal/command/root.go Normal file
View File

@ -0,0 +1,46 @@
package command
import (
"github.com/cirruslabs/orchard/internal/command/controller"
"github.com/cirruslabs/orchard/internal/command/create"
deletepkg "github.com/cirruslabs/orchard/internal/command/delete"
"github.com/cirruslabs/orchard/internal/command/dev"
"github.com/cirruslabs/orchard/internal/command/list"
"github.com/cirruslabs/orchard/internal/command/worker"
"github.com/spf13/cobra"
)
func NewRootCmd() *cobra.Command {
command := &cobra.Command{
Use: "orchard",
SilenceUsage: true,
SilenceErrors: true,
}
addGroupedCommands(command, "Working With Resources:",
create.NewCommand(),
list.NewCommand(),
deletepkg.NewCommand(),
)
addGroupedCommands(command, "Administrative Tasks:",
controller.NewCommand(),
worker.NewCommand(),
dev.NewCommand(),
)
return command
}
func addGroupedCommands(parent *cobra.Command, title string, commands ...*cobra.Command) {
group := &cobra.Group{
ID: title,
Title: title,
}
parent.AddGroup(group)
for _, command := range commands {
command.GroupID = group.ID
parent.AddCommand(command)
}
}

View File

@ -0,0 +1,34 @@
package worker
import (
"github.com/cirruslabs/orchard/internal/worker"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
func newRunCommand() *cobra.Command {
return &cobra.Command{
Use: "run",
RunE: runWorker,
}
}
func runWorker(cmd *cobra.Command, args []string) (err error) {
// Initialize the logger
logger, err := zap.NewProduction()
if err != nil {
return err
}
defer func() {
if syncErr := logger.Sync(); syncErr != nil && err == nil {
err = syncErr
}
}()
worker, err := worker.New(worker.WithDataDir(dataDir), worker.WithLogger(logger))
if err != nil {
return err
}
return worker.Run(cmd.Context())
}

View File

@ -0,0 +1,29 @@
package worker
import (
"github.com/cirruslabs/orchard/internal/orchardhome"
"github.com/spf13/cobra"
"log"
"path/filepath"
)
var dataDir string
func NewCommand() *cobra.Command {
command := &cobra.Command{
Use: "worker",
Short: "Run a worker on the local machine",
}
command.AddCommand(newRunCommand())
orchardHome, err := orchardhome.Path()
if err != nil {
log.Fatal(err)
}
command.PersistentFlags().StringVar(&dataDir, "data-dir", filepath.Join(orchardHome, "worker"),
"path to the data directory")
return command
}

View File

@ -0,0 +1,77 @@
package controller
import (
storepkg "github.com/cirruslabs/orchard/internal/controller/store"
"github.com/cirruslabs/orchard/internal/responder"
"github.com/gin-gonic/gin"
"net/http"
)
type storeTxFunc func(cb func(txn *storepkg.Txn) error) error
type apiTxFunc func(txn *storepkg.Txn) responder.Responder
func (controller *Controller) initAPI() *gin.Engine {
gin.SetMode(gin.DebugMode)
ginEngine := gin.Default()
// v1 API
v1 := ginEngine.Group("/v1")
// Workers
v1.POST("/workers", func(c *gin.Context) {
controller.createWorker(c).Respond(c)
})
v1.PUT("/workers/:name", func(c *gin.Context) {
controller.updateWorker(c).Respond(c)
})
v1.GET("/workers/:name", func(c *gin.Context) {
controller.getWorker(c).Respond(c)
})
v1.GET("/workers", func(c *gin.Context) {
controller.listWorkers(c).Respond(c)
})
v1.DELETE("/workers/:name", func(c *gin.Context) {
controller.deleteWorker(c).Respond(c)
})
// VMs
v1.POST("/vms", func(c *gin.Context) {
controller.createVM(c).Respond(c)
})
v1.PUT("/vms/:name", func(c *gin.Context) {
controller.updateVM(c).Respond(c)
})
v1.GET("/vms/:name", func(c *gin.Context) {
controller.getVM(c).Respond(c)
})
v1.GET("/vms", func(c *gin.Context) {
controller.listVMs(c).Respond(c)
})
v1.DELETE("/vms/:name", func(c *gin.Context) {
controller.deleteVM(c).Respond(c)
})
return ginEngine
}
func (controller *Controller) storeView(cb apiTxFunc) responder.Responder {
return mapTxFuncs(controller.store.View, cb)
}
func (controller *Controller) storeUpdate(cb apiTxFunc) responder.Responder {
return mapTxFuncs(controller.store.Update, cb)
}
func mapTxFuncs(txFunc storeTxFunc, cb apiTxFunc) responder.Responder {
var result responder.Responder
if err := txFunc(func(txn *storepkg.Txn) error {
result = cb(txn)
return nil
}); err != nil {
return responder.Code(http.StatusInternalServerError)
}
return result
}

View File

@ -0,0 +1,145 @@
package controller
import (
"errors"
storepkg "github.com/cirruslabs/orchard/internal/controller/store"
"github.com/cirruslabs/orchard/internal/responder"
"github.com/cirruslabs/orchard/pkg/resource/v1"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"net/http"
"time"
)
func (controller *Controller) createVM(ctx *gin.Context) responder.Responder {
var vm v1.VM
if err := ctx.ShouldBindJSON(&vm); err != nil {
return responder.Code(http.StatusBadRequest)
}
if vm.Name == "" || vm.Image == "" || vm.CPU == 0 || vm.Memory == 0 {
return responder.Code(http.StatusPreconditionFailed)
}
vm.Status = v1.VMStatusPending
vm.CreatedAt = time.Now()
vm.DeletedAt = time.Time{}
vm.UID = uuid.New().String()
vm.Generation = 0
return controller.storeUpdate(func(txn *storepkg.Txn) responder.Responder {
// Does the VM resource with this name already exists?
_, err := txn.GetVM(vm.Name)
if !errors.Is(err, storepkg.ErrNotFound) {
return responder.Code(http.StatusConflict)
}
if err := txn.SetVM(&vm); err != nil {
return responder.Code(http.StatusInternalServerError)
}
return responder.JSON(http.StatusOK, &vm)
})
}
func (controller *Controller) updateVM(ctx *gin.Context) responder.Responder {
var userVM v1.VM
if err := ctx.ShouldBindJSON(&userVM); err != nil {
return responder.Code(http.StatusBadRequest)
}
if userVM.Name == "" {
return responder.Code(http.StatusPreconditionFailed)
}
return controller.storeUpdate(func(txn *storepkg.Txn) responder.Responder {
dbVM, err := txn.GetVM(userVM.Name)
if err != nil {
if errors.Is(err, storepkg.ErrNotFound) {
return responder.Code(http.StatusNotFound)
}
return responder.Code(http.StatusInternalServerError)
}
dbVM.Status = userVM.Status
dbVM.Generation++
if err := txn.SetVM(dbVM); err != nil {
return responder.Code(http.StatusInternalServerError)
}
return responder.JSON(http.StatusOK, &dbVM)
})
}
func (controller *Controller) getVM(ctx *gin.Context) responder.Responder {
name := ctx.Param("name")
return controller.storeView(func(txn *storepkg.Txn) responder.Responder {
vm, err := txn.GetVM(name)
if err != nil {
if errors.Is(err, storepkg.ErrNotFound) {
return responder.Code(http.StatusNotFound)
}
return responder.Code(http.StatusInternalServerError)
}
return responder.JSON(http.StatusOK, &vm)
})
}
func (controller *Controller) listVMs(ctx *gin.Context) responder.Responder {
return controller.storeView(func(txn *storepkg.Txn) responder.Responder {
vms, err := txn.ListVMs()
if err != nil {
if errors.Is(err, storepkg.ErrNotFound) {
return responder.Code(http.StatusNotFound)
}
return responder.Code(http.StatusInternalServerError)
}
return responder.JSON(http.StatusOK, &vms)
})
}
func (controller *Controller) deleteVM(ctx *gin.Context) responder.Responder {
name := ctx.Param("name")
if ctx.Query("force") != "" {
return controller.storeUpdate(func(txn *storepkg.Txn) responder.Responder {
if err := txn.DeleteVM(name); err != nil {
if errors.Is(err, storepkg.ErrNotFound) {
return responder.Code(http.StatusNotFound)
}
return responder.Code(http.StatusInternalServerError)
}
return responder.Code(http.StatusOK)
})
}
return controller.storeUpdate(func(txn *storepkg.Txn) responder.Responder {
vm, err := txn.GetVM(name)
if err != nil {
if errors.Is(err, storepkg.ErrNotFound) {
return responder.Code(http.StatusNotFound)
}
return responder.Code(http.StatusInternalServerError)
}
vm.DeletedAt = time.Now()
if err := txn.SetVM(vm); err != nil {
return responder.Code(http.StatusInternalServerError)
}
return responder.Code(http.StatusOK)
})
}

View File

@ -0,0 +1,123 @@
package controller
import (
"errors"
storepkg "github.com/cirruslabs/orchard/internal/controller/store"
"github.com/cirruslabs/orchard/internal/responder"
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"net/http"
"time"
)
func (controller *Controller) createWorker(ctx *gin.Context) responder.Responder {
var worker v1.Worker
if err := ctx.ShouldBindJSON(&worker); err != nil {
return responder.Code(http.StatusBadRequest)
}
if worker.Name == "" {
return responder.Code(http.StatusPreconditionFailed)
}
currentTime := time.Now()
if worker.LastSeen.IsZero() {
worker.LastSeen = currentTime
}
worker.CreatedAt = currentTime
worker.DeletedAt = time.Time{}
worker.UID = uuid.New().String()
worker.Generation = 0
return controller.storeUpdate(func(txn *storepkg.Txn) responder.Responder {
// Does the worker resource with this name already exists?
_, err := txn.GetWorker(worker.Name)
if !errors.Is(err, storepkg.ErrNotFound) {
return responder.Code(http.StatusConflict)
}
if err := txn.SetWorker(&worker); err != nil {
return responder.Code(http.StatusInternalServerError)
}
return responder.JSON(200, &worker)
})
}
func (controller *Controller) updateWorker(ctx *gin.Context) responder.Responder {
var userWorker v1.Worker
if err := ctx.ShouldBindJSON(&userWorker); err != nil {
return responder.Code(http.StatusBadRequest)
}
return controller.storeUpdate(func(txn *storepkg.Txn) responder.Responder {
dbWorker, err := txn.GetWorker(userWorker.Name)
if err != nil {
if errors.Is(err, storepkg.ErrNotFound) {
return responder.Code(http.StatusNotFound)
}
return responder.Code(http.StatusInternalServerError)
}
dbWorker.LastSeen = userWorker.LastSeen
dbWorker.Generation++
if err := txn.SetWorker(dbWorker); err != nil {
return responder.Code(http.StatusInternalServerError)
}
return responder.JSON(200, &dbWorker)
})
}
func (controller *Controller) getWorker(ctx *gin.Context) responder.Responder {
name := ctx.Param("name")
return controller.storeView(func(txn *storepkg.Txn) responder.Responder {
worker, err := txn.GetWorker(name)
if err != nil {
if errors.Is(err, storepkg.ErrNotFound) {
return responder.Code(http.StatusNotFound)
}
return responder.Code(http.StatusInternalServerError)
}
return responder.JSON(http.StatusOK, &worker)
})
}
func (controller *Controller) listWorkers(ctx *gin.Context) responder.Responder {
return controller.storeView(func(txn *storepkg.Txn) responder.Responder {
workers, err := txn.ListWorkers()
if err != nil {
if errors.Is(err, storepkg.ErrNotFound) {
return responder.Code(http.StatusNotFound)
}
return responder.Code(http.StatusInternalServerError)
}
return responder.JSON(http.StatusOK, &workers)
})
}
func (controller *Controller) deleteWorker(ctx *gin.Context) responder.Responder {
name := ctx.Param("name")
return controller.storeUpdate(func(txn *storepkg.Txn) responder.Responder {
if err := txn.DeleteWorker(name); err != nil {
if errors.Is(err, storepkg.ErrNotFound) {
return responder.Code(http.StatusNotFound)
}
return responder.Code(http.StatusInternalServerError)
}
return responder.Code(http.StatusOK)
})
}

View File

@ -0,0 +1,96 @@
package controller
import (
"context"
"crypto/tls"
"fmt"
storepkg "github.com/cirruslabs/orchard/internal/controller/store"
"go.uber.org/zap"
"net"
"net/http"
)
type Controller struct {
dataDir string
listenAddr string
tlsConfig *tls.Config
listener net.Listener
httpServer *http.Server
store *storepkg.Store
logger *zap.SugaredLogger
}
func New(opts ...Option) (*Controller, error) {
controller := &Controller{}
// Apply options
for _, opt := range opts {
opt(controller)
}
// Apply defaults
if controller.dataDir == "" {
return nil, fmt.Errorf("%w: please specify the data directory path with WithDataDir()")
}
if controller.listenAddr == "" {
controller.listenAddr = ":6120"
}
if controller.logger == nil {
controller.logger = zap.NewNop().Sugar()
}
// Instantiate controller
store, err := storepkg.New(controller.dbPath())
if err != nil {
return nil, err
}
controller.store = store
listener, err := net.Listen("tcp", controller.listenAddr)
if err != nil {
return nil, err
}
if controller.tlsConfig != nil {
controller.listener = tls.NewListener(listener, controller.tlsConfig)
} else {
controller.listener = listener
}
controller.httpServer = &http.Server{
Handler: controller.initAPI(),
}
return controller, nil
}
func (controller *Controller) Run(ctx context.Context) error {
// Run the scheduler so that each VM will eventually
// be assigned to a specific Worker
go func() {
err := runScheduler(controller.store)
if err != nil {
panic(err)
}
}()
// Run the janitor so that inactive workers
// will eventually be removed from the DB
go func() {
err := controller.runJanitor(controller.store)
if err != nil {
panic(err)
}
}()
// A helper function to shut down the HTTP server on context cancellation
go func() {
<-ctx.Done()
controller.httpServer.Shutdown(ctx)
}()
if err := controller.httpServer.Serve(controller.listener); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,15 @@
package controller
import "path/filepath"
func (controller *Controller) dbPath() string {
return filepath.Join(controller.dataDir, "db")
}
func (controller *Controller) caCertPath() string {
return filepath.Join(controller.dataDir, "ca.crt")
}
func (controller *Controller) caKeyPath() string {
return filepath.Join(controller.dataDir, "ca.key")
}

View File

@ -0,0 +1,50 @@
package controller
import (
storepkg "github.com/cirruslabs/orchard/internal/controller/store"
"github.com/cirruslabs/orchard/pkg/resource/v1"
"time"
)
const janitorInterval = 5 * time.Second
func (controller *Controller) runJanitor(store *storepkg.Store) error {
ticker := time.Tick(janitorInterval)
for {
if err := controller.runJanitorInner(store); err != nil {
return err
}
<-ticker
}
}
func (controller *Controller) runJanitorInner(store *storepkg.Store) error {
var workers []*v1.Worker
var err error
err = store.View(func(txn *storepkg.Txn) error {
workers, err = txn.ListWorkers()
return err
})
if err != nil {
return err
}
for _, worker := range workers {
if time.Now().Sub(worker.LastSeen).Minutes() > 1 {
controller.logger.Debugf("removing outdated worker %s", worker.Name)
err := store.Update(func(txn *storepkg.Txn) error {
return txn.DeleteWorker(worker.Name)
})
if err != nil {
return err
}
}
}
return nil
}

View File

@ -0,0 +1,32 @@
package controller
import (
"crypto/tls"
"go.uber.org/zap"
)
type Option func(*Controller)
func WithDataDir(dataDir string) Option {
return func(controller *Controller) {
controller.dataDir = dataDir
}
}
func WithListenAddr(listenAddr string) Option {
return func(controller *Controller) {
controller.listenAddr = listenAddr
}
}
func WithTLSConfig(tlsConfig *tls.Config) Option {
return func(controller *Controller) {
controller.tlsConfig = tlsConfig
}
}
func WithLogger(logger *zap.Logger) Option {
return func(controller *Controller) {
controller.logger = logger.Sugar()
}
}

View File

@ -0,0 +1,72 @@
package controller
import (
storepkg "github.com/cirruslabs/orchard/internal/controller/store"
"github.com/cirruslabs/orchard/pkg/resource/v1"
"sort"
"time"
)
const schedulerInterval = 5 * time.Second
func runScheduler(store *storepkg.Store) error {
ticker := time.Tick(schedulerInterval)
for {
if err := runSchedulerInner(store); err != nil {
return err
}
<-ticker
}
}
func runSchedulerInner(store *storepkg.Store) error {
var vms []*v1.VM
var workers []*v1.Worker
var err error
err = store.View(func(txn *storepkg.Txn) error {
vms, err = txn.ListVMs()
if err != nil {
return err
}
workers, err = txn.ListWorkers()
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
// Sort VMs by date of creation
sort.Slice(vms, func(i, j int) bool {
return vms[i].CreatedAt.Before(vms[j].CreatedAt)
})
for _, vm := range vms {
if vm.Worker != "" {
continue
}
// Find an appropriate worker to run this VM on
for _, worker := range workers {
vm.Worker = worker.Name
err := store.Update(func(txn *storepkg.Txn) error {
return txn.SetVM(vm)
})
if err != nil {
return err
}
break
}
}
return nil
}

View File

@ -0,0 +1,20 @@
package store
import (
"errors"
"fmt"
"github.com/dgraph-io/badger/v3"
)
var (
ErrNotFound = errors.New("DB entry not found")
ErrBadgerFailed = errors.New("BadgerDB failed")
)
func mapErr(err error) error {
if errors.Is(err, badger.ErrKeyNotFound) {
return ErrNotFound
}
return fmt.Errorf("%w: %v", ErrBadgerFailed, err)
}

View File

@ -0,0 +1,24 @@
package store
import (
"github.com/dgraph-io/badger/v3"
)
type Store struct {
db *badger.DB
}
func New(dbPath string) (*Store, error) {
opts := badger.DefaultOptions(dbPath)
opts.SyncWrites = true
db, err := badger.Open(opts)
if err != nil {
return nil, err
}
return &Store{
db: db,
}, nil
}

View File

@ -0,0 +1,23 @@
package store
import "github.com/dgraph-io/badger/v3"
type Txn struct {
badgerTxn *badger.Txn
}
func (store *Store) View(cb func(txn *Txn) error) error {
return store.db.View(func(txn *badger.Txn) error {
return cb(&Txn{
badgerTxn: txn,
})
})
}
func (store *Store) Update(cb func(txn *Txn) error) error {
return store.db.Update(func(txn *badger.Txn) error {
return cb(&Txn{
badgerTxn: txn,
})
})
}

View File

@ -0,0 +1,96 @@
package store
import (
"encoding/json"
"github.com/cirruslabs/orchard/pkg/resource/v1"
"github.com/dgraph-io/badger/v3"
"path"
)
const SpaceVMs = "/vms"
func VMKey(name string) []byte {
return []byte(path.Join(SpaceVMs, name))
}
func (txn *Txn) GetVM(name string) (result *v1.VM, err error) {
defer func() {
err = mapErr(err)
}()
key := VMKey(name)
item, err := txn.badgerTxn.Get(key)
if err != nil {
return nil, err
}
valueBytes, err := item.ValueCopy(nil)
if err != nil {
return nil, err
}
var vm v1.VM
err = json.Unmarshal(valueBytes, &vm)
if err != nil {
return nil, err
}
return &vm, nil
}
func (txn *Txn) SetVM(vm *v1.VM) (err error) {
defer func() {
err = mapErr(err)
}()
key := VMKey(vm.Name)
valueBytes, err := json.Marshal(vm)
if err != nil {
return err
}
return txn.badgerTxn.Set(key, valueBytes)
}
func (txn *Txn) DeleteVM(name string) (err error) {
defer func() {
err = mapErr(err)
}()
key := VMKey(name)
return txn.badgerTxn.Delete(key)
}
func (txn *Txn) ListVMs() (result []*v1.VM, err error) {
defer func() {
err = mapErr(err)
}()
it := txn.badgerTxn.NewIterator(badger.IteratorOptions{
Prefix: []byte(SpaceVMs),
})
defer it.Close()
for it.Rewind(); it.Valid(); it.Next() {
item := it.Item()
vmBytes, err := item.ValueCopy(nil)
if err != nil {
return nil, err
}
var vm v1.VM
if err := json.Unmarshal(vmBytes, &vm); err != nil {
return nil, err
}
result = append(result, &vm)
}
return result, nil
}

View File

@ -0,0 +1,96 @@
package store
import (
"encoding/json"
"github.com/cirruslabs/orchard/pkg/resource/v1"
"github.com/dgraph-io/badger/v3"
"path"
)
const SpaceWorkers = "/workers"
func WorkerKey(name string) []byte {
return []byte(path.Join(SpaceWorkers, name))
}
func (txn *Txn) GetWorker(name string) (result *v1.Worker, err error) {
defer func() {
err = mapErr(err)
}()
key := WorkerKey(name)
item, err := txn.badgerTxn.Get(key)
if err != nil {
return nil, err
}
valueBytes, err := item.ValueCopy(nil)
if err != nil {
return nil, err
}
var worker v1.Worker
err = json.Unmarshal(valueBytes, &worker)
if err != nil {
return nil, err
}
return &worker, nil
}
func (txn *Txn) SetWorker(worker *v1.Worker) (err error) {
defer func() {
err = mapErr(err)
}()
key := WorkerKey(worker.Name)
valueBytes, err := json.Marshal(worker)
if err != nil {
return err
}
return txn.badgerTxn.Set(key, valueBytes)
}
func (txn *Txn) DeleteWorker(name string) (err error) {
defer func() {
err = mapErr(err)
}()
key := WorkerKey(name)
return txn.badgerTxn.Delete(key)
}
func (txn *Txn) ListWorkers() (result []*v1.Worker, err error) {
defer func() {
err = mapErr(err)
}()
it := txn.badgerTxn.NewIterator(badger.IteratorOptions{
Prefix: []byte(SpaceWorkers),
})
defer it.Close()
for it.Rewind(); it.Valid(); it.Next() {
item := it.Item()
vmBytes, err := item.ValueCopy(nil)
if err != nil {
return nil, err
}
var worker v1.Worker
if err := json.Unmarshal(vmBytes, &worker); err != nil {
return nil, err
}
result = append(result, &worker)
}
return result, nil
}

View File

@ -0,0 +1,21 @@
package orchardhome
import (
"os"
"path/filepath"
)
func Path() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
orchardDir := filepath.Join(homeDir, ".orchard")
if err := os.Mkdir(orchardDir, 0700); err != nil && !os.IsExist(err) {
return "", err
}
return orchardDir, nil
}

View File

@ -0,0 +1,35 @@
package responder
import "github.com/gin-gonic/gin"
type CodeResponder struct {
code int
headers map[string]string
DefaultResponder
}
func Code(code int, opts ...Option) *CodeResponder {
responder := &CodeResponder{
code: code,
headers: map[string]string{},
}
for _, opt := range opts {
opt(responder)
}
return responder
}
func (responder *CodeResponder) SetHeader(key string, value string) {
responder.headers[key] = value
}
func (responder *CodeResponder) Respond(c *gin.Context) {
for key, value := range responder.headers {
c.Header(key, value)
}
c.Status(responder.code)
}

View File

@ -0,0 +1,39 @@
package responder
import (
"github.com/gin-gonic/gin"
)
type JSONResponder struct {
code int
headers map[string]string
obj interface{}
DefaultResponder
}
func JSON(code int, obj interface{}, opts ...Option) *JSONResponder {
responder := &JSONResponder{
code: code,
headers: map[string]string{},
obj: obj,
}
for _, opt := range opts {
opt(responder)
}
return responder
}
func (responder *JSONResponder) SetHeader(key string, value string) {
responder.headers[key] = value
}
func (responder *JSONResponder) Respond(c *gin.Context) {
for key, value := range responder.headers {
c.Header(key, value)
}
c.JSON(responder.code, responder.obj)
}

View File

@ -0,0 +1,11 @@
package responder
type Option func(responder Responder)
func WithHeaders(headers map[string]string) Option {
return func(responder Responder) {
for key, value := range headers {
responder.SetHeader(key, value)
}
}
}

View File

@ -0,0 +1,15 @@
package responder
import (
"github.com/gin-gonic/gin"
)
type Responder interface {
Respond(c *gin.Context)
SetHeader(key string, value string)
}
type DefaultResponder struct{}
func (dr DefaultResponder) Respond(c *gin.Context) {}
func (dr DefaultResponder) SetHeader(key string, value string) {}

17
internal/worker/option.go Normal file
View File

@ -0,0 +1,17 @@
package worker
import "go.uber.org/zap"
type Option func(*Worker)
func WithDataDir(dataDir string) Option {
return func(worker *Worker) {
worker.dataDir = dataDir
}
}
func WithLogger(logger *zap.Logger) Option {
return func(worker *Worker) {
worker.logger = logger.Sugar()
}
}

View File

@ -0,0 +1,57 @@
package vmmanager
import (
"bytes"
"context"
"errors"
"fmt"
"os/exec"
"strings"
)
const tartCommandName = "tart"
var (
ErrTartNotFound = errors.New("tart command not found")
ErrTartFailed = errors.New("tart command returned non-zero exit code")
)
func Tart(
ctx context.Context,
args ...string,
) (string, string, error) {
cmd := exec.CommandContext(ctx, tartCommandName, args...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
if errors.Is(err, exec.ErrNotFound) {
return "", "", fmt.Errorf("%w: %s command not found in PATH, make sure Tart is installed",
ErrTartNotFound, tartCommandName)
}
if _, ok := err.(*exec.ExitError); ok {
// Tart command failed, redefine the error
// to be the Tart-specific output
err = fmt.Errorf("%w: %q", ErrTartFailed, firstNonEmptyLine(stderr.String(), stdout.String()))
}
}
return stdout.String(), stderr.String(), err
}
func firstNonEmptyLine(outputs ...string) string {
for _, output := range outputs {
for _, line := range strings.Split(output, "\n") {
if line != "" {
return line
}
}
}
return ""
}

View File

@ -0,0 +1,73 @@
package vmmanager
import (
"context"
"fmt"
"github.com/cirruslabs/orchard/pkg/resource/v1"
"sync"
)
type VM struct {
id string
vmResource *v1.VM
ctx context.Context
cancel context.CancelFunc
wg *sync.WaitGroup
}
func NewVM(vmResource *v1.VM) *VM {
ctx, cancel := context.WithCancel(context.Background())
vm := &VM{
id: fmt.Sprintf("orchard-%s-%s", vmResource.Name, vmResource.UID),
vmResource: vmResource,
ctx: ctx,
cancel: cancel,
wg: &sync.WaitGroup{},
}
vm.wg.Add(1)
go func() {
defer vm.wg.Done()
if err := vm.run(vm.ctx); err != nil {
vmResource.Status = v1.VMStatusFailed
}
}()
return vm
}
func (vm *VM) run(ctx context.Context) error {
_, _, err := Tart(ctx, "clone", vm.vmResource.Image, vm.id)
if err != nil {
return err
}
_, _, err = Tart(ctx, "run", vm.id)
if err != nil {
return err
}
return nil
}
func (vm *VM) Close() error {
_, _, _ = Tart(context.Background(), "stop", "--timeout", "5", vm.id)
vm.cancel()
vm.wg.Wait()
_, _, err := Tart(context.Background(), "delete", vm.id)
if err != nil {
return fmt.Errorf("failed to delete VM %s: %v", vm.id, err)
}
return nil
}

View File

@ -0,0 +1,58 @@
package vmmanager
import (
"fmt"
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
)
type VMManager struct {
vms map[string]*VM
}
func New() *VMManager {
return &VMManager{
vms: map[string]*VM{},
}
}
func (vmm *VMManager) Exists(vmResource *v1.VM) bool {
_, ok := vmm.vms[vmResource.UID]
return ok
}
func (vmm *VMManager) Get(vmResource *v1.VM) (*VM, error) {
managedVM, ok := vmm.vms[vmResource.UID]
if !ok {
return nil, fmt.Errorf("VM does not exist")
}
return managedVM, nil
}
func (vmm *VMManager) Create(vmResource *v1.VM) (*VM, error) {
if _, ok := vmm.vms[vmResource.UID]; ok {
return nil, fmt.Errorf("VM already exists")
}
managedVM := NewVM(vmResource)
vmm.vms[vmResource.UID] = managedVM
return managedVM, nil
}
func (vmm *VMManager) Delete(vmResource *v1.VM) error {
managedVM, ok := vmm.vms[vmResource.UID]
if !ok {
return fmt.Errorf("VM does not exist")
}
if err := managedVM.Close(); err != nil {
return err
}
delete(vmm.vms, vmResource.UID)
return nil
}

208
internal/worker/worker.go Normal file
View File

@ -0,0 +1,208 @@
package worker
import (
"context"
"errors"
"fmt"
"github.com/cirruslabs/orchard/internal/worker/vmmanager"
"github.com/cirruslabs/orchard/pkg/client"
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
"go.uber.org/zap"
"os"
"time"
)
const pollInterval = 5 * time.Second
var ErrPollFailed = errors.New("failed to poll controller")
type Worker struct {
dataDir string
name string
uid string
vmm *vmmanager.VMManager
client *client.Client
logger *zap.SugaredLogger
}
func New(opts ...Option) (*Worker, error) {
worker := &Worker{
vmm: vmmanager.New(),
}
// Apply options
for _, opt := range opts {
opt(worker)
}
// Apply defaults
if worker.name == "" {
hostname, err := os.Hostname()
if err != nil {
return nil, err
}
worker.name = hostname
}
if worker.logger == nil {
worker.logger = zap.NewNop().Sugar()
}
// Instantiate worker
client, err := client.New()
if err != nil {
return nil, err
}
worker.client = client
return worker, nil
}
func (worker *Worker) Run(ctx context.Context) error {
tickCh := time.Tick(pollInterval)
for {
if err := worker.registerWorker(ctx); err != nil {
worker.logger.Warnf("failed to register worker: %v", err)
select {
case <-tickCh:
// continue
case <-ctx.Done():
return ctx.Err()
}
continue
}
for {
if err := worker.updateWorker(ctx); err != nil {
worker.logger.Errorf("failed to update worker resource: %v", err)
select {
case <-tickCh:
// continue
case <-ctx.Done():
return ctx.Err()
}
break
}
if err := worker.syncVMs(ctx); err != nil {
worker.logger.Warnf("failed to sync VMs: %v", err)
select {
case <-tickCh:
// continue
case <-ctx.Done():
return ctx.Err()
}
break
}
select {
case <-tickCh:
// continue
case <-ctx.Done():
return ctx.Err()
}
}
}
}
func (worker *Worker) registerWorker(ctx context.Context) error {
workerResource := &v1.Worker{
Meta: v1.Meta{
Name: worker.name,
},
LastSeen: time.Now(),
}
workerResource, err := worker.client.Workers().Create(ctx, workerResource)
if err != nil {
return err
}
worker.uid = workerResource.UID
worker.logger.Infof("registered worker %s with UID %s", worker.name, workerResource.UID)
return nil
}
func (worker *Worker) updateWorker(ctx context.Context) error {
workerResource, err := worker.client.Workers().Get(ctx, worker.name)
if err != nil {
return fmt.Errorf("%w: failed to retrieve worker from the API: %v", ErrPollFailed, err)
}
if workerResource.UID != worker.uid {
return fmt.Errorf("%w: our UID is %s, controller's ID is %s", ErrPollFailed)
}
worker.logger.Debugf("got worker from the API")
workerResource.LastSeen = time.Now()
if err := worker.client.Workers().Update(ctx, workerResource); err != nil {
return fmt.Errorf("%w: failed to update worker in the API: %v", err)
}
worker.logger.Debugf("updated worker in the API")
return nil
}
func (worker *Worker) syncVMs(ctx context.Context) error {
vms, err := worker.client.VMs().List(ctx)
if err != nil {
return err
}
worker.logger.Infof("syncing %d VMs...", len(vms))
for _, vmResource := range vms {
if vmResource.Worker != worker.name {
continue
}
if !vmResource.DeletedAt.IsZero() {
worker.logger.Debugf("deleting VM %s (%s)", vmResource.Name, vmResource.UID)
// Delete VM locally, report to the controller
if worker.vmm.Exists(&vmResource) {
if err := worker.vmm.Delete(&vmResource); err != nil {
return err
}
}
if err := worker.client.VMs().Delete(ctx, vmResource.Name, true); err != nil {
return fmt.Errorf("%w: failed to delete VM %s (%s) from the API: %v",
ErrPollFailed, vmResource.Name, vmResource.UID, err)
}
worker.logger.Infof("deleted VM %s (%s)", vmResource.Name, vmResource.UID)
} else if !worker.vmm.Exists(&vmResource) {
worker.logger.Debugf("creating VM %s (%s)", vmResource.Name, vmResource.UID)
// Create or update VM locally, report to controller
_, err := worker.vmm.Create(&vmResource)
if err != nil {
return err
}
vmResource.Status = v1.VMStatusRunning
if err := worker.client.VMs().Update(ctx, &vmResource); err != nil {
return fmt.Errorf("%w: failed to update VM %s (%s) in the API: %v",
ErrPollFailed, vmResource.Name, vmResource.UID, err)
}
worker.logger.Infof("spawned VM %s (%s)", vmResource.Name, vmResource.UID)
}
}
return nil
}

140
pkg/client/client.go Normal file
View File

@ -0,0 +1,140 @@
package client
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
)
var (
ErrFailed = errors.New("API client failed")
)
type Client struct {
address string
tlsConfig *tls.Config
httpClient *http.Client
baseURL *url.URL
}
type Config struct {
Address string
TLSConfig *tls.Config
}
func New(opts ...Option) (*Client, error) {
client := &Client{}
// Apply options
for _, opt := range opts {
opt(client)
}
// Apply defaults
if client.address == "" {
client.address = "http://127.0.0.1:6120"
}
// Instantiate client
httpClient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: client.tlsConfig,
},
}
url, err := url.Parse(client.address)
if err != nil {
return nil, err
}
return &Client{
httpClient: httpClient,
baseURL: url,
}, nil
}
func (client *Client) request(
ctx context.Context,
method string,
path string,
in interface{},
out interface{},
params map[string]string,
) (*http.Response, error) {
var body io.Reader
if in != nil {
jsonBytes, err := json.Marshal(in)
if err != nil {
return nil, fmt.Errorf("%w to marshal request body: %v", ErrFailed, err)
}
body = bytes.NewBuffer(jsonBytes)
}
endpointURL, err := url.Parse("v1/" + path)
if err != nil {
return nil, fmt.Errorf("%w to parse API endpoint path: %v", ErrFailed, err)
}
endpointURL = &url.URL{
Scheme: client.baseURL.Scheme,
User: client.baseURL.User,
Host: client.baseURL.Host,
Path: endpointURL.Path,
RawPath: endpointURL.RawPath,
}
values := endpointURL.Query()
for key, value := range params {
values.Set(key, value)
}
endpointURL.RawQuery = values.Encode()
request, err := http.NewRequestWithContext(ctx, method, endpointURL.String(), body)
if err != nil {
return nil, fmt.Errorf("%w instantiate a request: %v", ErrFailed, err)
}
response, err := client.httpClient.Do(request)
if err != nil {
return nil, fmt.Errorf("%w to make a request: %v", ErrFailed, err)
}
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w to make a request: %d %s",
ErrFailed, response.StatusCode, http.StatusText(response.StatusCode))
}
if out != nil {
bodyBytes, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w to read response body: %v", ErrFailed, err)
}
if err := json.Unmarshal(bodyBytes, out); err != nil {
return nil, fmt.Errorf("%w to unmarshal response body: %v", ErrFailed, err)
}
}
return response, nil
}
func (client *Client) Workers() *WorkersService {
return &WorkersService{
client: client,
}
}
func (client *Client) VMs() *VMsService {
return &VMsService{
client: client,
}
}

17
pkg/client/option.go Normal file
View File

@ -0,0 +1,17 @@
package client
import "crypto/tls"
type Option func(*Client)
func WithAddress(address string) Option {
return func(client *Client) {
client.address = address
}
}
func WithTLSConfig(tlsConfig *tls.Config) Option {
return func(client *Client) {
client.tlsConfig = tlsConfig
}
}

72
pkg/client/vms.go Normal file
View File

@ -0,0 +1,72 @@
package client
import (
"context"
"fmt"
"github.com/cirruslabs/orchard/pkg/resource/v1"
"net/http"
)
type VMsService struct {
client *Client
}
func (service *VMsService) Create(ctx context.Context, vm *v1.VM) error {
_, err := service.client.request(ctx, http.MethodPost, "vms",
vm, nil, nil)
if err != nil {
return err
}
return nil
}
func (service *VMsService) List(ctx context.Context) ([]v1.VM, error) {
var vms []v1.VM
_, err := service.client.request(ctx, http.MethodGet, "vms",
nil, &vms, nil)
if err != nil {
return nil, err
}
return vms, nil
}
func (service *VMsService) Get(ctx context.Context, name string) (*v1.VM, error) {
var vm v1.VM
_, err := service.client.request(ctx, http.MethodGet, fmt.Sprintf("vms/%s", name),
nil, &vm, nil)
if err != nil {
return nil, err
}
return &vm, nil
}
func (service *VMsService) Update(ctx context.Context, vm *v1.VM) error {
_, err := service.client.request(ctx, http.MethodPut, fmt.Sprintf("vms/%s", vm.Name),
vm, nil, nil)
if err != nil {
return err
}
return nil
}
func (service *VMsService) Delete(ctx context.Context, name string, force bool) error {
params := map[string]string{}
if force {
params["force"] = "true"
}
_, err := service.client.request(ctx, http.MethodDelete, fmt.Sprintf("vms/%s", name),
nil, nil, params)
if err != nil {
return err
}
return nil
}

66
pkg/client/workers.go Normal file
View File

@ -0,0 +1,66 @@
package client
import (
"context"
"fmt"
"github.com/cirruslabs/orchard/pkg/resource/v1"
"net/http"
)
type WorkersService struct {
client *Client
}
func (service *WorkersService) Create(ctx context.Context, worker *v1.Worker) (*v1.Worker, error) {
_, err := service.client.request(ctx, http.MethodPost, "workers",
worker, &worker, nil)
if err != nil {
return nil, err
}
return worker, nil
}
func (service *WorkersService) List(ctx context.Context) ([]v1.Worker, error) {
var workers []v1.Worker
_, err := service.client.request(ctx, http.MethodGet, "workers",
nil, &workers, nil)
if err != nil {
return nil, err
}
return workers, nil
}
func (service *WorkersService) Get(ctx context.Context, name string) (*v1.Worker, error) {
var worker v1.Worker
_, err := service.client.request(ctx, http.MethodGet, fmt.Sprintf("workers/%s", name),
nil, &worker, nil)
if err != nil {
return nil, err
}
return &worker, nil
}
func (service *WorkersService) Update(ctx context.Context, worker *v1.Worker) error {
_, err := service.client.request(ctx, http.MethodPut, fmt.Sprintf("workers/%s", worker.Name),
worker, nil, nil)
if err != nil {
return err
}
return nil
}
func (service *WorkersService) Delete(ctx context.Context, name string) error {
_, err := service.client.request(ctx, http.MethodDelete, fmt.Sprintf("workers/%s", name),
nil, nil, nil)
if err != nil {
return err
}
return nil
}

74
pkg/resource/v1/v1.go Normal file
View File

@ -0,0 +1,74 @@
package v1
import (
"time"
)
// Meta is a common set of fields that apply to all resources managed by the Controller.
type Meta struct {
// Name is a human-readable resource identifier populated by the Worker or Client.
//
// There can't be multiple resources with the same Name in the DB at any given time.
Name string `json:"name"`
// CreatedAt is a useful field for scheduler prioritization.
//
// It is populated by the Controller with the current time
// when receiving a POST request.
CreatedAt time.Time `json:"createdAt"`
// DeletedAt is a useful field for graceful resource termination.
//
// It is populated by the Controller with the current time
// when receiving a DELETE request.
DeletedAt time.Time `json:"deletedAt"`
// UID is a useful field for avoiding data races within a single Name.
//
// It is populated by the Controller when receiving a POST request.
UID string `json:"uid"`
// Generation is a useful field for avoiding data races within a single UID.
//
// It is populated by the controller when receiving POST or PUT requests.
Generation int64 `json:"generation"`
}
type Worker struct {
// LastSeen is set by the Worker and is used by the Controller
// to track unhealthy Workers.
LastSeen time.Time
Meta
}
type VM struct {
Image string `json:"image"`
CPU uint64 `json:"cpu"`
Memory uint64 `json:"memory"`
Softnet bool `json:"softnet"`
Headless bool `json:"headless"`
// Status field is used to track the lifecycle of the VM associated with this resource.
Status VMStatus `json:"status"`
// Worker field is set by the Controller to assign this VM to a specific Worker.
Worker string `json:"worker"`
Meta
}
type VMStatus string
const (
// VMStatusPending is set by the Controller for all newly-created VM resources.
VMStatusPending VMStatus = "pending"
// VMStatusRunning is set by the Worker once it starts running
// the Virtual Machine associated with this VM resource.
VMStatusRunning VMStatus = "running"
// VMStatusFailed is set by both the Controller and the Worker to indicate a failure
// that prevented the VM resource from reaching the VMStatusRunning state.
VMStatusFailed VMStatus = "failed"
)