Add execution environment to external provider

The execution package is a common package that can be used by external
providers to load environment variables and stdin, in a coherent struct
that can be consumed by the various commands that need to execute as
part of the provider.

Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
This commit is contained in:
Gabriel Adrian Samfira 2023-03-14 18:24:24 +02:00
parent e67c08b1d4
commit 882d07a0da
No known key found for this signature in database
GPG key ID: 7D073DCC2C074CB5
4 changed files with 133 additions and 70 deletions

View file

@ -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"
)

View file

@ -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
}

View file

@ -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 {

View file

@ -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
}