Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Enclave connect #2117

Merged
merged 11 commits into from
Feb 6, 2024
60 changes: 46 additions & 14 deletions api/golang/core/lib/enclaves/enclave_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,26 +353,35 @@ func (enclaveCtx *EnclaveContext) GetServiceContext(serviceIdentifier string) (*
serviceIdentifier)
}

serviceCtxPrivatePorts, err := convertApiPortsToServiceContextPorts(serviceInfo.GetPrivatePorts())
serviceContext, err := enclaveCtx.convertServiceInfoToServiceContext(serviceInfo)
if err != nil {
return nil, stacktrace.Propagate(err, "An error occurred converting the private ports returned by the API to ports usable by the service context")
return nil, stacktrace.Propagate(err, "An error occurred converting the service info to a service context.")
}
serviceCtxPublicPorts, err := convertApiPortsToServiceContextPorts(serviceInfo.GetMaybePublicPorts())

return serviceContext, nil
}

// Docs available at https://docs.kurtosis.com/sdk#getservicecontexts--mapstring--bool---servicecontext-servicecontext
func (enclaveCtx *EnclaveContext) GetServiceContexts(serviceIdentifiers map[string]bool) (map[services.ServiceName]*services.ServiceContext, error) {
getServiceInfoArgs := binding_constructors.NewGetServicesArgs(serviceIdentifiers)
response, err := enclaveCtx.client.GetServices(context.Background(), getServiceInfoArgs)
if err != nil {
return nil, stacktrace.Propagate(err, "An error occurred converting the public ports returned by the API to ports usable by the service context")
return nil, stacktrace.Propagate(
err,
"An error occurred when trying to get info for services '%v'",
serviceIdentifiers)
}

serviceContext := services.NewServiceContext(
enclaveCtx.client,
services.ServiceName(serviceIdentifier),
services.ServiceUUID(serviceInfo.ServiceUuid),
serviceInfo.GetPrivateIpAddr(),
serviceCtxPrivatePorts,
serviceInfo.GetMaybePublicIpAddr(),
serviceCtxPublicPorts,
)
serviceContexts := make(map[services.ServiceName]*services.ServiceContext, len(response.GetServiceInfo()))
for _, serviceInfo := range response.GetServiceInfo() {
serviceContext, err := enclaveCtx.convertServiceInfoToServiceContext(serviceInfo)
if err != nil {
return nil, stacktrace.Propagate(err, "An error occurred converting the service info to a service context.")
}
serviceContexts[serviceContext.GetServiceName()] = serviceContext
}

return serviceContext, nil
return serviceContexts, nil
}

// Docs available at https://docs.kurtosis.com/sdk#getservices---mapservicename--serviceuuid-serviceidentifiers
Expand Down Expand Up @@ -636,6 +645,29 @@ func (enclaveCtx *EnclaveContext) uploadStarlarkPackage(packageId string, packag
return nil
}

func (enclaveCtx *EnclaveContext) convertServiceInfoToServiceContext(serviceInfo *kurtosis_core_rpc_api_bindings.ServiceInfo) (*services.ServiceContext, error) {
serviceCtxPrivatePorts, err := convertApiPortsToServiceContextPorts(serviceInfo.GetPrivatePorts())
if err != nil {
return nil, stacktrace.Propagate(err, "An error occurred converting the private ports returned by the API to ports usable by the service context")
}
serviceCtxPublicPorts, err := convertApiPortsToServiceContextPorts(serviceInfo.GetMaybePublicPorts())
if err != nil {
return nil, stacktrace.Propagate(err, "An error occurred converting the public ports returned by the API to ports usable by the service context")
}

serviceContext := services.NewServiceContext(
enclaveCtx.client,
services.ServiceName(serviceInfo.Name),
services.ServiceUUID(serviceInfo.ServiceUuid),
serviceInfo.GetPrivateIpAddr(),
serviceCtxPrivatePorts,
serviceInfo.GetMaybePublicIpAddr(),
serviceCtxPublicPorts,
)

return serviceContext, nil
}

func getKurtosisYaml(packageRootPath string) (*KurtosisYaml, error) {
kurtosisYamlFilepath := path.Join(packageRootPath, kurtosisYamlFilename)

Expand Down
2 changes: 2 additions & 0 deletions api/typescript/src/core/lib/enclaves/enclave_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,8 @@ export class EnclaveContext {
return ok(serviceContext);
}

// TODO: Add getServiceContexts
laurentluce marked this conversation as resolved.
Show resolved Hide resolved

// Docs available at https://docs.kurtosis.com/sdk#getservices---mapservicename--serviceuuid-serviceidentifiers
public async getServices(): Promise<Result<Map<ServiceName, ServiceUUID>, Error>> {
const getAllServicesArgMap: Map<string, boolean> = new Map<string,boolean>()
Expand Down
1 change: 1 addition & 0 deletions cli/cli/command_str_consts/command_str_consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const (
EnclaveStopCmdStr = "stop"
EnclaveRmCmdStr = "rm"
EnclaveDumpCmdStr = "dump"
EnclaveConnectCmdStr = "connect"
EngineCmdStr = "engine"
EngineLogsCmdStr = "logs"
EngineStartCmdStr = "start"
Expand Down
14 changes: 13 additions & 1 deletion cli/cli/commands/cloud/load/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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"
Expand Down Expand Up @@ -50,6 +51,18 @@ func run(ctx context.Context, _ *flags.ParsedFlags, args *args.ParsedArgs) error
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)
}

contextsConfigStore := store.GetContextsConfigStore()
currentContext, err := contextsConfigStore.GetCurrentContext()
if err != nil {
return stacktrace.Propagate(err, "An error occurred while retrieving the current context")
}

if currentContext.Uuid.Value == instanceID {
logrus.Infof("Cloud instance %s already loaded", instanceID)
return nil
}
laurentluce marked this conversation as resolved.
Show resolved Hide resolved

logrus.Infof("Loading cloud instance %s", instanceID)

apiKey, err := cloudhelper.LoadApiKey()
Expand Down Expand Up @@ -96,7 +109,6 @@ func run(ctx context.Context, _ *flags.ParsedFlags, args *args.ParsedArgs) error
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 {
Expand Down
131 changes: 131 additions & 0 deletions cli/cli/commands/enclave/connect/connect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package connect

import (
"context"
"fmt"
"strings"

"github.com/kurtosis-tech/kurtosis/api/golang/core/lib/services"
"github.com/kurtosis-tech/kurtosis/api/golang/engine/kurtosis_engine_rpc_api_bindings"
"github.com/kurtosis-tech/kurtosis/api/golang/engine/lib/kurtosis_context"
"github.com/kurtosis-tech/kurtosis/cli/cli/command_framework/highlevel/enclave_id_arg"
"github.com/kurtosis-tech/kurtosis/cli/cli/command_framework/highlevel/engine_consuming_kurtosis_command"
"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/helpers/portal_manager"
"github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface"
"github.com/kurtosis-tech/kurtosis/contexts-config-store/store"
"github.com/kurtosis-tech/kurtosis/metrics-library/golang/lib/metrics_client"
"github.com/kurtosis-tech/stacktrace"
"github.com/sirupsen/logrus"
)

const (
enclaveIdentifierArgKey = "enclave"
isEnclaveIdArgOptional = false
isEnclaveIdArgGreedy = false

kurtosisBackendCtxKey = "kurtosis-backend"
engineClientCtxKey = "engine-client"

portMappingSeparatorForLogs = ", "
)

var EnclaveConnectCmd = &engine_consuming_kurtosis_command.EngineConsumingKurtosisCommand{
CommandStr: command_str_consts.EnclaveConnectCmdStr,
ShortDescription: "Connects enclave",
LongDescription: "Connects the enclave with the given name",
KurtosisBackendContextKey: kurtosisBackendCtxKey,
EngineClientContextKey: engineClientCtxKey,
Flags: nil,
Args: []*args.ArgConfig{
enclave_id_arg.NewEnclaveIdentifierArg(
enclaveIdentifierArgKey,
engineClientCtxKey,
isEnclaveIdArgOptional,
isEnclaveIdArgGreedy,
),
},
RunFunc: run,
}

func init() {
}

func run(
ctx context.Context,
kurtosisBackend backend_interface.KurtosisBackend,
engineClient kurtosis_engine_rpc_api_bindings.EngineServiceClient,
_ metrics_client.MetricsClient,
_ *flags.ParsedFlags,
args *args.ParsedArgs,
) error {
enclaveIdentifier, err := args.GetNonGreedyArg(enclaveIdentifierArgKey)
if err != nil {
return stacktrace.Propagate(err, "An error occurred getting the enclave identifier arg using key '%v'", enclaveIdentifierArgKey)
}

currentContext, err := store.GetContextsConfigStore().GetCurrentContext()
if err != nil {
logrus.Warnf("Could not retrieve the current context. Kurtosis will assume context is local.")
logrus.Debugf("Error was: %v", err.Error())
return nil
}

if !store.IsRemote(currentContext) {
logrus.Info("Current context is local, not mapping enclave service ports")
return nil
}

logrus.Info("Connecting enclave...")
if err = connectAllEnclaveServices(ctx, enclaveIdentifier); err != nil {
return stacktrace.Propagate(err, "An error occurred connecting all enclave services")
}

logrus.Info("Enclave connected successfully")

return nil
}

func connectAllEnclaveServices(ctx context.Context, enclaveIdentifier string) error {

kurtosisCtx, err := kurtosis_context.NewKurtosisContextFromLocalEngine()
if err != nil {
return stacktrace.Propagate(err, "An error occurred creating Kurtosis Context from local engine")
}

enclaveCtx, err := kurtosisCtx.GetEnclaveContext(ctx, enclaveIdentifier)
if err != nil {
return stacktrace.Propagate(err, "An error occurred getting an enclave context from enclave info for enclave '%v'", enclaveIdentifier)
}
allServicesMap := map[string]bool{}
serviceContexts, err := enclaveCtx.GetServiceContexts(allServicesMap)
if err != nil {
return stacktrace.Propagate(err, "An error occurred retrieving the service contexts")
}

portsMapping := map[uint16]*services.PortSpec{}
for _, serviceCtx := range serviceContexts {
for _, portSpec := range serviceCtx.GetPublicPorts() {
portsMapping[portSpec.GetNumber()] = portSpec
}
}

portalManager := portal_manager.NewPortalManager()
successfullyMappedPorts, failedPorts, err := portalManager.MapPorts(ctx, portsMapping)
if err != nil {
var stringifiedPortMapping []string
for localPort, remotePort := range failedPorts {
stringifiedPortMapping = append(stringifiedPortMapping, fmt.Sprintf("%d:%d", localPort, remotePort.GetNumber()))
}
logrus.Warnf("The enclave was successfully run but the following port(s) could not be mapped locally: %s. "+
"The associated service(s) will not be reachable on the local host",
strings.Join(stringifiedPortMapping, portMappingSeparatorForLogs))
logrus.Debugf("Error was: %v", err.Error())
return nil
}
logrus.Infof("Successfully mapped %d ports. All services running inside the enclave are reachable locally on"+
" their ephemeral port numbers", len(successfullyMappedPorts))
return nil
}
2 changes: 2 additions & 0 deletions cli/cli/commands/enclave/enclave.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package enclave
import (
"github.com/kurtosis-tech/kurtosis/cli/cli/command_str_consts"
"github.com/kurtosis-tech/kurtosis/cli/cli/commands/enclave/add"
"github.com/kurtosis-tech/kurtosis/cli/cli/commands/enclave/connect"
"github.com/kurtosis-tech/kurtosis/cli/cli/commands/enclave/dump"
"github.com/kurtosis-tech/kurtosis/cli/cli/commands/enclave/inspect"
"github.com/kurtosis-tech/kurtosis/cli/cli/commands/enclave/ls"
Expand All @@ -31,4 +32,5 @@ func init() {
EnclaveCmd.AddCommand(stop.EnclaveStopCmd.MustGetCobraCommand())
EnclaveCmd.AddCommand(rm.EnclaveRmCmd.MustGetCobraCommand())
EnclaveCmd.AddCommand(dump.EnclaveDumpCmd.MustGetCobraCommand())
EnclaveCmd.AddCommand(connect.EnclaveConnectCmd.MustGetCobraCommand())
}
11 changes: 11 additions & 0 deletions docs/docs/api-reference/engine-apic-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,17 @@ Gets relevant information about a service (identified by the given service [iden

The [ServiceContext][servicecontext] representation of a service running in a Docker container.

### `getServiceContexts(Map<String, Boolean> serviceIdentifiers) -> Map<ServiceName, ServiceContext> serviceContexts`
Gets relevant information about services (identified by the given [service identifiers][service-identifiers]) that are running in the enclave.

**Args**

* `serviceIdentifiers`: The [service identifiers][service-identifiers] to indicate which services to retrieve.

**Returns**

* `serviceContexts`: A map of objects containing a mapping of Name -> [ServiceContext][servicecontext] for all the services inside the enclave.

### `getServices() -> Map<ServiceName, ServiceUUID> serviceIdentifiers`
Gets the Name and UUID of the current services in the enclave.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
} from "@chakra-ui/react";
import { FileDisplay } from "kurtosis-ui-components";
import { EnclaveFullInfo } from "../../types";

export type ConnectEnclaveModalProps = {
enclave: EnclaveFullInfo;
instanceUUID: string;
isOpen: boolean;
onClose: () => void;
};

export const ConnectEnclaveModal = ({ isOpen, onClose, enclave, instanceUUID }: ConnectEnclaveModalProps) => {
const commands = `
kurtosis cloud load ${instanceUUID}
kurtosis enclave ${enclave.name} connect
kurtosis enclave ${enclave.name} inspect`;
return (
<Modal closeOnOverlayClick={false} isOpen={isOpen} onClose={onClose} isCentered>
<ModalOverlay />
<ModalContent>
<ModalHeader>Connect to this enclave from the CLI</ModalHeader>
<ModalCloseButton />
<ModalBody>
<FileDisplay value={commands} title={"CLI Commands"} filename={`${enclave.name}--connect.sh`} />
</ModalBody>
<ModalFooter>
The enclave inspect command shows you the ephemeral port to use to connect to your user service.
</ModalFooter>
</ModalContent>
</Modal>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Button, ButtonProps, Tooltip } from "@chakra-ui/react";
import { useState } from "react";
import { FiTrash2 } from "react-icons/fi";
import { EnclaveFullInfo } from "../../types";
import { ConnectEnclaveModal } from "../modals/ConnectEnclaveModal";

type ConnectEnclaveButtonProps = ButtonProps & {
enclave: EnclaveFullInfo;
instanceUUID: string;
};

export const ConnectEnclaveButton = ({ enclave, instanceUUID, ...buttonProps }: ConnectEnclaveButtonProps) => {
const [showModal, setShowModal] = useState(false);

return (
<>
<Tooltip label={`Steps to connect to this enclave from the CLI.`} openDelay={1000}>
<Button
colorScheme={"green"}
leftIcon={<FiTrash2 />}
onClick={() => setShowModal(true)}
size={"sm"}
variant={"solid"}
{...buttonProps}
>
Connect
</Button>
</Tooltip>
<ConnectEnclaveModal
enclave={enclave}
instanceUUID={instanceUUID}
isOpen={showModal}
onClose={() => setShowModal(false)}
/>
</>
);
};
Loading
Loading