diff --git a/runner/providers/external/execution/commands.go b/runner/providers/external/execution/commands.go new file mode 100644 index 00000000..4d718a65 --- /dev/null +++ b/runner/providers/external/execution/commands.go @@ -0,0 +1,13 @@ +package execution + +type ExecutionCommand string + +const ( + CreateInstanceCommand ExecutionCommand = "CreateInstance" + DeleteInstanceCommand ExecutionCommand = "DeleteInstance" + GetInstanceCommand ExecutionCommand = "GetInstance" + ListInstancesCommand ExecutionCommand = "ListInstances" + StartInstanceCommand ExecutionCommand = "StartInstance" + StopInstanceCommand ExecutionCommand = "StopInstance" + RemoveAllInstancesCommand ExecutionCommand = "RemoveAllInstances" +) diff --git a/runner/providers/external/execution/execution.go b/runner/providers/external/execution/execution.go new file mode 100644 index 00000000..98800459 --- /dev/null +++ b/runner/providers/external/execution/execution.go @@ -0,0 +1,101 @@ +package execution + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "os" + + "github.com/cloudbase/garm/params" +) + +func GetEnvironment() (Environment, error) { + env := Environment{ + Command: ExecutionCommand(os.Getenv("GARM_COMMAND")), + ControllerID: os.Getenv("GARM_CONTROLLER_ID"), + PoolID: os.Getenv("GARM_POOL_ID"), + ProviderConfigFile: os.Getenv("GARM_PROVIDER_CONFIG_FILE"), + InstanceID: os.Getenv("GARM_INSTANCE_ID"), + } + + if env.Command == CreateInstanceCommand { + // We need to get the bootstrap params from stdin + info, err := os.Stdin.Stat() + if err != nil { + return Environment{}, fmt.Errorf("failed to get stdin: %w", err) + } + if info.Size() == 0 { + return Environment{}, fmt.Errorf("no data found on stdin") + } + + var data bytes.Buffer + if _, err := io.Copy(&data, os.Stdin); err != nil { + return Environment{}, fmt.Errorf("failed to copy bootstrap params") + } + + var bootstrapParams params.BootstrapInstance + if err := json.Unmarshal(data.Bytes(), &bootstrapParams); err != nil { + return Environment{}, fmt.Errorf("failed to decode instance params: %w", err) + } + env.BootstrapParams = bootstrapParams + } + + if err := env.Validate(); err != nil { + return Environment{}, fmt.Errorf("failed to validate execution environment: %w", err) + } + + return env, nil +} + +type Environment struct { + Command ExecutionCommand + ControllerID string + PoolID string + ProviderConfigFile string + InstanceID string + BootstrapParams params.BootstrapInstance +} + +func (e Environment) Validate() error { + if e.Command == "" { + return fmt.Errorf("missing GARM_COMMAND") + } + + if e.ProviderConfigFile == "" { + return fmt.Errorf("missing GARM_PROVIDER_CONFIG_FILE") + } + + if _, err := os.Lstat(e.ProviderConfigFile); err != nil { + return fmt.Errorf("error accessing config file: %w", err) + } + + switch e.Command { + case CreateInstanceCommand: + if e.BootstrapParams.Name == "" { + return fmt.Errorf("missing bootstrap params") + } + if e.ControllerID == "" { + return fmt.Errorf("missing controller ID") + } + if e.PoolID == "" { + return fmt.Errorf("missing pool ID") + } + case DeleteInstanceCommand, GetInstanceCommand, + StartInstanceCommand, StopInstanceCommand: + if e.InstanceID == "" { + return fmt.Errorf("missing instance ID") + } + case ListInstancesCommand: + if e.PoolID == "" { + return fmt.Errorf("missing pool ID") + } + case RemoveAllInstancesCommand: + if e.ControllerID == "" { + return fmt.Errorf("missing controller ID") + } + default: + return fmt.Errorf("unknown GARM_COMMAND: %s", e.Command) + } + return nil +} diff --git a/runner/providers/external/external.go b/runner/providers/external/external.go index 887d083e..a7dd6037 100644 --- a/runner/providers/external/external.go +++ b/runner/providers/external/external.go @@ -11,6 +11,7 @@ import ( "github.com/cloudbase/garm/params" "github.com/cloudbase/garm/runner/common" providerCommon "github.com/cloudbase/garm/runner/providers/common" + "github.com/cloudbase/garm/runner/providers/external/execution" "github.com/cloudbase/garm/util/exec" "github.com/pkg/errors" @@ -42,10 +43,6 @@ type external struct { execPath string } -func (e *external) configEnvVar() string { - return fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile) -} - func (e *external) validateCreateResult(inst params.Instance, bootstrapParams params.BootstrapInstance) error { if inst.ProviderID == "" { return garmErrors.NewProviderError("missing provider ID after create call") @@ -68,11 +65,12 @@ func (e *external) validateCreateResult(inst params.Instance, bootstrapParams pa // CreateInstance creates a new compute instance in the provider. func (e *external) CreateInstance(ctx context.Context, bootstrapParams params.BootstrapInstance) (params.Instance, error) { - asEnv := bootstrapParamsToEnv(bootstrapParams) - asEnv = append(asEnv, createInstanceCommand) - asEnv = append(asEnv, fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID)) - asEnv = append(asEnv, fmt.Sprintf("GARM_POOL_ID=%s", bootstrapParams.PoolID)) - asEnv = append(asEnv, e.configEnvVar()) + asEnv := []string{ + fmt.Sprintf("GARM_COMMAND=%s", execution.CreateInstanceCommand), + fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID), + fmt.Sprintf("GARM_POOL_ID=%s", bootstrapParams.PoolID), + fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile), + } asJs, err := json.Marshal(bootstrapParams) if err != nil { @@ -101,9 +99,9 @@ func (e *external) CreateInstance(ctx context.Context, bootstrapParams params.Bo // Delete instance will delete the instance in a provider. func (e *external) DeleteInstance(ctx context.Context, instance string) error { asEnv := []string{ - deleteInstanceCommand, - e.configEnvVar(), + fmt.Sprintf("GARM_COMMAND=%s", execution.DeleteInstanceCommand), fmt.Sprintf("GARM_INSTANCE_ID=%s", instance), + fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile), } _, err := exec.Exec(ctx, e.execPath, nil, asEnv) @@ -116,9 +114,9 @@ func (e *external) DeleteInstance(ctx context.Context, instance string) error { // GetInstance will return details about one instance. func (e *external) GetInstance(ctx context.Context, instance string) (params.Instance, error) { asEnv := []string{ - getInstanceCommand, - e.configEnvVar(), + fmt.Sprintf("GARM_COMMAND=%s", execution.GetInstanceCommand), fmt.Sprintf("GARM_INSTANCE_ID=%s", instance), + fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile), } // TODO(gabriel-samfira): handle error types. Of particular insterest is to @@ -138,9 +136,9 @@ func (e *external) GetInstance(ctx context.Context, instance string) (params.Ins // ListInstances will list all instances for a provider. func (e *external) ListInstances(ctx context.Context, poolID string) ([]params.Instance, error) { asEnv := []string{ - listInstancesCommand, - e.configEnvVar(), + fmt.Sprintf("GARM_COMMAND=%s", execution.ListInstancesCommand), fmt.Sprintf("GARM_POOL_ID=%s", poolID), + fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile), } out, err := exec.Exec(ctx, e.execPath, nil, asEnv) @@ -158,9 +156,9 @@ func (e *external) ListInstances(ctx context.Context, poolID string) ([]params.I // RemoveAllInstances will remove all instances created by this provider. func (e *external) RemoveAllInstances(ctx context.Context) error { asEnv := []string{ - removeAllInstancesCommand, - e.configEnvVar(), + fmt.Sprintf("GARM_COMMAND=%s", execution.RemoveAllInstancesCommand), fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID), + fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile), } _, err := exec.Exec(ctx, e.execPath, nil, asEnv) if err != nil { @@ -172,9 +170,9 @@ func (e *external) RemoveAllInstances(ctx context.Context) error { // Stop shuts down the instance. func (e *external) Stop(ctx context.Context, instance string, force bool) error { asEnv := []string{ - stopInstanceCommand, - e.configEnvVar(), + fmt.Sprintf("GARM_COMMAND=%s", execution.StopInstanceCommand), fmt.Sprintf("GARM_INSTANCE_ID=%s", instance), + fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile), } _, err := exec.Exec(ctx, e.execPath, nil, asEnv) if err != nil { @@ -186,9 +184,9 @@ func (e *external) Stop(ctx context.Context, instance string, force bool) error // Start boots up an instance. func (e *external) Start(ctx context.Context, instance string) error { asEnv := []string{ - startInstanceCommand, - e.configEnvVar(), + fmt.Sprintf("GARM_COMMAND=%s", execution.StartInstanceCommand), fmt.Sprintf("GARM_INSTANCE_ID=%s", instance), + fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile), } _, err := exec.Exec(ctx, e.execPath, nil, asEnv) if err != nil { diff --git a/runner/providers/external/util.go b/runner/providers/external/util.go deleted file mode 100644 index 36bb4ca9..00000000 --- a/runner/providers/external/util.go +++ /dev/null @@ -1,49 +0,0 @@ -package external - -import ( - "fmt" - "strings" - - "github.com/cloudbase/garm/params" -) - -const ( - envPrefix = "GARM" - - createInstanceCommand = "GARM_COMMAND=CreateInstance" - deleteInstanceCommand = "GARM_COMMAND=DeleteInstance" - getInstanceCommand = "GARM_COMMAND=GetInstance" - listInstancesCommand = "GARM_COMMAND=ListInstances" - startInstanceCommand = "GARM_COMMAND=StartInstance" - stopInstanceCommand = "GARM_COMMAND=StopInstance" - removeAllInstancesCommand = "GARM_COMMAND=RemoveAllInstances" -) - -func bootstrapParamsToEnv(param params.BootstrapInstance) []string { - ret := []string{ - fmt.Sprintf("%s_BOOTSTRAP_NAME='%s'", envPrefix, param.Name), - fmt.Sprintf("%s_BOOTSTRAP_OS_ARCH='%s'", envPrefix, param.OSArch), - fmt.Sprintf("%s_BOOTSTRAP_FLAVOR='%s'", envPrefix, param.Flavor), - fmt.Sprintf("%s_BOOTSTRAP_IMAGE='%s'", envPrefix, param.Image), - fmt.Sprintf("%s_BOOTSTRAP_POOL_ID='%s'", envPrefix, param.PoolID), - fmt.Sprintf("%s_BOOTSTRAP_INSTANCE_TOKEN='%s'", envPrefix, param.InstanceToken), - fmt.Sprintf("%s_BOOTSTRAP_CALLBACK_URL='%s'", envPrefix, param.CallbackURL), - fmt.Sprintf("%s_BOOTSTRAP_REPO_URL='%s'", envPrefix, param.RepoURL), - fmt.Sprintf("%s_BOOTSTRAP_LABELS='%s'", envPrefix, strings.Join(param.Labels, ",")), - } - - for idx, tool := range param.Tools { - ret = append(ret, fmt.Sprintf("%s_BOOTSTRAP_TOOLS_DOWNLOAD_URL_%d='%s'", envPrefix, idx, *tool.DownloadURL)) - ret = append(ret, fmt.Sprintf("%s_BOOTSTRAP_TOOLS_ARCH_%d='%s'", envPrefix, idx, *tool.Architecture)) - ret = append(ret, fmt.Sprintf("%s_BOOTSTRAP_TOOLS_OS_%d='%s'", envPrefix, idx, *tool.OS)) - ret = append(ret, fmt.Sprintf("%s_BOOTSTRAP_TOOLS_FILENAME_%d='%s'", envPrefix, idx, *tool.Filename)) - ret = append(ret, fmt.Sprintf("%s_BOOTSTRAP_TOOLS_SHA256_%d='%s'", envPrefix, idx, *tool.SHA256Checksum)) - } - - for idx, sshKey := range param.SSHKeys { - ret = append(ret, fmt.Sprintf("%s_BOOTSTRAP_SSH_KEY_%d='%s'", envPrefix, idx, sshKey)) - - } - - return ret -}