garm/runner/providers/external/external.go
2022-06-24 11:17:34 +00:00

206 lines
6.2 KiB
Go

package external
import (
"context"
"encoding/json"
"fmt"
"log"
"garm/config"
garmErrors "garm/errors"
"garm/params"
"garm/runner/common"
providerCommon "garm/runner/providers/common"
"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 != config.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,
}
}