Version provider interface
This commit is contained in:
parent
deb30e1d25
commit
03f280da59
17 changed files with 2168 additions and 342 deletions
|
|
@ -29,6 +29,10 @@ import (
|
|||
// whatever programming language you wish, while still remaining compatible
|
||||
// with garm.
|
||||
type External struct {
|
||||
// InterfaceVersion is the version of the interface that the external
|
||||
// provider implements. This is used to ensure compatibility between
|
||||
// the external provider and garm.
|
||||
InterfaceVersion string `toml:"interface_version" json:"interface-version"`
|
||||
// ConfigFile is the path on disk to a file which will be passed to
|
||||
// the external binary as an environment variable: GARM_PROVIDER_CONFIG
|
||||
// You can use this file for any configuration you need to do for the
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
params "github.com/cloudbase/garm/params"
|
||||
"github.com/cloudbase/garm/runner/common"
|
||||
)
|
||||
|
||||
// Provider is an autogenerated mock type for the Provider type
|
||||
|
|
@ -35,7 +36,7 @@ func (_m *Provider) AsParams() params.Provider {
|
|||
}
|
||||
|
||||
// CreateInstance provides a mock function with given fields: ctx, bootstrapParams
|
||||
func (_m *Provider) CreateInstance(ctx context.Context, bootstrapParams garm_provider_commonparams.BootstrapInstance) (garm_provider_commonparams.ProviderInstance, error) {
|
||||
func (_m *Provider) CreateInstance(ctx context.Context, bootstrapParams garm_provider_commonparams.BootstrapInstance, createInstanceParams common.CreateInstanceParams) (garm_provider_commonparams.ProviderInstance, error) {
|
||||
ret := _m.Called(ctx, bootstrapParams)
|
||||
|
||||
if len(ret) == 0 {
|
||||
|
|
@ -63,7 +64,7 @@ func (_m *Provider) CreateInstance(ctx context.Context, bootstrapParams garm_pro
|
|||
}
|
||||
|
||||
// DeleteInstance provides a mock function with given fields: ctx, instance
|
||||
func (_m *Provider) DeleteInstance(ctx context.Context, instance string) error {
|
||||
func (_m *Provider) DeleteInstance(ctx context.Context, instance string, deleteInstanceParams common.DeleteInstanceParams) error {
|
||||
ret := _m.Called(ctx, instance)
|
||||
|
||||
if len(ret) == 0 {
|
||||
|
|
@ -99,7 +100,7 @@ func (_m *Provider) DisableJITConfig() bool {
|
|||
}
|
||||
|
||||
// GetInstance provides a mock function with given fields: ctx, instance
|
||||
func (_m *Provider) GetInstance(ctx context.Context, instance string) (garm_provider_commonparams.ProviderInstance, error) {
|
||||
func (_m *Provider) GetInstance(ctx context.Context, instance string, getInstanceParams common.GetInstanceParams) (garm_provider_commonparams.ProviderInstance, error) {
|
||||
ret := _m.Called(ctx, instance)
|
||||
|
||||
if len(ret) == 0 {
|
||||
|
|
@ -127,7 +128,7 @@ func (_m *Provider) GetInstance(ctx context.Context, instance string) (garm_prov
|
|||
}
|
||||
|
||||
// ListInstances provides a mock function with given fields: ctx, poolID
|
||||
func (_m *Provider) ListInstances(ctx context.Context, poolID string) ([]garm_provider_commonparams.ProviderInstance, error) {
|
||||
func (_m *Provider) ListInstances(ctx context.Context, poolID string, listInstancesParams common.ListInstancesParams) ([]garm_provider_commonparams.ProviderInstance, error) {
|
||||
ret := _m.Called(ctx, poolID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
|
|
@ -157,7 +158,7 @@ func (_m *Provider) ListInstances(ctx context.Context, poolID string) ([]garm_pr
|
|||
}
|
||||
|
||||
// RemoveAllInstances provides a mock function with given fields: ctx
|
||||
func (_m *Provider) RemoveAllInstances(ctx context.Context) error {
|
||||
func (_m *Provider) RemoveAllInstances(ctx context.Context, removeAllInstances common.RemoveAllInstancesParams) error {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
if len(ret) == 0 {
|
||||
|
|
@ -175,7 +176,7 @@ func (_m *Provider) RemoveAllInstances(ctx context.Context) error {
|
|||
}
|
||||
|
||||
// Start provides a mock function with given fields: ctx, instance
|
||||
func (_m *Provider) Start(ctx context.Context, instance string) error {
|
||||
func (_m *Provider) Start(ctx context.Context, instance string, startParams common.StartParams) error {
|
||||
ret := _m.Called(ctx, instance)
|
||||
|
||||
if len(ret) == 0 {
|
||||
|
|
@ -193,7 +194,7 @@ func (_m *Provider) Start(ctx context.Context, instance string) error {
|
|||
}
|
||||
|
||||
// Stop provides a mock function with given fields: ctx, instance
|
||||
func (_m *Provider) Stop(ctx context.Context, instance string) error {
|
||||
func (_m *Provider) Stop(ctx context.Context, instance string, stopParams common.StopParams) error {
|
||||
ret := _m.Called(ctx, instance)
|
||||
|
||||
if len(ret) == 0 {
|
||||
|
|
|
|||
88
runner/common/params.go
Normal file
88
runner/common/params.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
// Copyright 2022 Cloudbase Solutions SRL
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package common
|
||||
|
||||
import "github.com/cloudbase/garm/params"
|
||||
|
||||
// Constants used for the provider interface version.
|
||||
const (
|
||||
Version010 = "v0.1.0"
|
||||
Version011 = "v0.1.1"
|
||||
)
|
||||
|
||||
// Each struct is a wrapper for the actual parameters struct for a specific version.
|
||||
// Version 0.1.0 doesn't have any specific parameters, so there is no need for a struct for it.
|
||||
type CreateInstanceParams struct {
|
||||
CreateInstanceV011 CreateInstanceV011Params
|
||||
}
|
||||
|
||||
type DeleteInstanceParams struct {
|
||||
DeleteInstanceV011 DeleteInstanceV011Params
|
||||
}
|
||||
|
||||
type GetInstanceParams struct {
|
||||
GetInstanceV011 GetInstanceV011Params
|
||||
}
|
||||
|
||||
type ListInstancesParams struct {
|
||||
ListInstancesV011 ListInstancesV011Params
|
||||
}
|
||||
|
||||
type RemoveAllInstancesParams struct {
|
||||
RemoveAllInstancesV011 RemoveAllInstancesV011Params
|
||||
}
|
||||
|
||||
type StopParams struct {
|
||||
StopV011 StopV011Params
|
||||
}
|
||||
|
||||
type StartParams struct {
|
||||
StartV011 StartV011Params
|
||||
}
|
||||
|
||||
// Struct for the base provider parameters.
|
||||
type ProviderBaseParams struct {
|
||||
PoolInfo params.Pool
|
||||
ControllerInfo params.ControllerInfo
|
||||
}
|
||||
|
||||
// Structs for version v0.1.1.
|
||||
type CreateInstanceV011Params struct {
|
||||
ProviderBaseParams
|
||||
}
|
||||
|
||||
type DeleteInstanceV011Params struct {
|
||||
ProviderBaseParams
|
||||
}
|
||||
|
||||
type GetInstanceV011Params struct {
|
||||
ProviderBaseParams
|
||||
}
|
||||
|
||||
type ListInstancesV011Params struct {
|
||||
ProviderBaseParams
|
||||
}
|
||||
|
||||
type RemoveAllInstancesV011Params struct {
|
||||
ProviderBaseParams
|
||||
}
|
||||
|
||||
type StopV011Params struct {
|
||||
ProviderBaseParams
|
||||
}
|
||||
|
||||
type StartV011Params struct {
|
||||
ProviderBaseParams
|
||||
}
|
||||
|
|
@ -24,19 +24,19 @@ import (
|
|||
//go:generate mockery --all
|
||||
type Provider interface {
|
||||
// CreateInstance creates a new compute instance in the provider.
|
||||
CreateInstance(ctx context.Context, bootstrapParams commonParams.BootstrapInstance) (commonParams.ProviderInstance, error)
|
||||
CreateInstance(ctx context.Context, bootstrapParams commonParams.BootstrapInstance, createInstanceParams CreateInstanceParams) (commonParams.ProviderInstance, error)
|
||||
// Delete instance will delete the instance in a provider.
|
||||
DeleteInstance(ctx context.Context, instance string) error
|
||||
DeleteInstance(ctx context.Context, instance string, deleteInstanceParams DeleteInstanceParams) error
|
||||
// GetInstance will return details about one instance.
|
||||
GetInstance(ctx context.Context, instance string) (commonParams.ProviderInstance, error)
|
||||
GetInstance(ctx context.Context, instance string, getInstanceParams GetInstanceParams) (commonParams.ProviderInstance, error)
|
||||
// ListInstances will list all instances for a provider.
|
||||
ListInstances(ctx context.Context, poolID string) ([]commonParams.ProviderInstance, error)
|
||||
ListInstances(ctx context.Context, poolID string, listInstancesParams ListInstancesParams) ([]commonParams.ProviderInstance, error)
|
||||
// RemoveAllInstances will remove all instances created by this provider.
|
||||
RemoveAllInstances(ctx context.Context) error
|
||||
RemoveAllInstances(ctx context.Context, removeAllInstancesParams RemoveAllInstancesParams) error
|
||||
// Stop shuts down the instance.
|
||||
Stop(ctx context.Context, instance string) error
|
||||
Stop(ctx context.Context, instance string, stopParams StopParams) error
|
||||
// Start boots up an instance.
|
||||
Start(ctx context.Context, instance string) error
|
||||
Start(ctx context.Context, instance string, startParams StartParams) error
|
||||
// DisableJITConfig tells us if the provider explicitly disables JIT configuration and
|
||||
// forces runner registration tokens to be used. This may happen if a provider has not yet
|
||||
// been updated to support JIT configuration.
|
||||
|
|
|
|||
|
|
@ -589,7 +589,15 @@ func (r *basePoolManager) cleanupOrphanedGithubRunners(runners []*github.Runner)
|
|||
slog.DebugContext(
|
||||
r.ctx, "updating instances cache for pool",
|
||||
"pool_id", pool.ID)
|
||||
poolInstances, err = provider.ListInstances(r.ctx, pool.ID)
|
||||
listInstancesParams := common.ListInstancesParams{
|
||||
ListInstancesV011: common.ListInstancesV011Params{
|
||||
ProviderBaseParams: common.ProviderBaseParams{
|
||||
PoolInfo: pool,
|
||||
ControllerInfo: r.controllerInfo,
|
||||
},
|
||||
},
|
||||
}
|
||||
poolInstances, err = provider.ListInstances(r.ctx, pool.ID, listInstancesParams)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "fetching instances for pool %s", pool.ID)
|
||||
}
|
||||
|
|
@ -654,7 +662,15 @@ func (r *basePoolManager) cleanupOrphanedGithubRunners(runners []*github.Runner)
|
|||
r.ctx, "instance was found in stopped state; starting",
|
||||
"runner_name", dbInstance.Name)
|
||||
|
||||
if err := provider.Start(r.ctx, dbInstance.ProviderID); err != nil {
|
||||
startParams := common.StartParams{
|
||||
StartV011: common.StartV011Params{
|
||||
ProviderBaseParams: common.ProviderBaseParams{
|
||||
PoolInfo: pool,
|
||||
ControllerInfo: r.controllerInfo,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := provider.Start(r.ctx, dbInstance.ProviderID, startParams); err != nil {
|
||||
return errors.Wrapf(err, "starting instance %s", dbInstance.ProviderID)
|
||||
}
|
||||
return nil
|
||||
|
|
@ -870,7 +886,15 @@ func (r *basePoolManager) addInstanceToProvider(instance params.Instance) error
|
|||
|
||||
defer func() {
|
||||
if instanceIDToDelete != "" {
|
||||
if err := provider.DeleteInstance(r.ctx, instanceIDToDelete); err != nil {
|
||||
deleteInstanceParams := common.DeleteInstanceParams{
|
||||
DeleteInstanceV011: common.DeleteInstanceV011Params{
|
||||
ProviderBaseParams: common.ProviderBaseParams{
|
||||
PoolInfo: pool,
|
||||
ControllerInfo: r.controllerInfo,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := provider.DeleteInstance(r.ctx, instanceIDToDelete, deleteInstanceParams); err != nil {
|
||||
if !errors.Is(err, runnerErrors.ErrNotFound) {
|
||||
slog.With(slog.Any("error", err)).ErrorContext(
|
||||
r.ctx, "failed to cleanup instance",
|
||||
|
|
@ -880,7 +904,15 @@ func (r *basePoolManager) addInstanceToProvider(instance params.Instance) error
|
|||
}
|
||||
}()
|
||||
|
||||
providerInstance, err := provider.CreateInstance(r.ctx, bootstrapArgs)
|
||||
createInstanceParams := common.CreateInstanceParams{
|
||||
CreateInstanceV011: common.CreateInstanceV011Params{
|
||||
ProviderBaseParams: common.ProviderBaseParams{
|
||||
PoolInfo: pool,
|
||||
ControllerInfo: r.controllerInfo,
|
||||
},
|
||||
},
|
||||
}
|
||||
providerInstance, err := provider.CreateInstance(r.ctx, bootstrapArgs, createInstanceParams)
|
||||
if err != nil {
|
||||
instanceIDToDelete = instance.Name
|
||||
return errors.Wrap(err, "creating instance")
|
||||
|
|
@ -1316,7 +1348,15 @@ func (r *basePoolManager) deleteInstanceFromProvider(ctx context.Context, instan
|
|||
"runner_name", instance.Name,
|
||||
"provider_id", identifier)
|
||||
|
||||
if err := provider.DeleteInstance(ctx, identifier); err != nil {
|
||||
deleteInstanceParams := common.DeleteInstanceParams{
|
||||
DeleteInstanceV011: common.DeleteInstanceV011Params{
|
||||
ProviderBaseParams: common.ProviderBaseParams{
|
||||
PoolInfo: pool,
|
||||
ControllerInfo: r.controllerInfo,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := provider.DeleteInstance(ctx, identifier, deleteInstanceParams); err != nil {
|
||||
return errors.Wrap(err, "removing instance")
|
||||
}
|
||||
|
||||
|
|
|
|||
332
runner/providers/external/external.go
vendored
332
runner/providers/external/external.go
vendored
|
|
@ -2,334 +2,22 @@ package external
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os/exec"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
garmErrors "github.com/cloudbase/garm-provider-common/errors"
|
||||
"github.com/cloudbase/garm-provider-common/execution"
|
||||
commonParams "github.com/cloudbase/garm-provider-common/params"
|
||||
garmExec "github.com/cloudbase/garm-provider-common/util/exec"
|
||||
"github.com/cloudbase/garm/config"
|
||||
"github.com/cloudbase/garm/metrics"
|
||||
"github.com/cloudbase/garm/params"
|
||||
"github.com/cloudbase/garm/runner/common"
|
||||
v010 "github.com/cloudbase/garm/runner/providers/v0.1.0"
|
||||
v011 "github.com/cloudbase/garm/runner/providers/v0.1.1"
|
||||
)
|
||||
|
||||
var _ common.Provider = (*external)(nil)
|
||||
|
||||
// NewProvider selects the provider based on the interface version
|
||||
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")
|
||||
}
|
||||
|
||||
envVars := cfg.External.GetEnvironmentVariables()
|
||||
|
||||
return &external{
|
||||
ctx: ctx,
|
||||
controllerID: controllerID,
|
||||
cfg: cfg,
|
||||
execPath: execPath,
|
||||
environmentVariables: envVars,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type external struct {
|
||||
ctx context.Context
|
||||
controllerID string
|
||||
cfg *config.Provider
|
||||
execPath string
|
||||
environmentVariables []string
|
||||
}
|
||||
|
||||
func (e *external) validateResult(inst commonParams.ProviderInstance) error {
|
||||
if inst.ProviderID == "" {
|
||||
return garmErrors.NewProviderError("missing provider ID")
|
||||
}
|
||||
|
||||
if inst.Name == "" {
|
||||
return garmErrors.NewProviderError("missing instance name")
|
||||
}
|
||||
|
||||
if !IsValidProviderStatus(inst.Status) {
|
||||
return garmErrors.NewProviderError("invalid status returned (%s)", inst.Status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateInstance creates a new compute instance in the provider.
|
||||
func (e *external) CreateInstance(ctx context.Context, bootstrapParams commonParams.BootstrapInstance) (commonParams.ProviderInstance, error) {
|
||||
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),
|
||||
}
|
||||
asEnv = append(asEnv, e.environmentVariables...)
|
||||
|
||||
asJs, err := json.Marshal(bootstrapParams)
|
||||
if err != nil {
|
||||
return commonParams.ProviderInstance{}, errors.Wrap(err, "serializing bootstrap params")
|
||||
}
|
||||
|
||||
metrics.InstanceOperationCount.WithLabelValues(
|
||||
"CreateInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
|
||||
out, err := garmExec.Exec(ctx, e.execPath, asJs, asEnv)
|
||||
if err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"CreateInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return commonParams.ProviderInstance{}, garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
|
||||
}
|
||||
|
||||
var param commonParams.ProviderInstance
|
||||
if err := json.Unmarshal(out, ¶m); err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"CreateInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return commonParams.ProviderInstance{}, garmErrors.NewProviderError("failed to decode response from binary: %s", err)
|
||||
}
|
||||
|
||||
if err := e.validateResult(param); err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"CreateInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return commonParams.ProviderInstance{}, garmErrors.NewProviderError("failed to validate result: %s", err)
|
||||
}
|
||||
|
||||
retAsJs, _ := json.MarshalIndent(param, "", " ")
|
||||
slog.DebugContext(
|
||||
ctx, "provider returned",
|
||||
"output", 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{
|
||||
fmt.Sprintf("GARM_COMMAND=%s", execution.DeleteInstanceCommand),
|
||||
fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID),
|
||||
fmt.Sprintf("GARM_INSTANCE_ID=%s", instance),
|
||||
fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile),
|
||||
}
|
||||
asEnv = append(asEnv, e.environmentVariables...)
|
||||
|
||||
metrics.InstanceOperationCount.WithLabelValues(
|
||||
"DeleteInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
_, err := garmExec.Exec(ctx, e.execPath, nil, asEnv)
|
||||
if err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
if !errors.As(err, &exitErr) || exitErr.ExitCode() != execution.ExitCodeNotFound {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"DeleteInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
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) (commonParams.ProviderInstance, error) {
|
||||
asEnv := []string{
|
||||
fmt.Sprintf("GARM_COMMAND=%s", execution.GetInstanceCommand),
|
||||
fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID),
|
||||
fmt.Sprintf("GARM_INSTANCE_ID=%s", instance),
|
||||
fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile),
|
||||
}
|
||||
asEnv = append(asEnv, e.environmentVariables...)
|
||||
|
||||
// nolint:golangci-lint,godox
|
||||
// TODO(gabriel-samfira): handle error types. Of particular interest is to
|
||||
// know when the error is ErrNotFound.
|
||||
metrics.InstanceOperationCount.WithLabelValues(
|
||||
"GetInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
out, err := garmExec.Exec(ctx, e.execPath, nil, asEnv)
|
||||
if err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"GetInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return commonParams.ProviderInstance{}, garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
|
||||
}
|
||||
|
||||
var param commonParams.ProviderInstance
|
||||
if err := json.Unmarshal(out, ¶m); err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"GetInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return commonParams.ProviderInstance{}, garmErrors.NewProviderError("failed to decode response from binary: %s", err)
|
||||
}
|
||||
|
||||
if err := e.validateResult(param); err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"GetInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return commonParams.ProviderInstance{}, garmErrors.NewProviderError("failed to validate result: %s", err)
|
||||
}
|
||||
|
||||
return param, nil
|
||||
}
|
||||
|
||||
// ListInstances will list all instances for a provider.
|
||||
func (e *external) ListInstances(ctx context.Context, poolID string) ([]commonParams.ProviderInstance, error) {
|
||||
asEnv := []string{
|
||||
fmt.Sprintf("GARM_COMMAND=%s", execution.ListInstancesCommand),
|
||||
fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID),
|
||||
fmt.Sprintf("GARM_POOL_ID=%s", poolID),
|
||||
fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile),
|
||||
}
|
||||
asEnv = append(asEnv, e.environmentVariables...)
|
||||
|
||||
metrics.InstanceOperationCount.WithLabelValues(
|
||||
"ListInstances", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
|
||||
out, err := garmExec.Exec(ctx, e.execPath, nil, asEnv)
|
||||
if err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"ListInstances", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return []commonParams.ProviderInstance{}, garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
|
||||
}
|
||||
|
||||
var param []commonParams.ProviderInstance
|
||||
if err := json.Unmarshal(out, ¶m); err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"ListInstances", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return []commonParams.ProviderInstance{}, garmErrors.NewProviderError("failed to decode response from binary: %s", err)
|
||||
}
|
||||
|
||||
ret := make([]commonParams.ProviderInstance, len(param))
|
||||
for idx, inst := range param {
|
||||
if err := e.validateResult(inst); err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"ListInstances", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return []commonParams.ProviderInstance{}, garmErrors.NewProviderError("failed to validate result: %s", err)
|
||||
}
|
||||
ret[idx] = inst
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// RemoveAllInstances will remove all instances created by this provider.
|
||||
func (e *external) RemoveAllInstances(ctx context.Context) error {
|
||||
asEnv := []string{
|
||||
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),
|
||||
}
|
||||
asEnv = append(asEnv, e.environmentVariables...)
|
||||
|
||||
metrics.InstanceOperationCount.WithLabelValues(
|
||||
"RemoveAllInstances", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
|
||||
_, err := garmExec.Exec(ctx, e.execPath, nil, asEnv)
|
||||
if err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"RemoveAllInstances", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
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) error {
|
||||
asEnv := []string{
|
||||
fmt.Sprintf("GARM_COMMAND=%s", execution.StopInstanceCommand),
|
||||
fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID),
|
||||
fmt.Sprintf("GARM_INSTANCE_ID=%s", instance),
|
||||
fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile),
|
||||
}
|
||||
asEnv = append(asEnv, e.environmentVariables...)
|
||||
|
||||
metrics.InstanceOperationCount.WithLabelValues(
|
||||
"Stop", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
_, err := garmExec.Exec(ctx, e.execPath, nil, asEnv)
|
||||
if err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"Stop", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
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{
|
||||
fmt.Sprintf("GARM_COMMAND=%s", execution.StartInstanceCommand),
|
||||
fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID),
|
||||
fmt.Sprintf("GARM_INSTANCE_ID=%s", instance),
|
||||
fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile),
|
||||
}
|
||||
asEnv = append(asEnv, e.environmentVariables...)
|
||||
|
||||
metrics.InstanceOperationCount.WithLabelValues(
|
||||
"Start", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
|
||||
_, err := garmExec.Exec(ctx, e.execPath, nil, asEnv)
|
||||
if err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"Start", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
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,
|
||||
switch cfg.External.InterfaceVersion {
|
||||
case common.Version010, "":
|
||||
return v010.NewProvider(ctx, cfg, controllerID)
|
||||
case common.Version011:
|
||||
return v011.NewProvider(ctx, cfg, controllerID)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported interface version: %s", cfg.External.InterfaceVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// DisableJITConfig tells us if the provider explicitly disables JIT configuration and
|
||||
// forces runner registration tokens to be used. This may happen if a provider has not yet
|
||||
// been updated to support JIT configuration.
|
||||
func (e *external) DisableJITConfig() bool {
|
||||
if e.cfg == nil {
|
||||
return false
|
||||
}
|
||||
return e.cfg.DisableJITConfig
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package external
|
||||
package util
|
||||
|
||||
import (
|
||||
commonParams "github.com/cloudbase/garm-provider-common/params"
|
||||
342
runner/providers/v0.1.0/external.go
Normal file
342
runner/providers/v0.1.0/external.go
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
package v010
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os/exec"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
garmErrors "github.com/cloudbase/garm-provider-common/errors"
|
||||
execution "github.com/cloudbase/garm-provider-common/execution/v0.1.0"
|
||||
commonParams "github.com/cloudbase/garm-provider-common/params"
|
||||
garmExec "github.com/cloudbase/garm-provider-common/util/exec"
|
||||
"github.com/cloudbase/garm/config"
|
||||
"github.com/cloudbase/garm/metrics"
|
||||
"github.com/cloudbase/garm/params"
|
||||
"github.com/cloudbase/garm/runner/common"
|
||||
"github.com/cloudbase/garm/runner/providers/util"
|
||||
)
|
||||
|
||||
var _ common.Provider = (*external)(nil)
|
||||
|
||||
// NewProvider creates a legacy external provider.
|
||||
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")
|
||||
}
|
||||
|
||||
// Set GARM_INTERFACE_VERSION to the version of the interface that the external
|
||||
// provider implements. This is used to ensure compatibility between the external
|
||||
// provider and garm
|
||||
|
||||
envVars := cfg.External.GetEnvironmentVariables()
|
||||
envVars = append(envVars, fmt.Sprintf("GARM_INTERFACE_VERSION=%s", common.Version010))
|
||||
|
||||
return &external{
|
||||
ctx: ctx,
|
||||
controllerID: controllerID,
|
||||
cfg: cfg,
|
||||
execPath: execPath,
|
||||
environmentVariables: envVars,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type external struct {
|
||||
ctx context.Context
|
||||
controllerID string
|
||||
cfg *config.Provider
|
||||
execPath string
|
||||
environmentVariables []string
|
||||
}
|
||||
|
||||
func (e *external) validateResult(inst commonParams.ProviderInstance) error {
|
||||
if inst.ProviderID == "" {
|
||||
return garmErrors.NewProviderError("missing provider ID")
|
||||
}
|
||||
|
||||
if inst.Name == "" {
|
||||
return garmErrors.NewProviderError("missing instance name")
|
||||
}
|
||||
|
||||
if !util.IsValidProviderStatus(inst.Status) {
|
||||
return garmErrors.NewProviderError("invalid status returned (%s)", inst.Status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateInstance creates a new compute instance in the provider.
|
||||
func (e *external) CreateInstance(ctx context.Context, bootstrapParams commonParams.BootstrapInstance, _ common.CreateInstanceParams) (commonParams.ProviderInstance, error) {
|
||||
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),
|
||||
}
|
||||
asEnv = append(asEnv, e.environmentVariables...)
|
||||
|
||||
asJs, err := json.Marshal(bootstrapParams)
|
||||
if err != nil {
|
||||
return commonParams.ProviderInstance{}, errors.Wrap(err, "serializing bootstrap params")
|
||||
}
|
||||
|
||||
metrics.InstanceOperationCount.WithLabelValues(
|
||||
"CreateInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
|
||||
out, err := garmExec.Exec(ctx, e.execPath, asJs, asEnv)
|
||||
if err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"CreateInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return commonParams.ProviderInstance{}, garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
|
||||
}
|
||||
|
||||
var param commonParams.ProviderInstance
|
||||
if err := json.Unmarshal(out, ¶m); err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"CreateInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return commonParams.ProviderInstance{}, garmErrors.NewProviderError("failed to decode response from binary: %s", err)
|
||||
}
|
||||
|
||||
if err := e.validateResult(param); err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"CreateInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return commonParams.ProviderInstance{}, garmErrors.NewProviderError("failed to validate result: %s", err)
|
||||
}
|
||||
|
||||
retAsJs, _ := json.MarshalIndent(param, "", " ")
|
||||
slog.DebugContext(
|
||||
ctx, "provider returned",
|
||||
"output", string(retAsJs))
|
||||
return param, nil
|
||||
}
|
||||
|
||||
// Delete instance will delete the instance in a provider.
|
||||
func (e *external) DeleteInstance(ctx context.Context, instance string, _ common.DeleteInstanceParams) error {
|
||||
asEnv := []string{
|
||||
fmt.Sprintf("GARM_COMMAND=%s", execution.DeleteInstanceCommand),
|
||||
fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID),
|
||||
fmt.Sprintf("GARM_INSTANCE_ID=%s", instance),
|
||||
fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile),
|
||||
}
|
||||
asEnv = append(asEnv, e.environmentVariables...)
|
||||
|
||||
metrics.InstanceOperationCount.WithLabelValues(
|
||||
"DeleteInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
_, err := garmExec.Exec(ctx, e.execPath, nil, asEnv)
|
||||
if err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
if !errors.As(err, &exitErr) || exitErr.ExitCode() != execution.ExitCodeNotFound {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"DeleteInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
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, _ common.GetInstanceParams) (commonParams.ProviderInstance, error) {
|
||||
asEnv := []string{
|
||||
fmt.Sprintf("GARM_COMMAND=%s", execution.GetInstanceCommand),
|
||||
fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID),
|
||||
fmt.Sprintf("GARM_INSTANCE_ID=%s", instance),
|
||||
fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile),
|
||||
}
|
||||
asEnv = append(asEnv, e.environmentVariables...)
|
||||
|
||||
// nolint:golangci-lint,godox
|
||||
// TODO(gabriel-samfira): handle error types. Of particular interest is to
|
||||
// know when the error is ErrNotFound.
|
||||
metrics.InstanceOperationCount.WithLabelValues(
|
||||
"GetInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
out, err := garmExec.Exec(ctx, e.execPath, nil, asEnv)
|
||||
if err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"GetInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return commonParams.ProviderInstance{}, garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
|
||||
}
|
||||
|
||||
var param commonParams.ProviderInstance
|
||||
if err := json.Unmarshal(out, ¶m); err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"GetInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return commonParams.ProviderInstance{}, garmErrors.NewProviderError("failed to decode response from binary: %s", err)
|
||||
}
|
||||
|
||||
if err := e.validateResult(param); err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"GetInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return commonParams.ProviderInstance{}, garmErrors.NewProviderError("failed to validate result: %s", err)
|
||||
}
|
||||
|
||||
return param, nil
|
||||
}
|
||||
|
||||
// ListInstances will list all instances for a provider.
|
||||
func (e *external) ListInstances(ctx context.Context, poolID string, _ common.ListInstancesParams) ([]commonParams.ProviderInstance, error) {
|
||||
asEnv := []string{
|
||||
fmt.Sprintf("GARM_COMMAND=%s", execution.ListInstancesCommand),
|
||||
fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID),
|
||||
fmt.Sprintf("GARM_POOL_ID=%s", poolID),
|
||||
fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile),
|
||||
}
|
||||
asEnv = append(asEnv, e.environmentVariables...)
|
||||
|
||||
metrics.InstanceOperationCount.WithLabelValues(
|
||||
"ListInstances", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
|
||||
out, err := garmExec.Exec(ctx, e.execPath, nil, asEnv)
|
||||
if err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"ListInstances", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return []commonParams.ProviderInstance{}, garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
|
||||
}
|
||||
|
||||
var param []commonParams.ProviderInstance
|
||||
if err := json.Unmarshal(out, ¶m); err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"ListInstances", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return []commonParams.ProviderInstance{}, garmErrors.NewProviderError("failed to decode response from binary: %s", err)
|
||||
}
|
||||
|
||||
ret := make([]commonParams.ProviderInstance, len(param))
|
||||
for idx, inst := range param {
|
||||
if err := e.validateResult(inst); err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"ListInstances", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return []commonParams.ProviderInstance{}, garmErrors.NewProviderError("failed to validate result: %s", err)
|
||||
}
|
||||
ret[idx] = inst
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// RemoveAllInstances will remove all instances created by this provider.
|
||||
func (e *external) RemoveAllInstances(ctx context.Context, _ common.RemoveAllInstancesParams) error {
|
||||
asEnv := []string{
|
||||
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),
|
||||
}
|
||||
asEnv = append(asEnv, e.environmentVariables...)
|
||||
|
||||
metrics.InstanceOperationCount.WithLabelValues(
|
||||
"RemoveAllInstances", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
|
||||
_, err := garmExec.Exec(ctx, e.execPath, nil, asEnv)
|
||||
if err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"RemoveAllInstances", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
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, _ common.StopParams) error {
|
||||
asEnv := []string{
|
||||
fmt.Sprintf("GARM_COMMAND=%s", execution.StopInstanceCommand),
|
||||
fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID),
|
||||
fmt.Sprintf("GARM_INSTANCE_ID=%s", instance),
|
||||
fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile),
|
||||
}
|
||||
asEnv = append(asEnv, e.environmentVariables...)
|
||||
|
||||
metrics.InstanceOperationCount.WithLabelValues(
|
||||
"Stop", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
_, err := garmExec.Exec(ctx, e.execPath, nil, asEnv)
|
||||
if err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"Stop", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
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, _ common.StartParams) error {
|
||||
asEnv := []string{
|
||||
fmt.Sprintf("GARM_COMMAND=%s", execution.StartInstanceCommand),
|
||||
fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID),
|
||||
fmt.Sprintf("GARM_INSTANCE_ID=%s", instance),
|
||||
fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile),
|
||||
}
|
||||
asEnv = append(asEnv, e.environmentVariables...)
|
||||
|
||||
metrics.InstanceOperationCount.WithLabelValues(
|
||||
"Start", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
|
||||
_, err := garmExec.Exec(ctx, e.execPath, nil, asEnv)
|
||||
if err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"Start", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// DisableJITConfig tells us if the provider explicitly disables JIT configuration and
|
||||
// forces runner registration tokens to be used. This may happen if a provider has not yet
|
||||
// been updated to support JIT configuration.
|
||||
func (e *external) DisableJITConfig() bool {
|
||||
if e.cfg == nil {
|
||||
return false
|
||||
}
|
||||
return e.cfg.DisableJITConfig
|
||||
}
|
||||
394
runner/providers/v0.1.1/external.go
Normal file
394
runner/providers/v0.1.1/external.go
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
package v011
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os/exec"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
garmErrors "github.com/cloudbase/garm-provider-common/errors"
|
||||
execution "github.com/cloudbase/garm-provider-common/execution/v0.1.1"
|
||||
commonParams "github.com/cloudbase/garm-provider-common/params"
|
||||
garmExec "github.com/cloudbase/garm-provider-common/util/exec"
|
||||
"github.com/cloudbase/garm/config"
|
||||
"github.com/cloudbase/garm/metrics"
|
||||
"github.com/cloudbase/garm/params"
|
||||
"github.com/cloudbase/garm/runner/common"
|
||||
"github.com/cloudbase/garm/runner/providers/util"
|
||||
)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// Set GARM_INTERFACE_VERSION to the version of the interface that the external
|
||||
// provider implements. This is used to ensure compatibility between the external
|
||||
// provider and garm
|
||||
envVars := cfg.External.GetEnvironmentVariables()
|
||||
envVars = append(envVars, fmt.Sprintf("GARM_INTERFACE_VERSION=%s", cfg.External.InterfaceVersion))
|
||||
|
||||
return &external{
|
||||
ctx: ctx,
|
||||
controllerID: controllerID,
|
||||
cfg: cfg,
|
||||
execPath: execPath,
|
||||
environmentVariables: envVars,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type external struct {
|
||||
ctx context.Context
|
||||
cfg *config.Provider
|
||||
controllerID string
|
||||
execPath string
|
||||
environmentVariables []string
|
||||
}
|
||||
|
||||
func (e *external) validateResult(inst commonParams.ProviderInstance) error {
|
||||
if inst.ProviderID == "" {
|
||||
return garmErrors.NewProviderError("missing provider ID")
|
||||
}
|
||||
|
||||
if inst.Name == "" {
|
||||
return garmErrors.NewProviderError("missing instance name")
|
||||
}
|
||||
|
||||
if !util.IsValidProviderStatus(inst.Status) {
|
||||
return garmErrors.NewProviderError("invalid status returned (%s)", inst.Status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateInstance creates a new compute instance in the provider.
|
||||
func (e *external) CreateInstance(ctx context.Context, bootstrapParams commonParams.BootstrapInstance, _ common.CreateInstanceParams) (commonParams.ProviderInstance, error) {
|
||||
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),
|
||||
}
|
||||
asEnv = append(asEnv, e.environmentVariables...)
|
||||
|
||||
asJs, err := json.Marshal(bootstrapParams)
|
||||
if err != nil {
|
||||
return commonParams.ProviderInstance{}, errors.Wrap(err, "serializing bootstrap params")
|
||||
}
|
||||
|
||||
metrics.InstanceOperationCount.WithLabelValues(
|
||||
"CreateInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
|
||||
out, err := garmExec.Exec(ctx, e.execPath, asJs, asEnv)
|
||||
if err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"CreateInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return commonParams.ProviderInstance{}, garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
|
||||
}
|
||||
|
||||
var param commonParams.ProviderInstance
|
||||
if err := json.Unmarshal(out, ¶m); err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"CreateInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return commonParams.ProviderInstance{}, garmErrors.NewProviderError("failed to decode response from binary: %s", err)
|
||||
}
|
||||
|
||||
if err := e.validateResult(param); err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"CreateInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return commonParams.ProviderInstance{}, garmErrors.NewProviderError("failed to validate result: %s", err)
|
||||
}
|
||||
|
||||
retAsJs, _ := json.MarshalIndent(param, "", " ")
|
||||
slog.DebugContext(
|
||||
ctx, "provider returned",
|
||||
"output", string(retAsJs))
|
||||
return param, nil
|
||||
}
|
||||
|
||||
// Delete instance will delete the instance in a provider.
|
||||
func (e *external) DeleteInstance(ctx context.Context, instance string, deleteInstanceParams common.DeleteInstanceParams) error {
|
||||
extraspecs := deleteInstanceParams.DeleteInstanceV011.PoolInfo.ExtraSpecs
|
||||
extraspecsValue, err := json.Marshal(extraspecs)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "serializing extraspecs")
|
||||
}
|
||||
// Encode the extraspecs as base64 to avoid issues with special characters.
|
||||
base64EncodedExtraSpecs := base64.StdEncoding.EncodeToString(extraspecsValue)
|
||||
asEnv := []string{
|
||||
fmt.Sprintf("GARM_COMMAND=%s", execution.DeleteInstanceCommand),
|
||||
fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID),
|
||||
fmt.Sprintf("GARM_INSTANCE_ID=%s", instance),
|
||||
fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile),
|
||||
fmt.Sprintf("GARM_POOL_ID=%s", deleteInstanceParams.DeleteInstanceV011.PoolInfo.ID),
|
||||
fmt.Sprintf("GARM_POOL_EXTRASPECS=%s", base64EncodedExtraSpecs),
|
||||
}
|
||||
asEnv = append(asEnv, e.environmentVariables...)
|
||||
|
||||
metrics.InstanceOperationCount.WithLabelValues(
|
||||
"DeleteInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
_, err = garmExec.Exec(ctx, e.execPath, nil, asEnv)
|
||||
if err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
if !errors.As(err, &exitErr) || exitErr.ExitCode() != execution.ExitCodeNotFound {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"DeleteInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
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, getInstanceParams common.GetInstanceParams) (commonParams.ProviderInstance, error) {
|
||||
extraspecs := getInstanceParams.GetInstanceV011.PoolInfo.ExtraSpecs
|
||||
extraspecsValue, err := json.Marshal(extraspecs)
|
||||
if err != nil {
|
||||
return commonParams.ProviderInstance{}, errors.Wrap(err, "serializing extraspecs")
|
||||
}
|
||||
// Encode the extraspecs as base64 to avoid issues with special characters.
|
||||
base64EncodedExtraSpecs := base64.StdEncoding.EncodeToString(extraspecsValue)
|
||||
asEnv := []string{
|
||||
fmt.Sprintf("GARM_COMMAND=%s", execution.GetInstanceCommand),
|
||||
fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID),
|
||||
fmt.Sprintf("GARM_INSTANCE_ID=%s", instance),
|
||||
fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile),
|
||||
fmt.Sprintf("GARM_POOL_ID=%s", getInstanceParams.GetInstanceV011.PoolInfo.ID),
|
||||
fmt.Sprintf("GARM_POOL_EXTRASPECS=%s", base64EncodedExtraSpecs),
|
||||
}
|
||||
asEnv = append(asEnv, e.environmentVariables...)
|
||||
|
||||
// nolint:golangci-lint,godox
|
||||
// TODO(gabriel-samfira): handle error types. Of particular interest is to
|
||||
// know when the error is ErrNotFound.
|
||||
metrics.InstanceOperationCount.WithLabelValues(
|
||||
"GetInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
out, err := garmExec.Exec(ctx, e.execPath, nil, asEnv)
|
||||
if err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"GetInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return commonParams.ProviderInstance{}, garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
|
||||
}
|
||||
|
||||
var param commonParams.ProviderInstance
|
||||
if err := json.Unmarshal(out, ¶m); err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"GetInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return commonParams.ProviderInstance{}, garmErrors.NewProviderError("failed to decode response from binary: %s", err)
|
||||
}
|
||||
|
||||
if err := e.validateResult(param); err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"GetInstance", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return commonParams.ProviderInstance{}, garmErrors.NewProviderError("failed to validate result: %s", err)
|
||||
}
|
||||
|
||||
return param, nil
|
||||
}
|
||||
|
||||
// ListInstances will list all instances for a provider.
|
||||
func (e *external) ListInstances(ctx context.Context, poolID string, listInstancesParams common.ListInstancesParams) ([]commonParams.ProviderInstance, error) {
|
||||
extraspecs := listInstancesParams.ListInstancesV011.PoolInfo.ExtraSpecs
|
||||
extraspecsValue, err := json.Marshal(extraspecs)
|
||||
if err != nil {
|
||||
return []commonParams.ProviderInstance{}, errors.Wrap(err, "serializing extraspecs")
|
||||
}
|
||||
// Encode the extraspecs as base64 to avoid issues with special characters.
|
||||
base64EncodedExtraSpecs := base64.StdEncoding.EncodeToString(extraspecsValue)
|
||||
asEnv := []string{
|
||||
fmt.Sprintf("GARM_COMMAND=%s", execution.ListInstancesCommand),
|
||||
fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID),
|
||||
fmt.Sprintf("GARM_POOL_ID=%s", poolID),
|
||||
fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile),
|
||||
fmt.Sprintf("GARM_POOL_EXTRASPECS=%s", base64EncodedExtraSpecs),
|
||||
}
|
||||
asEnv = append(asEnv, e.environmentVariables...)
|
||||
|
||||
metrics.InstanceOperationCount.WithLabelValues(
|
||||
"ListInstances", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
|
||||
out, err := garmExec.Exec(ctx, e.execPath, nil, asEnv)
|
||||
if err == nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"ListInstances", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return []commonParams.ProviderInstance{}, garmErrors.NewProviderError("provider binary %s returned error: %s", e.execPath, err)
|
||||
}
|
||||
|
||||
var param []commonParams.ProviderInstance
|
||||
if err := json.Unmarshal(out, ¶m); err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"ListInstances", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return []commonParams.ProviderInstance{}, garmErrors.NewProviderError("failed to decode response from binary: %s", err)
|
||||
}
|
||||
|
||||
ret := make([]commonParams.ProviderInstance, len(param))
|
||||
for idx, inst := range param {
|
||||
if err := e.validateResult(inst); err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"ListInstances", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
return []commonParams.ProviderInstance{}, garmErrors.NewProviderError("failed to validate result: %s", err)
|
||||
}
|
||||
ret[idx] = inst
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// RemoveAllInstances will remove all instances created by this provider.
|
||||
func (e *external) RemoveAllInstances(ctx context.Context, removeAllInstances common.RemoveAllInstancesParams) error {
|
||||
extraspecs := removeAllInstances.RemoveAllInstancesV011.PoolInfo.ExtraSpecs
|
||||
extraspecsValue, err := json.Marshal(extraspecs)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "serializing extraspecs")
|
||||
}
|
||||
// Encode the extraspecs as base64 to avoid issues with special characters.
|
||||
base64EncodedExtraSpecs := base64.StdEncoding.EncodeToString(extraspecsValue)
|
||||
asEnv := []string{
|
||||
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),
|
||||
fmt.Sprintf("GARM_POOL_ID=%s", removeAllInstances.RemoveAllInstancesV011.PoolInfo.ID),
|
||||
fmt.Sprintf("GARM_POOL_EXTRASPECS=%s", base64EncodedExtraSpecs),
|
||||
}
|
||||
asEnv = append(asEnv, e.environmentVariables...)
|
||||
|
||||
metrics.InstanceOperationCount.WithLabelValues(
|
||||
"RemoveAllInstances", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
|
||||
_, err = garmExec.Exec(ctx, e.execPath, nil, asEnv)
|
||||
if err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"RemoveAllInstances", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
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, stopParams common.StopParams) error {
|
||||
extraspecs := stopParams.StopV011.PoolInfo.ExtraSpecs
|
||||
extraspecsValue, err := json.Marshal(extraspecs)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "serializing extraspecs")
|
||||
}
|
||||
// Encode the extraspecs as base64 to avoid issues with special characters.
|
||||
base64EncodedExtraSpecs := base64.StdEncoding.EncodeToString(extraspecsValue)
|
||||
asEnv := []string{
|
||||
fmt.Sprintf("GARM_COMMAND=%s", execution.StopInstanceCommand),
|
||||
fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID),
|
||||
fmt.Sprintf("GARM_INSTANCE_ID=%s", instance),
|
||||
fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile),
|
||||
fmt.Sprintf("GARM_POOL_ID=%s", stopParams.StopV011.PoolInfo.ID),
|
||||
fmt.Sprintf("GARM_POOL_EXTRASPECS=%s", base64EncodedExtraSpecs),
|
||||
}
|
||||
asEnv = append(asEnv, e.environmentVariables...)
|
||||
|
||||
metrics.InstanceOperationCount.WithLabelValues(
|
||||
"Stop", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
_, err = garmExec.Exec(ctx, e.execPath, nil, asEnv)
|
||||
if err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"Stop", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
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, startParams common.StartParams) error {
|
||||
extraspecs := startParams.StartV011.PoolInfo.ExtraSpecs
|
||||
extraspecsValue, err := json.Marshal(extraspecs)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "serializing extraspecs")
|
||||
}
|
||||
// Encode the extraspecs as base64 to avoid issues with special characters.
|
||||
base64EncodedExtraSpecs := base64.StdEncoding.EncodeToString(extraspecsValue)
|
||||
asEnv := []string{
|
||||
fmt.Sprintf("GARM_COMMAND=%s", execution.StartInstanceCommand),
|
||||
fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID),
|
||||
fmt.Sprintf("GARM_INSTANCE_ID=%s", instance),
|
||||
fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile),
|
||||
fmt.Sprintf("GARM_POOL_ID=%s", startParams.StartV011.PoolInfo.ID),
|
||||
fmt.Sprintf("GARM_POOL_EXTRASPECS=%s", base64EncodedExtraSpecs),
|
||||
}
|
||||
asEnv = append(asEnv, e.environmentVariables...)
|
||||
|
||||
metrics.InstanceOperationCount.WithLabelValues(
|
||||
"Start", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
|
||||
_, err = garmExec.Exec(ctx, e.execPath, nil, asEnv)
|
||||
if err != nil {
|
||||
metrics.InstanceOperationFailedCount.WithLabelValues(
|
||||
"Start", // label: operation
|
||||
e.cfg.Name, // label: provider
|
||||
).Inc()
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// DisableJITConfig tells us if the provider explicitly disables JIT configuration and
|
||||
// forces runner registration tokens to be used. This may happen if a provider has not yet
|
||||
// been updated to support JIT configuration.
|
||||
func (e *external) DisableJITConfig() bool {
|
||||
if e.cfg == nil {
|
||||
return false
|
||||
}
|
||||
return e.cfg.DisableJITConfig
|
||||
}
|
||||
494
vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.0/execution_test.go
generated
vendored
Normal file
494
vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.0/execution_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
// Copyright 2023 Cloudbase Solutions SRL
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package execution
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
gErrors "github.com/cloudbase/garm-provider-common/errors"
|
||||
"github.com/cloudbase/garm-provider-common/params"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testExternalProvider struct {
|
||||
mockErr error
|
||||
mockInstance params.ProviderInstance
|
||||
}
|
||||
|
||||
func (e *testExternalProvider) CreateInstance(ctx context.Context, bootstrapParams params.BootstrapInstance) (params.ProviderInstance, error) {
|
||||
if e.mockErr != nil {
|
||||
return params.ProviderInstance{}, e.mockErr
|
||||
}
|
||||
return e.mockInstance, nil
|
||||
}
|
||||
|
||||
func (p *testExternalProvider) DeleteInstance(context.Context, string) error {
|
||||
if p.mockErr != nil {
|
||||
return p.mockErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *testExternalProvider) GetInstance(context.Context, string) (params.ProviderInstance, error) {
|
||||
if p.mockErr != nil {
|
||||
return params.ProviderInstance{}, p.mockErr
|
||||
}
|
||||
return p.mockInstance, nil
|
||||
}
|
||||
|
||||
func (p *testExternalProvider) ListInstances(context.Context, string) ([]params.ProviderInstance, error) {
|
||||
if p.mockErr != nil {
|
||||
return nil, p.mockErr
|
||||
}
|
||||
return []params.ProviderInstance{p.mockInstance}, nil
|
||||
}
|
||||
|
||||
func (p *testExternalProvider) RemoveAllInstances(context.Context) error {
|
||||
if p.mockErr != nil {
|
||||
return p.mockErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *testExternalProvider) Stop(context.Context, string, bool) error {
|
||||
if p.mockErr != nil {
|
||||
return p.mockErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *testExternalProvider) Start(context.Context, string) error {
|
||||
if p.mockErr != nil {
|
||||
return p.mockErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestResolveErrorToExitCode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
code int
|
||||
}{
|
||||
{
|
||||
name: "nil error",
|
||||
err: nil,
|
||||
code: 0,
|
||||
},
|
||||
{
|
||||
name: "not found error",
|
||||
err: gErrors.ErrNotFound,
|
||||
code: ExitCodeNotFound,
|
||||
},
|
||||
{
|
||||
name: "duplicate entity error",
|
||||
err: gErrors.ErrDuplicateEntity,
|
||||
code: ExitCodeDuplicate,
|
||||
},
|
||||
{
|
||||
name: "other error",
|
||||
err: errors.New("other error"),
|
||||
code: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
code := ResolveErrorToExitCode(tc.err)
|
||||
require.Equal(t, tc.code, code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateEnvironment(t *testing.T) {
|
||||
// Create a temporary file
|
||||
tmpfile, err := os.CreateTemp("", "provider-config")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// clean up the temporary file
|
||||
t.Cleanup(func() { os.RemoveAll(tmpfile.Name()) })
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
env Environment
|
||||
errString string
|
||||
}{
|
||||
{
|
||||
name: "valid environment",
|
||||
env: Environment{
|
||||
Command: CreateInstanceCommand,
|
||||
ControllerID: "controller-id",
|
||||
PoolID: "pool-id",
|
||||
ProviderConfigFile: tmpfile.Name(),
|
||||
InstanceID: "instance-id",
|
||||
BootstrapParams: params.BootstrapInstance{
|
||||
Name: "instance-name",
|
||||
},
|
||||
},
|
||||
errString: "",
|
||||
},
|
||||
{
|
||||
name: "invalid command",
|
||||
env: Environment{
|
||||
Command: "",
|
||||
},
|
||||
errString: "missing GARM_COMMAND",
|
||||
},
|
||||
{
|
||||
name: "invalid provider config file",
|
||||
env: Environment{
|
||||
Command: CreateInstanceCommand,
|
||||
ProviderConfigFile: "",
|
||||
},
|
||||
errString: "missing GARM_PROVIDER_CONFIG_FILE",
|
||||
},
|
||||
{
|
||||
name: "error accessing config file",
|
||||
env: Environment{
|
||||
Command: CreateInstanceCommand,
|
||||
ProviderConfigFile: "invalid-file",
|
||||
},
|
||||
errString: "error accessing config file",
|
||||
},
|
||||
{
|
||||
name: "invalid controller ID",
|
||||
env: Environment{
|
||||
Command: CreateInstanceCommand,
|
||||
ProviderConfigFile: tmpfile.Name(),
|
||||
},
|
||||
errString: "missing GARM_CONTROLLER_ID",
|
||||
},
|
||||
|
||||
{
|
||||
name: "invalid instance ID",
|
||||
env: Environment{
|
||||
Command: DeleteInstanceCommand,
|
||||
ProviderConfigFile: tmpfile.Name(),
|
||||
ControllerID: "controller-id",
|
||||
InstanceID: "",
|
||||
},
|
||||
errString: "missing instance ID",
|
||||
},
|
||||
{
|
||||
name: "invalid pool ID",
|
||||
env: Environment{
|
||||
Command: ListInstancesCommand,
|
||||
ProviderConfigFile: tmpfile.Name(),
|
||||
ControllerID: "controller-id",
|
||||
PoolID: "",
|
||||
},
|
||||
errString: "missing pool ID",
|
||||
},
|
||||
{
|
||||
name: "invalid bootstrap params",
|
||||
env: Environment{
|
||||
Command: CreateInstanceCommand,
|
||||
ProviderConfigFile: tmpfile.Name(),
|
||||
ControllerID: "controller-id",
|
||||
PoolID: "pool-id",
|
||||
BootstrapParams: params.BootstrapInstance{},
|
||||
},
|
||||
errString: "missing bootstrap params",
|
||||
},
|
||||
{
|
||||
name: "missing pool ID",
|
||||
env: Environment{
|
||||
Command: CreateInstanceCommand,
|
||||
ProviderConfigFile: tmpfile.Name(),
|
||||
ControllerID: "controller-id",
|
||||
PoolID: "",
|
||||
BootstrapParams: params.BootstrapInstance{
|
||||
Name: "instance-name",
|
||||
},
|
||||
},
|
||||
errString: "missing pool ID",
|
||||
},
|
||||
{
|
||||
name: "unknown command",
|
||||
env: Environment{
|
||||
Command: "unknown-command",
|
||||
ProviderConfigFile: tmpfile.Name(),
|
||||
ControllerID: "controller-id",
|
||||
PoolID: "pool-id",
|
||||
BootstrapParams: params.BootstrapInstance{
|
||||
Name: "instance-name",
|
||||
},
|
||||
},
|
||||
errString: "unknown GARM_COMMAND",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.env.Validate()
|
||||
if tc.errString == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
require.Regexp(t, tc.errString, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
providerEnv Environment
|
||||
providerInstance params.ProviderInstance
|
||||
providerErr error
|
||||
expectedErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "Valid environment",
|
||||
providerEnv: Environment{
|
||||
Command: CreateInstanceCommand,
|
||||
},
|
||||
providerInstance: params.ProviderInstance{
|
||||
Name: "test-instance",
|
||||
OSType: params.Linux,
|
||||
},
|
||||
providerErr: nil,
|
||||
expectedErrMsg: "",
|
||||
},
|
||||
{
|
||||
name: "Failed to create instance",
|
||||
providerEnv: Environment{
|
||||
Command: CreateInstanceCommand,
|
||||
},
|
||||
providerInstance: params.ProviderInstance{
|
||||
Name: "test-instance",
|
||||
OSType: params.Linux,
|
||||
},
|
||||
providerErr: fmt.Errorf("error creating test-instance"),
|
||||
expectedErrMsg: "failed to create instance in provider: error creating test-instance",
|
||||
},
|
||||
{
|
||||
name: "Failed to get instance",
|
||||
providerEnv: Environment{
|
||||
Command: GetInstanceCommand,
|
||||
},
|
||||
providerInstance: params.ProviderInstance{
|
||||
Name: "test-instance",
|
||||
OSType: params.Linux,
|
||||
},
|
||||
providerErr: fmt.Errorf("error getting test-instance"),
|
||||
expectedErrMsg: "failed to get instance from provider: error getting test-instance",
|
||||
},
|
||||
{
|
||||
name: "Failed to list instances",
|
||||
providerEnv: Environment{
|
||||
Command: ListInstancesCommand,
|
||||
},
|
||||
providerInstance: params.ProviderInstance{
|
||||
Name: "test-instance",
|
||||
OSType: params.Linux,
|
||||
},
|
||||
providerErr: fmt.Errorf("error listing instances"),
|
||||
expectedErrMsg: "failed to list instances from provider: error listing instances",
|
||||
},
|
||||
{
|
||||
name: "Failed to delete instance",
|
||||
providerEnv: Environment{
|
||||
Command: DeleteInstanceCommand,
|
||||
},
|
||||
providerInstance: params.ProviderInstance{
|
||||
Name: "test-instance",
|
||||
OSType: params.Linux,
|
||||
},
|
||||
providerErr: fmt.Errorf("error deleting test-instance"),
|
||||
expectedErrMsg: "failed to delete instance from provider: error deleting test-instance",
|
||||
},
|
||||
{
|
||||
name: "Failed to remove all instances",
|
||||
providerEnv: Environment{
|
||||
Command: RemoveAllInstancesCommand,
|
||||
},
|
||||
providerInstance: params.ProviderInstance{
|
||||
Name: "test-instance",
|
||||
OSType: params.Linux,
|
||||
},
|
||||
providerErr: fmt.Errorf("error removing all instances"),
|
||||
expectedErrMsg: "failed to destroy environment: error removing all instances",
|
||||
},
|
||||
{
|
||||
name: "Failed to start instance",
|
||||
providerEnv: Environment{
|
||||
Command: StartInstanceCommand,
|
||||
},
|
||||
providerInstance: params.ProviderInstance{
|
||||
Name: "test-instance",
|
||||
OSType: params.Linux,
|
||||
},
|
||||
providerErr: fmt.Errorf("error starting test-instance"),
|
||||
expectedErrMsg: "failed to start instance: error starting test-instance",
|
||||
},
|
||||
{
|
||||
name: "Failed to stop instance",
|
||||
providerEnv: Environment{
|
||||
Command: StopInstanceCommand,
|
||||
},
|
||||
providerInstance: params.ProviderInstance{
|
||||
Name: "test-instance",
|
||||
OSType: params.Linux,
|
||||
},
|
||||
providerErr: fmt.Errorf("error stopping test-instance"),
|
||||
expectedErrMsg: "failed to stop instance: error stopping test-instance",
|
||||
},
|
||||
{
|
||||
name: "Invalid command",
|
||||
providerEnv: Environment{
|
||||
Command: "invalid-command",
|
||||
},
|
||||
providerInstance: params.ProviderInstance{
|
||||
Name: "test-instance",
|
||||
OSType: params.Linux,
|
||||
},
|
||||
providerErr: nil,
|
||||
expectedErrMsg: "invalid command: invalid-command",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
testExternalProvider := testExternalProvider{
|
||||
mockErr: tc.providerErr,
|
||||
mockInstance: tc.providerInstance,
|
||||
}
|
||||
|
||||
out, err := Run(context.Background(), &testExternalProvider, tc.providerEnv)
|
||||
|
||||
if tc.expectedErrMsg == "" {
|
||||
require.NoError(t, err)
|
||||
expectedJs, marshalErr := json.Marshal(tc.providerInstance)
|
||||
require.NoError(t, marshalErr)
|
||||
require.Equal(t, string(expectedJs), out)
|
||||
} else {
|
||||
require.Equal(t, err.Error(), tc.expectedErrMsg)
|
||||
require.Equal(t, "", out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEnvironment(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
stdinData string
|
||||
envData map[string]string
|
||||
errString string
|
||||
}{
|
||||
{
|
||||
name: "The environment is valid",
|
||||
stdinData: `{"name": "test"}`,
|
||||
errString: "",
|
||||
},
|
||||
{
|
||||
name: "Data is missing from stdin",
|
||||
stdinData: ``,
|
||||
errString: "CreateInstance requires data passed into stdin",
|
||||
},
|
||||
{
|
||||
name: "Invalid JSON",
|
||||
stdinData: `bogus`,
|
||||
errString: "failed to decode instance params: invalid character 'b' looking for beginning of value",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
// Create a temporary file
|
||||
tmpfile, err := os.CreateTemp("", "test-get-env")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// clean up the temporary file
|
||||
t.Cleanup(func() { os.RemoveAll(tmpfile.Name()) })
|
||||
|
||||
// Write some test data to the temporary file
|
||||
if _, err := tmpfile.Write([]byte(tc.stdinData)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Rewind the temporary file to the beginning
|
||||
if _, err := tmpfile.Seek(0, 0); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Clean up the temporary file
|
||||
t.Cleanup(func() { os.RemoveAll(tmpfile.Name()) })
|
||||
|
||||
oldStdin := os.Stdin
|
||||
defer func() { os.Stdin = oldStdin }() // Restore original Stdin
|
||||
|
||||
os.Stdin = tmpfile // mock os.Stdin
|
||||
|
||||
for key, value := range tc.envData {
|
||||
os.Setenv(key, value)
|
||||
}
|
||||
|
||||
// Define the environment variables
|
||||
os.Setenv("GARM_COMMAND", "CreateInstance")
|
||||
os.Setenv("GARM_CONTROLLER_ID", "test-controller-id")
|
||||
os.Setenv("GARM_POOL_ID", "test-pool-id")
|
||||
os.Setenv("GARM_PROVIDER_CONFIG_FILE", tmpfile.Name())
|
||||
|
||||
// Clean up the environment variables
|
||||
t.Cleanup(func() {
|
||||
for key := range tc.envData {
|
||||
os.Unsetenv(key)
|
||||
}
|
||||
})
|
||||
|
||||
env, err := GetEnvironment()
|
||||
if tc.errString == "" {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, CreateInstanceCommand, env.Command)
|
||||
} else {
|
||||
require.Equal(t, tc.errString, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEnvValidateFailed(t *testing.T) {
|
||||
// Create a temporary file
|
||||
tmpfile, err := os.CreateTemp("", "test-get-env")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// clean up the temporary file
|
||||
t.Cleanup(func() { os.RemoveAll(tmpfile.Name()) })
|
||||
|
||||
os.Setenv("GARM_COMMAND", "unknown-command")
|
||||
os.Setenv("GARM_CONTROLLER_ID", "test-controller-id")
|
||||
os.Setenv("GARM_POOL_ID", "test-pool-id")
|
||||
os.Setenv("GARM_PROVIDER_CONFIG_FILE", tmpfile.Name())
|
||||
|
||||
// Clean up the environment variables
|
||||
t.Cleanup(func() {
|
||||
os.Unsetenv("GARM_COMMAND")
|
||||
os.Unsetenv("GARM_CONTROLLER_ID")
|
||||
os.Unsetenv("GARM_POOL_ID")
|
||||
os.Unsetenv("GARM_PROVIDER_CONFIG_FILE")
|
||||
})
|
||||
|
||||
_, err = GetEnvironment()
|
||||
require.Error(t, err)
|
||||
require.Equal(t, "failed to validate execution environment: unknown GARM_COMMAND: unknown-command", err.Error())
|
||||
}
|
||||
28
vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.1/commands.go
generated
vendored
Normal file
28
vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.1/commands.go
generated
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright 2023 Cloudbase Solutions SRL
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
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"
|
||||
GetVersionInfoCommand ExecutionCommand = "GetVersionInfo"
|
||||
)
|
||||
212
vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.1/execution.go
generated
vendored
Normal file
212
vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.1/execution.go
generated
vendored
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
// Copyright 2023 Cloudbase Solutions SRL
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package execution
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
gErrors "github.com/cloudbase/garm-provider-common/errors"
|
||||
"github.com/cloudbase/garm-provider-common/params"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
)
|
||||
|
||||
const (
|
||||
// ExitCodeNotFound is an exit code that indicates a Not Found error
|
||||
ExitCodeNotFound int = 30
|
||||
// ExitCodeDuplicate is an exit code that indicates a duplicate error
|
||||
ExitCodeDuplicate int = 31
|
||||
)
|
||||
|
||||
func ResolveErrorToExitCode(err error) int {
|
||||
if err != nil {
|
||||
if errors.Is(err, gErrors.ErrNotFound) {
|
||||
return ExitCodeNotFound
|
||||
} else if errors.Is(err, gErrors.ErrDuplicateEntity) {
|
||||
return ExitCodeDuplicate
|
||||
}
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
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"),
|
||||
InterfaceVersion: os.Getenv("GARM_INTERFACE_VERSION"),
|
||||
ExtraSpecs: os.Getenv("GARM_POOL_EXTRASPECS"),
|
||||
}
|
||||
|
||||
// If this is a CreateInstance command, we need to get the bootstrap params
|
||||
// from stdin
|
||||
if env.Command == CreateInstanceCommand {
|
||||
if isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd()) {
|
||||
return Environment{}, fmt.Errorf("%s requires data passed into stdin", CreateInstanceCommand)
|
||||
}
|
||||
|
||||
var data bytes.Buffer
|
||||
if _, err := io.Copy(&data, os.Stdin); err != nil {
|
||||
return Environment{}, fmt.Errorf("failed to copy bootstrap params")
|
||||
}
|
||||
|
||||
if data.Len() == 0 {
|
||||
return Environment{}, fmt.Errorf("%s requires data passed into stdin", CreateInstanceCommand)
|
||||
}
|
||||
|
||||
var bootstrapParams params.BootstrapInstance
|
||||
if err := json.Unmarshal(data.Bytes(), &bootstrapParams); err != nil {
|
||||
return Environment{}, fmt.Errorf("failed to decode instance params: %w", err)
|
||||
}
|
||||
if bootstrapParams.ExtraSpecs == nil {
|
||||
// Initialize ExtraSpecs as an empty JSON object
|
||||
bootstrapParams.ExtraSpecs = json.RawMessage([]byte("{}"))
|
||||
}
|
||||
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
|
||||
InterfaceVersion string
|
||||
ExtraSpecs 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)
|
||||
}
|
||||
|
||||
if e.ControllerID == "" {
|
||||
return fmt.Errorf("missing GARM_CONTROLLER_ID")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
if e.PoolID == "" {
|
||||
return fmt.Errorf("missing pool 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
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, provider ExternalProvider, env Environment) (string, error) {
|
||||
var ret string
|
||||
switch env.Command {
|
||||
case CreateInstanceCommand:
|
||||
instance, err := provider.CreateInstance(ctx, env.BootstrapParams)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create instance in provider: %w", err)
|
||||
}
|
||||
|
||||
asJs, err := json.Marshal(instance)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal response: %w", err)
|
||||
}
|
||||
ret = string(asJs)
|
||||
case GetInstanceCommand:
|
||||
instance, err := provider.GetInstance(ctx, env.InstanceID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get instance from provider: %w", err)
|
||||
}
|
||||
asJs, err := json.Marshal(instance)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal response: %w", err)
|
||||
}
|
||||
ret = string(asJs)
|
||||
case ListInstancesCommand:
|
||||
instances, err := provider.ListInstances(ctx, env.PoolID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to list instances from provider: %w", err)
|
||||
}
|
||||
asJs, err := json.Marshal(instances)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal response: %w", err)
|
||||
}
|
||||
ret = string(asJs)
|
||||
case DeleteInstanceCommand:
|
||||
if err := provider.DeleteInstance(ctx, env.InstanceID); err != nil {
|
||||
return "", fmt.Errorf("failed to delete instance from provider: %w", err)
|
||||
}
|
||||
case RemoveAllInstancesCommand:
|
||||
if err := provider.RemoveAllInstances(ctx); err != nil {
|
||||
return "", fmt.Errorf("failed to destroy environment: %w", err)
|
||||
}
|
||||
case StartInstanceCommand:
|
||||
if err := provider.Start(ctx, env.InstanceID); err != nil {
|
||||
return "", fmt.Errorf("failed to start instance: %w", err)
|
||||
}
|
||||
case StopInstanceCommand:
|
||||
if err := provider.Stop(ctx, env.InstanceID, true); err != nil {
|
||||
return "", fmt.Errorf("failed to stop instance: %w", err)
|
||||
}
|
||||
default:
|
||||
return "", fmt.Errorf("invalid command: %s", env.Command)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
494
vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.1/execution_test.go
generated
vendored
Normal file
494
vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.1/execution_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
// Copyright 2023 Cloudbase Solutions SRL
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package execution
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
gErrors "github.com/cloudbase/garm-provider-common/errors"
|
||||
"github.com/cloudbase/garm-provider-common/params"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testExternalProvider struct {
|
||||
mockErr error
|
||||
mockInstance params.ProviderInstance
|
||||
}
|
||||
|
||||
func (e *testExternalProvider) CreateInstance(ctx context.Context, bootstrapParams params.BootstrapInstance) (params.ProviderInstance, error) {
|
||||
if e.mockErr != nil {
|
||||
return params.ProviderInstance{}, e.mockErr
|
||||
}
|
||||
return e.mockInstance, nil
|
||||
}
|
||||
|
||||
func (p *testExternalProvider) DeleteInstance(context.Context, string) error {
|
||||
if p.mockErr != nil {
|
||||
return p.mockErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *testExternalProvider) GetInstance(context.Context, string) (params.ProviderInstance, error) {
|
||||
if p.mockErr != nil {
|
||||
return params.ProviderInstance{}, p.mockErr
|
||||
}
|
||||
return p.mockInstance, nil
|
||||
}
|
||||
|
||||
func (p *testExternalProvider) ListInstances(context.Context, string) ([]params.ProviderInstance, error) {
|
||||
if p.mockErr != nil {
|
||||
return nil, p.mockErr
|
||||
}
|
||||
return []params.ProviderInstance{p.mockInstance}, nil
|
||||
}
|
||||
|
||||
func (p *testExternalProvider) RemoveAllInstances(context.Context) error {
|
||||
if p.mockErr != nil {
|
||||
return p.mockErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *testExternalProvider) Stop(context.Context, string, bool) error {
|
||||
if p.mockErr != nil {
|
||||
return p.mockErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *testExternalProvider) Start(context.Context, string) error {
|
||||
if p.mockErr != nil {
|
||||
return p.mockErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestResolveErrorToExitCode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
code int
|
||||
}{
|
||||
{
|
||||
name: "nil error",
|
||||
err: nil,
|
||||
code: 0,
|
||||
},
|
||||
{
|
||||
name: "not found error",
|
||||
err: gErrors.ErrNotFound,
|
||||
code: ExitCodeNotFound,
|
||||
},
|
||||
{
|
||||
name: "duplicate entity error",
|
||||
err: gErrors.ErrDuplicateEntity,
|
||||
code: ExitCodeDuplicate,
|
||||
},
|
||||
{
|
||||
name: "other error",
|
||||
err: errors.New("other error"),
|
||||
code: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
code := ResolveErrorToExitCode(tc.err)
|
||||
require.Equal(t, tc.code, code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateEnvironment(t *testing.T) {
|
||||
// Create a temporary file
|
||||
tmpfile, err := os.CreateTemp("", "provider-config")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// clean up the temporary file
|
||||
t.Cleanup(func() { os.RemoveAll(tmpfile.Name()) })
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
env Environment
|
||||
errString string
|
||||
}{
|
||||
{
|
||||
name: "valid environment",
|
||||
env: Environment{
|
||||
Command: CreateInstanceCommand,
|
||||
ControllerID: "controller-id",
|
||||
PoolID: "pool-id",
|
||||
ProviderConfigFile: tmpfile.Name(),
|
||||
InstanceID: "instance-id",
|
||||
BootstrapParams: params.BootstrapInstance{
|
||||
Name: "instance-name",
|
||||
},
|
||||
},
|
||||
errString: "",
|
||||
},
|
||||
{
|
||||
name: "invalid command",
|
||||
env: Environment{
|
||||
Command: "",
|
||||
},
|
||||
errString: "missing GARM_COMMAND",
|
||||
},
|
||||
{
|
||||
name: "invalid provider config file",
|
||||
env: Environment{
|
||||
Command: CreateInstanceCommand,
|
||||
ProviderConfigFile: "",
|
||||
},
|
||||
errString: "missing GARM_PROVIDER_CONFIG_FILE",
|
||||
},
|
||||
{
|
||||
name: "error accessing config file",
|
||||
env: Environment{
|
||||
Command: CreateInstanceCommand,
|
||||
ProviderConfigFile: "invalid-file",
|
||||
},
|
||||
errString: "error accessing config file",
|
||||
},
|
||||
{
|
||||
name: "invalid controller ID",
|
||||
env: Environment{
|
||||
Command: CreateInstanceCommand,
|
||||
ProviderConfigFile: tmpfile.Name(),
|
||||
},
|
||||
errString: "missing GARM_CONTROLLER_ID",
|
||||
},
|
||||
|
||||
{
|
||||
name: "invalid instance ID",
|
||||
env: Environment{
|
||||
Command: DeleteInstanceCommand,
|
||||
ProviderConfigFile: tmpfile.Name(),
|
||||
ControllerID: "controller-id",
|
||||
InstanceID: "",
|
||||
},
|
||||
errString: "missing instance ID",
|
||||
},
|
||||
{
|
||||
name: "invalid pool ID",
|
||||
env: Environment{
|
||||
Command: ListInstancesCommand,
|
||||
ProviderConfigFile: tmpfile.Name(),
|
||||
ControllerID: "controller-id",
|
||||
PoolID: "",
|
||||
},
|
||||
errString: "missing pool ID",
|
||||
},
|
||||
{
|
||||
name: "invalid bootstrap params",
|
||||
env: Environment{
|
||||
Command: CreateInstanceCommand,
|
||||
ProviderConfigFile: tmpfile.Name(),
|
||||
ControllerID: "controller-id",
|
||||
PoolID: "pool-id",
|
||||
BootstrapParams: params.BootstrapInstance{},
|
||||
},
|
||||
errString: "missing bootstrap params",
|
||||
},
|
||||
{
|
||||
name: "missing pool ID",
|
||||
env: Environment{
|
||||
Command: CreateInstanceCommand,
|
||||
ProviderConfigFile: tmpfile.Name(),
|
||||
ControllerID: "controller-id",
|
||||
PoolID: "",
|
||||
BootstrapParams: params.BootstrapInstance{
|
||||
Name: "instance-name",
|
||||
},
|
||||
},
|
||||
errString: "missing pool ID",
|
||||
},
|
||||
{
|
||||
name: "unknown command",
|
||||
env: Environment{
|
||||
Command: "unknown-command",
|
||||
ProviderConfigFile: tmpfile.Name(),
|
||||
ControllerID: "controller-id",
|
||||
PoolID: "pool-id",
|
||||
BootstrapParams: params.BootstrapInstance{
|
||||
Name: "instance-name",
|
||||
},
|
||||
},
|
||||
errString: "unknown GARM_COMMAND",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.env.Validate()
|
||||
if tc.errString == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
require.Regexp(t, tc.errString, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
providerEnv Environment
|
||||
providerInstance params.ProviderInstance
|
||||
providerErr error
|
||||
expectedErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "Valid environment",
|
||||
providerEnv: Environment{
|
||||
Command: CreateInstanceCommand,
|
||||
},
|
||||
providerInstance: params.ProviderInstance{
|
||||
Name: "test-instance",
|
||||
OSType: params.Linux,
|
||||
},
|
||||
providerErr: nil,
|
||||
expectedErrMsg: "",
|
||||
},
|
||||
{
|
||||
name: "Failed to create instance",
|
||||
providerEnv: Environment{
|
||||
Command: CreateInstanceCommand,
|
||||
},
|
||||
providerInstance: params.ProviderInstance{
|
||||
Name: "test-instance",
|
||||
OSType: params.Linux,
|
||||
},
|
||||
providerErr: fmt.Errorf("error creating test-instance"),
|
||||
expectedErrMsg: "failed to create instance in provider: error creating test-instance",
|
||||
},
|
||||
{
|
||||
name: "Failed to get instance",
|
||||
providerEnv: Environment{
|
||||
Command: GetInstanceCommand,
|
||||
},
|
||||
providerInstance: params.ProviderInstance{
|
||||
Name: "test-instance",
|
||||
OSType: params.Linux,
|
||||
},
|
||||
providerErr: fmt.Errorf("error getting test-instance"),
|
||||
expectedErrMsg: "failed to get instance from provider: error getting test-instance",
|
||||
},
|
||||
{
|
||||
name: "Failed to list instances",
|
||||
providerEnv: Environment{
|
||||
Command: ListInstancesCommand,
|
||||
},
|
||||
providerInstance: params.ProviderInstance{
|
||||
Name: "test-instance",
|
||||
OSType: params.Linux,
|
||||
},
|
||||
providerErr: fmt.Errorf("error listing instances"),
|
||||
expectedErrMsg: "failed to list instances from provider: error listing instances",
|
||||
},
|
||||
{
|
||||
name: "Failed to delete instance",
|
||||
providerEnv: Environment{
|
||||
Command: DeleteInstanceCommand,
|
||||
},
|
||||
providerInstance: params.ProviderInstance{
|
||||
Name: "test-instance",
|
||||
OSType: params.Linux,
|
||||
},
|
||||
providerErr: fmt.Errorf("error deleting test-instance"),
|
||||
expectedErrMsg: "failed to delete instance from provider: error deleting test-instance",
|
||||
},
|
||||
{
|
||||
name: "Failed to remove all instances",
|
||||
providerEnv: Environment{
|
||||
Command: RemoveAllInstancesCommand,
|
||||
},
|
||||
providerInstance: params.ProviderInstance{
|
||||
Name: "test-instance",
|
||||
OSType: params.Linux,
|
||||
},
|
||||
providerErr: fmt.Errorf("error removing all instances"),
|
||||
expectedErrMsg: "failed to destroy environment: error removing all instances",
|
||||
},
|
||||
{
|
||||
name: "Failed to start instance",
|
||||
providerEnv: Environment{
|
||||
Command: StartInstanceCommand,
|
||||
},
|
||||
providerInstance: params.ProviderInstance{
|
||||
Name: "test-instance",
|
||||
OSType: params.Linux,
|
||||
},
|
||||
providerErr: fmt.Errorf("error starting test-instance"),
|
||||
expectedErrMsg: "failed to start instance: error starting test-instance",
|
||||
},
|
||||
{
|
||||
name: "Failed to stop instance",
|
||||
providerEnv: Environment{
|
||||
Command: StopInstanceCommand,
|
||||
},
|
||||
providerInstance: params.ProviderInstance{
|
||||
Name: "test-instance",
|
||||
OSType: params.Linux,
|
||||
},
|
||||
providerErr: fmt.Errorf("error stopping test-instance"),
|
||||
expectedErrMsg: "failed to stop instance: error stopping test-instance",
|
||||
},
|
||||
{
|
||||
name: "Invalid command",
|
||||
providerEnv: Environment{
|
||||
Command: "invalid-command",
|
||||
},
|
||||
providerInstance: params.ProviderInstance{
|
||||
Name: "test-instance",
|
||||
OSType: params.Linux,
|
||||
},
|
||||
providerErr: nil,
|
||||
expectedErrMsg: "invalid command: invalid-command",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
testExternalProvider := testExternalProvider{
|
||||
mockErr: tc.providerErr,
|
||||
mockInstance: tc.providerInstance,
|
||||
}
|
||||
|
||||
out, err := Run(context.Background(), &testExternalProvider, tc.providerEnv)
|
||||
|
||||
if tc.expectedErrMsg == "" {
|
||||
require.NoError(t, err)
|
||||
expectedJs, marshalErr := json.Marshal(tc.providerInstance)
|
||||
require.NoError(t, marshalErr)
|
||||
require.Equal(t, string(expectedJs), out)
|
||||
} else {
|
||||
require.Equal(t, err.Error(), tc.expectedErrMsg)
|
||||
require.Equal(t, "", out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEnvironment(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
stdinData string
|
||||
envData map[string]string
|
||||
errString string
|
||||
}{
|
||||
{
|
||||
name: "The environment is valid",
|
||||
stdinData: `{"name": "test"}`,
|
||||
errString: "",
|
||||
},
|
||||
{
|
||||
name: "Data is missing from stdin",
|
||||
stdinData: ``,
|
||||
errString: "CreateInstance requires data passed into stdin",
|
||||
},
|
||||
{
|
||||
name: "Invalid JSON",
|
||||
stdinData: `bogus`,
|
||||
errString: "failed to decode instance params: invalid character 'b' looking for beginning of value",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
// Create a temporary file
|
||||
tmpfile, err := os.CreateTemp("", "test-get-env")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// clean up the temporary file
|
||||
t.Cleanup(func() { os.RemoveAll(tmpfile.Name()) })
|
||||
|
||||
// Write some test data to the temporary file
|
||||
if _, err := tmpfile.Write([]byte(tc.stdinData)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Rewind the temporary file to the beginning
|
||||
if _, err := tmpfile.Seek(0, 0); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Clean up the temporary file
|
||||
t.Cleanup(func() { os.RemoveAll(tmpfile.Name()) })
|
||||
|
||||
oldStdin := os.Stdin
|
||||
defer func() { os.Stdin = oldStdin }() // Restore original Stdin
|
||||
|
||||
os.Stdin = tmpfile // mock os.Stdin
|
||||
|
||||
for key, value := range tc.envData {
|
||||
os.Setenv(key, value)
|
||||
}
|
||||
|
||||
// Define the environment variables
|
||||
os.Setenv("GARM_COMMAND", "CreateInstance")
|
||||
os.Setenv("GARM_CONTROLLER_ID", "test-controller-id")
|
||||
os.Setenv("GARM_POOL_ID", "test-pool-id")
|
||||
os.Setenv("GARM_PROVIDER_CONFIG_FILE", tmpfile.Name())
|
||||
|
||||
// Clean up the environment variables
|
||||
t.Cleanup(func() {
|
||||
for key := range tc.envData {
|
||||
os.Unsetenv(key)
|
||||
}
|
||||
})
|
||||
|
||||
env, err := GetEnvironment()
|
||||
if tc.errString == "" {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, CreateInstanceCommand, env.Command)
|
||||
} else {
|
||||
require.Equal(t, tc.errString, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEnvValidateFailed(t *testing.T) {
|
||||
// Create a temporary file
|
||||
tmpfile, err := os.CreateTemp("", "test-get-env")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// clean up the temporary file
|
||||
t.Cleanup(func() { os.RemoveAll(tmpfile.Name()) })
|
||||
|
||||
os.Setenv("GARM_COMMAND", "unknown-command")
|
||||
os.Setenv("GARM_CONTROLLER_ID", "test-controller-id")
|
||||
os.Setenv("GARM_POOL_ID", "test-pool-id")
|
||||
os.Setenv("GARM_PROVIDER_CONFIG_FILE", tmpfile.Name())
|
||||
|
||||
// Clean up the environment variables
|
||||
t.Cleanup(func() {
|
||||
os.Unsetenv("GARM_COMMAND")
|
||||
os.Unsetenv("GARM_CONTROLLER_ID")
|
||||
os.Unsetenv("GARM_POOL_ID")
|
||||
os.Unsetenv("GARM_PROVIDER_CONFIG_FILE")
|
||||
})
|
||||
|
||||
_, err = GetEnvironment()
|
||||
require.Error(t, err)
|
||||
require.Equal(t, "failed to validate execution environment: unknown GARM_COMMAND: unknown-command", err.Error())
|
||||
}
|
||||
41
vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.1/interface.go
generated
vendored
Normal file
41
vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.1/interface.go
generated
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright 2023 Cloudbase Solutions SRL
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
// not use this file except in compliance with the License. You may obtain
|
||||
// a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
// License for the specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package execution
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cloudbase/garm-provider-common/params"
|
||||
)
|
||||
|
||||
// ExternalProvider defines an interface that external providers need to implement.
|
||||
// This is very similar to the common.Provider interface, and was redefined here to
|
||||
// decouple it, in case it may diverge from native providers.
|
||||
type ExternalProvider interface {
|
||||
// CreateInstance creates a new compute instance in the provider.
|
||||
CreateInstance(ctx context.Context, bootstrapParams params.BootstrapInstance) (params.ProviderInstance, error)
|
||||
// Delete instance will delete the instance in a provider.
|
||||
DeleteInstance(ctx context.Context, instance string) error
|
||||
// GetInstance will return details about one instance.
|
||||
GetInstance(ctx context.Context, instance string) (params.ProviderInstance, error)
|
||||
// ListInstances will list all instances for a provider.
|
||||
ListInstances(ctx context.Context, poolID string) ([]params.ProviderInstance, error)
|
||||
// RemoveAllInstances will remove all instances created by this provider.
|
||||
RemoveAllInstances(ctx context.Context) error
|
||||
// Stop shuts down the instance.
|
||||
Stop(ctx context.Context, instance string, force bool) error
|
||||
// Start boots up an instance.
|
||||
Start(ctx context.Context, instance string) error
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue