garm/runner/providers/external/external.go
Gabriel Adrian Samfira 0074af9370
Move some defaults and types from config
The params package should not depend on config. The params packages
should be consumable by external applications that wish to interact with
garm, and it makes no sense to pull in the config package just for some
constants. As such, the following changes have been made:

  * Moved some types from config to params
  * Moved defaults in a new leaf package called appdefaults

Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
2023-03-14 14:15:10 +02:00

206 lines
6.3 KiB
Go

package external
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/cloudbase/garm/config"
garmErrors "github.com/cloudbase/garm/errors"
"github.com/cloudbase/garm/params"
"github.com/cloudbase/garm/runner/common"
providerCommon "github.com/cloudbase/garm/runner/providers/common"
"github.com/cloudbase/garm/util/exec"
"github.com/pkg/errors"
)
var _ common.Provider = (*external)(nil)
func NewProvider(ctx context.Context, cfg *config.Provider, controllerID string) (common.Provider, error) {
if cfg.ProviderType != params.ExternalProvider {
return nil, garmErrors.NewBadRequestError("invalid provider config")
}
execPath, err := cfg.External.ExecutablePath()
if err != nil {
return nil, errors.Wrap(err, "fetching executable path")
}
return &external{
ctx: ctx,
controllerID: controllerID,
cfg: cfg,
execPath: execPath,
}, nil
}
type external struct {
ctx context.Context
controllerID string
cfg *config.Provider
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")
}
if inst.Name == "" {
return garmErrors.NewProviderError("missing instance name after create call")
}
if inst.OSName == "" || inst.OSArch == "" || inst.OSType == "" {
// we can still function without this info (I think)
log.Printf("WARNING: missing OS information after create call")
}
if !providerCommon.IsValidStatus(inst.Status) {
return garmErrors.NewProviderError("invalid status returned (%s) after create call", inst.Status)
}
return nil
}
// 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())
asJs, err := json.Marshal(bootstrapParams)
if err != nil {
return params.Instance{}, errors.Wrap(err, "serializing bootstrap params")
}
out, err := exec.Exec(ctx, e.execPath, asJs, asEnv)
if err != nil {
return params.Instance{}, garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
}
var param params.Instance
if err := json.Unmarshal(out, &param); err != nil {
return params.Instance{}, garmErrors.NewProviderError("failed to decode response from binary: %s", err)
}
if err := e.validateCreateResult(param, bootstrapParams); err != nil {
return params.Instance{}, garmErrors.NewProviderError("failed to validate result: %s", err)
}
retAsJs, _ := json.MarshalIndent(param, "", " ")
log.Printf("provider returned: %s", string(retAsJs))
return param, nil
}
// 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_INSTANCE_ID=%s", instance),
}
_, err := exec.Exec(ctx, e.execPath, nil, asEnv)
if err != nil {
return garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
}
return nil
}
// 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_INSTANCE_ID=%s", instance),
}
// TODO(gabriel-samfira): handle error types. Of particular insterest is to
// know when the error is ErrNotFound.
out, err := exec.Exec(ctx, e.execPath, nil, asEnv)
if err != nil {
return params.Instance{}, garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
}
var param params.Instance
if err := json.Unmarshal(out, &param); err != nil {
return params.Instance{}, garmErrors.NewProviderError("failed to decode response from binary: %s", err)
}
return param, nil
}
// 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_POOL_ID=%s", poolID),
}
out, err := exec.Exec(ctx, e.execPath, nil, asEnv)
if err != nil {
return []params.Instance{}, garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
}
var param []params.Instance
if err := json.Unmarshal(out, &param); err != nil {
return []params.Instance{}, garmErrors.NewProviderError("failed to decode response from binary: %s", err)
}
return param, nil
}
// 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_CONTROLLER_ID=%s", e.controllerID),
}
_, err := exec.Exec(ctx, e.execPath, nil, asEnv)
if err != nil {
return garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
}
return nil
}
// 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_INSTANCE_ID=%s", instance),
}
_, err := exec.Exec(ctx, e.execPath, nil, asEnv)
if err != nil {
return garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
}
return nil
}
// Start boots up an instance.
func (e *external) Start(ctx context.Context, instance string) error {
asEnv := []string{
startInstanceCommand,
e.configEnvVar(),
fmt.Sprintf("GARM_INSTANCE_ID=%s", instance),
}
_, err := exec.Exec(ctx, e.execPath, nil, asEnv)
if err != nil {
return garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
}
return nil
}
func (e *external) AsParams() params.Provider {
return params.Provider{
Name: e.cfg.Name,
Description: e.cfg.Description,
ProviderType: e.cfg.ProviderType,
}
}