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:
parent
dcbbb8b8de
commit
92e8732d46
26
README.md
26
README.md
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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=
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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 ""
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
)
|
||||
Loading…
Reference in New Issue