Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add
kurtosis cloud load to CLI
(#882)
## Description: In this PR we are adding the `kurtosis cloud load <instance_id>` option that enables the user to load the config from the cloud and automatically switch the context. To use the command the user must export an API key to the env var: `KURTOSIS_CLOUD_API_KEY` ## Is this change user facing? YES ## References (if applicable): <!-- Add relevant Github Issues, Discord threads, or other helpful information. -->
- Loading branch information
1 parent
0f33b5b
commit b2db8c9
Showing
38 changed files
with
1,375 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package cloud | ||
|
||
import ( | ||
"crypto/tls" | ||
"crypto/x509" | ||
api "github.com/kurtosis-tech/kurtosis/cloud/api/golang/kurtosis_backend_server_rpc_api_bindings" | ||
"github.com/kurtosis-tech/stacktrace" | ||
"google.golang.org/grpc" | ||
"google.golang.org/grpc/credentials" | ||
) | ||
|
||
func CreateCloudClient(connectionStr string, caCertChain string) (api.KurtosisCloudBackendServerClient, error) { | ||
caCertChainBytes := []byte(caCertChain) | ||
p := x509.NewCertPool() | ||
p.AppendCertsFromPEM(caCertChainBytes) | ||
|
||
tlsConfig := &tls.Config{ | ||
Rand: nil, | ||
Time: nil, | ||
Certificates: nil, | ||
NameToCertificate: nil, | ||
GetCertificate: nil, | ||
GetClientCertificate: nil, | ||
GetConfigForClient: nil, | ||
VerifyPeerCertificate: nil, | ||
VerifyConnection: nil, | ||
RootCAs: p, | ||
NextProtos: nil, | ||
ServerName: "", | ||
ClientAuth: 0, | ||
ClientCAs: nil, | ||
InsecureSkipVerify: false, | ||
CipherSuites: nil, | ||
PreferServerCipherSuites: false, | ||
SessionTicketsDisabled: false, | ||
SessionTicketKey: [32]byte{}, | ||
ClientSessionCache: nil, | ||
MinVersion: 0, | ||
MaxVersion: 0, | ||
CurvePreferences: nil, | ||
DynamicRecordSizingDisabled: false, | ||
Renegotiation: 0, | ||
KeyLogWriter: nil, | ||
} | ||
conn, err := grpc.Dial(connectionStr, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))) | ||
if err != nil { | ||
return nil, stacktrace.Propagate( | ||
err, | ||
"An error occurred creating a connection to the Kurtosis Cloud server at '%v'", | ||
connectionStr, | ||
) | ||
} | ||
client := api.NewKurtosisCloudBackendServerClient(conn) | ||
return client, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
67 changes: 67 additions & 0 deletions
67
cli/cli/command_framework/highlevel/instance_id_arg/instance_id_arg.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package instance_id_arg | ||
|
||
import ( | ||
"context" | ||
"github.com/kurtosis-tech/kurtosis/cli/cli/command_framework/lowlevel/args" | ||
"github.com/kurtosis-tech/kurtosis/cli/cli/command_framework/lowlevel/flags" | ||
"github.com/kurtosis-tech/stacktrace" | ||
) | ||
|
||
const ( | ||
defaultIsRequired = true | ||
defaultValueEmpty = "" | ||
validInstanceIdLength = 32 | ||
) | ||
|
||
// InstanceIdentifierArg pre-builds instance identifier arg which has tab-completion and validation ready out-of-the-box | ||
func InstanceIdentifierArg( | ||
// The arg key where this context identifier argument will be stored | ||
argKey string, | ||
isGreedy bool, | ||
) *args.ArgConfig { | ||
|
||
validate := getValidationFunc(argKey, isGreedy) | ||
|
||
return &args.ArgConfig{ | ||
Key: argKey, | ||
IsOptional: defaultIsRequired, | ||
DefaultValue: defaultValueEmpty, | ||
IsGreedy: isGreedy, | ||
ValidationFunc: validate, | ||
ArgCompletionProvider: args.NewManualCompletionsProvider(getCompletionsFunc()), | ||
} | ||
} | ||
|
||
func getCompletionsFunc() func(ctx context.Context, flags *flags.ParsedFlags, previousArgs *args.ParsedArgs) ([]string, error) { | ||
return func(ctx context.Context, flags *flags.ParsedFlags, previousArgs *args.ParsedArgs) ([]string, error) { | ||
// TODO: Given the instance id and the API Key, we could potentially query the API for instance ids to | ||
// auto complete the typing but those endpoints don't exist (yet). | ||
return []string{}, nil | ||
} | ||
} | ||
|
||
func getValidationFunc(argKey string, isGreedy bool) func(context.Context, *flags.ParsedFlags, *args.ParsedArgs) error { | ||
return func(ctx context.Context, flags *flags.ParsedFlags, args *args.ParsedArgs) error { | ||
var instanceIdsToValidate []string | ||
if isGreedy { | ||
instanceID, err := args.GetGreedyArg(argKey) | ||
if err != nil { | ||
return stacktrace.Propagate(err, "Expected a value for greedy arg '%v' but didn't find one", argKey) | ||
} | ||
instanceIdsToValidate = instanceID | ||
} else { | ||
instanceID, err := args.GetNonGreedyArg(argKey) | ||
if err != nil { | ||
return stacktrace.Propagate(err, "Expected a value for non-greedy arg '%v' but didn't find one", argKey) | ||
} | ||
instanceIdsToValidate = []string{instanceID} | ||
} | ||
|
||
for _, instanceIdToValidate := range instanceIdsToValidate { | ||
if len(instanceIdToValidate) < validInstanceIdLength { | ||
return stacktrace.NewError("Instance Id is not valid: %s", instanceIdsToValidate) | ||
} | ||
} | ||
return nil | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package cloud | ||
|
||
import ( | ||
"github.com/kurtosis-tech/kurtosis/cli/cli/command_str_consts" | ||
"github.com/kurtosis-tech/kurtosis/cli/cli/commands/cloud/load" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
// CloudCmd Suppressing exhaustruct requirement because this struct has ~40 properties | ||
// nolint: exhaustruct | ||
var CloudCmd = &cobra.Command{ | ||
Use: command_str_consts.CloudCmdStr, | ||
Short: "Manage Kurtosis cloud instances", | ||
RunE: nil, | ||
} | ||
|
||
func init() { | ||
CloudCmd.AddCommand(load.LoadCmd.MustGetCobraCommand()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
package load | ||
|
||
import ( | ||
"context" | ||
"encoding/base64" | ||
"fmt" | ||
"github.com/kurtosis-tech/kurtosis/api/golang/engine/lib/cloud" | ||
"github.com/kurtosis-tech/kurtosis/cli/cli/command_framework/highlevel/instance_id_arg" | ||
"github.com/kurtosis-tech/kurtosis/cli/cli/command_framework/lowlevel" | ||
"github.com/kurtosis-tech/kurtosis/cli/cli/command_framework/lowlevel/args" | ||
"github.com/kurtosis-tech/kurtosis/cli/cli/command_framework/lowlevel/flags" | ||
"github.com/kurtosis-tech/kurtosis/cli/cli/command_str_consts" | ||
"github.com/kurtosis-tech/kurtosis/cli/cli/commands/kurtosis_context/add" | ||
"github.com/kurtosis-tech/kurtosis/cli/cli/commands/kurtosis_context/context_switch" | ||
"github.com/kurtosis-tech/kurtosis/cli/cli/kurtosis_config" | ||
"github.com/kurtosis-tech/kurtosis/cli/cli/kurtosis_config/resolved_config" | ||
api "github.com/kurtosis-tech/kurtosis/cloud/api/golang/kurtosis_backend_server_rpc_api_bindings" | ||
"github.com/kurtosis-tech/kurtosis/contexts-config-store/store" | ||
"github.com/kurtosis-tech/stacktrace" | ||
"github.com/sirupsen/logrus" | ||
"os" | ||
) | ||
|
||
const ( | ||
instanceIdentifierArgKey = "instance-id" | ||
instanceIdentifierArgIsGreedy = false | ||
kurtosisCloudApiKeyEnvVarArg = "KURTOSIS_CLOUD_API_KEY" | ||
) | ||
|
||
var LoadCmd = &lowlevel.LowlevelKurtosisCommand{ | ||
CommandStr: command_str_consts.CloudLoadCmdStr, | ||
ShortDescription: "Load a Kurtosis Cloud instance", | ||
LongDescription: "Load a remote Kurtosis Cloud instance by providing the instance id." + | ||
"Note, the remote instance must be in a running state for this operation to complete successfully", | ||
Flags: []*flags.FlagConfig{}, | ||
Args: []*args.ArgConfig{ | ||
instance_id_arg.InstanceIdentifierArg(instanceIdentifierArgKey, instanceIdentifierArgIsGreedy), | ||
}, | ||
PreValidationAndRunFunc: nil, | ||
RunFunc: run, | ||
PostValidationAndRunFunc: nil, | ||
} | ||
|
||
func run(ctx context.Context, _ *flags.ParsedFlags, args *args.ParsedArgs) error { | ||
instanceID, err := args.GetNonGreedyArg(instanceIdentifierArgKey) | ||
if err != nil { | ||
return stacktrace.Propagate(err, "Expected a value for instance id arg '%v' but none was found; "+ | ||
"this is a bug in the Kurtosis CLI!", instanceIdentifierArgKey) | ||
} | ||
logrus.Infof("Loading cloud instance %s", instanceID) | ||
|
||
apiKey, err := loadApiKey() | ||
if err != nil { | ||
return stacktrace.Propagate(err, "Could not load an API Key. Check that it's defined using the "+ | ||
"%s env var and it's a valid (active) key", kurtosisCloudApiKeyEnvVarArg) | ||
} | ||
|
||
cloudConfig, err := getCloudConfig() | ||
if err != nil { | ||
return stacktrace.Propagate(err, "An error occured while loading the Cloud Config") | ||
} | ||
// Create the connection | ||
connectionStr := fmt.Sprintf("%s:%d", cloudConfig.ApiUrl, cloudConfig.Port) | ||
client, err := cloud.CreateCloudClient(connectionStr, cloudConfig.CertificateChain) | ||
if err != nil { | ||
return stacktrace.Propagate(err, "Error building client for Kurtosis Cloud") | ||
} | ||
|
||
getConfigArgs := &api.GetCloudInstanceConfigArgs{ | ||
ApiKey: *apiKey, | ||
InstanceId: instanceID, | ||
} | ||
result, err := client.GetCloudInstanceConfig(ctx, getConfigArgs) | ||
if err != nil { | ||
return stacktrace.Propagate(err, "An error occurred while calling the Kurtosis Cloud API") | ||
} | ||
decodedConfigBytes, err := base64.StdEncoding.DecodeString(result.ContextConfig) | ||
if err != nil { | ||
return stacktrace.Propagate(err, "Failed to base64 decode context config") | ||
} | ||
|
||
parsedContext, err := add.ParseContextData(decodedConfigBytes) | ||
if err != nil { | ||
return stacktrace.Propagate(err, "Unable to decode context config") | ||
} | ||
|
||
contextsConfigStore := store.GetContextsConfigStore() | ||
// We first have to remove the context incase it's already loaded | ||
err = contextsConfigStore.RemoveContext(parsedContext.Uuid) | ||
if err != nil { | ||
return stacktrace.Propagate(err, "While attempting to reload the context with uuid %s an error occurred while removing it from the context store", parsedContext.Uuid) | ||
} | ||
if add.AddContext(parsedContext) != nil { | ||
return stacktrace.Propagate(err, "Unable to add context to context store") | ||
} | ||
contextIdentifier := parsedContext.GetName() | ||
return context_switch.SwitchContext(ctx, contextIdentifier) | ||
} | ||
|
||
func loadApiKey() (*string, error) { | ||
apiKey := os.Getenv(kurtosisCloudApiKeyEnvVarArg) | ||
if len(apiKey) < 1 { | ||
return nil, stacktrace.NewError("No API Key was found. An API Key must be provided as env var %s", kurtosisCloudApiKeyEnvVarArg) | ||
} | ||
logrus.Info("Successfully Loaded API Key...") | ||
return &apiKey, nil | ||
} | ||
|
||
func getCloudConfig() (*resolved_config.KurtosisCloudConfig, error) { | ||
// Get the configuration | ||
kurtosisConfigStore := kurtosis_config.GetKurtosisConfigStore() | ||
configProvider := kurtosis_config.NewKurtosisConfigProvider(kurtosisConfigStore) | ||
kurtosisConfig, err := configProvider.GetOrInitializeConfig() | ||
if err != nil { | ||
return nil, stacktrace.Propagate(err, "Failed to get or initialize Kurtosis configuration") | ||
} | ||
if kurtosisConfig.GetCloudConfig() == nil { | ||
return nil, stacktrace.Propagate(err, "No cloud config was found. This is an internal Kurtosis error.") | ||
} | ||
cloudConfig := kurtosisConfig.GetCloudConfig() | ||
|
||
if cloudConfig.Port == 0 { | ||
cloudConfig.Port = resolved_config.DefaultCloudConfigPort | ||
} | ||
if len(cloudConfig.ApiUrl) < 1 { | ||
cloudConfig.ApiUrl = resolved_config.DefaultCloudConfigApiUrl | ||
} | ||
if len(cloudConfig.CertificateChain) < 1 { | ||
cloudConfig.CertificateChain = resolved_config.DefaultCertificateChain | ||
} | ||
|
||
return cloudConfig, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.