Introduce "orchard context" (#18)
This commit is contained in:
parent
a7264370f5
commit
0b9b96b8c9
5
go.mod
5
go.mod
|
|
@ -6,15 +6,20 @@ 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/gofrs/flock v0.8.1
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gosuri/uitable v0.0.4
|
||||
github.com/manifoldco/promptui v0.9.0
|
||||
github.com/spf13/cobra v1.6.0
|
||||
go.uber.org/zap v1.24.0
|
||||
golang.org/x/exp v0.0.0-20230203172020-98cc5a0785f9
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // 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
|
||||
|
|
|
|||
13
go.sum
13
go.sum
|
|
@ -9,6 +9,12 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
|
|||
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/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/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=
|
||||
|
|
@ -44,6 +50,8 @@ github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJ
|
|||
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/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
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=
|
||||
|
|
@ -92,6 +100,8 @@ 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/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
||||
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=
|
||||
|
|
@ -170,6 +180,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
|||
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/exp v0.0.0-20230203172020-98cc5a0785f9 h1:frX3nT9RkKybPnjyI+yvZh6ZucTZatCCEm9D47sZ2zo=
|
||||
golang.org/x/exp v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
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=
|
||||
|
|
@ -194,6 +206,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
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-20181122145206-62eef0e2fa9b/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=
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var contextName string
|
||||
|
||||
func NewCommand() *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "context",
|
||||
Short: "Manage client/worker → controller contexts",
|
||||
}
|
||||
|
||||
command.AddCommand(
|
||||
newCreateCommand(),
|
||||
newListCommand(),
|
||||
newDefaultCommand(),
|
||||
newDeleteCommand(),
|
||||
)
|
||||
|
||||
return command
|
||||
}
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cirruslabs/orchard/internal/config"
|
||||
"github.com/cirruslabs/orchard/internal/controller"
|
||||
"github.com/cirruslabs/orchard/pkg/client"
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/spf13/cobra"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var ErrCreateFailed = errors.New("failed to create context")
|
||||
|
||||
var force bool
|
||||
|
||||
func newCreateCommand() *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a new context",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runCreate,
|
||||
}
|
||||
|
||||
command.PersistentFlags().StringVar(&contextName, "name", "default",
|
||||
"context name to use")
|
||||
command.PersistentFlags().BoolVar(&force, "force", false,
|
||||
"create the context even if a context with the same name already exists")
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func runCreate(cmd *cobra.Command, args []string) error {
|
||||
addr := args[0]
|
||||
|
||||
if !strings.HasPrefix(addr, "https://") && !strings.HasPrefix(addr, "http://") {
|
||||
addr = "https://" + addr
|
||||
}
|
||||
|
||||
controllerURL, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if controllerURL.Port() == "" {
|
||||
controllerURL.Host += fmt.Sprintf(":%d", controller.DefaultPort)
|
||||
}
|
||||
|
||||
// Establish trust
|
||||
trustedControllerCertificate, err := probeControllerCertificate(controllerURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check that the API is accessible
|
||||
privatePool := x509.NewCertPool()
|
||||
privatePool.AddCert(trustedControllerCertificate)
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS13,
|
||||
RootCAs: privatePool,
|
||||
ServerName: controller.DefaultServerName,
|
||||
}
|
||||
|
||||
client, err := client.New(
|
||||
client.WithAddress(controllerURL.String()),
|
||||
client.WithTLSConfig(tlsConfig),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := client.Check(cmd.Context()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create and save the context
|
||||
configHandle, err := config.NewHandle()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
certificatePEMBytes := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: trustedControllerCertificate.Raw,
|
||||
})
|
||||
|
||||
return configHandle.CreateContext(contextName, config.Context{
|
||||
URL: controllerURL.String(),
|
||||
Certificate: certificatePEMBytes,
|
||||
}, force)
|
||||
}
|
||||
|
||||
func probeControllerCertificate(controllerURL *url.URL) (*x509.Certificate, error) {
|
||||
// Do not use PKI
|
||||
emptyPool := x509.NewCertPool()
|
||||
|
||||
//nolint:gosec // since we're not using PKI, InsecureSkipVerify is a must here
|
||||
insecureTLSConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS13,
|
||||
RootCAs: emptyPool,
|
||||
ServerName: controller.DefaultServerName,
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
|
||||
var controllerCert *x509.Certificate
|
||||
|
||||
insecureTLSConfig.VerifyConnection = func(state tls.ConnectionState) error {
|
||||
if controllerCert != nil {
|
||||
return fmt.Errorf("%w: encountered more than one certificate while probing the controller",
|
||||
ErrCreateFailed)
|
||||
}
|
||||
|
||||
if len(state.PeerCertificates) != 1 {
|
||||
return fmt.Errorf("%w: controller presented %d certificate(s), expected only one",
|
||||
ErrCreateFailed, len(state.PeerCertificates))
|
||||
}
|
||||
|
||||
controllerCert = state.PeerCertificates[0]
|
||||
controllerCertFingerprint := sha256.Sum256(controllerCert.Raw)
|
||||
formattedControllerCertFingerprint := formatFingerprint(controllerCertFingerprint[:])
|
||||
|
||||
shortControllerName := controllerURL.Hostname()
|
||||
if controllerURL.Port() != strconv.FormatUint(controller.DefaultPort, 10) {
|
||||
shortControllerName += ":" + controllerURL.Port()
|
||||
}
|
||||
|
||||
fmt.Printf("The authencity of controller %s cannot be established.\n", shortControllerName)
|
||||
fmt.Printf("Certificate SHA-256 fingerprint is %s.\n", formattedControllerCertFingerprint)
|
||||
|
||||
promptTemplates := &promptui.PromptTemplates{
|
||||
Prompt: "{{ . }} ",
|
||||
Valid: "{{ . }} ",
|
||||
Invalid: "{{ . }} ",
|
||||
Success: "{{ . }} ",
|
||||
ValidationError: "{{ . }} ",
|
||||
}
|
||||
prompt := promptui.Prompt{
|
||||
Label: "Are you sure you want to establish trust to this certificate? (yes/no)",
|
||||
Validate: func(s string) error {
|
||||
if s != "yes" && s != "no" {
|
||||
//nolint:goerr113,golint,stylecheck // this is not a standard error
|
||||
return fmt.Errorf("Please specify \"yes\" or \"no\".")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Templates: promptTemplates,
|
||||
}
|
||||
|
||||
promptResult, err := prompt.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch promptResult {
|
||||
case "yes":
|
||||
return nil
|
||||
case "no":
|
||||
return fmt.Errorf("%w: certificate verification failed: no trust decision received from the user",
|
||||
ErrCreateFailed)
|
||||
default:
|
||||
return fmt.Errorf("%w: certificate verification failed: received unsupported answer from the user",
|
||||
ErrCreateFailed)
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := tls.Dial("tcp", controllerURL.Host, insecureTLSConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = conn.Close()
|
||||
}()
|
||||
|
||||
return controllerCert, nil
|
||||
}
|
||||
|
||||
func formatFingerprint(fingerprint []byte) string {
|
||||
var fingerprintPieces []string
|
||||
|
||||
for _, piece := range fingerprint {
|
||||
fingerprintPieces = append(fingerprintPieces, fmt.Sprintf("%02X", piece))
|
||||
}
|
||||
|
||||
return strings.Join(fingerprintPieces, " ")
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"github.com/cirruslabs/orchard/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newDefaultCommand() *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "default",
|
||||
Short: "Set a default context to use for client/worker commands",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runDefault,
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func runDefault(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
|
||||
configHandle, err := config.NewHandle()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return configHandle.SetDefaultContext(name)
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"github.com/cirruslabs/orchard/internal/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newDeleteCommand() *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "delete",
|
||||
Short: "Delete a context",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runDelete,
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func runDelete(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
|
||||
configHandle, err := config.NewHandle()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return configHandle.DeleteContext(name)
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/cirruslabs/orchard/internal/config"
|
||||
"github.com/gosuri/uitable"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newListCommand() *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List contexts",
|
||||
RunE: runList,
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func runList(cmd *cobra.Command, args []string) error {
|
||||
configHandle, err := config.NewHandle()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config, err := configHandle.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
table := uitable.New()
|
||||
|
||||
table.AddRow("Name", "URL")
|
||||
|
||||
for name, context := range config.Contexts {
|
||||
table.AddRow(name, context.URL)
|
||||
}
|
||||
|
||||
fmt.Println(table)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -119,7 +119,7 @@ func generateSelfSignedControllerCertificate() (tls.Certificate, error) {
|
|||
BasicConstraintsValid: true,
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||
DNSNames: []string{"orchard-controller"},
|
||||
DNSNames: []string{controller.DefaultServerName},
|
||||
}
|
||||
|
||||
certBytes, err := x509.CreateCertificate(cryptorand.Reader, cert, cert, privateKey.Public(), privateKey)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"github.com/cirruslabs/orchard/internal/command/context"
|
||||
"github.com/cirruslabs/orchard/internal/command/controller"
|
||||
"github.com/cirruslabs/orchard/internal/command/create"
|
||||
deletepkg "github.com/cirruslabs/orchard/internal/command/deletecmd"
|
||||
|
|
@ -24,6 +25,7 @@ func NewRootCmd() *cobra.Command {
|
|||
)
|
||||
|
||||
addGroupedCommands(command, "Administrative Tasks:",
|
||||
context.NewCommand(),
|
||||
controller.NewCommand(),
|
||||
worker.NewCommand(),
|
||||
dev.NewCommand(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Base64 []byte
|
||||
|
||||
func (b64 Base64) MarshalYAML() (interface{}, error) {
|
||||
return base64.StdEncoding.EncodeToString(b64), nil
|
||||
}
|
||||
|
||||
func (b64 *Base64) UnmarshalYAML(value *yaml.Node) error {
|
||||
result, err := base64.StdEncoding.DecodeString(value.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*b64 = result
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
DefaultContext string `yaml:"default-context,omitempty"`
|
||||
Contexts map[string]Context `yaml:"contexts,omitempty"`
|
||||
}
|
||||
|
||||
func (config *Config) SetContext(name string, context Context) {
|
||||
config.Contexts[name] = context
|
||||
|
||||
if config.DefaultContext == "" {
|
||||
config.DefaultContext = name
|
||||
}
|
||||
}
|
||||
|
||||
func (config *Config) RetrieveContext(name string) (Context, bool) {
|
||||
context, ok := config.Contexts[name]
|
||||
|
||||
return context, ok
|
||||
}
|
||||
|
||||
func (config *Config) RetrieveDefaultContext() (Context, bool) {
|
||||
if config.DefaultContext == "" {
|
||||
return Context{}, false
|
||||
}
|
||||
|
||||
return config.RetrieveContext(config.DefaultContext)
|
||||
}
|
||||
|
||||
func (config *Config) DeleteContext(name string) error {
|
||||
_, exists := config.Contexts[name]
|
||||
if !exists {
|
||||
return fmt.Errorf("%w: no such context: %q", ErrConfigConflict, name)
|
||||
}
|
||||
|
||||
delete(config.Contexts, name)
|
||||
|
||||
if config.DefaultContext == name {
|
||||
config.DefaultContext = ""
|
||||
|
||||
for name := range config.Contexts {
|
||||
config.DefaultContext = name
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"github.com/cirruslabs/orchard/internal/controller"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
URL string `yaml:"url,omitempty"`
|
||||
Certificate Base64 `yaml:"certificate,omitempty"`
|
||||
}
|
||||
|
||||
func (context *Context) TLSConfig() (*tls.Config, error) {
|
||||
if len(context.Certificate) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
privatePool := x509.NewCertPool()
|
||||
|
||||
if ok := privatePool.AppendCertsFromPEM(context.Certificate); !ok {
|
||||
return nil, fmt.Errorf("%w: failed to load context's certificate", ErrConfigReadFailed)
|
||||
}
|
||||
|
||||
return &tls.Config{
|
||||
MinVersion: tls.VersionTLS13,
|
||||
ServerName: controller.DefaultServerName,
|
||||
RootCAs: privatePool,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package config
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrConfigReadFailed = errors.New("failed to read configuration file")
|
||||
ErrConfigWriteFailed = errors.New("failed to write configuration file")
|
||||
ErrConfigConflict = errors.New("conflict while operating on configuration file")
|
||||
)
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cirruslabs/orchard/internal/controller"
|
||||
"github.com/cirruslabs/orchard/internal/orchardhome"
|
||||
"github.com/gofrs/flock"
|
||||
"gopkg.in/yaml.v3"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const configName = "orchard.yml"
|
||||
|
||||
type Handle struct {
|
||||
configPath string
|
||||
}
|
||||
|
||||
func NewHandle() (*Handle, error) {
|
||||
orchardHomeDir, err := orchardhome.Path()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Handle{
|
||||
configPath: filepath.Join(orchardHomeDir, configName),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (handle *Handle) Config() (*Config, error) {
|
||||
config := Config{
|
||||
Contexts: map[string]Context{},
|
||||
}
|
||||
|
||||
configBytes, err := os.ReadFile(handle.configPath)
|
||||
if err != nil {
|
||||
// Handle a case where the config file is not created yet
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return &Config{
|
||||
Contexts: map[string]Context{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w: %v", ErrConfigReadFailed, err)
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(configBytes, &config); err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid YAML: %v", ErrConfigReadFailed, err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func (handle *Handle) SetConfig(config *Config) error {
|
||||
configBytes, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: failed to marshal YAML: %v", ErrConfigWriteFailed, err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(handle.configPath, configBytes, 0600); err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrConfigWriteFailed, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handle *Handle) CreateContext(name string, context Context, force bool) error {
|
||||
lock := flock.New(handle.configPath)
|
||||
if err := lock.Lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = lock.Unlock()
|
||||
}()
|
||||
|
||||
config, err := handle.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, exists := config.RetrieveContext(name)
|
||||
if exists && !force {
|
||||
return fmt.Errorf("%w: context %q already exists", ErrConfigConflict, name)
|
||||
}
|
||||
|
||||
config.SetContext(name, context)
|
||||
|
||||
return handle.SetConfig(config)
|
||||
}
|
||||
|
||||
func (handle *Handle) DefaultContext() (Context, error) {
|
||||
lock := flock.New(handle.configPath)
|
||||
if err := lock.Lock(); err != nil {
|
||||
return Context{}, err
|
||||
}
|
||||
defer func() {
|
||||
_ = lock.Unlock()
|
||||
}()
|
||||
|
||||
config, err := handle.Config()
|
||||
if err != nil {
|
||||
return Context{}, err
|
||||
}
|
||||
|
||||
defaultContext, ok := config.RetrieveDefaultContext()
|
||||
if !ok {
|
||||
defaultContext = Context{
|
||||
URL: fmt.Sprintf("http://127.0.0.1:%d", controller.DefaultPort),
|
||||
}
|
||||
|
||||
config.SetContext("default", defaultContext)
|
||||
|
||||
if err := handle.SetConfig(config); err != nil {
|
||||
return Context{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return defaultContext, nil
|
||||
}
|
||||
|
||||
func (handle *Handle) SetDefaultContext(name string) error {
|
||||
lock := flock.New(handle.configPath)
|
||||
if err := lock.Lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = lock.Unlock()
|
||||
}()
|
||||
|
||||
config, err := handle.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, ok := config.RetrieveContext(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: no such context: %q", ErrConfigConflict, name)
|
||||
}
|
||||
|
||||
config.DefaultContext = name
|
||||
|
||||
return handle.SetConfig(config)
|
||||
}
|
||||
|
||||
func (handle *Handle) DeleteContext(name string) error {
|
||||
lock := flock.New(handle.configPath)
|
||||
if err := lock.Lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = lock.Unlock()
|
||||
}()
|
||||
|
||||
config, err := handle.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := config.DeleteContext(name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return handle.SetConfig(config)
|
||||
}
|
||||
|
|
@ -17,6 +17,11 @@ func (controller *Controller) initAPI() *gin.Engine {
|
|||
// v1 API
|
||||
v1 := ginEngine.Group("/v1")
|
||||
|
||||
// A way to for the clients to check that the API is working
|
||||
v1.GET("/", func(c *gin.Context) {
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
// Workers
|
||||
v1.POST("/workers", func(c *gin.Context) {
|
||||
controller.createWorker(c).Respond(c)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultPort = 6120
|
||||
DefaultServerName = "orchard-controller"
|
||||
)
|
||||
|
||||
var ErrInitFailed = errors.New("controller initialization failed")
|
||||
|
||||
type Controller struct {
|
||||
|
|
@ -38,7 +43,7 @@ func New(opts ...Option) (*Controller, error) {
|
|||
ErrInitFailed)
|
||||
}
|
||||
if controller.listenAddr == "" {
|
||||
controller.listenAddr = ":6120"
|
||||
controller.listenAddr = fmt.Sprintf(":%d", DefaultPort)
|
||||
}
|
||||
if controller.logger == nil {
|
||||
controller.logger = zap.NewNop().Sugar()
|
||||
|
|
|
|||
|
|
@ -1,20 +1,26 @@
|
|||
package orchardhome
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var ErrFailed = errors.New("failed to retrieve Orchard's home directory path")
|
||||
|
||||
func Path() (string, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", fmt.Errorf("%w: failed to retrieve current user's home directory %v",
|
||||
ErrFailed, err)
|
||||
}
|
||||
|
||||
orchardDir := filepath.Join(homeDir, ".orchard")
|
||||
|
||||
if err := os.Mkdir(orchardDir, 0700); err != nil && !os.IsExist(err) {
|
||||
return "", err
|
||||
return "", fmt.Errorf("%w: cannot create directory %s: %v",
|
||||
ErrFailed, orchardDir, err)
|
||||
}
|
||||
|
||||
return orchardDir, nil
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/cirruslabs/orchard/internal/config"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
|
@ -39,7 +40,23 @@ func New(opts ...Option) (*Client, error) {
|
|||
|
||||
// Apply defaults
|
||||
if client.address == "" {
|
||||
client.address = "http://127.0.0.1:6120"
|
||||
configHandle, err := config.NewHandle()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defaultContext, err := configHandle.DefaultContext()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client.address = defaultContext.URL
|
||||
|
||||
tlsConfig, err := defaultContext.TLSConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client.tlsConfig = tlsConfig
|
||||
}
|
||||
|
||||
// Instantiate client
|
||||
|
|
@ -130,6 +147,10 @@ func (client *Client) request(
|
|||
return nil
|
||||
}
|
||||
|
||||
func (client *Client) Check(ctx context.Context) error {
|
||||
return client.request(ctx, http.MethodGet, "/", nil, nil, nil)
|
||||
}
|
||||
|
||||
func (client *Client) Workers() *WorkersService {
|
||||
return &WorkersService{
|
||||
client: client,
|
||||
|
|
|
|||
Loading…
Reference in New Issue