orchard create vm: support --image-pull-policy=Always (#110)

This commit is contained in:
Nikolay Edigaryev 2023-07-26 17:43:14 +04:00 committed by GitHub
parent fd88ce5890
commit 6759618f28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 199 additions and 18 deletions

View File

@ -22,6 +22,7 @@ var resources map[string]string
var restartPolicy string
var startupScript string
var hostDirsRaw []string
var imagePullPolicy string
func newCreateVMCommand() *cobra.Command {
command := &cobra.Command{
@ -39,15 +40,19 @@ func newCreateVMCommand() *cobra.Command {
command.PersistentFlags().BoolVar(&headless, "headless", true, "whether to run without graphics")
command.PersistentFlags().StringToStringVar(&resources, "resources", map[string]string{},
"resources to request for this VM")
command.PersistentFlags().StringVar(&restartPolicy, "restart-policy", "Never",
"restart policy for this VM: specify \"Never\" to never restart or \"OnFailure\" "+
"to only restart when the VM fails")
command.PersistentFlags().StringVar(&restartPolicy, "restart-policy", string(v1.RestartPolicyNever),
fmt.Sprintf("restart policy for this VM: specify %q to never restart or %q "+
"to only restart when the VM fails", v1.RestartPolicyNever, v1.RestartPolicyOnFailure))
command.PersistentFlags().StringVar(&startupScript, "startup-script", "",
"startup script (e.g. --startup-script=\"sync\") or a path to a script file prefixed with \"@\" "+
"(e.g. \"--startup-script=@script.sh\")")
command.PersistentFlags().StringSliceVar(&hostDirsRaw, "host-dirs", []string{},
"host directories to mount to the VM, can be specified multiple times and/or be comma-separated "+
"(see \"tart run\"'s --dir argument for syntax)")
command.PersistentFlags().StringVar(&imagePullPolicy, "image-pull-policy", string(v1.ImagePullPolicyIfNotPresent),
fmt.Sprintf("image pull policy for this VM, by default the image is only only pulled if it doesn't "+
"exist in the cache (%q), specify %q to always try to pull the image",
v1.ImagePullPolicyIfNotPresent, v1.ImagePullPolicyAlways))
return command
}
@ -88,6 +93,12 @@ func runCreateVM(cmd *cobra.Command, args []string) error {
return fmt.Errorf("%w: %v", ErrVMFailed, err)
}
// Convert image pull policy
vm.ImagePullPolicy, err = v1.NewImagePullPolicyFromString(imagePullPolicy)
if err != nil {
return fmt.Errorf("%w: %v", ErrVMFailed, err)
}
// Convert restart policy
vm.RestartPolicy, err = v1.NewRestartPolicyFromString(restartPolicy)
if err != nil {

View File

@ -39,10 +39,7 @@ func runGetClusterSettings(cmd *cobra.Command, args []string) error {
return policy.String()
})
hostDirPoliciesDescription := strings.Join(hostDirPoliciesAsStrings, ",")
if hostDirPoliciesDescription == "" {
hostDirPoliciesDescription = "none"
}
table.AddRow("hostDir policies", hostDirPoliciesDescription)
table.AddRow("hostDir policies", nonEmptyOrNone(hostDirPoliciesDescription))
fmt.Println(table)

View File

@ -17,7 +17,16 @@ func NewCommand() *cobra.Command {
newGetBootstrapTokenCommand(),
newGetClusterSettingsCommand(),
newGetServiceAccountCommand(),
newGetVMCommand(),
)
return command
}
func nonEmptyOrNone(s string) string {
if s != "" {
return s
}
return "none"
}

106
internal/command/get/vm.go Normal file
View File

@ -0,0 +1,106 @@
package get
import (
"fmt"
"github.com/cirruslabs/orchard/internal/structpath"
"github.com/cirruslabs/orchard/pkg/client"
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
"github.com/dustin/go-humanize"
"github.com/gosuri/uitable"
"github.com/samber/lo"
"github.com/spf13/cobra"
"strings"
"time"
)
func newGetVMCommand() *cobra.Command {
command := &cobra.Command{
Use: "vm NAME",
Short: "Retrieve a VM and it's fields",
RunE: runGetVM,
Args: cobra.ExactArgs(1),
}
return command
}
func runGetVM(cmd *cobra.Command, args []string) error {
name := args[0]
client, err := client.New()
if err != nil {
return err
}
// Ability to retrieve resource fields (e.g. "orchard get vm macos/status")
splits := strings.Split(name, "/")
var path []string
if len(splits) > 1 {
name = splits[0]
path = splits[1:]
}
vm, err := client.VMs().Get(cmd.Context(), name)
if err != nil {
return err
}
// Ability to retrieve resource fields (e.g. "orchard get vm macos/status")
if len(path) != 0 {
result, ok := structpath.Lookup(*vm, path)
if !ok {
return fmt.Errorf("%w: failed to find the specified field \"%s\" or the field is not a string",
ErrGetFailed, strings.Join(path, "/"))
}
fmt.Println(result)
return nil
}
table := uitable.New()
table.AddRow("Name", vm.Name)
createdAtInfo := humanize.RelTime(vm.CreatedAt, time.Now(), "ago", "in the future")
table.AddRow("Created", createdAtInfo)
table.AddRow("Image", vm.Image)
table.AddRow("Image pull policy", vm.ImagePullPolicy)
table.AddRow("CPU", vm.CPU)
table.AddRow("Memory", vm.Memory)
table.AddRow("Softnet enabled", vm.NetSoftnet)
table.AddRow("Bridged networking interface", nonEmptyOrNone(vm.NetBridged))
table.AddRow("Headless mode", vm.Headless)
table.AddRow("Status", vm.Status)
table.AddRow("Status message", vm.StatusMessage)
table.AddRow("Assigned worker", nonEmptyOrNone(vm.Worker))
table.AddRow("Restart policy", vm.RestartPolicy)
restartedAtInfo := "never"
if !vm.RestartedAt.IsZero() {
restartedAtInfo = humanize.RelTime(vm.RestartedAt, time.Now(), "ago", "in the future")
}
table.AddRow("Restarted", restartedAtInfo)
table.AddRow("Restart count", vm.RestartCount)
var resourcesInfo string
if len(vm.Resources) != 0 {
resourceDescriptions := lo.MapToSlice(vm.Resources, func(key string, value uint64) string {
return fmt.Sprintf("%s: %d", key, value)
})
resourcesInfo = strings.Join(resourceDescriptions, "\n")
}
table.AddRow("Resources", nonEmptyOrNone(resourcesInfo))
var hostDirsInfo string
if len(vm.HostDirs) != 0 {
hostDirsDescriptions := lo.Map(vm.HostDirs, func(hostDir v1.HostDir, index int) string {
return hostDir.String()
})
hostDirsInfo = strings.Join(hostDirsDescriptions, "\n")
}
table.AddRow("Host directories", nonEmptyOrNone(hostDirsInfo))
fmt.Println(table)
return nil
}

View File

@ -50,6 +50,16 @@ func (controller *Controller) createVM(ctx *gin.Context) responder.Responder {
vm.Resources[v1.ResourceTartVMs] = 1
}
// Validate image pull policy and provide a default value if it's missing
if vm.ImagePullPolicy != "" {
if _, err := v1.NewImagePullPolicyFromString(string(vm.ImagePullPolicy)); err != nil {
return responder.JSON(http.StatusPreconditionFailed,
NewErrorResponse("unsupported image pull policy: %q", vm.ImagePullPolicy))
}
} else {
vm.ImagePullPolicy = v1.ImagePullPolicyIfNotPresent
}
// Validate restart policy and provide a default value if it's missing
if vm.RestartPolicy != "" {
if _, err := v1.NewRestartPolicyFromString(string(vm.RestartPolicy)); err != nil {

View File

@ -1,6 +1,7 @@
package structpath
import (
"fmt"
"reflect"
"strings"
)
@ -23,9 +24,14 @@ func Lookup(target interface{}, path []string) (s string, ok bool) {
}
result, ok := currentValue.Interface().(string)
if !ok {
return "", false
if ok {
return result, true
}
return result, true
stringerResult, ok := currentValue.Interface().(fmt.Stringer)
if ok {
return stringerResult.String(), true
}
return "", false
}

View File

@ -59,11 +59,22 @@ func NewVM(
wg: &sync.WaitGroup{},
}
// Clone the VM so `run` and `ip` are not racing
// Optionally pull and clone the VM so that `run` and `ip` will not be racing
vm.logger.Debugf("creating VM")
if vmResource.ImagePullPolicy == v1.ImagePullPolicyAlways {
_, _, err := tart.Tart(ctx, vm.logger, "pull", vm.Resource.Image)
if err != nil {
vm.setErr(fmt.Errorf("failed to pull the VM: %w", err))
return vm, nil
}
}
if err := vm.cloneAndConfigure(ctx); err != nil {
return nil, fmt.Errorf("failed to clone the VM: %w", err)
vm.setErr(fmt.Errorf("failed to clone the VM: %w", err))
return vm, nil
}
vm.wg.Add(1)

View File

@ -0,0 +1,26 @@
package v1
import (
"errors"
"fmt"
)
var ErrInvalidImagePullPolicy = errors.New("invalid image pull policy")
type ImagePullPolicy string
const (
ImagePullPolicyIfNotPresent ImagePullPolicy = "IfNotPresent"
ImagePullPolicyAlways ImagePullPolicy = "Always"
)
func NewImagePullPolicyFromString(s string) (ImagePullPolicy, error) {
switch s {
case string(ImagePullPolicyIfNotPresent):
return ImagePullPolicyIfNotPresent, nil
case string(ImagePullPolicyAlways):
return ImagePullPolicyAlways, nil
default:
return "", fmt.Errorf("%w %q", ErrInvalidImagePullPolicy, s)
}
}

View File

@ -19,12 +19,13 @@ type Meta struct {
}
type VM struct {
Image string `json:"image"`
CPU uint64 `json:"cpu"`
Memory uint64 `json:"memory"`
NetSoftnet bool `json:"net-softnet"`
NetBridged string `json:"net-bridged"`
Headless bool `json:"headless"`
Image string `json:"image"`
ImagePullPolicy ImagePullPolicy `json:"imagePullPolicy"`
CPU uint64 `json:"cpu"`
Memory uint64 `json:"memory"`
NetSoftnet bool `json:"net-softnet"`
NetBridged string `json:"net-bridged"`
Headless bool `json:"headless"`
// Status field is used to track the lifecycle of the VM associated with this resource.
Status VMStatus `json:"status"`
@ -78,6 +79,10 @@ func (vm VM) TerminalState() bool {
type VMStatus string
func (vmStatus VMStatus) String() string {
return string(vmStatus)
}
const (
// VMStatusPending is set by the Controller for all newly-created VM resources.
VMStatusPending VMStatus = "pending"