diff --git a/execution/common/commands.go b/execution/common/commands.go new file mode 100644 index 0000000..d95d928 --- /dev/null +++ b/execution/common/commands.go @@ -0,0 +1,91 @@ +// 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" +) + +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/execution/v0.1.0/commands.go b/execution/common/versions.go similarity index 56% rename from execution/v0.1.0/commands.go rename to execution/common/versions.go index b7976bf..ebdbbb8 100644 --- a/execution/v0.1.0/commands.go +++ b/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/execution/execution.go b/execution/execution.go new file mode 100644 index 0000000..a6da559 --- /dev/null +++ b/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/execution/v0.1.0/execution.go b/execution/v0.1.0/execution.go index 948fe3a..ef0e66b 100644 --- a/execution/v0.1.0/execution.go +++ b/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 := env.InterfaceVersion + ret = string(version) default: return "", fmt.Errorf("invalid command: %s", env.Command) } diff --git a/execution/v0.1.0/execution_test.go b/execution/v0.1.0/execution_test.go index 459f9d8..a859b1f 100644 --- a/execution/v0.1.0/execution_test.go +++ b/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,7 +465,7 @@ 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()) } diff --git a/execution/v0.1.0/interface.go b/execution/v0.1.0/interface.go index 24e39e0..670a2fa 100644 --- a/execution/v0.1.0/interface.go +++ b/execution/v0.1.0/interface.go @@ -12,7 +12,7 @@ // License for the specific language governing permissions and limitations // under the License. -package execution +package executionv010 import ( "context" @@ -38,4 +38,6 @@ type ExternalProvider interface { 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/execution/v0.1.1/commands.go b/execution/v0.1.1/commands.go index 2b42efc..c9facc0 100644 --- a/execution/v0.1.1/commands.go +++ b/execution/v0.1.1/commands.go @@ -12,17 +12,14 @@ // License for the specific language governing permissions and limitations // under the License. -package execution +package executionv011 -type ExecutionCommand string +import ( + common "github.com/cloudbase/garm-provider-common/execution/common" +) 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" + ValidatePoolInfoCommand common.ExecutionCommand = "ValidatePoolInfo" + GetConfigJSONSchemaCommand common.ExecutionCommand = "GetConfigJSONSchema" + GetExtraSpecsJSONSchemaCommand common.ExecutionCommand = "GetExtraSpecsJSONSchema" ) diff --git a/execution/v0.1.1/execution.go b/execution/v0.1.1/execution.go index 4294757..d69ef20 100644 --- a/execution/v0.1.1/execution.go +++ b/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 := env.InterfaceVersion + ret = string(version) + case 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 GetConfigJSONSchemaCommand: + schema, err := provider.GetConfigJSONSchema(ctx) + if err != nil { + return "", fmt.Errorf("failed to get config JSON schema: %w", err) + } + ret = schema + case 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/execution/v0.1.1/execution_test.go b/execution/v0.1.1/execution_test.go index de8eb28..f36250e 100644 --- a/execution/v0.1.1/execution_test.go +++ b/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,13 +150,13 @@ 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(), @@ -149,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", @@ -181,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: "", @@ -191,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: "", @@ -201,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", @@ -212,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: "", @@ -225,7 +246,7 @@ func TestValidateEnvironment(t *testing.T) { }, { name: "unknown command", - env: Environment{ + env: EnvironmentV011{ Command: "unknown-command", ProviderConfigFile: tmpfile.Name(), ControllerID: "controller-id", @@ -254,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", @@ -273,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", @@ -285,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", @@ -297,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", @@ -309,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", @@ -321,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", @@ -333,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", @@ -345,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", @@ -357,7 +378,7 @@ func TestRun(t *testing.T) { }, { name: "Invalid command", - providerEnv: Environment{ + providerEnv: EnvironmentV011{ Command: "invalid-command", }, providerInstance: params.ProviderInstance{ @@ -460,7 +481,7 @@ 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()) } diff --git a/execution/v0.1.1/interface.go b/execution/v0.1.1/interface.go index 24e39e0..6d1154a 100644 --- a/execution/v0.1.1/interface.go +++ b/execution/v0.1.1/interface.go @@ -12,7 +12,7 @@ // License for the specific language governing permissions and limitations // under the License. -package execution +package executionv011 import ( "context" @@ -38,4 +38,12 @@ type ExternalProvider interface { 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 + // 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/go.mod b/go.mod index 6be9c44..9101310 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/stretchr/testify v1.8.4 github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569 golang.org/x/crypto v0.12.0 + golang.org/x/mod v0.20.0 golang.org/x/sys v0.11.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index 6a5b072..7cb5ad6 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569 h1:xzABM9let0HLLq github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569/go.mod h1:2Ly+NIftZN4de9zRmENdYbvPQeaVIYKWpLFStLFEBgI= golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/vendor/golang.org/x/mod/LICENSE b/vendor/golang.org/x/mod/LICENSE new file mode 100644 index 0000000..2a7cf70 --- /dev/null +++ b/vendor/golang.org/x/mod/LICENSE @@ -0,0 +1,27 @@ +Copyright 2009 The Go Authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google LLC nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/golang.org/x/mod/PATENTS b/vendor/golang.org/x/mod/PATENTS new file mode 100644 index 0000000..7330990 --- /dev/null +++ b/vendor/golang.org/x/mod/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the Go project. + +Google hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) +patent license to make, have made, use, offer to sell, sell, import, +transfer and otherwise run, modify and propagate the contents of this +implementation of Go, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of Go. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of Go or any code incorporated within this +implementation of Go constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of Go +shall terminate as of the date such litigation is filed. diff --git a/vendor/golang.org/x/mod/semver/semver.go b/vendor/golang.org/x/mod/semver/semver.go new file mode 100644 index 0000000..9a2dfd3 --- /dev/null +++ b/vendor/golang.org/x/mod/semver/semver.go @@ -0,0 +1,401 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package semver implements comparison of semantic version strings. +// In this package, semantic version strings must begin with a leading "v", +// as in "v1.0.0". +// +// The general form of a semantic version string accepted by this package is +// +// vMAJOR[.MINOR[.PATCH[-PRERELEASE][+BUILD]]] +// +// where square brackets indicate optional parts of the syntax; +// MAJOR, MINOR, and PATCH are decimal integers without extra leading zeros; +// PRERELEASE and BUILD are each a series of non-empty dot-separated identifiers +// using only alphanumeric characters and hyphens; and +// all-numeric PRERELEASE identifiers must not have leading zeros. +// +// This package follows Semantic Versioning 2.0.0 (see semver.org) +// with two exceptions. First, it requires the "v" prefix. Second, it recognizes +// vMAJOR and vMAJOR.MINOR (with no prerelease or build suffixes) +// as shorthands for vMAJOR.0.0 and vMAJOR.MINOR.0. +package semver + +import "sort" + +// parsed returns the parsed form of a semantic version string. +type parsed struct { + major string + minor string + patch string + short string + prerelease string + build string +} + +// IsValid reports whether v is a valid semantic version string. +func IsValid(v string) bool { + _, ok := parse(v) + return ok +} + +// Canonical returns the canonical formatting of the semantic version v. +// It fills in any missing .MINOR or .PATCH and discards build metadata. +// Two semantic versions compare equal only if their canonical formattings +// are identical strings. +// The canonical invalid semantic version is the empty string. +func Canonical(v string) string { + p, ok := parse(v) + if !ok { + return "" + } + if p.build != "" { + return v[:len(v)-len(p.build)] + } + if p.short != "" { + return v + p.short + } + return v +} + +// Major returns the major version prefix of the semantic version v. +// For example, Major("v2.1.0") == "v2". +// If v is an invalid semantic version string, Major returns the empty string. +func Major(v string) string { + pv, ok := parse(v) + if !ok { + return "" + } + return v[:1+len(pv.major)] +} + +// MajorMinor returns the major.minor version prefix of the semantic version v. +// For example, MajorMinor("v2.1.0") == "v2.1". +// If v is an invalid semantic version string, MajorMinor returns the empty string. +func MajorMinor(v string) string { + pv, ok := parse(v) + if !ok { + return "" + } + i := 1 + len(pv.major) + if j := i + 1 + len(pv.minor); j <= len(v) && v[i] == '.' && v[i+1:j] == pv.minor { + return v[:j] + } + return v[:i] + "." + pv.minor +} + +// Prerelease returns the prerelease suffix of the semantic version v. +// For example, Prerelease("v2.1.0-pre+meta") == "-pre". +// If v is an invalid semantic version string, Prerelease returns the empty string. +func Prerelease(v string) string { + pv, ok := parse(v) + if !ok { + return "" + } + return pv.prerelease +} + +// Build returns the build suffix of the semantic version v. +// For example, Build("v2.1.0+meta") == "+meta". +// If v is an invalid semantic version string, Build returns the empty string. +func Build(v string) string { + pv, ok := parse(v) + if !ok { + return "" + } + return pv.build +} + +// Compare returns an integer comparing two versions according to +// semantic version precedence. +// The result will be 0 if v == w, -1 if v < w, or +1 if v > w. +// +// An invalid semantic version string is considered less than a valid one. +// All invalid semantic version strings compare equal to each other. +func Compare(v, w string) int { + pv, ok1 := parse(v) + pw, ok2 := parse(w) + if !ok1 && !ok2 { + return 0 + } + if !ok1 { + return -1 + } + if !ok2 { + return +1 + } + if c := compareInt(pv.major, pw.major); c != 0 { + return c + } + if c := compareInt(pv.minor, pw.minor); c != 0 { + return c + } + if c := compareInt(pv.patch, pw.patch); c != 0 { + return c + } + return comparePrerelease(pv.prerelease, pw.prerelease) +} + +// Max canonicalizes its arguments and then returns the version string +// that compares greater. +// +// Deprecated: use [Compare] instead. In most cases, returning a canonicalized +// version is not expected or desired. +func Max(v, w string) string { + v = Canonical(v) + w = Canonical(w) + if Compare(v, w) > 0 { + return v + } + return w +} + +// ByVersion implements [sort.Interface] for sorting semantic version strings. +type ByVersion []string + +func (vs ByVersion) Len() int { return len(vs) } +func (vs ByVersion) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] } +func (vs ByVersion) Less(i, j int) bool { + cmp := Compare(vs[i], vs[j]) + if cmp != 0 { + return cmp < 0 + } + return vs[i] < vs[j] +} + +// Sort sorts a list of semantic version strings using [ByVersion]. +func Sort(list []string) { + sort.Sort(ByVersion(list)) +} + +func parse(v string) (p parsed, ok bool) { + if v == "" || v[0] != 'v' { + return + } + p.major, v, ok = parseInt(v[1:]) + if !ok { + return + } + if v == "" { + p.minor = "0" + p.patch = "0" + p.short = ".0.0" + return + } + if v[0] != '.' { + ok = false + return + } + p.minor, v, ok = parseInt(v[1:]) + if !ok { + return + } + if v == "" { + p.patch = "0" + p.short = ".0" + return + } + if v[0] != '.' { + ok = false + return + } + p.patch, v, ok = parseInt(v[1:]) + if !ok { + return + } + if len(v) > 0 && v[0] == '-' { + p.prerelease, v, ok = parsePrerelease(v) + if !ok { + return + } + } + if len(v) > 0 && v[0] == '+' { + p.build, v, ok = parseBuild(v) + if !ok { + return + } + } + if v != "" { + ok = false + return + } + ok = true + return +} + +func parseInt(v string) (t, rest string, ok bool) { + if v == "" { + return + } + if v[0] < '0' || '9' < v[0] { + return + } + i := 1 + for i < len(v) && '0' <= v[i] && v[i] <= '9' { + i++ + } + if v[0] == '0' && i != 1 { + return + } + return v[:i], v[i:], true +} + +func parsePrerelease(v string) (t, rest string, ok bool) { + // "A pre-release version MAY be denoted by appending a hyphen and + // a series of dot separated identifiers immediately following the patch version. + // Identifiers MUST comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-]. + // Identifiers MUST NOT be empty. Numeric identifiers MUST NOT include leading zeroes." + if v == "" || v[0] != '-' { + return + } + i := 1 + start := 1 + for i < len(v) && v[i] != '+' { + if !isIdentChar(v[i]) && v[i] != '.' { + return + } + if v[i] == '.' { + if start == i || isBadNum(v[start:i]) { + return + } + start = i + 1 + } + i++ + } + if start == i || isBadNum(v[start:i]) { + return + } + return v[:i], v[i:], true +} + +func parseBuild(v string) (t, rest string, ok bool) { + if v == "" || v[0] != '+' { + return + } + i := 1 + start := 1 + for i < len(v) { + if !isIdentChar(v[i]) && v[i] != '.' { + return + } + if v[i] == '.' { + if start == i { + return + } + start = i + 1 + } + i++ + } + if start == i { + return + } + return v[:i], v[i:], true +} + +func isIdentChar(c byte) bool { + return 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' || c == '-' +} + +func isBadNum(v string) bool { + i := 0 + for i < len(v) && '0' <= v[i] && v[i] <= '9' { + i++ + } + return i == len(v) && i > 1 && v[0] == '0' +} + +func isNum(v string) bool { + i := 0 + for i < len(v) && '0' <= v[i] && v[i] <= '9' { + i++ + } + return i == len(v) +} + +func compareInt(x, y string) int { + if x == y { + return 0 + } + if len(x) < len(y) { + return -1 + } + if len(x) > len(y) { + return +1 + } + if x < y { + return -1 + } else { + return +1 + } +} + +func comparePrerelease(x, y string) int { + // "When major, minor, and patch are equal, a pre-release version has + // lower precedence than a normal version. + // Example: 1.0.0-alpha < 1.0.0. + // Precedence for two pre-release versions with the same major, minor, + // and patch version MUST be determined by comparing each dot separated + // identifier from left to right until a difference is found as follows: + // identifiers consisting of only digits are compared numerically and + // identifiers with letters or hyphens are compared lexically in ASCII + // sort order. Numeric identifiers always have lower precedence than + // non-numeric identifiers. A larger set of pre-release fields has a + // higher precedence than a smaller set, if all of the preceding + // identifiers are equal. + // Example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < + // 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0." + if x == y { + return 0 + } + if x == "" { + return +1 + } + if y == "" { + return -1 + } + for x != "" && y != "" { + x = x[1:] // skip - or . + y = y[1:] // skip - or . + var dx, dy string + dx, x = nextIdent(x) + dy, y = nextIdent(y) + if dx != dy { + ix := isNum(dx) + iy := isNum(dy) + if ix != iy { + if ix { + return -1 + } else { + return +1 + } + } + if ix { + if len(dx) < len(dy) { + return -1 + } + if len(dx) > len(dy) { + return +1 + } + } + if dx < dy { + return -1 + } else { + return +1 + } + } + } + if x == "" { + return -1 + } else { + return +1 + } +} + +func nextIdent(x string) (dx, rest string) { + i := 0 + for i < len(x) && x[i] != '.' { + i++ + } + return x[:i], x[i:] +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 43063f2..cef0811 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -38,6 +38,9 @@ golang.org/x/crypto/chacha20poly1305 golang.org/x/crypto/hkdf golang.org/x/crypto/internal/alias golang.org/x/crypto/internal/poly1305 +# golang.org/x/mod v0.20.0 +## explicit; go 1.18 +golang.org/x/mod/semver # golang.org/x/sys v0.11.0 ## explicit; go 1.17 golang.org/x/sys/cpu