From 535d153a1e2848648350fc79ce85bb5dbce5c972 Mon Sep 17 00:00:00 2001 From: luke-lombardi <33990301+luke-lombardi@users.noreply.github.com> Date: Thu, 2 Jan 2025 16:20:14 -0500 Subject: [PATCH] Feat: beam shell (#817) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a shell command to let users connect to containers with their stub config. It can be used with any decorator that inherits from the `DeployableMixin`. Screenshot 2025-01-01 at 6 52 13 PM --- bin/gen_proto.sh | 3 + docker/Dockerfile.runner | 2 +- pkg/abstractions/shell/http.go | 107 +++++ pkg/abstractions/shell/shell.go | 365 ++++++++++++++++++ pkg/abstractions/shell/shell.proto | 18 + pkg/api/v1/stub.go | 2 +- pkg/common/key_events.go | 4 + pkg/common/url.go | 2 +- pkg/common/url_test.go | 11 +- pkg/gateway/gateway.go | 18 + pkg/gateway/services/stub.go | 6 +- .../020_add_shell_stub_type.go | 44 +++ pkg/types/backend.go | 1 + pkg/worker/base_runc_config.json | 12 +- proto/shell.pb.go | 243 ++++++++++++ proto/shell_grpc.pb.go | 109 ++++++ sdk/poetry.lock | 228 ++++++++++- sdk/pyproject.toml | 1 + sdk/src/beta9/abstractions/base/runner.py | 15 + sdk/src/beta9/abstractions/mixins.py | 56 ++- sdk/src/beta9/abstractions/shell.py | 342 ++++++++++++++++ sdk/src/beta9/cli/main.py | 2 + sdk/src/beta9/cli/shell.py | 79 ++++ sdk/src/beta9/clients/shell/__init__.py | 46 +++ 24 files changed, 1701 insertions(+), 15 deletions(-) create mode 100644 pkg/abstractions/shell/http.go create mode 100644 pkg/abstractions/shell/shell.go create mode 100644 pkg/abstractions/shell/shell.proto create mode 100644 pkg/repository/backend_postgres_migrations/020_add_shell_stub_type.go create mode 100644 proto/shell.pb.go create mode 100644 proto/shell_grpc.pb.go create mode 100644 sdk/src/beta9/abstractions/shell.py create mode 100644 sdk/src/beta9/cli/shell.py create mode 100644 sdk/src/beta9/clients/shell/__init__.py diff --git a/bin/gen_proto.sh b/bin/gen_proto.sh index 9011d4a8d..246e50a4b 100755 --- a/bin/gen_proto.sh +++ b/bin/gen_proto.sh @@ -43,3 +43,6 @@ protoc -I ./pkg/abstractions/experimental/signal/ --python_betterproto_beta9_out protoc -I ./pkg/abstractions/experimental/bot/ --go_out=./proto --go_opt=paths=source_relative --go-grpc_out=./proto --go-grpc_opt=paths=source_relative ./pkg/abstractions/experimental/bot/bot.proto protoc -I ./pkg/abstractions/experimental/bot/ --python_betterproto_beta9_out=./sdk/src/beta9/clients/ ./pkg/abstractions/experimental/bot/bot.proto + +protoc -I ./pkg/abstractions/shell/ --go_out=./proto --go_opt=paths=source_relative --go-grpc_out=./proto --go-grpc_opt=paths=source_relative ./pkg/abstractions/shell/shell.proto +protoc -I ./pkg/abstractions/shell/ --python_betterproto_beta9_out=./sdk/src/beta9/clients/ ./pkg/abstractions/shell/shell.proto diff --git a/docker/Dockerfile.runner b/docker/Dockerfile.runner index eff303e63..e49b4f13e 100644 --- a/docker/Dockerfile.runner +++ b/docker/Dockerfile.runner @@ -6,7 +6,7 @@ ENV DEBIAN_FRONTEND=noninteractive RUN <> "/root/.profile"; + echo "cd /mnt/code" >> "/root/.profile"; + echo "export TERM=xterm-256color" >> "/root/.bashrc"; + echo "alias ls='ls --color=auto'" >> "/root/.bashrc"; + echo "alias ll='ls -lart --color=auto'" >> "/root/.bashrc"; + sed -i 's/^#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config; + sed -i 's/^#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config; + sed -i 's/^#PubkeyAuthentication.*/PubkeyAuthentication no/' /etc/ssh/sshd_config; + echo "AllowUsers $USERNAME" >> /etc/ssh/sshd_config; + echo "PermitUserEnvironment yes" >> /etc/ssh/sshd_config; + printenv > /etc/environment; + exec /usr/sbin/sshd -D -p 8001 + `, token) + + entryPoint := []string{ + "/bin/bash", + "-c", + startupCommand, + } + + err = ss.rdb.Set(ctx, Keys.shellContainerTTL(containerId), "1", time.Duration(shellContainerTtlS)*time.Second).Err() + if err != nil { + return &pb.CreateShellResponse{ + Ok: false, + ErrMsg: "Failed to set shell container ttl", + }, nil + } + + err = ss.scheduler.Run(&types.ContainerRequest{ + ContainerId: containerId, + Env: env, + Cpu: stubConfig.Runtime.Cpu, + Memory: stubConfig.Runtime.Memory, + GpuRequest: gpuRequest, + GpuCount: uint32(gpuCount), + ImageId: stubConfig.Runtime.ImageId, + StubId: stub.ExternalId, + WorkspaceId: authInfo.Workspace.ExternalId, + Workspace: *authInfo.Workspace, + EntryPoint: entryPoint, + Mounts: mounts, + Stub: *stub, + }) + if err != nil { + return &pb.CreateShellResponse{ + Ok: false, + ErrMsg: "Failed to run shell container", + }, nil + } + + err = ss.waitForContainer(ctx, containerId, containerWaitTimeoutDurationS) + if err != nil { + ss.scheduler.Stop(&types.StopContainerArgs{ + ContainerId: containerId, + Force: true, + }) + + return &pb.CreateShellResponse{ + Ok: false, + ErrMsg: "Failed to wait for shell container", + }, nil + } + + return &pb.CreateShellResponse{ + Ok: true, + ContainerId: containerId, + Token: token, + }, nil +} + +func (ss *SSHShellService) genContainerId(stubId string) string { + return fmt.Sprintf("%s-%s-%s", shellContainerPrefix, stubId, uuid.New().String()[:8]) +} + +func (ss *SSHShellService) keepAlive(ctx context.Context, containerId string, done <-chan struct{}) { + ticker := time.NewTicker(containerKeepAliveIntervalS) + defer ticker.Stop() + + for { + select { + case <-ss.ctx.Done(): + return + case <-ctx.Done(): + return + case <-done: + return + case <-ticker.C: + ss.rdb.Set(ctx, Keys.shellContainerTTL(containerId), "1", time.Duration(shellContainerTtlS)*time.Second).Err() + } + } +} + +func (ss *SSHShellService) waitForContainer(ctx context.Context, containerId string, timeout time.Duration) error { + timeoutCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + for { + select { + case <-ss.ctx.Done(): + return nil + case <-timeoutCtx.Done(): + return fmt.Errorf("timed out waiting for container to be available") + default: + containerState, err := ss.containerRepo.GetContainerState(containerId) + if err != nil { + return err + } + + if containerState.Status == types.ContainerStatusRunning { + return nil + } + + time.Sleep(containerWaitPollIntervalS) + } + } +} + +func generateToken(length int) (string, error) { + byteLength := (length*6 + 7) / 8 // Calculate the number of bytes needed + + randomBytes := make([]byte, byteLength) + _, err := rand.Read(randomBytes) + if err != nil { + return "", err + } + + token := base64.URLEncoding.EncodeToString(randomBytes) + return token[:length], nil +} + +// Redis keys +var ( + shellContainerTTL string = "shell:container_ttl:%s" +) + +var Keys = &keys{} + +type keys struct{} + +func (k *keys) shellContainerTTL(containerId string) string { + return fmt.Sprintf(shellContainerTTL, containerId) +} diff --git a/pkg/abstractions/shell/shell.proto b/pkg/abstractions/shell/shell.proto new file mode 100644 index 000000000..05de38f2f --- /dev/null +++ b/pkg/abstractions/shell/shell.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +option go_package = "github.com/beam-cloud/beta9/proto"; + +package shell; + +service ShellService { + rpc CreateShell(CreateShellRequest) returns (CreateShellResponse) {} +} + +message CreateShellRequest { string stub_id = 1; } + +message CreateShellResponse { + bool ok = 1; + string container_id = 2; + string token = 3; + string err_msg = 4; +} diff --git a/pkg/api/v1/stub.go b/pkg/api/v1/stub.go index 00fb25e57..67ee5b1b6 100644 --- a/pkg/api/v1/stub.go +++ b/pkg/api/v1/stub.go @@ -115,7 +115,7 @@ func (g *StubGroup) GetURL(ctx echo.Context) error { // Get URL for Serves if stub.Type.IsServe() { - invokeUrl := common.BuildServeURL(g.config.GatewayService.HTTP.GetExternalURL(), filter.URLType, stub) + invokeUrl := common.BuildStubURL(g.config.GatewayService.HTTP.GetExternalURL(), filter.URLType, stub) return ctx.JSON(http.StatusOK, map[string]string{"url": invokeUrl}) } diff --git a/pkg/common/key_events.go b/pkg/common/key_events.go index cdd5c082b..9102805a6 100644 --- a/pkg/common/key_events.go +++ b/pkg/common/key_events.go @@ -49,6 +49,10 @@ func (kem *KeyEventManager) fetchExistingKeys(patternPrefix string) ([]string, e return trimmedKeys, nil } +func (kem *KeyEventManager) TrimKeyspacePrefix(key string) string { + return strings.TrimPrefix(key, keyspacePrefix) +} + func (kem *KeyEventManager) ListenForPattern(ctx context.Context, patternPrefix string, keyEventChan chan KeyEvent) error { existingKeys, err := kem.fetchExistingKeys(patternPrefix) if err != nil { diff --git a/pkg/common/url.go b/pkg/common/url.go index f713a8200..0287f20f4 100644 --- a/pkg/common/url.go +++ b/pkg/common/url.go @@ -31,7 +31,7 @@ func BuildDeploymentURL(externalUrl, urlType string, stub *types.StubWithRelated return fmt.Sprintf("%s://%s/%s/%s/v%d", parsedUrl.Scheme, parsedUrl.Host, stub.Type.Kind(), deployment.Name, deployment.Version) } -func BuildServeURL(externalUrl, urlType string, stub *types.StubWithRelated) string { +func BuildStubURL(externalUrl, urlType string, stub *types.StubWithRelated) string { parsedUrl, err := url.Parse(externalUrl) if err != nil { return "" diff --git a/pkg/common/url_test.go b/pkg/common/url_test.go index e85b56de9..8cc05095d 100644 --- a/pkg/common/url_test.go +++ b/pkg/common/url_test.go @@ -76,6 +76,15 @@ func TestBuildServeURL(t *testing.T) { invokeType: InvokeUrlTypeHost, expectedUrl: "http://e9c29586-c465-4a67-9c9b-25293d1ce77b.app.example.com", }, + { + name: "returns host-based URL", + stub: &types.StubWithRelated{Stub: types.Stub{ + ExternalId: "fbedeff-c465-4a67-9c9b-25293d1ce77b", + Type: types.StubType(types.StubTypeShell), + }}, + invokeType: InvokeUrlTypeHost, + expectedUrl: "http://fbedeff-c465-4a67-9c9b-25293d1ce77b.app.example.com", + }, { name: "returns path-based URL", stub: &types.StubWithRelated{Stub: types.Stub{ @@ -89,7 +98,7 @@ func TestBuildServeURL(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - got := BuildServeURL(externalUrl, test.invokeType, test.stub) + got := BuildStubURL(externalUrl, test.invokeType, test.stub) assert.Equal(t, test.expectedUrl, got) }) } diff --git a/pkg/gateway/gateway.go b/pkg/gateway/gateway.go index 1a2ff75bf..8c5b7f7a2 100644 --- a/pkg/gateway/gateway.go +++ b/pkg/gateway/gateway.go @@ -22,6 +22,7 @@ import ( "github.com/beam-cloud/beta9/pkg/abstractions/endpoint" bot "github.com/beam-cloud/beta9/pkg/abstractions/experimental/bot" _signal "github.com/beam-cloud/beta9/pkg/abstractions/experimental/signal" + _shell "github.com/beam-cloud/beta9/pkg/abstractions/shell" "github.com/beam-cloud/beta9/pkg/abstractions/function" "github.com/beam-cloud/beta9/pkg/abstractions/image" @@ -376,6 +377,23 @@ func (g *Gateway) registerServices() error { } pb.RegisterBotServiceServer(g.grpcServer, botService) + // Register shell service + ss, err := _shell.NewSSHShellService(g.ctx, _shell.ShellServiceOpts{ + Config: g.Config, + RedisClient: g.RedisClient, + Scheduler: g.Scheduler, + BackendRepo: g.BackendRepo, + WorkspaceRepo: g.WorkspaceRepo, + ContainerRepo: g.ContainerRepo, + Tailscale: g.Tailscale, + EventRepo: g.EventRepo, + RouteGroup: g.rootRouteGroup, + }) + if err != nil { + return err + } + pb.RegisterShellServiceServer(g.grpcServer, ss) + // Register scheduler s, err := scheduler.NewSchedulerService(g.Scheduler) if err != nil { diff --git a/pkg/gateway/services/stub.go b/pkg/gateway/services/stub.go index b8bf9b9d0..7c440b0d5 100644 --- a/pkg/gateway/services/stub.go +++ b/pkg/gateway/services/stub.go @@ -255,9 +255,9 @@ func (gws *GatewayService) GetURL(ctx context.Context, in *pb.GetURLRequest) (*p in.UrlType = gws.appConfig.GatewayService.InvokeURLType } - // Get URL for Serves - if stub.Type.IsServe() { - invokeUrl := common.BuildServeURL(gws.appConfig.GatewayService.HTTP.GetExternalURL(), in.UrlType, stub) + // Get URL for Serves or Shells + if stub.Type.IsServe() || stub.Type.Kind() == types.StubTypeShell { + invokeUrl := common.BuildStubURL(gws.appConfig.GatewayService.HTTP.GetExternalURL(), in.UrlType, stub) return &pb.GetURLResponse{ Ok: true, Url: invokeUrl, diff --git a/pkg/repository/backend_postgres_migrations/020_add_shell_stub_type.go b/pkg/repository/backend_postgres_migrations/020_add_shell_stub_type.go new file mode 100644 index 000000000..d32f96995 --- /dev/null +++ b/pkg/repository/backend_postgres_migrations/020_add_shell_stub_type.go @@ -0,0 +1,44 @@ +package backend_postgres_migrations + +import ( + "context" + "database/sql" + "fmt" + + "github.com/pressly/goose/v3" +) + +func init() { + goose.AddMigrationContext(upAddShellStubType, downRemoveShellStubType) +} + +func upAddShellStubType(ctx context.Context, tx *sql.Tx) error { + newStubTypes := []string{"shell"} + + for _, stubType := range newStubTypes { + addEnumSQL := fmt.Sprintf(` +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_type t + JOIN pg_enum e ON t.oid = e.enumtypid + JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace + WHERE n.nspname = 'public' AND t.typname = 'stub_type' AND e.enumlabel = '%s' + ) THEN + EXECUTE 'ALTER TYPE stub_type ADD VALUE ' || quote_literal('%s'); + END IF; +END +$$;`, stubType, stubType) + + if _, err := tx.Exec(addEnumSQL); err != nil { + return err + } + } + + return nil +} + +func downRemoveShellStubType(ctx context.Context, tx *sql.Tx) error { + // PostgreSQL doesn't support removing values from an ENUM directly + return nil +} diff --git a/pkg/types/backend.go b/pkg/types/backend.go index 6a324b2fa..de64e2f29 100644 --- a/pkg/types/backend.go +++ b/pkg/types/backend.go @@ -208,6 +208,7 @@ const ( StubTypeFunctionDeployment string = "function/deployment" StubTypeFunctionServe string = "function/serve" StubTypeContainer string = "container" + StubTypeShell string = "shell" StubTypeTaskQueue string = "taskqueue" StubTypeTaskQueueDeployment string = "taskqueue/deployment" StubTypeTaskQueueServe string = "taskqueue/serve" diff --git a/pkg/worker/base_runc_config.json b/pkg/worker/base_runc_config.json index adaa9562f..40e42414a 100644 --- a/pkg/worker/base_runc_config.json +++ b/pkg/worker/base_runc_config.json @@ -30,7 +30,8 @@ "CAP_FOWNER", "CAP_SETGID", "CAP_SETUID", - "CAP_SETFCAP" + "CAP_SETFCAP", + "CAP_SYS_CHROOT" ], "effective": [ "CAP_AUDIT_WRITE", @@ -42,7 +43,8 @@ "CAP_FOWNER", "CAP_SETGID", "CAP_SETUID", - "CAP_SETFCAP" + "CAP_SETFCAP", + "CAP_SYS_CHROOT" ], "permitted": [ "CAP_AUDIT_WRITE", @@ -54,7 +56,8 @@ "CAP_FOWNER", "CAP_SETGID", "CAP_SETUID", - "CAP_SETFCAP" + "CAP_SETFCAP", + "CAP_SYS_CHROOT" ], "ambient": [ "CAP_AUDIT_WRITE", @@ -66,7 +69,8 @@ "CAP_FOWNER", "CAP_SETGID", "CAP_SETUID", - "CAP_SETFCAP" + "CAP_SETFCAP", + "CAP_SYS_CHROOT" ] }, "rlimits": [], diff --git a/proto/shell.pb.go b/proto/shell.pb.go new file mode 100644 index 000000000..1e1d02c11 --- /dev/null +++ b/proto/shell.pb.go @@ -0,0 +1,243 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc v4.25.1 +// source: shell.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type CreateShellRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StubId string `protobuf:"bytes,1,opt,name=stub_id,json=stubId,proto3" json:"stub_id,omitempty"` +} + +func (x *CreateShellRequest) Reset() { + *x = CreateShellRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_shell_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateShellRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateShellRequest) ProtoMessage() {} + +func (x *CreateShellRequest) ProtoReflect() protoreflect.Message { + mi := &file_shell_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateShellRequest.ProtoReflect.Descriptor instead. +func (*CreateShellRequest) Descriptor() ([]byte, []int) { + return file_shell_proto_rawDescGZIP(), []int{0} +} + +func (x *CreateShellRequest) GetStubId() string { + if x != nil { + return x.StubId + } + return "" +} + +type CreateShellResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ok bool `protobuf:"varint,1,opt,name=ok,proto3" json:"ok,omitempty"` + ContainerId string `protobuf:"bytes,2,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"` + Token string `protobuf:"bytes,3,opt,name=token,proto3" json:"token,omitempty"` + ErrMsg string `protobuf:"bytes,4,opt,name=err_msg,json=errMsg,proto3" json:"err_msg,omitempty"` +} + +func (x *CreateShellResponse) Reset() { + *x = CreateShellResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_shell_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateShellResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateShellResponse) ProtoMessage() {} + +func (x *CreateShellResponse) ProtoReflect() protoreflect.Message { + mi := &file_shell_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateShellResponse.ProtoReflect.Descriptor instead. +func (*CreateShellResponse) Descriptor() ([]byte, []int) { + return file_shell_proto_rawDescGZIP(), []int{1} +} + +func (x *CreateShellResponse) GetOk() bool { + if x != nil { + return x.Ok + } + return false +} + +func (x *CreateShellResponse) GetContainerId() string { + if x != nil { + return x.ContainerId + } + return "" +} + +func (x *CreateShellResponse) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *CreateShellResponse) GetErrMsg() string { + if x != nil { + return x.ErrMsg + } + return "" +} + +var File_shell_proto protoreflect.FileDescriptor + +var file_shell_proto_rawDesc = []byte{ + 0x0a, 0x0b, 0x73, 0x68, 0x65, 0x6c, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x73, + 0x68, 0x65, 0x6c, 0x6c, 0x22, 0x2d, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x68, + 0x65, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x74, + 0x75, 0x62, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x75, + 0x62, 0x49, 0x64, 0x22, 0x77, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x68, 0x65, + 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x6b, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12, 0x14, 0x0a, + 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x12, 0x17, 0x0a, 0x07, 0x65, 0x72, 0x72, 0x5f, 0x6d, 0x73, 0x67, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x65, 0x72, 0x72, 0x4d, 0x73, 0x67, 0x32, 0x56, 0x0a, 0x0c, + 0x53, 0x68, 0x65, 0x6c, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x46, 0x0a, 0x0b, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x68, 0x65, 0x6c, 0x6c, 0x12, 0x19, 0x2e, 0x73, 0x68, + 0x65, 0x6c, 0x6c, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x68, 0x65, 0x6c, 0x6c, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x73, 0x68, 0x65, 0x6c, 0x6c, 0x2e, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x68, 0x65, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x42, 0x23, 0x5a, 0x21, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x62, 0x65, 0x61, 0x6d, 0x2d, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x62, 0x65, + 0x74, 0x61, 0x39, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_shell_proto_rawDescOnce sync.Once + file_shell_proto_rawDescData = file_shell_proto_rawDesc +) + +func file_shell_proto_rawDescGZIP() []byte { + file_shell_proto_rawDescOnce.Do(func() { + file_shell_proto_rawDescData = protoimpl.X.CompressGZIP(file_shell_proto_rawDescData) + }) + return file_shell_proto_rawDescData +} + +var file_shell_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_shell_proto_goTypes = []interface{}{ + (*CreateShellRequest)(nil), // 0: shell.CreateShellRequest + (*CreateShellResponse)(nil), // 1: shell.CreateShellResponse +} +var file_shell_proto_depIdxs = []int32{ + 0, // 0: shell.ShellService.CreateShell:input_type -> shell.CreateShellRequest + 1, // 1: shell.ShellService.CreateShell:output_type -> shell.CreateShellResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_shell_proto_init() } +func file_shell_proto_init() { + if File_shell_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_shell_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateShellRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_shell_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateShellResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_shell_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_shell_proto_goTypes, + DependencyIndexes: file_shell_proto_depIdxs, + MessageInfos: file_shell_proto_msgTypes, + }.Build() + File_shell_proto = out.File + file_shell_proto_rawDesc = nil + file_shell_proto_goTypes = nil + file_shell_proto_depIdxs = nil +} diff --git a/proto/shell_grpc.pb.go b/proto/shell_grpc.pb.go new file mode 100644 index 000000000..639ed515f --- /dev/null +++ b/proto/shell_grpc.pb.go @@ -0,0 +1,109 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v4.25.1 +// source: shell.proto + +package proto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + ShellService_CreateShell_FullMethodName = "/shell.ShellService/CreateShell" +) + +// ShellServiceClient is the client API for ShellService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ShellServiceClient interface { + CreateShell(ctx context.Context, in *CreateShellRequest, opts ...grpc.CallOption) (*CreateShellResponse, error) +} + +type shellServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewShellServiceClient(cc grpc.ClientConnInterface) ShellServiceClient { + return &shellServiceClient{cc} +} + +func (c *shellServiceClient) CreateShell(ctx context.Context, in *CreateShellRequest, opts ...grpc.CallOption) (*CreateShellResponse, error) { + out := new(CreateShellResponse) + err := c.cc.Invoke(ctx, ShellService_CreateShell_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ShellServiceServer is the server API for ShellService service. +// All implementations must embed UnimplementedShellServiceServer +// for forward compatibility +type ShellServiceServer interface { + CreateShell(context.Context, *CreateShellRequest) (*CreateShellResponse, error) + mustEmbedUnimplementedShellServiceServer() +} + +// UnimplementedShellServiceServer must be embedded to have forward compatible implementations. +type UnimplementedShellServiceServer struct { +} + +func (UnimplementedShellServiceServer) CreateShell(context.Context, *CreateShellRequest) (*CreateShellResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateShell not implemented") +} +func (UnimplementedShellServiceServer) mustEmbedUnimplementedShellServiceServer() {} + +// UnsafeShellServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ShellServiceServer will +// result in compilation errors. +type UnsafeShellServiceServer interface { + mustEmbedUnimplementedShellServiceServer() +} + +func RegisterShellServiceServer(s grpc.ServiceRegistrar, srv ShellServiceServer) { + s.RegisterService(&ShellService_ServiceDesc, srv) +} + +func _ShellService_CreateShell_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateShellRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ShellServiceServer).CreateShell(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ShellService_CreateShell_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ShellServiceServer).CreateShell(ctx, req.(*CreateShellRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ShellService_ServiceDesc is the grpc.ServiceDesc for ShellService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ShellService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "shell.ShellService", + HandlerType: (*ShellServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateShell", + Handler: _ShellService_CreateShell_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "shell.proto", +} diff --git a/sdk/poetry.lock b/sdk/poetry.lock index 82e387727..9477f5a37 100644 --- a/sdk/poetry.lock +++ b/sdk/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "annotated-types" @@ -72,6 +72,44 @@ tests = ["attrs[tests-no-zope]", "zope-interface"] tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +[[package]] +name = "bcrypt" +version = "4.2.1" +description = "Modern password hashing for your software and your servers" +optional = false +python-versions = ">=3.7" +files = [ + {file = "bcrypt-4.2.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:1340411a0894b7d3ef562fb233e4b6ed58add185228650942bdc885362f32c17"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ee315739bc8387aa36ff127afc99120ee452924e0df517a8f3e4c0187a0f5f"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dbd0747208912b1e4ce730c6725cb56c07ac734b3629b60d4398f082ea718ad"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:aaa2e285be097050dba798d537b6efd9b698aa88eef52ec98d23dcd6d7cf6fea"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:76d3e352b32f4eeb34703370e370997065d28a561e4a18afe4fef07249cb4396"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:b7703ede632dc945ed1172d6f24e9f30f27b1b1a067f32f68bf169c5f08d0425"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89df2aea2c43be1e1fa066df5f86c8ce822ab70a30e4c210968669565c0f4685"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:04e56e3fe8308a88b77e0afd20bec516f74aecf391cdd6e374f15cbed32783d6"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cfdf3d7530c790432046c40cda41dfee8c83e29482e6a604f8930b9930e94139"}, + {file = "bcrypt-4.2.1-cp37-abi3-win32.whl", hash = "sha256:adadd36274510a01f33e6dc08f5824b97c9580583bd4487c564fc4617b328005"}, + {file = "bcrypt-4.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:8c458cd103e6c5d1d85cf600e546a639f234964d0228909d8f8dbeebff82d526"}, + {file = "bcrypt-4.2.1-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:8ad2f4528cbf0febe80e5a3a57d7a74e6635e41af1ea5675282a33d769fba413"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:909faa1027900f2252a9ca5dfebd25fc0ef1417943824783d1c8418dd7d6df4a"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cde78d385d5e93ece5479a0a87f73cd6fa26b171c786a884f955e165032b262c"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:533e7f3bcf2f07caee7ad98124fab7499cb3333ba2274f7a36cf1daee7409d99"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:687cf30e6681eeda39548a93ce9bfbb300e48b4d445a43db4298d2474d2a1e54"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:041fa0155c9004eb98a232d54da05c0b41d4b8e66b6fc3cb71b4b3f6144ba837"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f85b1ffa09240c89aa2e1ae9f3b1c687104f7b2b9d2098da4e923f1b7082d331"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c6f5fa3775966cca251848d4d5393ab016b3afed251163c1436fefdec3b02c84"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:807261df60a8b1ccd13e6599c779014a362ae4e795f5c59747f60208daddd96d"}, + {file = "bcrypt-4.2.1-cp39-abi3-win32.whl", hash = "sha256:b588af02b89d9fad33e5f98f7838bf590d6d692df7153647724a7f20c186f6bf"}, + {file = "bcrypt-4.2.1-cp39-abi3-win_amd64.whl", hash = "sha256:e84e0e6f8e40a242b11bce56c313edc2be121cec3e0ec2d76fce01f6af33c07c"}, + {file = "bcrypt-4.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:76132c176a6d9953cdc83c296aeaed65e1a708485fd55abf163e0d9f8f16ce0e"}, + {file = "bcrypt-4.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e158009a54c4c8bc91d5e0da80920d048f918c61a581f0a63e4e93bb556d362f"}, + {file = "bcrypt-4.2.1.tar.gz", hash = "sha256:6765386e3ab87f569b276988742039baab087b2cdb01e809d74e74503c2faafe"}, +] + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + [[package]] name = "betterproto-beta9" version = "2.0.0b7" @@ -153,6 +191,85 @@ files = [ {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, ] +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "charset-normalizer" version = "3.4.0" @@ -405,6 +522,55 @@ files = [ python-dateutil = "*" pytz = ">2021.1" +[[package]] +name = "cryptography" +version = "43.0.3" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, + {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, + {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, + {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, + {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, + {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, + {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + [[package]] name = "docstring-parser" version = "0.16" @@ -888,6 +1054,27 @@ files = [ {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] +[[package]] +name = "paramiko" +version = "3.5.0" +description = "SSH2 protocol library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "paramiko-3.5.0-py3-none-any.whl", hash = "sha256:1fedf06b085359051cd7d0d270cebe19e755a8a921cc2ddbfa647fb0cd7d68f9"}, + {file = "paramiko-3.5.0.tar.gz", hash = "sha256:ad11e540da4f55cedda52931f1a3f812a8238a7af7f62a60de538cd80bb28124"}, +] + +[package.dependencies] +bcrypt = ">=3.2" +cryptography = ">=3.3" +pynacl = ">=1.5" + +[package.extras] +all = ["gssapi (>=1.4.1)", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] +gssapi = ["gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] +invoke = ["invoke (>=2.0)"] + [[package]] name = "pathspec" version = "0.12.1" @@ -964,6 +1151,17 @@ files = [ {file = "protobuf-4.25.5.tar.gz", hash = "sha256:7f8249476b4a9473645db7f8ab42b02fe1488cbe5fb72fddd445e0665afd8584"}, ] +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + [[package]] name = "pydantic" version = "2.10.4" @@ -1110,6 +1308,32 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pynacl" +version = "1.5.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, + {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, +] + +[package.dependencies] +cffi = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] + [[package]] name = "pytest" version = "8.3.4" @@ -1480,4 +1704,4 @@ test = ["websockets"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "ea7a7e055c5235752d3e84ff318191eff7bfea0088b454eb9f8f1a0986be0361" +content-hash = "1667031eb69e1901059f36bf0ab5c23c9686d9fb3fbcb23681d88f9fe09099c6" diff --git a/sdk/pyproject.toml b/sdk/pyproject.toml index 2c906cce0..611188dd5 100644 --- a/sdk/pyproject.toml +++ b/sdk/pyproject.toml @@ -27,6 +27,7 @@ croniter = "^3.0.3" websocket-client = "^1.8.0" prompt-toolkit = "^3.0.48" requests = "^2.32.3" +paramiko = "^3.5.0" [tool.poetry.group.dev.dependencies] pytest = "^8.2.0" diff --git a/sdk/src/beta9/abstractions/base/runner.py b/sdk/src/beta9/abstractions/base/runner.py index 59382e923..f58baa954 100644 --- a/sdk/src/beta9/abstractions/base/runner.py +++ b/sdk/src/beta9/abstractions/base/runner.py @@ -27,6 +27,7 @@ SecretVar, ) from ...clients.gateway import TaskPolicy as TaskPolicyProto +from ...clients.shell import ShellServiceStub from ...config import ConfigContext, SDKSettings, get_config_context, get_settings from ...env import called_on_import from ...sync import FileSyncer, SyncEventHandler @@ -46,12 +47,15 @@ ASGI_STUB_TYPE = "asgi" SCHEDULE_STUB_TYPE = "schedule" BOT_STUB_TYPE = "bot" +SHELL_STUB_TYPE = "shell" + TASKQUEUE_DEPLOYMENT_STUB_TYPE = "taskqueue/deployment" ENDPOINT_DEPLOYMENT_STUB_TYPE = "endpoint/deployment" ASGI_DEPLOYMENT_STUB_TYPE = "asgi/deployment" FUNCTION_DEPLOYMENT_STUB_TYPE = "function/deployment" SCHEDULE_DEPLOYMENT_STUB_TYPE = "schedule/deployment" BOT_DEPLOYMENT_STUB_TYPE = "bot/deployment" + TASKQUEUE_SERVE_STUB_TYPE = "taskqueue/serve" ENDPOINT_SERVE_STUB_TYPE = "endpoint/serve" ASGI_SERVE_STUB_TYPE = "asgi/serve" @@ -141,6 +145,7 @@ def __init__( self._map_callable_to_attr(attr="on_start", func=on_start) self._gateway_stub: Optional[GatewayServiceStub] = None + self._shell_stub: Optional[ShellServiceStub] = None self.syncer: FileSyncer = FileSyncer(self.gateway_stub) self.settings: SDKSettings = get_settings() self.config_context: ConfigContext = get_config_context() @@ -209,6 +214,16 @@ def gateway_stub(self) -> GatewayServiceStub: def gateway_stub(self, value) -> None: self._gateway_stub = value + @property + def shell_stub(self) -> ShellServiceStub: + if not self._shell_stub: + self._shell_stub = ShellServiceStub(self.channel) + return self._shell_stub + + @shell_stub.setter + def shell_stub(self, value) -> None: + self._shell_stub = value + def _parse_cpu_to_millicores(self, cpu: Union[float, str]) -> int: """ Parse the cpu argument to an integer value in millicores. diff --git a/sdk/src/beta9/abstractions/mixins.py b/sdk/src/beta9/abstractions/mixins.py index 39dd7c7c2..d9140f94c 100644 --- a/sdk/src/beta9/abstractions/mixins.py +++ b/sdk/src/beta9/abstractions/mixins.py @@ -1,16 +1,20 @@ +import urllib.parse from typing import Any, Callable, ClassVar, Optional from .. import terminal -from ..clients.gateway import DeployStubRequest, DeployStubResponse +from ..abstractions.base.runner import SHELL_STUB_TYPE +from ..channel import with_grpc_error_handling +from ..clients.gateway import DeployStubRequest, DeployStubResponse, GetUrlRequest, GetUrlResponse +from ..clients.shell import CreateShellRequest from ..config import ConfigContext from .base.runner import RunnerAbstraction +from .shell import SSHShell class DeployableMixin: func: Callable parent: RunnerAbstraction deployment_id: Optional[str] = None - deployment_stub_type: ClassVar[str] def _validate(self): @@ -60,3 +64,51 @@ def deploy( self.parent.print_invocation_snippet(**invocation_details_options) return deploy_response.ok + + @with_grpc_error_handling + def shell(self, url_type: str = ""): + stub_type = SHELL_STUB_TYPE + + if not self.parent.prepare_runtime( + func=self.func, stub_type=stub_type, force_create_stub=True + ): + return False + + # First, spin up the shell container + with terminal.progress("Creating shell..."): + create_shell_response = self.parent.shell_stub.create_shell( + CreateShellRequest( + stub_id=self.parent.stub_id, + ) + ) + if not create_shell_response.ok: + return terminal.error(f"Failed to create shell: {create_shell_response.err_msg} ❌") + + # Then, we can retrieve the URL and issue a CONNECT request / establish a tunnel + res: GetUrlResponse = self.parent.gateway_stub.get_url( + GetUrlRequest( + stub_id=self.parent.stub_id, + deployment_id=getattr(self, "deployment_id", ""), + url_type=url_type, + ) + ) + if not res.ok: + return terminal.error(f"Failed to get shell connection URL: {res.err_msg} ❌") + + # Parse the URL to extract the container_id + parsed_url = urllib.parse.urlparse(res.url) + proxy_host, proxy_port = parsed_url.hostname, parsed_url.port + container_id = create_shell_response.container_id + ssh_token = create_shell_response.token + + with SSHShell( + host=proxy_host, + port=proxy_port, + path=parsed_url.path, + container_id=container_id, + stub_id=self.parent.stub_id, + auth_token=self.parent.config_context.token, + username="root", + password=ssh_token, + ) as shell: + shell.start() diff --git a/sdk/src/beta9/abstractions/shell.py b/sdk/src/beta9/abstractions/shell.py new file mode 100644 index 000000000..03fdbe827 --- /dev/null +++ b/sdk/src/beta9/abstractions/shell.py @@ -0,0 +1,342 @@ +import os +import socket +import struct +import sys +import time +import traceback +from dataclasses import dataclass +from typing import Optional + +from .. import terminal +from ..env import is_local + +if is_local(): + import paramiko + + +def create_connect_tunnel( + proxy_host: str, proxy_port: int, path: str, stub_id: str, container_id: str, auth_token: str +) -> socket.socket: + """ + 1. Connect to the proxy_host:proxy_port over TCP. + 2. Send an HTTP CONNECT request for the correct path. + 3. If we get a 200 response, return the socket as a raw tunnel. + """ + + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect((proxy_host, proxy_port)) + + # Construct the correct CONNECT request path + connect_path = f"{path}/{container_id}" + + connect_req = ( + f"CONNECT {connect_path} HTTP/1.1\r\n" + f"Host: {proxy_host}:{proxy_port}\r\n" + f"Proxy-Connection: Keep-Alive\r\n" + f"Authorization: Bearer {auth_token}\r\n" + f"\r\n" + ) + s.sendall(connect_req.encode("ascii")) + + response = b"" + while b"\r\n\r\n" not in response: + chunk = s.recv(4096) + if not chunk: + break + response += chunk + + response_str = response.decode("ascii", errors="replace") + if "200 OK" not in response_str: + s.close() + raise ConnectionError(f"CONNECT failed. Response:\n{response_str}") + + # If we reach here, we have a raw TCP tunnel to "container_id" + return s + + +@dataclass +class SSHShell: + """Interactive ssh shell that can be used as a context manager - for use with 'shell' command""" + + host: str + port: int + path: str + container_id: str + stub_id: str + auth_token: str + username: str + password: str + transport: Optional["paramiko.Transport"] = None + + def _open(self): + self.socket: Optional[socket.socket] = None + self.channel: Optional["paramiko.Channel"] = None + + try: + self.socket = create_connect_tunnel( + self.host, + self.port, + self.path, + self.stub_id, + self.container_id, + self.auth_token, + ) + except BaseException: + terminal.error("Failed to establish ssh tunnel") + + self.transport = paramiko.Transport( + self.socket + ) # Initialize a transport with the tunnel socket + + self.transport.start_client() + self.transport.auth_password(self.username, self.password) + self.channel = self.transport.open_session() + + # Get terminal size - https://stackoverflow.com/a/943921 + rows, columns = os.popen("stty size", "r").read().split() + + self.channel.get_pty( + term=os.getenv("TERM", "xterm-256color"), width=int(columns), height=int(rows) + ) + self.channel.invoke_shell() + + def _close(self): + if self.channel: + self.channel.close() + + if self.transport: + self.transport.close() + + if self.socket: + try: + # Set SO_LINGER to zero to forcefully close the socket + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0)) + self.socket.shutdown(socket.SHUT_RDWR) + except OSError: + pass # Ignore any errors that occur after the socket is already closed + finally: + self.socket.close() + + def __enter__(self): + try: + with terminal.progress("Connecting..."): + self._open() + except paramiko.SSHException: + self._close() + terminal.error(f"SSH error occurred: {traceback.format_exc()}") + except BaseException: + self._close() + terminal.error(f"Unexpected error occurred: {traceback.format_exc()}") + + return self + + def __exit__(self, exception_type, exception_value, traceback): + self._close() + + def start(self): + """Start the interactive shell session.""" + try: + interactive_shell(self.channel) + + # Check the exit status after the shell session ends + exit_status = self.channel.recv_exit_status() + if exit_status != 0: + terminal.warn("Lost connection to shell, attempting to reconnect in 5 seconds...") + time.sleep(5) + + with terminal.progress("Connecting..."): + self._open() + + self.start() + + except paramiko.SSHException: + self._close() + terminal.error(f"SSH error occurred in shell: {traceback.format_exc()}") + except BaseException: + self._close() + terminal.error(f"Unexpected error occurred in shell: {traceback.format_exc()}") + + +""" + NOTE: much of the interactive shell code below is pulled from paramiko's examples, with a few slight modifications for use here. + Original license / source information is as follows: +""" +# Source: https://github.com/paramiko/paramiko/blob/main/demos/interactive.py + +# Copyright (C) 2003-2007 Robey Pointer +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +# https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Bracketed-Paste-Mode +START_PASTE = "\x1b\x5b\x32\x30\x30\x7e" # ESC[200~ +END_PASTE = "\x1b\x5b\x32\x30\x31\x7e" # ESC[201~ + +# windows does not have termios... +try: + import termios + import tty + + has_termios = True +except ImportError: + has_termios = False + + +def is_int(val: str) -> bool: + try: + int(val) + return True + except Exception: + return False + + +def interactive_shell(chan: "paramiko.Channel"): + if has_termios: + posix_shell(chan) + else: + windows_shell(chan) + + +def posix_readkey() -> str: + """Get a keypress. If an escaped key is pressed, the full sequence is + read and returned. + + Copied from readchar: + https://github.com/magmax/python-readchar/blob/master/readchar/_posix_read.py#L30 + """ + + c1 = sys.stdin.read(1) + + if c1 != "\x1b": # ESC + return c1 + + c2 = sys.stdin.read(1) + if c2 not in "\x4f\x5b": # O[ + return c1 + c2 + + c3 = sys.stdin.read(1) + if c3 not in "\x31\x32\x33\x35\x36": # 12356 + return c1 + c2 + c3 + + c4 = sys.stdin.read(1) + if c4 not in "\x30\x31\x33\x34\x35\x37\x38\x39": # 01345789 + return c1 + c2 + c3 + c4 + + c5 = sys.stdin.read(1) + key = c1 + c2 + c3 + c4 + c5 + + # Bracketed Paste Mode: # https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Bracketed-Paste-Mode + if key == START_PASTE[:-1] or key == END_PASTE[:-1]: + c6 = sys.stdin.read(1) + return key + c6 + + return key + + +def windows_readkey() -> str: + """Reads the next keypress. If an escaped key is pressed, the full + sequence is read and returned. + + Copied from readchar: + https://github.com/magmax/python-readchar/blob/master/readchar/_win_read.py#LL14C1-L30C24 + """ + + ch = sys.stdin.read(1) + + # if it is a normal character: + if ch not in "\x00\xe0": + return ch + + # if it is a scpeal key, read second half: + ch2 = sys.stdin.read(1) + + return "\x00" + ch2 + + +def posix_shell(chan: "paramiko.Channel"): # noqa: C901 + import select + + oldtty = termios.tcgetattr(sys.stdin) + + try: + tty.setraw(sys.stdin.fileno()) + tty.setcbreak(sys.stdin.fileno()) + chan.settimeout(0.0) + while True: + r, w, e = select.select([chan, sys.stdin], [], []) + if chan in r: + try: + x = chan.recv(1024).decode() + if len(x) == 0: + sys.stdout.write("\r\n") + break + sys.stdout.write(x) + sys.stdout.flush() + except socket.timeout: + pass + if sys.stdin in r: + key = posix_readkey() + # When pasting something, we need to read the entire pasted blob at once + # Otherwise it'll hang until the next key press. + # This has to do with how 'select.select' detects changes. + # A paste is a single event of many characters, so we must handle them all as one event + if key == START_PASTE: + # Start reading the pasted text + key = posix_readkey() + # Until we reach the end of the pasted text + while key != END_PASTE: + chan.send(key) + key = posix_readkey() + # We've exhausted the paste event, wait for next event + continue + + if len(key) == 0: + break + chan.send(key) + + finally: + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty) + + +# thanks to Mike Looijmans for this code +def windows_shell(chan: "paramiko.Channel"): + import threading + + sys.stdout.write("Line-buffered terminal emulation. Press F6 or ^Z to send EOF.\r\n\r\n") + + def writeall(sock): + while True: + data = sock.recv(256) + if not data: + sys.stdout.write("\r\n*** EOF ***\r\n\r\n") + sys.stdout.flush() + break + sys.stdout.write(data) + sys.stdout.flush() + + writer = threading.Thread(target=writeall, args=(chan,)) + writer.start() + + try: + while True: + d = windows_readkey() + if not d: + break + chan.send(d) + except EOFError: + # user hit ^Z or F6 + pass diff --git a/sdk/src/beta9/cli/main.py b/sdk/src/beta9/cli/main.py index 8a7168825..a553eadb2 100644 --- a/sdk/src/beta9/cli/main.py +++ b/sdk/src/beta9/cli/main.py @@ -16,6 +16,7 @@ pool, secret, serve, + shell, task, token, volume, @@ -100,6 +101,7 @@ def load_cli(check_config=True, **kwargs: Any) -> CLI: cli.register(secret) cli.register(token) cli.register(worker) + cli.register(shell) if check_config: cli.check_config() diff --git a/sdk/src/beta9/cli/shell.py b/sdk/src/beta9/cli/shell.py new file mode 100644 index 000000000..f0e7d5893 --- /dev/null +++ b/sdk/src/beta9/cli/shell.py @@ -0,0 +1,79 @@ +import importlib +import os +import sys +from pathlib import Path + +import click + +from .. import terminal +from ..channel import ServiceClient +from ..cli import extraclick +from .extraclick import ClickCommonGroup + + +@click.group(cls=ClickCommonGroup) +def common(**_): + pass + + +@common.command( + name="shell", + help=""" + Connect to a container with the same config as your handler. + + ENTRYPOINT is in the format of "file:function". + """, + epilog=""" + Examples: + + {cli_name} shell app.py:handler + + {cli_name} shell app.py:my_func + \b + """, +) +@click.argument( + "entrypoint", + nargs=1, + required=True, +) +@click.option( + "--url-type", + help="The type of URL to get back. [default is determined by the server] ", + type=click.Choice(["host", "path"]), +) +@extraclick.pass_service_client +@click.pass_context +def shell( + ctx: click.Context, + service: ServiceClient, + entrypoint: str, + url_type: str = "path", +): + current_dir = os.getcwd() + if current_dir not in sys.path: + sys.path.insert(0, current_dir) + + module_path, obj_name, *_ = entrypoint.split(":") if ":" in entrypoint else (entrypoint, "") + module_name = module_path.replace(".py", "").replace(os.path.sep, ".") + + if not Path(module_path).exists(): + terminal.error(f"Unable to find file: '{module_path}'") + + if not obj_name: + terminal.error( + "Invalid handler function specified. Expected format: beam shell [file.py]:[function]" + ) + + module = importlib.import_module(module_name) + + user_obj = getattr(module, obj_name, None) + if user_obj is None: + terminal.error( + f"Invalid handler function specified. Make sure '{module_path}' contains the function: '{obj_name}'" + ) + + if hasattr(user_obj, "set_handler"): + user_obj.set_handler(f"{module_name}:{obj_name}") + + user_obj.shell(url_type=url_type) # type:ignore diff --git a/sdk/src/beta9/clients/shell/__init__.py b/sdk/src/beta9/clients/shell/__init__.py new file mode 100644 index 000000000..000ea4476 --- /dev/null +++ b/sdk/src/beta9/clients/shell/__init__.py @@ -0,0 +1,46 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# sources: shell.proto +# plugin: python-betterproto +# This file has been @generated + +from dataclasses import dataclass +from typing import ( + TYPE_CHECKING, + Dict, + Optional, +) + +import betterproto +import grpc +from betterproto.grpcstub.grpcio_client import SyncServiceStub +from betterproto.grpcstub.grpclib_server import ServiceBase + + +if TYPE_CHECKING: + import grpclib.server + from betterproto.grpcstub.grpclib_client import MetadataLike + from grpclib.metadata import Deadline + + +@dataclass(eq=False, repr=False) +class CreateShellRequest(betterproto.Message): + stub_id: str = betterproto.string_field(1) + + +@dataclass(eq=False, repr=False) +class CreateShellResponse(betterproto.Message): + ok: bool = betterproto.bool_field(1) + container_id: str = betterproto.string_field(2) + token: str = betterproto.string_field(3) + err_msg: str = betterproto.string_field(4) + + +class ShellServiceStub(SyncServiceStub): + def create_shell( + self, create_shell_request: "CreateShellRequest" + ) -> "CreateShellResponse": + return self._unary_unary( + "/shell.ShellService/CreateShell", + CreateShellRequest, + CreateShellResponse, + )(create_shell_request)