From 510eb8ef37f998c9632e1fc001cc50d78fb3cfcb Mon Sep 17 00:00:00 2001 From: Fabian Fulga Date: Wed, 21 Aug 2024 15:59:54 +0300 Subject: [PATCH] Update garm-provider-common package --- runner/providers/v0.1.0/external.go | 18 +- runner/providers/v0.1.1/external.go | 18 +- .../execution/common/commands.go | 98 +++++ .../execution/common/interface.go | 43 +++ .../commands.go => common/versions.go} | 15 +- .../execution/execution.go | 80 ++++ .../execution/v0.1.0/execution.go | 107 ++---- .../execution/v0.1.0/execution_test.go | 92 ++--- .../execution/v0.1.0/interface.go | 22 +- .../execution/v0.1.1/execution.go | 123 +++--- .../execution/v0.1.1/execution_test.go | 108 +++--- .../execution/v0.1.1/interface.go | 26 +- .../garm-provider-common/params/github.go | 14 + .../params/github_test.go | 186 ++++++++++ .../util/exec/exec_nix_test.go | 49 +++ .../commands.go => util/exec/exec_test.go} | 36 +- .../util/exec/exec_windows_test.go | 57 +++ .../garm-provider-common/util/seal_test.go | 115 ++++++ .../garm-provider-common/util/util_test.go | 351 ++++++++++++++++++ .../util/websocket/reader.go | 184 +++++++++ .../util/websocket/util.go | 37 ++ 21 files changed, 1483 insertions(+), 296 deletions(-) create mode 100644 vendor/github.com/cloudbase/garm-provider-common/execution/common/commands.go create mode 100644 vendor/github.com/cloudbase/garm-provider-common/execution/common/interface.go rename vendor/github.com/cloudbase/garm-provider-common/execution/{v0.1.0/commands.go => common/versions.go} (56%) create mode 100644 vendor/github.com/cloudbase/garm-provider-common/execution/execution.go create mode 100644 vendor/github.com/cloudbase/garm-provider-common/params/github_test.go create mode 100644 vendor/github.com/cloudbase/garm-provider-common/util/exec/exec_nix_test.go rename vendor/github.com/cloudbase/garm-provider-common/{execution/v0.1.1/commands.go => util/exec/exec_test.go} (51%) create mode 100644 vendor/github.com/cloudbase/garm-provider-common/util/exec/exec_windows_test.go create mode 100644 vendor/github.com/cloudbase/garm-provider-common/util/seal_test.go create mode 100644 vendor/github.com/cloudbase/garm-provider-common/util/util_test.go create mode 100644 vendor/github.com/cloudbase/garm-provider-common/util/websocket/reader.go create mode 100644 vendor/github.com/cloudbase/garm-provider-common/util/websocket/util.go diff --git a/runner/providers/v0.1.0/external.go b/runner/providers/v0.1.0/external.go index 5f47002e..704c414d 100644 --- a/runner/providers/v0.1.0/external.go +++ b/runner/providers/v0.1.0/external.go @@ -10,7 +10,7 @@ import ( "github.com/pkg/errors" garmErrors "github.com/cloudbase/garm-provider-common/errors" - execution "github.com/cloudbase/garm-provider-common/execution/v0.1.0" + commonExecution "github.com/cloudbase/garm-provider-common/execution/common" commonParams "github.com/cloudbase/garm-provider-common/params" garmExec "github.com/cloudbase/garm-provider-common/util/exec" "github.com/cloudbase/garm/config" @@ -76,7 +76,7 @@ func (e *external) validateResult(inst commonParams.ProviderInstance) error { // 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_COMMAND=%s", commonExecution.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), @@ -129,7 +129,7 @@ func (e *external) CreateInstance(ctx context.Context, bootstrapParams commonPar // 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_COMMAND=%s", commonExecution.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), @@ -143,7 +143,7 @@ func (e *external) DeleteInstance(ctx context.Context, instance string, _ common _, err := garmExec.Exec(ctx, e.execPath, nil, asEnv) if err != nil { var exitErr *exec.ExitError - if !errors.As(err, &exitErr) || exitErr.ExitCode() != execution.ExitCodeNotFound { + if !errors.As(err, &exitErr) || exitErr.ExitCode() != commonExecution.ExitCodeNotFound { metrics.InstanceOperationFailedCount.WithLabelValues( "DeleteInstance", // label: operation e.cfg.Name, // label: provider @@ -157,7 +157,7 @@ func (e *external) DeleteInstance(ctx context.Context, instance string, _ common // 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_COMMAND=%s", commonExecution.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), @@ -203,7 +203,7 @@ func (e *external) GetInstance(ctx context.Context, instance string, _ common.Ge // 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_COMMAND=%s", commonExecution.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), @@ -250,7 +250,7 @@ func (e *external) ListInstances(ctx context.Context, poolID string, _ common.Li // 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_COMMAND=%s", commonExecution.RemoveAllInstancesCommand), fmt.Sprintf("GARM_CONTROLLER_ID=%s", e.controllerID), fmt.Sprintf("GARM_PROVIDER_CONFIG_FILE=%s", e.cfg.External.ConfigFile), } @@ -275,7 +275,7 @@ func (e *external) RemoveAllInstances(ctx context.Context, _ common.RemoveAllIns // 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_COMMAND=%s", commonExecution.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), @@ -300,7 +300,7 @@ func (e *external) Stop(ctx context.Context, instance string, _ common.StopParam // 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_COMMAND=%s", commonExecution.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), diff --git a/runner/providers/v0.1.1/external.go b/runner/providers/v0.1.1/external.go index babc0a65..ad582c42 100644 --- a/runner/providers/v0.1.1/external.go +++ b/runner/providers/v0.1.1/external.go @@ -11,7 +11,7 @@ import ( "github.com/pkg/errors" garmErrors "github.com/cloudbase/garm-provider-common/errors" - execution "github.com/cloudbase/garm-provider-common/execution/v0.1.1" + commonExecution "github.com/cloudbase/garm-provider-common/execution/common" commonParams "github.com/cloudbase/garm-provider-common/params" garmExec "github.com/cloudbase/garm-provider-common/util/exec" "github.com/cloudbase/garm/config" @@ -75,7 +75,7 @@ func (e *external) validateResult(inst commonParams.ProviderInstance) error { // 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_COMMAND=%s", commonExecution.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), @@ -135,7 +135,7 @@ func (e *external) DeleteInstance(ctx context.Context, instance string, deleteIn // 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_COMMAND=%s", commonExecution.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), @@ -151,7 +151,7 @@ func (e *external) DeleteInstance(ctx context.Context, instance string, deleteIn _, err = garmExec.Exec(ctx, e.execPath, nil, asEnv) if err != nil { var exitErr *exec.ExitError - if !errors.As(err, &exitErr) || exitErr.ExitCode() != execution.ExitCodeNotFound { + if !errors.As(err, &exitErr) || exitErr.ExitCode() != commonExecution.ExitCodeNotFound { metrics.InstanceOperationFailedCount.WithLabelValues( "DeleteInstance", // label: operation e.cfg.Name, // label: provider @@ -172,7 +172,7 @@ func (e *external) GetInstance(ctx context.Context, instance string, getInstance // 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_COMMAND=%s", commonExecution.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), @@ -227,7 +227,7 @@ func (e *external) ListInstances(ctx context.Context, poolID string, listInstanc // 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_COMMAND=%s", commonExecution.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), @@ -282,7 +282,7 @@ func (e *external) RemoveAllInstances(ctx context.Context, removeAllInstances co // 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_COMMAND=%s", commonExecution.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), @@ -316,7 +316,7 @@ func (e *external) Stop(ctx context.Context, instance string, stopParams common. // 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_COMMAND=%s", commonExecution.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), @@ -350,7 +350,7 @@ func (e *external) Start(ctx context.Context, instance string, startParams commo // 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_COMMAND=%s", commonExecution.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), diff --git a/vendor/github.com/cloudbase/garm-provider-common/execution/common/commands.go b/vendor/github.com/cloudbase/garm-provider-common/execution/common/commands.go new file mode 100644 index 00000000..c4e3a607 --- /dev/null +++ b/vendor/github.com/cloudbase/garm-provider-common/execution/common/commands.go @@ -0,0 +1,98 @@ +// 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 common + +import ( + "bytes" + "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" +) + +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" + GetVersionCommand ExecutionCommand = "GetVersion" +) + +// V0.1.1 commands +const ( + ValidatePoolInfoCommand ExecutionCommand = "ValidatePoolInfo" + GetConfigJSONSchemaCommand ExecutionCommand = "GetConfigJSONSchema" + GetExtraSpecsJSONSchemaCommand ExecutionCommand = "GetExtraSpecsJSONSchema" +) + +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 GetBoostrapParamsFromStdin(c ExecutionCommand) (params.BootstrapInstance, error) { + var bootstrapParams params.BootstrapInstance + if c == CreateInstanceCommand { + if isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd()) { + return params.BootstrapInstance{}, fmt.Errorf("%s requires data passed into stdin", CreateInstanceCommand) + } + + var data bytes.Buffer + if _, err := io.Copy(&data, os.Stdin); err != nil { + return params.BootstrapInstance{}, fmt.Errorf("failed to copy bootstrap params") + } + + if data.Len() == 0 { + return params.BootstrapInstance{}, fmt.Errorf("%s requires data passed into stdin", CreateInstanceCommand) + } + + if err := json.Unmarshal(data.Bytes(), &bootstrapParams); err != nil { + return params.BootstrapInstance{}, 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("{}")) + } + + return bootstrapParams, nil + } + + // If the command is not CreateInstance, we don't need to read from stdin + return params.BootstrapInstance{}, nil +} + +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 +} diff --git a/vendor/github.com/cloudbase/garm-provider-common/execution/common/interface.go b/vendor/github.com/cloudbase/garm-provider-common/execution/common/interface.go new file mode 100644 index 00000000..d00afe92 --- /dev/null +++ b/vendor/github.com/cloudbase/garm-provider-common/execution/common/interface.go @@ -0,0 +1,43 @@ +// 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 common + +import ( + "context" + + "github.com/cloudbase/garm-provider-common/params" +) + +// ExternalProvider defines a common 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 + // GetVersion returns the version of the provider. + GetVersion(ctx context.Context) string +} diff --git a/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.0/commands.go b/vendor/github.com/cloudbase/garm-provider-common/execution/common/versions.go similarity index 56% rename from vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.0/commands.go rename to vendor/github.com/cloudbase/garm-provider-common/execution/common/versions.go index b7976bfb..ebdbbb8c 100644 --- a/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.0/commands.go +++ b/vendor/github.com/cloudbase/garm-provider-common/execution/common/versions.go @@ -12,16 +12,11 @@ // License for the specific language governing permissions and limitations // under the License. -package execution - -type ExecutionCommand string +package common const ( - CreateInstanceCommand ExecutionCommand = "CreateInstance" - DeleteInstanceCommand ExecutionCommand = "DeleteInstance" - GetInstanceCommand ExecutionCommand = "GetInstance" - ListInstancesCommand ExecutionCommand = "ListInstances" - StartInstanceCommand ExecutionCommand = "StartInstance" - StopInstanceCommand ExecutionCommand = "StopInstance" - RemoveAllInstancesCommand ExecutionCommand = "RemoveAllInstances" + // Version v0.1.0 + Version010 = "v0.1.0" + // Version v0.1.1 + Version011 = "v0.1.1" ) diff --git a/vendor/github.com/cloudbase/garm-provider-common/execution/execution.go b/vendor/github.com/cloudbase/garm-provider-common/execution/execution.go new file mode 100644 index 00000000..a6da5596 --- /dev/null +++ b/vendor/github.com/cloudbase/garm-provider-common/execution/execution.go @@ -0,0 +1,80 @@ +// 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" + "fmt" + "os" + + "github.com/cloudbase/garm-provider-common/execution/common" + executionv010 "github.com/cloudbase/garm-provider-common/execution/v0.1.0" + executionv011 "github.com/cloudbase/garm-provider-common/execution/v0.1.1" +) + +type ExternalProvider interface { + executionv010.ExternalProvider + executionv011.ExternalProvider +} + +type Environment struct { + EnvironmentV010 executionv010.EnvironmentV010 + EnvironmentV011 executionv011.EnvironmentV011 + InterfaceVersion string + ProviderConfigFile string + ControllerID string +} + +func GetEnvironment() (Environment, error) { + interfaceVersion := os.Getenv("GARM_INTERFACE_VERSION") + + switch interfaceVersion { + case common.Version010: + env, err := executionv010.GetEnvironment() + if err != nil { + return Environment{}, err + } + return Environment{ + EnvironmentV010: env, + ProviderConfigFile: env.ProviderConfigFile, + ControllerID: env.ControllerID, + InterfaceVersion: interfaceVersion, + }, nil + case common.Version011: + env, err := executionv011.GetEnvironment() + if err != nil { + return Environment{}, err + } + return Environment{ + EnvironmentV011: env, + ProviderConfigFile: env.ProviderConfigFile, + ControllerID: env.ControllerID, + InterfaceVersion: interfaceVersion, + }, nil + default: + return Environment{}, fmt.Errorf("unsupported interface version: %s", interfaceVersion) + } +} + +func Run(ctx context.Context, provider ExternalProvider, env Environment) (string, error) { + switch env.InterfaceVersion { + case common.Version010: + return executionv010.Run(ctx, provider, env.EnvironmentV010) + case common.Version011: + return executionv011.Run(ctx, provider, env.EnvironmentV011) + default: + return "", fmt.Errorf("unsupported interface version: %s", env.InterfaceVersion) + } +} diff --git a/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.0/execution.go b/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.0/execution.go index 948fe3a7..6b426687 100644 --- a/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.0/execution.go +++ b/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.0/execution.go @@ -12,45 +12,23 @@ // License for the specific language governing permissions and limitations // under the License. -package execution +package executionv010 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" -) + "golang.org/x/mod/semver" -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 + common "github.com/cloudbase/garm-provider-common/execution/common" + "github.com/cloudbase/garm-provider-common/params" ) -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")), +func GetEnvironment() (EnvironmentV010, error) { + env := EnvironmentV010{ + Command: common.ExecutionCommand(os.Getenv("GARM_COMMAND")), ControllerID: os.Getenv("GARM_CONTROLLER_ID"), PoolID: os.Getenv("GARM_POOL_ID"), ProviderConfigFile: os.Getenv("GARM_PROVIDER_CONFIG_FILE"), @@ -59,48 +37,34 @@ func GetEnvironment() (Environment, error) { // 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) - } + boostrapParams, err := common.GetBoostrapParamsFromStdin(env.Command) + if err != nil { + return EnvironmentV010{}, fmt.Errorf("failed to get bootstrap params: %w", err) + } + env.BootstrapParams = boostrapParams - 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 env.InterfaceVersion == "" { + env.InterfaceVersion = common.Version010 } if err := env.Validate(); err != nil { - return Environment{}, fmt.Errorf("failed to validate execution environment: %w", err) + return EnvironmentV010{}, fmt.Errorf("failed to validate execution environment: %w", err) } return env, nil } -type Environment struct { - Command ExecutionCommand +type EnvironmentV010 struct { + Command common.ExecutionCommand ControllerID string PoolID string ProviderConfigFile string InstanceID string + InterfaceVersion string BootstrapParams params.BootstrapInstance } -func (e Environment) Validate() error { +func (e EnvironmentV010) Validate() error { if e.Command == "" { return fmt.Errorf("missing GARM_COMMAND") } @@ -118,7 +82,7 @@ func (e Environment) Validate() error { } switch e.Command { - case CreateInstanceCommand: + case common.CreateInstanceCommand: if e.BootstrapParams.Name == "" { return fmt.Errorf("missing bootstrap params") } @@ -128,29 +92,33 @@ func (e Environment) Validate() error { if e.PoolID == "" { return fmt.Errorf("missing pool ID") } - case DeleteInstanceCommand, GetInstanceCommand, - StartInstanceCommand, StopInstanceCommand: + case common.DeleteInstanceCommand, common.GetInstanceCommand, + common.StartInstanceCommand, common.StopInstanceCommand: if e.InstanceID == "" { return fmt.Errorf("missing instance ID") } - case ListInstancesCommand: + case common.ListInstancesCommand: if e.PoolID == "" { return fmt.Errorf("missing pool ID") } - case RemoveAllInstancesCommand: + case common.RemoveAllInstancesCommand: if e.ControllerID == "" { return fmt.Errorf("missing controller ID") } + case common.GetVersionCommand: + if semver.IsValid(e.InterfaceVersion) { + return fmt.Errorf("invalid interface version: %s", e.InterfaceVersion) + } default: return fmt.Errorf("unknown GARM_COMMAND: %s", e.Command) } return nil } -func Run(ctx context.Context, provider ExternalProvider, env Environment) (string, error) { +func Run(ctx context.Context, provider ExternalProvider, env EnvironmentV010) (string, error) { var ret string switch env.Command { - case CreateInstanceCommand: + case common.CreateInstanceCommand: instance, err := provider.CreateInstance(ctx, env.BootstrapParams) if err != nil { return "", fmt.Errorf("failed to create instance in provider: %w", err) @@ -161,7 +129,7 @@ func Run(ctx context.Context, provider ExternalProvider, env Environment) (strin return "", fmt.Errorf("failed to marshal response: %w", err) } ret = string(asJs) - case GetInstanceCommand: + case common.GetInstanceCommand: instance, err := provider.GetInstance(ctx, env.InstanceID) if err != nil { return "", fmt.Errorf("failed to get instance from provider: %w", err) @@ -171,7 +139,7 @@ func Run(ctx context.Context, provider ExternalProvider, env Environment) (strin return "", fmt.Errorf("failed to marshal response: %w", err) } ret = string(asJs) - case ListInstancesCommand: + case common.ListInstancesCommand: instances, err := provider.ListInstances(ctx, env.PoolID) if err != nil { return "", fmt.Errorf("failed to list instances from provider: %w", err) @@ -181,22 +149,25 @@ func Run(ctx context.Context, provider ExternalProvider, env Environment) (strin return "", fmt.Errorf("failed to marshal response: %w", err) } ret = string(asJs) - case DeleteInstanceCommand: + case common.DeleteInstanceCommand: if err := provider.DeleteInstance(ctx, env.InstanceID); err != nil { return "", fmt.Errorf("failed to delete instance from provider: %w", err) } - case RemoveAllInstancesCommand: + case common.RemoveAllInstancesCommand: if err := provider.RemoveAllInstances(ctx); err != nil { return "", fmt.Errorf("failed to destroy environment: %w", err) } - case StartInstanceCommand: + case common.StartInstanceCommand: if err := provider.Start(ctx, env.InstanceID); err != nil { return "", fmt.Errorf("failed to start instance: %w", err) } - case StopInstanceCommand: + case common.StopInstanceCommand: if err := provider.Stop(ctx, env.InstanceID, true); err != nil { return "", fmt.Errorf("failed to stop instance: %w", err) } + case common.GetVersionCommand: + version := provider.GetVersion(ctx) + ret = string(version) default: return "", fmt.Errorf("invalid command: %s", env.Command) } diff --git a/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.0/execution_test.go b/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.0/execution_test.go index 459f9d8b..e8b7bf98 100644 --- a/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.0/execution_test.go +++ b/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.0/execution_test.go @@ -12,7 +12,7 @@ // License for the specific language governing permissions and limitations // under the License. -package execution +package executionv010 import ( "context" @@ -24,6 +24,7 @@ import ( "testing" gErrors "github.com/cloudbase/garm-provider-common/errors" + common "github.com/cloudbase/garm-provider-common/execution/common" "github.com/cloudbase/garm-provider-common/params" "github.com/stretchr/testify/require" ) @@ -82,6 +83,11 @@ func (p *testExternalProvider) Start(context.Context, string) error { return nil } +func (p *testExternalProvider) GetVersion(context.Context) string { + //TODO: Implement this + return "0.1.0" +} + func TestResolveErrorToExitCode(t *testing.T) { tests := []struct { name string @@ -96,12 +102,12 @@ func TestResolveErrorToExitCode(t *testing.T) { { name: "not found error", err: gErrors.ErrNotFound, - code: ExitCodeNotFound, + code: common.ExitCodeNotFound, }, { name: "duplicate entity error", err: gErrors.ErrDuplicateEntity, - code: ExitCodeDuplicate, + code: common.ExitCodeDuplicate, }, { name: "other error", @@ -112,7 +118,7 @@ func TestResolveErrorToExitCode(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - code := ResolveErrorToExitCode(tc.err) + code := common.ResolveErrorToExitCode(tc.err) require.Equal(t, tc.code, code) }) } @@ -129,13 +135,13 @@ func TestValidateEnvironment(t *testing.T) { tests := []struct { name string - env Environment + env EnvironmentV010 errString string }{ { name: "valid environment", - env: Environment{ - Command: CreateInstanceCommand, + env: EnvironmentV010{ + Command: common.CreateInstanceCommand, ControllerID: "controller-id", PoolID: "pool-id", ProviderConfigFile: tmpfile.Name(), @@ -148,31 +154,31 @@ func TestValidateEnvironment(t *testing.T) { }, { name: "invalid command", - env: Environment{ + env: EnvironmentV010{ Command: "", }, errString: "missing GARM_COMMAND", }, { name: "invalid provider config file", - env: Environment{ - Command: CreateInstanceCommand, + env: EnvironmentV010{ + Command: common.CreateInstanceCommand, ProviderConfigFile: "", }, errString: "missing GARM_PROVIDER_CONFIG_FILE", }, { name: "error accessing config file", - env: Environment{ - Command: CreateInstanceCommand, + env: EnvironmentV010{ + Command: common.CreateInstanceCommand, ProviderConfigFile: "invalid-file", }, errString: "error accessing config file", }, { name: "invalid controller ID", - env: Environment{ - Command: CreateInstanceCommand, + env: EnvironmentV010{ + Command: common.CreateInstanceCommand, ProviderConfigFile: tmpfile.Name(), }, errString: "missing GARM_CONTROLLER_ID", @@ -180,8 +186,8 @@ func TestValidateEnvironment(t *testing.T) { { name: "invalid instance ID", - env: Environment{ - Command: DeleteInstanceCommand, + env: EnvironmentV010{ + Command: common.DeleteInstanceCommand, ProviderConfigFile: tmpfile.Name(), ControllerID: "controller-id", InstanceID: "", @@ -190,8 +196,8 @@ func TestValidateEnvironment(t *testing.T) { }, { name: "invalid pool ID", - env: Environment{ - Command: ListInstancesCommand, + env: EnvironmentV010{ + Command: common.ListInstancesCommand, ProviderConfigFile: tmpfile.Name(), ControllerID: "controller-id", PoolID: "", @@ -200,8 +206,8 @@ func TestValidateEnvironment(t *testing.T) { }, { name: "invalid bootstrap params", - env: Environment{ - Command: CreateInstanceCommand, + env: EnvironmentV010{ + Command: common.CreateInstanceCommand, ProviderConfigFile: tmpfile.Name(), ControllerID: "controller-id", PoolID: "pool-id", @@ -211,8 +217,8 @@ func TestValidateEnvironment(t *testing.T) { }, { name: "missing pool ID", - env: Environment{ - Command: CreateInstanceCommand, + env: EnvironmentV010{ + Command: common.CreateInstanceCommand, ProviderConfigFile: tmpfile.Name(), ControllerID: "controller-id", PoolID: "", @@ -224,7 +230,7 @@ func TestValidateEnvironment(t *testing.T) { }, { name: "unknown command", - env: Environment{ + env: EnvironmentV010{ Command: "unknown-command", ProviderConfigFile: tmpfile.Name(), ControllerID: "controller-id", @@ -253,15 +259,15 @@ func TestValidateEnvironment(t *testing.T) { func TestRun(t *testing.T) { tests := []struct { name string - providerEnv Environment + providerEnv EnvironmentV010 providerInstance params.ProviderInstance providerErr error expectedErrMsg string }{ { name: "Valid environment", - providerEnv: Environment{ - Command: CreateInstanceCommand, + providerEnv: EnvironmentV010{ + Command: common.CreateInstanceCommand, }, providerInstance: params.ProviderInstance{ Name: "test-instance", @@ -272,8 +278,8 @@ func TestRun(t *testing.T) { }, { name: "Failed to create instance", - providerEnv: Environment{ - Command: CreateInstanceCommand, + providerEnv: EnvironmentV010{ + Command: common.CreateInstanceCommand, }, providerInstance: params.ProviderInstance{ Name: "test-instance", @@ -284,8 +290,8 @@ func TestRun(t *testing.T) { }, { name: "Failed to get instance", - providerEnv: Environment{ - Command: GetInstanceCommand, + providerEnv: EnvironmentV010{ + Command: common.GetInstanceCommand, }, providerInstance: params.ProviderInstance{ Name: "test-instance", @@ -296,8 +302,8 @@ func TestRun(t *testing.T) { }, { name: "Failed to list instances", - providerEnv: Environment{ - Command: ListInstancesCommand, + providerEnv: EnvironmentV010{ + Command: common.ListInstancesCommand, }, providerInstance: params.ProviderInstance{ Name: "test-instance", @@ -308,8 +314,8 @@ func TestRun(t *testing.T) { }, { name: "Failed to delete instance", - providerEnv: Environment{ - Command: DeleteInstanceCommand, + providerEnv: EnvironmentV010{ + Command: common.DeleteInstanceCommand, }, providerInstance: params.ProviderInstance{ Name: "test-instance", @@ -320,8 +326,8 @@ func TestRun(t *testing.T) { }, { name: "Failed to remove all instances", - providerEnv: Environment{ - Command: RemoveAllInstancesCommand, + providerEnv: EnvironmentV010{ + Command: common.RemoveAllInstancesCommand, }, providerInstance: params.ProviderInstance{ Name: "test-instance", @@ -332,8 +338,8 @@ func TestRun(t *testing.T) { }, { name: "Failed to start instance", - providerEnv: Environment{ - Command: StartInstanceCommand, + providerEnv: EnvironmentV010{ + Command: common.StartInstanceCommand, }, providerInstance: params.ProviderInstance{ Name: "test-instance", @@ -344,8 +350,8 @@ func TestRun(t *testing.T) { }, { name: "Failed to stop instance", - providerEnv: Environment{ - Command: StopInstanceCommand, + providerEnv: EnvironmentV010{ + Command: common.StopInstanceCommand, }, providerInstance: params.ProviderInstance{ Name: "test-instance", @@ -356,7 +362,7 @@ func TestRun(t *testing.T) { }, { name: "Invalid command", - providerEnv: Environment{ + providerEnv: EnvironmentV010{ Command: "invalid-command", }, providerInstance: params.ProviderInstance{ @@ -459,9 +465,9 @@ func TestGetEnvironment(t *testing.T) { env, err := GetEnvironment() if tc.errString == "" { require.NoError(t, err) - require.Equal(t, CreateInstanceCommand, env.Command) + require.Equal(t, common.CreateInstanceCommand, env.Command) } else { - require.Equal(t, tc.errString, err.Error()) + require.ErrorContains(t, err, tc.errString) } } } diff --git a/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.0/interface.go b/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.0/interface.go index 24e39e09..7b18b2b4 100644 --- a/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.0/interface.go +++ b/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.0/interface.go @@ -12,30 +12,16 @@ // License for the specific language governing permissions and limitations // under the License. -package execution +package executionv010 import ( - "context" - - "github.com/cloudbase/garm-provider-common/params" + common "github.com/cloudbase/garm-provider-common/execution/common" ) // 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 + // The common ExternalProvider interface + common.ExternalProvider } diff --git a/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.1/execution.go b/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.1/execution.go index 42947575..9dcc4ad7 100644 --- a/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.1/execution.go +++ b/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.1/execution.go @@ -12,45 +12,23 @@ // License for the specific language governing permissions and limitations // under the License. -package execution +package executionv011 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" -) + "golang.org/x/mod/semver" -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 + common "github.com/cloudbase/garm-provider-common/execution/common" + "github.com/cloudbase/garm-provider-common/params" ) -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")), +func GetEnvironment() (EnvironmentV011, error) { + env := EnvironmentV011{ + Command: common.ExecutionCommand(os.Getenv("GARM_COMMAND")), ControllerID: os.Getenv("GARM_CONTROLLER_ID"), PoolID: os.Getenv("GARM_POOL_ID"), ProviderConfigFile: os.Getenv("GARM_PROVIDER_CONFIG_FILE"), @@ -61,40 +39,25 @@ func GetEnvironment() (Environment, error) { // 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) - } + boostrapParams, err := common.GetBoostrapParamsFromStdin(env.Command) + if err != nil { + return EnvironmentV011{}, fmt.Errorf("failed to get bootstrap params: %w", err) + } + env.BootstrapParams = boostrapParams - 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 env.InterfaceVersion == "" { + env.InterfaceVersion = common.Version010 } if err := env.Validate(); err != nil { - return Environment{}, fmt.Errorf("failed to validate execution environment: %w", err) + return EnvironmentV011{}, fmt.Errorf("failed to validate execution environment: %w", err) } return env, nil } -type Environment struct { - Command ExecutionCommand +type EnvironmentV011 struct { + Command common.ExecutionCommand ControllerID string PoolID string ProviderConfigFile string @@ -104,7 +67,7 @@ type Environment struct { BootstrapParams params.BootstrapInstance } -func (e Environment) Validate() error { +func (e EnvironmentV011) Validate() error { if e.Command == "" { return fmt.Errorf("missing GARM_COMMAND") } @@ -122,7 +85,7 @@ func (e Environment) Validate() error { } switch e.Command { - case CreateInstanceCommand: + case common.CreateInstanceCommand: if e.BootstrapParams.Name == "" { return fmt.Errorf("missing bootstrap params") } @@ -132,33 +95,36 @@ func (e Environment) Validate() error { if e.PoolID == "" { return fmt.Errorf("missing pool ID") } - case DeleteInstanceCommand, GetInstanceCommand, - StartInstanceCommand, StopInstanceCommand: + case common.DeleteInstanceCommand, common.GetInstanceCommand, + common.StartInstanceCommand, common.StopInstanceCommand: if e.InstanceID == "" { return fmt.Errorf("missing instance ID") } if e.PoolID == "" { return fmt.Errorf("missing pool ID") } - case ListInstancesCommand: + case common.ListInstancesCommand: if e.PoolID == "" { return fmt.Errorf("missing pool ID") } - - case RemoveAllInstancesCommand: + case common.RemoveAllInstancesCommand: if e.ControllerID == "" { return fmt.Errorf("missing controller ID") } + case common.GetVersionCommand: + if semver.IsValid(e.InterfaceVersion) { + return fmt.Errorf("invalid interface version: %s", e.InterfaceVersion) + } default: return fmt.Errorf("unknown GARM_COMMAND: %s", e.Command) } return nil } -func Run(ctx context.Context, provider ExternalProvider, env Environment) (string, error) { +func Run(ctx context.Context, provider ExternalProvider, env EnvironmentV011) (string, error) { var ret string switch env.Command { - case CreateInstanceCommand: + case common.CreateInstanceCommand: instance, err := provider.CreateInstance(ctx, env.BootstrapParams) if err != nil { return "", fmt.Errorf("failed to create instance in provider: %w", err) @@ -169,7 +135,7 @@ func Run(ctx context.Context, provider ExternalProvider, env Environment) (strin return "", fmt.Errorf("failed to marshal response: %w", err) } ret = string(asJs) - case GetInstanceCommand: + case common.GetInstanceCommand: instance, err := provider.GetInstance(ctx, env.InstanceID) if err != nil { return "", fmt.Errorf("failed to get instance from provider: %w", err) @@ -179,7 +145,7 @@ func Run(ctx context.Context, provider ExternalProvider, env Environment) (strin return "", fmt.Errorf("failed to marshal response: %w", err) } ret = string(asJs) - case ListInstancesCommand: + case common.ListInstancesCommand: instances, err := provider.ListInstances(ctx, env.PoolID) if err != nil { return "", fmt.Errorf("failed to list instances from provider: %w", err) @@ -189,22 +155,41 @@ func Run(ctx context.Context, provider ExternalProvider, env Environment) (strin return "", fmt.Errorf("failed to marshal response: %w", err) } ret = string(asJs) - case DeleteInstanceCommand: + case common.DeleteInstanceCommand: if err := provider.DeleteInstance(ctx, env.InstanceID); err != nil { return "", fmt.Errorf("failed to delete instance from provider: %w", err) } - case RemoveAllInstancesCommand: + case common.RemoveAllInstancesCommand: if err := provider.RemoveAllInstances(ctx); err != nil { return "", fmt.Errorf("failed to destroy environment: %w", err) } - case StartInstanceCommand: + case common.StartInstanceCommand: if err := provider.Start(ctx, env.InstanceID); err != nil { return "", fmt.Errorf("failed to start instance: %w", err) } - case StopInstanceCommand: + case common.StopInstanceCommand: if err := provider.Stop(ctx, env.InstanceID, true); err != nil { return "", fmt.Errorf("failed to stop instance: %w", err) } + case common.GetVersionCommand: + version := provider.GetVersion(ctx) + ret = string(version) + case common.ValidatePoolInfoCommand: + if err := provider.ValidatePoolInfo(ctx, env.BootstrapParams.Image, env.BootstrapParams.Flavor, env.ProviderConfigFile, env.ExtraSpecs); err != nil { + return "", fmt.Errorf("failed to validate pool info: %w", err) + } + case common.GetConfigJSONSchemaCommand: + schema, err := provider.GetConfigJSONSchema(ctx) + if err != nil { + return "", fmt.Errorf("failed to get config JSON schema: %w", err) + } + ret = schema + case common.GetExtraSpecsJSONSchemaCommand: + schema, err := provider.GetExtraSpecsJSONSchema(ctx) + if err != nil { + return "", fmt.Errorf("failed to get extra specs JSON schema: %w", err) + } + ret = schema default: return "", fmt.Errorf("invalid command: %s", env.Command) } diff --git a/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.1/execution_test.go b/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.1/execution_test.go index 459f9d8b..2d0f5e6a 100644 --- a/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.1/execution_test.go +++ b/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.1/execution_test.go @@ -12,7 +12,7 @@ // License for the specific language governing permissions and limitations // under the License. -package execution +package executionv011 import ( "context" @@ -24,6 +24,7 @@ import ( "testing" gErrors "github.com/cloudbase/garm-provider-common/errors" + common "github.com/cloudbase/garm-provider-common/execution/common" "github.com/cloudbase/garm-provider-common/params" "github.com/stretchr/testify/require" ) @@ -82,6 +83,26 @@ func (p *testExternalProvider) Start(context.Context, string) error { return nil } +func (p *testExternalProvider) GetVersion(context.Context) string { + //TODO: implement + return "v0.1.1" +} + +func (p *testExternalProvider) ValidatePoolInfo(context.Context, string, string, string, string) error { + //TODO: implement + return nil +} + +func (p *testExternalProvider) GetConfigJSONSchema(context.Context) (string, error) { + //TODO: implement + return "", nil +} + +func (p *testExternalProvider) GetExtraSpecsJSONSchema(context.Context) (string, error) { + //TODO: implement + return "", nil +} + func TestResolveErrorToExitCode(t *testing.T) { tests := []struct { name string @@ -96,12 +117,12 @@ func TestResolveErrorToExitCode(t *testing.T) { { name: "not found error", err: gErrors.ErrNotFound, - code: ExitCodeNotFound, + code: common.ExitCodeNotFound, }, { name: "duplicate entity error", err: gErrors.ErrDuplicateEntity, - code: ExitCodeDuplicate, + code: common.ExitCodeDuplicate, }, { name: "other error", @@ -112,7 +133,7 @@ func TestResolveErrorToExitCode(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - code := ResolveErrorToExitCode(tc.err) + code := common.ResolveErrorToExitCode(tc.err) require.Equal(t, tc.code, code) }) } @@ -129,17 +150,18 @@ func TestValidateEnvironment(t *testing.T) { tests := []struct { name string - env Environment + env EnvironmentV011 errString string }{ { name: "valid environment", - env: Environment{ - Command: CreateInstanceCommand, + env: EnvironmentV011{ + Command: common.CreateInstanceCommand, ControllerID: "controller-id", PoolID: "pool-id", ProviderConfigFile: tmpfile.Name(), InstanceID: "instance-id", + InterfaceVersion: "v0.1.1", BootstrapParams: params.BootstrapInstance{ Name: "instance-name", }, @@ -148,31 +170,31 @@ func TestValidateEnvironment(t *testing.T) { }, { name: "invalid command", - env: Environment{ + env: EnvironmentV011{ Command: "", }, errString: "missing GARM_COMMAND", }, { name: "invalid provider config file", - env: Environment{ - Command: CreateInstanceCommand, + env: EnvironmentV011{ + Command: common.CreateInstanceCommand, ProviderConfigFile: "", }, errString: "missing GARM_PROVIDER_CONFIG_FILE", }, { name: "error accessing config file", - env: Environment{ - Command: CreateInstanceCommand, + env: EnvironmentV011{ + Command: common.CreateInstanceCommand, ProviderConfigFile: "invalid-file", }, errString: "error accessing config file", }, { name: "invalid controller ID", - env: Environment{ - Command: CreateInstanceCommand, + env: EnvironmentV011{ + Command: common.CreateInstanceCommand, ProviderConfigFile: tmpfile.Name(), }, errString: "missing GARM_CONTROLLER_ID", @@ -180,8 +202,8 @@ func TestValidateEnvironment(t *testing.T) { { name: "invalid instance ID", - env: Environment{ - Command: DeleteInstanceCommand, + env: EnvironmentV011{ + Command: common.DeleteInstanceCommand, ProviderConfigFile: tmpfile.Name(), ControllerID: "controller-id", InstanceID: "", @@ -190,8 +212,8 @@ func TestValidateEnvironment(t *testing.T) { }, { name: "invalid pool ID", - env: Environment{ - Command: ListInstancesCommand, + env: EnvironmentV011{ + Command: common.ListInstancesCommand, ProviderConfigFile: tmpfile.Name(), ControllerID: "controller-id", PoolID: "", @@ -200,8 +222,8 @@ func TestValidateEnvironment(t *testing.T) { }, { name: "invalid bootstrap params", - env: Environment{ - Command: CreateInstanceCommand, + env: EnvironmentV011{ + Command: common.CreateInstanceCommand, ProviderConfigFile: tmpfile.Name(), ControllerID: "controller-id", PoolID: "pool-id", @@ -211,8 +233,8 @@ func TestValidateEnvironment(t *testing.T) { }, { name: "missing pool ID", - env: Environment{ - Command: CreateInstanceCommand, + env: EnvironmentV011{ + Command: common.CreateInstanceCommand, ProviderConfigFile: tmpfile.Name(), ControllerID: "controller-id", PoolID: "", @@ -224,7 +246,7 @@ func TestValidateEnvironment(t *testing.T) { }, { name: "unknown command", - env: Environment{ + env: EnvironmentV011{ Command: "unknown-command", ProviderConfigFile: tmpfile.Name(), ControllerID: "controller-id", @@ -253,15 +275,15 @@ func TestValidateEnvironment(t *testing.T) { func TestRun(t *testing.T) { tests := []struct { name string - providerEnv Environment + providerEnv EnvironmentV011 providerInstance params.ProviderInstance providerErr error expectedErrMsg string }{ { name: "Valid environment", - providerEnv: Environment{ - Command: CreateInstanceCommand, + providerEnv: EnvironmentV011{ + Command: common.CreateInstanceCommand, }, providerInstance: params.ProviderInstance{ Name: "test-instance", @@ -272,8 +294,8 @@ func TestRun(t *testing.T) { }, { name: "Failed to create instance", - providerEnv: Environment{ - Command: CreateInstanceCommand, + providerEnv: EnvironmentV011{ + Command: common.CreateInstanceCommand, }, providerInstance: params.ProviderInstance{ Name: "test-instance", @@ -284,8 +306,8 @@ func TestRun(t *testing.T) { }, { name: "Failed to get instance", - providerEnv: Environment{ - Command: GetInstanceCommand, + providerEnv: EnvironmentV011{ + Command: common.GetInstanceCommand, }, providerInstance: params.ProviderInstance{ Name: "test-instance", @@ -296,8 +318,8 @@ func TestRun(t *testing.T) { }, { name: "Failed to list instances", - providerEnv: Environment{ - Command: ListInstancesCommand, + providerEnv: EnvironmentV011{ + Command: common.ListInstancesCommand, }, providerInstance: params.ProviderInstance{ Name: "test-instance", @@ -308,8 +330,8 @@ func TestRun(t *testing.T) { }, { name: "Failed to delete instance", - providerEnv: Environment{ - Command: DeleteInstanceCommand, + providerEnv: EnvironmentV011{ + Command: common.DeleteInstanceCommand, }, providerInstance: params.ProviderInstance{ Name: "test-instance", @@ -320,8 +342,8 @@ func TestRun(t *testing.T) { }, { name: "Failed to remove all instances", - providerEnv: Environment{ - Command: RemoveAllInstancesCommand, + providerEnv: EnvironmentV011{ + Command: common.RemoveAllInstancesCommand, }, providerInstance: params.ProviderInstance{ Name: "test-instance", @@ -332,8 +354,8 @@ func TestRun(t *testing.T) { }, { name: "Failed to start instance", - providerEnv: Environment{ - Command: StartInstanceCommand, + providerEnv: EnvironmentV011{ + Command: common.StartInstanceCommand, }, providerInstance: params.ProviderInstance{ Name: "test-instance", @@ -344,8 +366,8 @@ func TestRun(t *testing.T) { }, { name: "Failed to stop instance", - providerEnv: Environment{ - Command: StopInstanceCommand, + providerEnv: EnvironmentV011{ + Command: common.StopInstanceCommand, }, providerInstance: params.ProviderInstance{ Name: "test-instance", @@ -356,7 +378,7 @@ func TestRun(t *testing.T) { }, { name: "Invalid command", - providerEnv: Environment{ + providerEnv: EnvironmentV011{ Command: "invalid-command", }, providerInstance: params.ProviderInstance{ @@ -459,9 +481,9 @@ func TestGetEnvironment(t *testing.T) { env, err := GetEnvironment() if tc.errString == "" { require.NoError(t, err) - require.Equal(t, CreateInstanceCommand, env.Command) + require.Equal(t, common.CreateInstanceCommand, env.Command) } else { - require.Equal(t, tc.errString, err.Error()) + require.ErrorContains(t, err, tc.errString) } } } diff --git a/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.1/interface.go b/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.1/interface.go index 24e39e09..0bcc7655 100644 --- a/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.1/interface.go +++ b/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.1/interface.go @@ -12,30 +12,24 @@ // License for the specific language governing permissions and limitations // under the License. -package execution +package executionv011 import ( "context" - "github.com/cloudbase/garm-provider-common/params" + "github.com/cloudbase/garm-provider-common/execution/common" ) // 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 + // The common ExternalProvider interface + common.ExternalProvider + // ValidatePoolInfo will validate the pool info and return an error if it's not valid. + ValidatePoolInfo(ctx context.Context, image string, flavor string, providerConfig string, extraspecs string) error + // GetConfigJSONSchema will return the JSON schema for the provider's configuration. + GetConfigJSONSchema(ctx context.Context) (string, error) + // GetExtraSpecsJSONSchema will return the JSON schema for the provider's extra specs. + GetExtraSpecsJSONSchema(ctx context.Context) (string, error) } diff --git a/vendor/github.com/cloudbase/garm-provider-common/params/github.go b/vendor/github.com/cloudbase/garm-provider-common/params/github.go index 9f64de9d..c3e9a0a4 100644 --- a/vendor/github.com/cloudbase/garm-provider-common/params/github.go +++ b/vendor/github.com/cloudbase/garm-provider-common/params/github.go @@ -1,3 +1,17 @@ +// 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 params // RunnerApplicationDownload represents a binary for the self-hosted runner application that can be downloaded. diff --git a/vendor/github.com/cloudbase/garm-provider-common/params/github_test.go b/vendor/github.com/cloudbase/garm-provider-common/params/github_test.go new file mode 100644 index 00000000..0a20022c --- /dev/null +++ b/vendor/github.com/cloudbase/garm-provider-common/params/github_test.go @@ -0,0 +1,186 @@ +// 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 params + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetArchitecture(t *testing.T) { + architecture := "x86" + tests := []struct { + name string + r *RunnerApplicationDownload + want string + }{ + { + name: "nil RunnerApplicationDownload", + r: nil, + want: "", + }, + { + name: "nil architecture", + r: &RunnerApplicationDownload{}, + want: "", + }, + { + name: "architecture", + r: &RunnerApplicationDownload{ + Architecture: &architecture, + }, + want: architecture, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + arch := tt.r.GetArchitecture() + assert.Equal(t, tt.want, arch) + }) + } +} + +func TestGetFilename(t *testing.T) { + filename := "filename" + tests := []struct { + name string + r *RunnerApplicationDownload + want string + }{ + { + name: "nil RunnerApplicationDownload", + r: nil, + want: "", + }, + { + name: "nil filename", + r: &RunnerApplicationDownload{}, + want: "", + }, + { + name: "filename", + r: &RunnerApplicationDownload{ + Filename: &filename, + }, + want: filename, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filename := tt.r.GetFilename() + assert.Equal(t, tt.want, filename) + }) + } +} + +func TestGetOS(t *testing.T) { + os := "linux" + tests := []struct { + name string + r *RunnerApplicationDownload + want string + }{ + { + name: "nil RunnerApplicationDownload", + r: nil, + want: "", + }, + { + name: "nil os", + r: &RunnerApplicationDownload{}, + want: "", + }, + { + name: "os", + r: &RunnerApplicationDownload{ + OS: &os, + }, + want: os, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os := tt.r.GetOS() + assert.Equal(t, tt.want, os) + }) + } +} + +func TestGetSHA256Checksum(t *testing.T) { + checksum := "test-checksum" + tests := []struct { + name string + r *RunnerApplicationDownload + want string + }{ + { + name: "nil RunnerApplicationDownload", + r: nil, + want: "", + }, + { + name: "nil checksum", + r: &RunnerApplicationDownload{}, + want: "", + }, + { + name: "checksum", + r: &RunnerApplicationDownload{ + SHA256Checksum: &checksum, + }, + want: checksum, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + checksum := tt.r.GetSHA256Checksum() + assert.Equal(t, tt.want, checksum) + }) + } +} + +func TestGetTempDownloadToken(t *testing.T) { + token := "test-token" + tests := []struct { + name string + r *RunnerApplicationDownload + want string + }{ + { + name: "nil RunnerApplicationDownload", + r: nil, + want: "", + }, + { + name: "nil token", + r: &RunnerApplicationDownload{}, + want: "", + }, + { + name: "token", + r: &RunnerApplicationDownload{ + TempDownloadToken: &token, + }, + want: token, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + token := tt.r.GetTempDownloadToken() + assert.Equal(t, tt.want, token) + }) + } +} diff --git a/vendor/github.com/cloudbase/garm-provider-common/util/exec/exec_nix_test.go b/vendor/github.com/cloudbase/garm-provider-common/util/exec/exec_nix_test.go new file mode 100644 index 00000000..6e220f1c --- /dev/null +++ b/vendor/github.com/cloudbase/garm-provider-common/util/exec/exec_nix_test.go @@ -0,0 +1,49 @@ +//go:build !windows +// +build !windows + +// 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 exec + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIsExecutable(t *testing.T) { + // Create a temporary file with executable permission. + err := os.WriteFile("file.sh", []byte(`!#/bin/sh`), 0o755) + if err != nil { + t.Fatalf("failed to create temporary file: %s", err) + } + // Remove the temporary file when the test finishes. + defer os.Remove("file.sh") + + // Test that the function returns true for an executable file. + require.True(t, IsExecutable("file.sh")) + + // Create a temporary file without executable permission. + err = os.WriteFile("file2.txt", []byte(`!#/bin/sh`), 0o644) + if err != nil { + t.Fatalf("failed to create temporary file: %s", err) + } + // Remove the temporary file when the test finishes. + defer os.Remove("file2.txt") + + // Test that the function returns false for a non-executable file. + require.False(t, IsExecutable("file2.txt")) +} diff --git a/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.1/commands.go b/vendor/github.com/cloudbase/garm-provider-common/util/exec/exec_test.go similarity index 51% rename from vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.1/commands.go rename to vendor/github.com/cloudbase/garm-provider-common/util/exec/exec_test.go index 2b42efc8..1ee894f2 100644 --- a/vendor/github.com/cloudbase/garm-provider-common/execution/v0.1.1/commands.go +++ b/vendor/github.com/cloudbase/garm-provider-common/util/exec/exec_test.go @@ -12,17 +12,31 @@ // License for the specific language governing permissions and limitations // under the License. -package execution +package exec -type ExecutionCommand string +import ( + "context" + "testing" -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" + "github.com/stretchr/testify/require" ) + +func TestExecSuccess(t *testing.T) { + ctx := context.Background() + providerBin := "gofmt" + stdinData := []byte("") + environ := []string{"TEST=1"} + + _, err := Exec(ctx, providerBin, stdinData, environ) + require.NoError(t, err) +} + +func TestExecFail(t *testing.T) { + ctx := context.Background() + providerBin := "garm-provider-test" + stdinData := []byte("test") + environ := []string{"TEST=1"} + + _, err := Exec(ctx, providerBin, stdinData, environ) + require.ErrorContains(t, err, "provider binary failed with stdout") +} diff --git a/vendor/github.com/cloudbase/garm-provider-common/util/exec/exec_windows_test.go b/vendor/github.com/cloudbase/garm-provider-common/util/exec/exec_windows_test.go new file mode 100644 index 00000000..f4425fc2 --- /dev/null +++ b/vendor/github.com/cloudbase/garm-provider-common/util/exec/exec_windows_test.go @@ -0,0 +1,57 @@ +// 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 exec + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIsExecutable(t *testing.T) { + tests := []struct { + path string + pathExt string + executable bool + }{ + { + path: "file.exe", + pathExt: ".exe", + executable: true, + }, + { + path: "file.bat", + pathExt: ".exe;.bat", + executable: true, + }, + { + path: "file.sh", + pathExt: ".exe;.bat", + executable: false, + }, + { + path: "file", + pathExt: ".exe;.bat", + executable: false, + }, + } + + for _, test := range tests { + os.Setenv("PATHEXT", test.pathExt) + executable := IsExecutable(test.path) + require.Equal(t, test.executable, executable) + } +} diff --git a/vendor/github.com/cloudbase/garm-provider-common/util/seal_test.go b/vendor/github.com/cloudbase/garm-provider-common/util/seal_test.go new file mode 100644 index 00000000..a40d9e9f --- /dev/null +++ b/vendor/github.com/cloudbase/garm-provider-common/util/seal_test.go @@ -0,0 +1,115 @@ +// 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 util + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +var ( + EncryptionPassphrase = "bocyasicgatEtenOubwonIbsudNutDom" + WeakEncryptionPassphrase = "test123" +) + +func TestSealUnseal(t *testing.T) { + data, err := Seal([]byte("test"), []byte(EncryptionPassphrase)) + require.NoError(t, err) + + // Data should unmarshal in an Envelope + var envelope Envelope + err = json.Unmarshal(data, &envelope) + require.NoError(t, err) + + // Should be able to decrypt + decrypted, err := Unseal(data, []byte(EncryptionPassphrase)) + require.NoError(t, err) + require.Equal(t, []byte("test"), decrypted) +} + +func TestAes256EncodeDecode(t *testing.T) { + // Should be able to encrypt + encrypted, err := Aes256Encode([]byte("test"), EncryptionPassphrase) + require.NoError(t, err) + + // Should be able to decrypt + decrypted, err := Aes256Decode(encrypted, EncryptionPassphrase) + require.NoError(t, err) + require.Equal(t, []byte("test"), decrypted) +} + +func TestAes256EncodeDecodeWeakSecret(t *testing.T) { + _, err := Aes256Encode([]byte("test"), WeakEncryptionPassphrase) + require.NotNil(t, err) + require.EqualError(t, err, "invalid passphrase length (expected length 32 characters)") + + _, err = Aes256Decode([]byte("test"), WeakEncryptionPassphrase) + require.NotNil(t, err) + require.EqualError(t, err, "invalid passphrase length (expected length 32 characters)") +} + +func TestUnsealAes256EncodedData(t *testing.T) { + encrypted, err := Aes256Encode([]byte("test"), EncryptionPassphrase) + require.NoError(t, err) + + // Should be able to decrypt + decrypted, err := Unseal(encrypted, []byte(EncryptionPassphrase)) + require.NoError(t, err) + require.Equal(t, []byte("test"), decrypted) +} + +func TestSealUnsealWeakSecret(t *testing.T) { + _, err := Seal([]byte("test"), []byte(WeakEncryptionPassphrase)) + require.NotNil(t, err) + require.EqualError(t, err, "invalid passphrase length (expected length 32 characters)") + + // The data is irrelevant. We expect to error out on the passphrase length. + _, err = Unseal([]byte("test"), []byte(WeakEncryptionPassphrase)) + require.NotNil(t, err) + require.EqualError(t, err, "invalid passphrase length (expected length 32 characters)") +} + +func TestAes256EncodeDecodeString(t *testing.T) { + encrypted, err := Aes256EncodeString("test", EncryptionPassphrase) + require.NoError(t, err) + + decrypted, err := Aes256DecodeString(encrypted, EncryptionPassphrase) + require.NoError(t, err) + require.Equal(t, "test", decrypted) +} + +func TestAes256EncodeStringWeakSecret(t *testing.T) { + _, err := Aes256EncodeString("test", WeakEncryptionPassphrase) + require.NotNil(t, err) + require.EqualError(t, err, "invalid passphrase length (expected length 32 characters)") +} + +func TestAes256DecodeWrongEncryptedString(t *testing.T) { + _, err := Aes256DecodeString([]byte(""), EncryptionPassphrase) + require.NotNil(t, err) + require.EqualError(t, err, "failed to decrypt text") +} + +func TestAes256DecodeWrongDecryptionPassphrase(t *testing.T) { + encrypted, err := Aes256EncodeString("test", EncryptionPassphrase) + require.NoError(t, err) + + // We pass a wrong decryption passphrase, that it's still 32 characters long. + _, err = Aes256DecodeString(encrypted, "wrong passphrase-1234-1234-12345") + require.NotNil(t, err) + require.EqualError(t, err, "failed to decrypt text") +} diff --git a/vendor/github.com/cloudbase/garm-provider-common/util/util_test.go b/vendor/github.com/cloudbase/garm-provider-common/util/util_test.go new file mode 100644 index 00000000..403da9db --- /dev/null +++ b/vendor/github.com/cloudbase/garm-provider-common/util/util_test.go @@ -0,0 +1,351 @@ +// 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 util + +import ( + "bytes" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path" + "runtime" + "testing" + + runnerErrors "github.com/cloudbase/garm-provider-common/errors" + "github.com/cloudbase/garm-provider-common/params" + "github.com/stretchr/testify/require" + lumberjack "gopkg.in/natefinch/lumberjack.v2" +) + +func TestResolveToGithubArch(t *testing.T) { + ghArch, err := ResolveToGithubArch("amd64") + require.NoError(t, err) + require.Equal(t, "x64", ghArch) +} + +func TestResolveToGithubArchUnknown(t *testing.T) { + arch := "some-unknown-arch" + + _, err := ResolveToGithubArch(arch) + require.Error(t, err) + require.EqualError(t, runnerErrors.NewNotFoundError("arch %s is unknown", arch), err.Error()) +} + +func TestResolveToGithubOSType(t *testing.T) { + ghOSType, err := ResolveToGithubOSType("linux") + require.NoError(t, err) + require.Equal(t, "linux", ghOSType) +} + +func TestResolveToGithubOSTypeUnknown(t *testing.T) { + osType := "some-unknown-os" + + _, err := ResolveToGithubOSType(osType) + require.Error(t, err) + require.EqualError(t, runnerErrors.NewNotFoundError("os %s is unknown", osType), err.Error()) +} + +func TestResolveToGithubTag(t *testing.T) { + ghOSTag, err := ResolveToGithubTag("linux") + require.NoError(t, err) + require.Equal(t, "Linux", ghOSTag) +} + +func TestResolveToGithubTagUnknown(t *testing.T) { + osTag := params.OSType("some-unknown-os") + + _, err := ResolveToGithubTag(osTag) + require.Error(t, err) + require.EqualError(t, runnerErrors.NewNotFoundError("os %s is unknown", osTag), err.Error()) +} + +func TestIsValidEmail(t *testing.T) { + validEmail := "test@example.com" + + isValid := IsValidEmail(validEmail) + require.True(t, isValid) + require.Equal(t, true, isValid) +} + +func TestIsInvalidEmail(t *testing.T) { + validEmail := "invalid-email" + + isValid := IsValidEmail(validEmail) + require.False(t, isValid) + require.Equal(t, false, isValid) +} + +func TestIsAlphanumeric(t *testing.T) { + validString := "test123" + + isValid := IsAlphanumeric(validString) + require.True(t, isValid) + require.Equal(t, true, isValid) +} + +func TestIsAlphanumericInvalid(t *testing.T) { + validString := "test@123" + + isValid := IsAlphanumeric(validString) + require.False(t, isValid) + require.Equal(t, false, isValid) +} + +func TestGetLoggingWriterEmptyLogFile(t *testing.T) { + writer, err := GetLoggingWriter("") + require.NoError(t, err) + require.Equal(t, os.Stdout, writer) +} + +func TestGetLoggingWriterValidLogFile(t *testing.T) { + // Create a temporary directory for log files. + dir, err := os.MkdirTemp("", "garm-log") + if err != nil { + t.Fatalf("failed to create temporary directory: %s", err) + } + // Remove the temporary directory when the test finishes. + t.Cleanup(func() { os.RemoveAll(dir) }) + + // Create a log file path within the temporary directory. + logFile := path.Join(dir, "test.log") + + writer, err := GetLoggingWriter(logFile) + require.NoError(t, err) + + // Assert that the writer is of type *lumberjack.Logger and it has the + // expected settings. + logger, ok := writer.(*lumberjack.Logger) + require.True(t, ok) + require.Equal(t, logFile, logger.Filename) + require.Equal(t, 500, logger.MaxSize) + require.Equal(t, 3, logger.MaxBackups) + require.Equal(t, 28, logger.MaxAge) + require.Equal(t, true, logger.Compress) +} + +func TestGetLoggingWriterFailedToCreateLogFolder(t *testing.T) { + // Add a log file path that includes a directory that does not exist. + var logFile string + if runtime.GOOS == "windows" { + logFile = "T:/test.log" + } else { + logFile = "/non-existent-dir/test.log" + } + + writer, err := GetLoggingWriter(logFile) + require.Equal(t, nil, writer) + require.Error(t, err) + require.EqualError(t, err, "failed to create log folder") +} + +func TestGetLoggingWriterPermisionDenied(t *testing.T) { + // Create a temporary directory for testing + dir, err := os.MkdirTemp("", "test-dir") + if err != nil { + t.Fatalf("failed to create temporary directory: %s", err) + } + // Remove the temporary directory when the test finishes. + t.Cleanup(func() { os.RemoveAll(dir) }) + + // Remove execute permission from the temporary directory + err = os.Chmod(dir, 0644) + if err != nil { + t.Fatalf("failed to remove execute permission from temporary directory: %s", err) + } + + var dirPath string + if runtime.GOOS == "windows" { + dirPath = "T:/non-existent-dir" + } else { + dirPath = "/non-existent-dir/" + } + + _, err = GetLoggingWriter(path.Join(dir, dirPath, "test.log")) + require.Error(t, err) + require.EqualError(t, err, "failed to create log folder") +} + +func TestConvertFileToBase64(t *testing.T) { + // Create a temporary file with some test data to be converted to base64. + err := os.WriteFile("file.txt", []byte("test"), 0o644) + if err != nil { + t.Fatalf("failed to create temporary file: %s", err) + } + // Remove the temporary file when the test finishes. + defer os.Remove("file.txt") + + base64Data, err := ConvertFileToBase64("file.txt") + require.NoError(t, err) + require.Equal(t, "dGVzdA==", base64Data) +} + +func TestConvertFileToBase64FileNotFound(t *testing.T) { + _, err := ConvertFileToBase64("") + require.Error(t, err) + require.ErrorContains(t, err, "reading file") +} + +func TestOSToOSType(t *testing.T) { + osType, err := OSToOSType("windows") + require.NoError(t, err) + require.Equal(t, params.Windows, osType) +} + +func TestOSToOSTypeUnknown(t *testing.T) { + os := "some-unknown-os" + + osType, err := OSToOSType(os) + require.Error(t, err) + require.Equal(t, params.Unknown, osType) + require.EqualError(t, err, fmt.Sprintf("no OS to OS type mapping for %s", os)) +} + +func TestGetTools(t *testing.T) { + ghArch, err := ResolveToGithubArch("amd64") + if err != nil { + t.Fatalf("failed to resolve to github arch: %s", err) + } + + ghOS, err := ResolveToGithubOSType("linux") + if err != nil { + t.Fatalf("failed to resolve to github os type: %s", err) + } + + tools := []params.RunnerApplicationDownload{ + { + OS: &ghOS, + Architecture: &ghArch, + }, + } + + ghTools, err := GetTools("linux", "amd64", tools) + require.NoError(t, err) + require.Equal(t, "linux", *ghTools.OS) + require.Equal(t, "x64", *ghTools.Architecture) +} + +func TestGetToolsUnsupportedOSType(t *testing.T) { + osType := params.OSType("some-unknown-os") + + _, err := GetTools(osType, "amd64", nil) + require.Error(t, err) + require.EqualError(t, err, fmt.Sprintf("unsupported OS type: %s", osType)) +} + +func TestGetToolsUnsupportedOSArch(t *testing.T) { + osArch := params.OSArch("some-unknown-arch") + + _, err := GetTools("linux", osArch, nil) + require.Error(t, err) + require.EqualError(t, err, fmt.Sprintf("unsupported OS arch: %s", osArch)) +} + +func TestGetToolsFailed(t *testing.T) { + osType := params.OSType("linux") + osArch := params.OSArch("amd64") + + _, err := GetTools(osType, osArch, nil) + require.Error(t, err) + require.EqualError(t, err, fmt.Sprintf("failed to find tools for OS %s and arch %s", osType, osArch)) +} + +func TestGetRandomString(t *testing.T) { + randomString, err := GetRandomString(10) + require.NoError(t, err) + require.Equal(t, 10, len(randomString)) +} + +func TestPaswsordToBcrypt(t *testing.T) { + hash, err := PaswsordToBcrypt("random-password") + require.NoError(t, err) + require.Equal(t, 60, len(hash)) +} + +func TestPaswsordToBcryptFailed(t *testing.T) { + // We define a long password that exceeds the maximum allowed length for bcrypt + password := "we-pass-a-password-that-is-more-than-72-bytes-long-which-is-the-maximum-allowed" + + hash, err := PaswsordToBcrypt(password) + require.Error(t, err) + require.Equal(t, "", hash) + require.EqualError(t, err, "failed to hash password") +} + +func TestNewLoggingMiddleware(t *testing.T) { + // Create a buffer to capture log output + var buf bytes.Buffer + + // Create a test handler + testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + // Create a new logging middleware using the test buffer + loggingMiddleware := NewLoggingMiddleware(&buf) + + // Create a test request and response recorder + req := httptest.NewRequest("GET", "/", nil) + w := httptest.NewRecorder() + + // Serve the request through the logging middleware + loggingMiddleware(testHandler).ServeHTTP(w, req) + + // Assert that the request was served successfully and the log output + require.Equal(t, http.StatusOK, w.Code) + require.Contains(t, buf.String(), "GET / HTTP/1.1") +} + +func TestSanitizeLogEntry(t *testing.T) { + sanitizedEntry := SanitizeLogEntry("test\n") + require.Equal(t, "test", sanitizedEntry) +} + +func TestNewID(t *testing.T) { + // Create a new ID + id := NewID() + + // Assert that the ID is 12 characters long and is alphanumeric + require.Equal(t, 12, len(id)) + require.True(t, IsAlphanumeric(id)) +} + +func TestUTF16FromString(t *testing.T) { + utf16String, err := UTF16FromString("test") + require.NoError(t, err) + require.Equal(t, []uint16{116, 101, 115, 116, 0}, utf16String) +} + +func TestUTF16ToString(t *testing.T) { + string := UTF16ToString([]uint16{116, 101, 115, 116, 0}) + require.Equal(t, "test", string) +} + +func TestUint16ToByteArray(t *testing.T) { + byteArray := Uint16ToByteArray([]uint16{116, 101, 115, 116, 0, 0}) + require.Equal(t, []byte{116, 0, 101, 0, 115, 0, 116, 0, 0, 0}, byteArray) +} + +func TestUTF16EncodedByteArrayFromString(t *testing.T) { + utf16EncodedByteArray, err := UTF16EncodedByteArrayFromString("test") + require.NoError(t, err) + require.Equal(t, []byte{116, 0, 101, 0, 115, 0, 116, 0}, utf16EncodedByteArray) +} + +func TestCompressData(t *testing.T) { + compressedData, err := CompressData([]byte("test")) + require.NoError(t, err) + require.Equal(t, []byte{31, 139, 8, 0, 0, 0, 0, 0, 0, 255, 42, 73, 45, 46, 1, 0, 0, 0, 255, 255, 1, 0, 0, 255, 255, 12, 126, 127, 216, 4, 0, 0, 0}, compressedData) +} diff --git a/vendor/github.com/cloudbase/garm-provider-common/util/websocket/reader.go b/vendor/github.com/cloudbase/garm-provider-common/util/websocket/reader.go new file mode 100644 index 00000000..92ed2edf --- /dev/null +++ b/vendor/github.com/cloudbase/garm-provider-common/util/websocket/reader.go @@ -0,0 +1,184 @@ +package websocket + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "net/url" + "sync" + "time" + + "github.com/gorilla/websocket" +) + +const ( + // Time allowed to write a message to the peer. + writeWait = 10 * time.Second + + // Time allowed to read the next pong message from the peer. + pongWait = 60 * time.Second + + // Send pings to peer with this period. Must be less than pongWait. + pingPeriod = (pongWait * 9) / 10 + + // Maximum message size allowed from peer. + maxMessageSize = 16384 // 16 KB +) + +// MessageHandler is a function that processes a message received from a websocket connection. +type MessageHandler func(msgType int, msg []byte) error + +type APIErrorResponse struct { + Error string `json:"error"` + Details string `json:"details"` +} + +// NewReader creates a new websocket reader. The reader will pass on any message it receives to the +// handler function. The handler function should return an error if it fails to process the message. +func NewReader(ctx context.Context, baseURL, pth, token string, handler MessageHandler) (*Reader, error) { + parsedURL, err := url.Parse(baseURL) + if err != nil { + return nil, err + } + + wsScheme := "ws" + if parsedURL.Scheme == "https" { + wsScheme = "wss" + } + u := url.URL{Scheme: wsScheme, Host: parsedURL.Host, Path: pth} + header := http.Header{} + header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + + return &Reader{ + ctx: ctx, + url: u, + header: header, + handler: handler, + done: make(chan struct{}), + }, nil +} + +type Reader struct { + ctx context.Context + url url.URL + header http.Header + + done chan struct{} + running bool + + handler MessageHandler + + conn *websocket.Conn + mux sync.Mutex + writeMux sync.Mutex +} + +func (w *Reader) Stop() { + w.mux.Lock() + defer w.mux.Unlock() + if !w.running { + return + } + w.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + w.conn.Close() + close(w.done) + w.running = false +} + +func (w *Reader) Done() <-chan struct{} { + return w.done +} + +func (w *Reader) WriteMessage(messageType int, data []byte) error { + // The websocket package does not support concurrent writes and panics if it + // detects that one has occurred, so we need to lock the writeMux to prevent + // concurrent writes to the same connection. + w.writeMux.Lock() + defer w.writeMux.Unlock() + if !w.running { + return fmt.Errorf("websocket is not running") + } + if err := w.conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil { + return err + } + return w.conn.WriteMessage(messageType, data) +} + +func (w *Reader) Start() error { + w.mux.Lock() + defer w.mux.Unlock() + if w.running { + return nil + } + + c, response, err := websocket.DefaultDialer.Dial(w.url.String(), w.header) + if err != nil { + var resp APIErrorResponse + var msg string + var status string + if response != nil { + if response.Body != nil { + if err := json.NewDecoder(response.Body).Decode(&resp); err == nil { + msg = resp.Details + } + } + status = response.Status + } + return fmt.Errorf("failed to stream logs: %q %s (%s)", err, msg, status) + } + w.conn = c + w.running = true + go w.loop() + go w.handlerReader() + return nil +} + +func (w *Reader) handlerReader() { + defer w.Stop() + w.writeMux.Lock() + w.conn.SetReadLimit(maxMessageSize) + w.conn.SetReadDeadline(time.Now().Add(pongWait)) + w.conn.SetPongHandler(func(string) error { w.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) + w.writeMux.Unlock() + for { + msgType, message, err := w.conn.ReadMessage() + if err != nil { + if IsErrorOfInterest(err) { + // TODO(gabriel-samfira): we should allow for an error channel that can be used to signal + // the caller that the connection has been closed. + slog.With(slog.Any("error", err)).Error("reading log message") + } + return + } + if w.handler != nil { + if err := w.handler(msgType, message); err != nil { + slog.With(slog.Any("error", err)).Error("handling log message") + } + } + } +} + +func (w *Reader) loop() { + defer w.Stop() + ticker := time.NewTicker(pingPeriod) + defer ticker.Stop() + for { + select { + case <-w.ctx.Done(): + return + case <-w.Done(): + return + case <-ticker.C: + w.writeMux.Lock() + w.conn.SetWriteDeadline(time.Now().Add(writeWait)) + err := w.conn.WriteMessage(websocket.PingMessage, nil) + if err != nil { + w.writeMux.Unlock() + return + } + w.writeMux.Unlock() + } + } +} diff --git a/vendor/github.com/cloudbase/garm-provider-common/util/websocket/util.go b/vendor/github.com/cloudbase/garm-provider-common/util/websocket/util.go new file mode 100644 index 00000000..88c02fa5 --- /dev/null +++ b/vendor/github.com/cloudbase/garm-provider-common/util/websocket/util.go @@ -0,0 +1,37 @@ +package websocket + +import ( + "errors" + "net" + + "github.com/gorilla/websocket" +) + +func IsErrorOfInterest(err error) bool { + if err == nil { + return false + } + + if errors.Is(err, websocket.ErrCloseSent) { + return false + } + + if errors.Is(err, websocket.ErrBadHandshake) { + return false + } + + if errors.Is(err, net.ErrClosed) { + return false + } + + asCloseErr, ok := err.(*websocket.CloseError) + if ok { + switch asCloseErr.Code { + case websocket.CloseNormalClosure, websocket.CloseGoingAway, + websocket.CloseNoStatusReceived, websocket.CloseAbnormalClosure: + return false + } + } + + return true +}