From 121937706d48c8f8c35eb574e6c4d8e8222d78e3 Mon Sep 17 00:00:00 2001 From: Diego Tavares Date: Fri, 9 Aug 2024 15:38:59 -0700 Subject: [PATCH 1/4] Rest gateway (#1449) Merge changes from local fork into ASWF repo --------- Signed-off-by: Diego Tavares --- .gitignore | 1 + VERSION.in | 2 +- proto/comment.proto | 2 + proto/criterion.proto | 1 + proto/cue.proto | 2 + proto/department.proto | 2 + proto/depend.proto | 1 + proto/facility.proto | 1 + proto/filter.proto | 1 + proto/host.proto | 1 + proto/job.proto | 2 + proto/limit.proto | 2 + proto/renderPartition.proto | 1 + proto/report.proto | 2 + proto/rqd.proto | 2 + proto/service.proto | 1 + proto/show.proto | 2 + proto/subscription.proto | 2 + proto/task.proto | 2 + rest_gateway/Dockerfile | 68 ++++++++ rest_gateway/README.md | 163 ++++++++++++++++++ rest_gateway/opencue_gateway/gateway.go | 8 + rest_gateway/opencue_gateway/main.go | 88 ++++++++++ .../k8s/kustomize/base/rest-gateway.yaml | 54 ++++++ 24 files changed, 410 insertions(+), 1 deletion(-) create mode 100644 rest_gateway/Dockerfile create mode 100644 rest_gateway/README.md create mode 100644 rest_gateway/opencue_gateway/gateway.go create mode 100644 rest_gateway/opencue_gateway/main.go create mode 100644 spideploy/k8s/kustomize/base/rest-gateway.yaml diff --git a/.gitignore b/.gitignore index 370dbd6b1..61f559333 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ venv*/ .coverage coverage.xml htmlcov/ +rest_gateway/*.tar\.gz /.env .envrc .vscode diff --git a/VERSION.in b/VERSION.in index 61d2f3576..c74e8a041 100644 --- a/VERSION.in +++ b/VERSION.in @@ -1 +1 @@ -0.34 +0.35 diff --git a/proto/comment.proto b/proto/comment.proto index 5538aa00d..ee0c36aac 100644 --- a/proto/comment.proto +++ b/proto/comment.proto @@ -5,6 +5,8 @@ package comment; option java_package = "com.imageworks.spcue.grpc.comment"; option java_multiple_files = true; +option go_package = "opencue_gateway/gen/go"; + // The Comment class contains comment data for any entity that supports // commenting. Currently these are [Job] and [Host]. diff --git a/proto/criterion.proto b/proto/criterion.proto index 079f3dc2a..b6f77a41a 100644 --- a/proto/criterion.proto +++ b/proto/criterion.proto @@ -4,6 +4,7 @@ package criterion; option java_package = "com.imageworks.spcue.grpc.criterion"; option java_multiple_files = true; +option go_package = "opencue_gateway/gen/go"; // -------- Primary Message Types --------] diff --git a/proto/cue.proto b/proto/cue.proto index d1c2fe33d..ce5aba553 100644 --- a/proto/cue.proto +++ b/proto/cue.proto @@ -5,6 +5,8 @@ package cue; option java_package = "com.imageworks.spcue.grpc.cue"; option java_multiple_files = true; +option go_package = "opencue_gateway/gen/go"; + // Core System level objects and methods diff --git a/proto/department.proto b/proto/department.proto index 59fb98690..dd2af3183 100644 --- a/proto/department.proto +++ b/proto/department.proto @@ -5,6 +5,8 @@ package department; option java_package = "com.imageworks.spcue.grpc.department"; option java_multiple_files = true; +option go_package = "opencue_gateway/gen/go"; + import "task.proto"; // Departments diff --git a/proto/depend.proto b/proto/depend.proto index 70220968a..edbbe58f6 100644 --- a/proto/depend.proto +++ b/proto/depend.proto @@ -5,6 +5,7 @@ package depend; option java_package = "com.imageworks.spcue.grpc.depend"; option java_multiple_files = true; +option go_package = "opencue_gateway/gen/go"; // -------- Services -------- diff --git a/proto/facility.proto b/proto/facility.proto index ae7f3d4b8..1bce07627 100644 --- a/proto/facility.proto +++ b/proto/facility.proto @@ -7,6 +7,7 @@ option java_multiple_files = true; import "host.proto"; import "subscription.proto"; +option go_package = "opencue_gateway/gen/go"; // -------- Services -------- diff --git a/proto/filter.proto b/proto/filter.proto index 184097782..7425ff1bc 100644 --- a/proto/filter.proto +++ b/proto/filter.proto @@ -7,6 +7,7 @@ option java_multiple_files = true; import "job.proto"; +option go_package = "opencue_gateway/gen/go"; // -------- Services -------- diff --git a/proto/host.proto b/proto/host.proto index 381d46b20..0aa41cbb6 100644 --- a/proto/host.proto +++ b/proto/host.proto @@ -10,6 +10,7 @@ import "criterion.proto"; import "job.proto"; import "renderPartition.proto"; +option go_package = "opencue_gateway/gen/go"; // -------- Services --------] diff --git a/proto/job.proto b/proto/job.proto index 2149148b6..0962d7896 100644 --- a/proto/job.proto +++ b/proto/job.proto @@ -5,6 +5,8 @@ package job; option java_package = "com.imageworks.spcue.grpc.job"; option java_multiple_files = true; +option go_package = "opencue_gateway/gen/go"; + import "comment.proto"; import "depend.proto"; import "limit.proto"; diff --git a/proto/limit.proto b/proto/limit.proto index 28a30cecb..ddc2cc108 100644 --- a/proto/limit.proto +++ b/proto/limit.proto @@ -5,6 +5,8 @@ package limit; option java_package = "com.imageworks.spcue.grpc.limit"; option java_multiple_files = true; +option go_package = "opencue_gateway/gen/go"; + // The Limit class contains data that allows users to place limits // on the maximum number of running frames for a certain type of layer. diff --git a/proto/renderPartition.proto b/proto/renderPartition.proto index c8553ed3c..29c64e9c2 100644 --- a/proto/renderPartition.proto +++ b/proto/renderPartition.proto @@ -5,6 +5,7 @@ package renderPartition; option java_package = "com.imageworks.spcue.grpc.renderpartition"; option java_multiple_files = true; +option go_package = "opencue_gateway/gen/go"; // -------- Services --------] diff --git a/proto/report.proto b/proto/report.proto index 441f810c3..5ba8f9253 100644 --- a/proto/report.proto +++ b/proto/report.proto @@ -5,6 +5,8 @@ package report; option java_package = "com.imageworks.spcue.grpc.report"; option java_multiple_files = true; +option go_package = "opencue_gateway/gen/go"; + import "host.proto"; // Interface to handle RQD pings. diff --git a/proto/rqd.proto b/proto/rqd.proto index f6e0d8790..1ff75a4be 100644 --- a/proto/rqd.proto +++ b/proto/rqd.proto @@ -5,6 +5,8 @@ package rqd; option java_package = "com.imageworks.spcue.grpc.rqd"; option java_multiple_files = true; +option go_package = "opencue_gateway/gen/go"; + import "report.proto"; // Interface for issuing commands to an RQD instance. diff --git a/proto/service.proto b/proto/service.proto index 57110bdaf..2c0a74774 100644 --- a/proto/service.proto +++ b/proto/service.proto @@ -5,6 +5,7 @@ package service; option java_package = "com.imageworks.spcue.grpc.service"; option java_multiple_files = true; +option go_package = "opencue_gateway/gen/go"; // Interface for managing Service objects diff --git a/proto/show.proto b/proto/show.proto index f0fbf34b4..8f2134b1f 100644 --- a/proto/show.proto +++ b/proto/show.proto @@ -5,6 +5,8 @@ package show; option java_package = "com.imageworks.spcue.grpc.show"; option java_multiple_files = true; +option go_package = "opencue_gateway/gen/go"; + import "department.proto"; import "filter.proto"; import "host.proto"; diff --git a/proto/subscription.proto b/proto/subscription.proto index 8ac100e7e..399cc1d91 100644 --- a/proto/subscription.proto +++ b/proto/subscription.proto @@ -5,6 +5,8 @@ package subscription; option java_package = "com.imageworks.spcue.grpc.subscription"; option java_multiple_files = true; +option go_package = "opencue_gateway/gen/go"; + // Subscriptions // A subscription is what a show sets up when they want to use hosts in // an allocation. diff --git a/proto/task.proto b/proto/task.proto index 647a937e5..23fc55783 100644 --- a/proto/task.proto +++ b/proto/task.proto @@ -5,6 +5,8 @@ package task; option java_package = "com.imageworks.spcue.grpc.task"; option java_multiple_files = true; +option go_package = "opencue_gateway/gen/go"; + // Tasks // Tasks are shot priorities for a specific dept diff --git a/rest_gateway/Dockerfile b/rest_gateway/Dockerfile new file mode 100644 index 000000000..e03c3fb05 --- /dev/null +++ b/rest_gateway/Dockerfile @@ -0,0 +1,68 @@ +# Build from project root +FROM rockylinux:9.3 as rocky-golang + +WORKDIR /src/go +RUN dnf install -y 'dnf-command(config-manager)' && dnf config-manager --set-enabled crb + +# WARN: Download do tarball and update file path accordingly +COPY rest_gateway/Go_1.22.2_Linux_ARM64.tar.gz golang.tar.gz + +RUN tar -C /usr/local -xzf golang.tar.gz +ENV PATH="$PATH:/usr/local/go/bin" + +# WARN: Uncoment if your environment requires a proxy +#ENV GOPROXY=artifactory.yourcompany.com/go-proxy +ENV GO111MODULE=on +ENV GOSUMDB=off + +RUN go version + +RUN rm -rf /src/go + +FROM rocky-golang AS build + +RUN dnf install -y \ + git \ + protobuf-compiler \ + && dnf clean all +WORKDIR /app +ENV PATH=$PATH:/root/go/bin:/opt/protobuf3/usr/bin/ + +# Copy all of the staged files (protos plus go source) +COPY ./proto /app/proto +COPY ./rest_gateway/opencue_gateway /app/opencue_gateway +# COPY ./lib /app/lib + + +WORKDIR /app/opencue_gateway +RUN go mod init opencue_gateway && go mod tidy + +RUN go install \ + github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \ + github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 \ + google.golang.org/protobuf/cmd/protoc-gen-go \ + google.golang.org/grpc/cmd/protoc-gen-go-grpc + +# Generate go grpc code +RUN mkdir -p gen/go && \ + protoc -I ../proto/ \ + --go_out ./gen/go/ \ + --go_opt paths=source_relative \ + --go-grpc_out ./gen/go/ \ + --go-grpc_opt paths=source_relative \ + ../proto/*.proto + +# Generate grpc-gateway handlers +RUN protoc -I ../proto/ --grpc-gateway_out ./gen/go \ + --grpc-gateway_opt paths=source_relative \ + --grpc-gateway_opt generate_unbound_methods=true \ + ../proto/*.proto + +# Build project +RUN go build -o grpc_gateway main.go + +FROM rockylinux:9.3 +COPY --from=build /app/opencue_gateway/grpc_gateway /app/ + +EXPOSE 8448 +ENTRYPOINT ["/app/grpc_gateway"] diff --git a/rest_gateway/README.md b/rest_gateway/README.md new file mode 100644 index 000000000..db358f543 --- /dev/null +++ b/rest_gateway/README.md @@ -0,0 +1,163 @@ +# Opencue Rest Gateway + +A gateway to provide a REST endpoint to opencue gRPC API. + +## How does it work + +This is a go serviced based on the official [grpc-gateway project](https://github.com/grpc-ecosystem/grpc-gateway) +that compiles opencue's proto files into a go service that provides a REST interface and redirect calls to the +grpc endpoint. + +## Running the service + +Running the service is very simple: + * Read and modify the rest_gateway/Dockerfile according to your environment and build the gateway image using docker. + * Run the image providing the environment variable `CUEBOT_ENDPOINT=your.cuebot.server:8443` + +## REST interface + +All service rpc calls are accessible: + * HTTP method is POST + * URI path is built from the service’s name and method: // (e.g.: /show.ShowInterface/FindShow) + * HTTP body is a JSON with the request object: e.g.: + ```proto + message ShowFindShowRequest { + string name = 1; + } + ``` + becomes: + ```json + { + "name": "value for name" + } + ``` + * HTTP response is a JSON object with the formatted response + +### Example (getting a show): +show.proto: +```proto +service ShowInterface { + // Find a show with the specified name. + rpc FindShow(ShowFindShowRequest) returns (ShowFindShowResponse); +} + +message ShowFindShowRequest { + string name = 1; +} +message ShowFindShowResponse { + Show show = 1; +} +message Show { + string id = 1; + string name = 2; + float default_min_cores = 3; + float default_max_cores = 4; + string comment_email = 5; + bool booking_enabled = 6; + bool dispatch_enabled = 7; + bool active = 8; + ShowStats show_stats = 9; + float default_min_gpus = 10; + float default_max_gpus = 11; +} +``` +request (gateway running on `http://opencue-gateway.apps.com`): +```bash +curl -i -X POST http://opencue-gateway.apps.com/show.ShowInterface/FindShow -d '{"name": "ashow"}` +``` +response +```bash +HTTP/1.1 200 OK +Content-Type: application/json +Grpc-Metadata-Content-Type: application/grpc +Grpc-Metadata-Grpc-Accept-Encoding: gzip +Date: Tue, 12 Dec 2023 18:05:18 GMT +Content-Length: 501 + +{"show":{"id":"00000000-0000-0000-0000-99999999999999","name":"ashow","defaultMinCores":1,"defaultMaxCores":10,"commentEmail":"middle-tier@imageworks.com","bookingEnabled":true,"dispatchEnabled":true,"active":true,"showStats":{"runningFrames":75,"deadFrames":14,"pendingFrames":1814,"pendingJobs":175,"createdJobCount":"2353643","createdFrameCount":"10344702","renderedFrameCount":"9733366","failedFrameCount":"1096394","reservedCores":252,"reservedGpus":0},"defaultMinGpus":100,"defaultMaxGpus":100000}} +``` + +### Example (getting frames for a job): +job.proto: +```proto +service JobInterface { + // Returns all frame objects that match FrameSearchCriteria + rpc GetFrames(JobGetFramesRequest) returns (JobGetFramesResponse); +} + +message JobGetFramesRequest { + Job job = 1; + FrameSearchCriteria req = 2; +} + +message Job { + string id = 1; + JobState state = 2; + string name = 3; + string shot = 4; + string show = 5; + string user = 6; + string group = 7; + string facility = 8; + string os = 9; + oneof uid_optional { + int32 uid = 10; + } + int32 priority = 11; + float min_cores = 12; + float max_cores = 13; + string log_dir = 14; + bool is_paused = 15; + bool has_comment = 16; + bool auto_eat = 17; + int32 start_time = 18; + int32 stop_time = 19; + JobStats job_stats = 20; + float min_gpus = 21; + float max_gpus = 22; +} + +// Object for frame searching +message FrameSearchCriteria { + repeated string ids = 1; + repeated string frames = 2; + repeated string layers = 3; + FrameStateSeq states = 4; + string frame_range = 5; + string memory_range = 6; + string duration_range = 7; + int32 page = 8; + int32 limit = 9; + int32 change_date = 10; + int32 max_results = 11; + int32 offset = 12; + bool include_finished = 13; +} + +message JobGetFramesResponse { + FrameSeq frames = 1; +} + +// A sequence of Frames +message FrameSeq { + repeated Frame frames = 1; +} +``` +request (gateway running on `http://opencue-gateway.apps.com`): + +Note: it is important to include 'page' and 'limit' when getting frames for a job. +```bash +curl -i -X POST http://opencue-gateway.apps.com/job.JobInterface/GetFrames -d '{"job":{"id":"9999999999-b8d7-9999-a29c-99999999999999"}, "req": {"include_finished":true,"page":1,"limit":100}}' +``` +response +```bash +HTTP/1.1 200 OK +content-type: application/json +grpc-metadata-content-type: application/grpc +grpc-metadata-grpc-accept-encoding: gzip +date: Tue, 13 Feb 2024 17:15:49 GMT +transfer-encoding: chunked +set-cookie: 3d3a38cc45d028e42e93031e0ccc9b1e=534d34fde72242856a7fdadc27260929; path=/; HttpOnly + +{"frames":{"frames":[{"id":"9999999", "name":"0001-some_frame_0999990", "layerName":"h", "number":1, "state":"WAITING", "retryCount":0, "exitStatus":-1, "dispatchOrder":0, "startTime":0, "stopTime":0, "maxRss":"0", "usedMemory":"0", "reservedMemory":"0", "reservedGpuMemory":"0", "lastResource":"/0.00/0", "checkpointState":"DISABLED", "checkpointCount":0, "totalCoreTime":0, "lluTime":1707842141, "totalGpuTime":0, "maxGpuMemory":"0", "usedGpuMemory":"0", "frameStateDisplayOverride":null}, {"id":"10fa17d4-9313-4924-86f5-380c5b2a25d8", "name":"0002-some_frame_0999990", "layerName":"some_frame_0999990", "number":2, "state":"WAITING", "retryCount":0, "exitStatus":-1, "dispatchOrder":1, "startTime":0, "stopTime":0, "maxRss":"0", "usedMemory":"0", "reservedMemory":"0", "reservedGpuMemory":"0", "lastResource":"/0.00/0", "checkpointState":"DISABLED", "checkpointCount":0, "totalCoreTime":0, "lluTime":1707842141, "totalGpuTime":0, "maxGpuMemory":"0", "usedGpuMemory":"0", "frameStateDisplayOverride":null}, {"id":"8d39c602-0b27-4b1e-a09b-4fa35db40e55", "name":"0003-some_frame_0999990", "layerName":"some_frame_0999990", "number":3, "state":"WAITING", "retryCount":0, "exitStatus":-1, "dispatchOrder":2, "startTime":0, "stopTime":0, "maxRss":"0", "usedMemory":"0", "reservedMemory":"0", "reservedGpuMemory":"0", "lastResource":"/0.00/0", "checkpointState":"DISABLED", "checkpointCount":0, "totalCoreTime":0, "lluTime":1707842141, "totalGpuTime":0, "maxGpuMemory":"0", "usedGpuMemory":"0", "frameStateDisplayOverride":null}, {"id":"d418a837-e974-4716-9105-296f495bc407", "name":"0004-some_frame_0999990", "layerName":"some_frame_0999990", "number":4, "state":"WAITING", "retryCount":0, "exitStatus":-1, "dispatchOrder":3, "startTime":0, "stopTime":0, "maxRss":"0", "usedMemory":"0", "reservedMemory":"0", "reservedGpuMemory":"0", "lastResource":"/0.00/0", "checkpointState":"DISABLED", "checkpointCount":0, "totalCoreTime":0, "lluTime":1707842141, "totalGpuTime":0, "maxGpuMemory":"0", "usedGpuMemory":"0", "frameStateDisplayOverride":null}, {"id":"d2113372-99999-4c05-8100-9999999", "name":"0005-some_frame_0999990", "layerName":"some_frame_0999990", "number":5, "state":"WAITING", "retryCount":0, "exitStatus":-1, "dispatchOrder":4, "startTime":0, "stopTime":0, "maxRss":"0", "usedMemory":"0", "reservedMemory":"0", "reservedGpuMemory":"0", "lastResource":"/0.00/0", "checkpointState":"DISABLED", "checkpointCount":0, "totalCoreTime":0, "lluTime":1707842141, "totalGpuTime":0, "maxGpuMemory":"0", "usedGpuMemory":"0", "frameStateDisplayOverride":null}]}} +``` \ No newline at end of file diff --git a/rest_gateway/opencue_gateway/gateway.go b/rest_gateway/opencue_gateway/gateway.go new file mode 100644 index 000000000..86d3c81ce --- /dev/null +++ b/rest_gateway/opencue_gateway/gateway.go @@ -0,0 +1,8 @@ +package tools + +import ( + _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway" + _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2" + _ "google.golang.org/grpc/cmd/protoc-gen-go-grpc" + _ "google.golang.org/protobuf/cmd/protoc-gen-go" +) \ No newline at end of file diff --git a/rest_gateway/opencue_gateway/main.go b/rest_gateway/opencue_gateway/main.go new file mode 100644 index 000000000..55f8355a3 --- /dev/null +++ b/rest_gateway/opencue_gateway/main.go @@ -0,0 +1,88 @@ +package main +import ( + "context" + "flag" + "net/http" + "os" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/grpclog" + + gw "opencue_gateway/gen/go" // Update + ) + +func getEnv(key, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} + +func run() error { + grpcServerEndpoint := getEnv("CUEBOT_ENDPOINT", "opencuetest01.your.test.server:8443") + port := getEnv("REST_PORT", "8448") + + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + // Register gRPC server endpoint + // Note: Make sure the gRPC server is running properly and accessible + mux := runtime.NewServeMux() + opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} + + // show.proto + errShow := gw.RegisterShowInterfaceHandlerFromEndpoint(ctx, mux, grpcServerEndpoint, opts) + if errShow != nil { + return errShow + } + + errFrame := gw.RegisterFrameInterfaceHandlerFromEndpoint(ctx, mux, grpcServerEndpoint, opts) + if errFrame != nil { + return errFrame + } + errGroup := gw.RegisterGroupInterfaceHandlerFromEndpoint(ctx, mux, grpcServerEndpoint, opts) + if errGroup != nil { + return errGroup + } + errJob := gw.RegisterJobInterfaceHandlerFromEndpoint(ctx, mux, grpcServerEndpoint, opts) + if errJob != nil { + return errJob + } + errLayer := gw.RegisterLayerInterfaceHandlerFromEndpoint(ctx, mux, grpcServerEndpoint, opts) + if errLayer != nil { + return errLayer + } + + // host.proto + errDeed := gw.RegisterDeedInterfaceHandlerFromEndpoint(ctx, mux, grpcServerEndpoint, opts) + if errDeed != nil { + return errDeed + } + errHost := gw.RegisterHostInterfaceHandlerFromEndpoint(ctx, mux, grpcServerEndpoint, opts) + if errHost != nil { + return errHost + } + errOwner := gw.RegisterOwnerInterfaceHandlerFromEndpoint(ctx, mux, grpcServerEndpoint, opts) + if errOwner != nil { + return errOwner + } + errProc := gw.RegisterProcInterfaceHandlerFromEndpoint(ctx, mux, grpcServerEndpoint, opts) + if errProc != nil { + return errProc + } + + + // Start HTTP server (and proxy calls to gRPC server endpoint) + return http.ListenAndServe(":" + port, mux) +} + +func main() { + flag.Parse() + + if err := run(); err != nil { + grpclog.Fatal(err) + } +} \ No newline at end of file diff --git a/spideploy/k8s/kustomize/base/rest-gateway.yaml b/spideploy/k8s/kustomize/base/rest-gateway.yaml new file mode 100644 index 000000000..bdb099654 --- /dev/null +++ b/spideploy/k8s/kustomize/base/rest-gateway.yaml @@ -0,0 +1,54 @@ +kind: Deployment +apiVersion: apps/v1 +metadata: + name: opencue-rest-gateway + namespace: opencue + labels: + app: opencue + component: rest-gateway +spec: + replicas: 1 + selector: + matchLabels: + app: opencue + component: rest-gateway + template: + metadata: + creationTimestamp: null + labels: + app: opencue + component: rest-gateway + spec: + containers: + - name: rest-gateway + imagePullPolicy: Always + terminationMessagePolicy: File + ports: + - containerPort: 8448 + image: docker-local.artifactory.spimageworks.com/gitlab/spi/dev/infrastructure/api/opencue/rest_gateway:latest + env: + - name: CUEBOT_ENDPOINT + values: cuebot-grpc.opencue.svc.cluster.local:8443 + resources: + limits: + cpu: "1000m" + memory: "2Gi" + requests: + cpu: "200m" + memory: "1Gi" + restartPolicy: Always + terminationGracePeriodSeconds: 10 +--- +apiVersion: v1 +kind: Service +metadata: + name: rest-gateway-service + namespace: opencue +spec: + selector: + app: opencue + component: rest-gateway + ports: + - protocol: TCP + port: 8448 + targetPort: 8448 \ No newline at end of file From db5a805e4cb92041bebd504e6ea5bd8a6a1cb71d Mon Sep 17 00:00:00 2001 From: Zach-Fong Date: Fri, 9 Aug 2024 16:57:35 -0700 Subject: [PATCH 2/4] [rest_gateway] Add authentication to gRPC REST gateway and include unit testing (#1453) Features and improvements 1. JWT authentication for gRPC REST gateway - Integrated JWT authentication for secure communication. - Implemented middleware to validate JWT tokens from HTTP requests' authorization headers. 2. Include unit testing - Added Go tests for the REST gateway to ensure authentication middleware functionality and error handling. Co-authored-by: Zachary Fong Co-authored-by: Ramon Figueiredo Co-authored-by: Diego Tavares --- rest_gateway/.gitignore | 1 + rest_gateway/Dockerfile | 40 +++------ rest_gateway/README.md | 39 +++++--- rest_gateway/opencue_gateway/main.go | 84 ++++++++++++++++-- rest_gateway/opencue_gateway/main_test.go | 88 +++++++++++++++++++ rest_gateway/opencue_gateway/tools/gateway.go | 8 ++ 6 files changed, 212 insertions(+), 48 deletions(-) create mode 100644 rest_gateway/.gitignore create mode 100644 rest_gateway/opencue_gateway/main_test.go create mode 100644 rest_gateway/opencue_gateway/tools/gateway.go diff --git a/rest_gateway/.gitignore b/rest_gateway/.gitignore new file mode 100644 index 000000000..4c49bd78f --- /dev/null +++ b/rest_gateway/.gitignore @@ -0,0 +1 @@ +.env diff --git a/rest_gateway/Dockerfile b/rest_gateway/Dockerfile index e03c3fb05..c70b966c8 100644 --- a/rest_gateway/Dockerfile +++ b/rest_gateway/Dockerfile @@ -1,30 +1,9 @@ -# Build from project root -FROM rockylinux:9.3 as rocky-golang +FROM centos7.6-go1.21:latest AS build -WORKDIR /src/go -RUN dnf install -y 'dnf-command(config-manager)' && dnf config-manager --set-enabled crb - -# WARN: Download do tarball and update file path accordingly -COPY rest_gateway/Go_1.22.2_Linux_ARM64.tar.gz golang.tar.gz - -RUN tar -C /usr/local -xzf golang.tar.gz -ENV PATH="$PATH:/usr/local/go/bin" - -# WARN: Uncoment if your environment requires a proxy -#ENV GOPROXY=artifactory.yourcompany.com/go-proxy -ENV GO111MODULE=on -ENV GOSUMDB=off - -RUN go version - -RUN rm -rf /src/go - -FROM rocky-golang AS build - -RUN dnf install -y \ +RUN yum install -y \ git \ - protobuf-compiler \ - && dnf clean all + protobuf3-compiler \ + && yum clean all WORKDIR /app ENV PATH=$PATH:/root/go/bin:/opt/protobuf3/usr/bin/ @@ -33,13 +12,14 @@ COPY ./proto /app/proto COPY ./rest_gateway/opencue_gateway /app/opencue_gateway # COPY ./lib /app/lib - + WORKDIR /app/opencue_gateway RUN go mod init opencue_gateway && go mod tidy RUN go install \ github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \ github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 \ + github.com/golang-jwt/jwt/v5 \ google.golang.org/protobuf/cmd/protoc-gen-go \ google.golang.org/grpc/cmd/protoc-gen-go-grpc @@ -58,11 +38,17 @@ RUN protoc -I ../proto/ --grpc-gateway_out ./gen/go \ --grpc-gateway_opt generate_unbound_methods=true \ ../proto/*.proto +# Uncomment this to run go tests +# RUN go test -v + # Build project RUN go build -o grpc_gateway main.go -FROM rockylinux:9.3 +FROM centos-7.6.1810:latest COPY --from=build /app/opencue_gateway/grpc_gateway /app/ +# Ensure logs folder is created and has correct permissions +RUN mkdir -p /logs && chmod 755 /logs + EXPOSE 8448 ENTRYPOINT ["/app/grpc_gateway"] diff --git a/rest_gateway/README.md b/rest_gateway/README.md index db358f543..5b97453eb 100644 --- a/rest_gateway/README.md +++ b/rest_gateway/README.md @@ -6,19 +6,20 @@ A gateway to provide a REST endpoint to opencue gRPC API. This is a go serviced based on the official [grpc-gateway project](https://github.com/grpc-ecosystem/grpc-gateway) that compiles opencue's proto files into a go service that provides a REST interface and redirect calls to the -grpc endpoint. +grpc endpoint. All API calls over the REST interface requires an authentication header with a json web token as the bearer. -## Running the service - -Running the service is very simple: - * Read and modify the rest_gateway/Dockerfile according to your environment and build the gateway image using docker. - * Run the image providing the environment variable `CUEBOT_ENDPOINT=your.cuebot.server:8443` +**Note:** In the examples below, the REST gateway is available at OPENCUE_REST_GATEWAY_URL. Remember to replace OPENCUE_REST_GATEWAY_URL with the appropriate URL. ## REST interface All service rpc calls are accessible: * HTTP method is POST * URI path is built from the service’s name and method: // (e.g.: /show.ShowInterface/FindShow) + * HTTP header must have an authorization with a jwt token as the bearer. e.g: + ```headers: { + "Authorization": `Bearer ${jwtToken}`, + }, + ``` * HTTP body is a JSON with the request object: e.g.: ```proto message ShowFindShowRequest { @@ -61,9 +62,9 @@ message Show { float default_max_gpus = 11; } ``` -request (gateway running on `http://opencue-gateway.apps.com`): +request (gateway running on `OPENCUE_REST_GATEWAY_URL`): ```bash -curl -i -X POST http://opencue-gateway.apps.com/show.ShowInterface/FindShow -d '{"name": "ashow"}` +curl -i -H "Authorization: Bearer jwtToken" -X POST OPENCUE_REST_GATEWAY_URL/show.ShowInterface/FindShow -d '{"name": "testshow"}` ``` response ```bash @@ -74,7 +75,7 @@ Grpc-Metadata-Grpc-Accept-Encoding: gzip Date: Tue, 12 Dec 2023 18:05:18 GMT Content-Length: 501 -{"show":{"id":"00000000-0000-0000-0000-99999999999999","name":"ashow","defaultMinCores":1,"defaultMaxCores":10,"commentEmail":"middle-tier@imageworks.com","bookingEnabled":true,"dispatchEnabled":true,"active":true,"showStats":{"runningFrames":75,"deadFrames":14,"pendingFrames":1814,"pendingJobs":175,"createdJobCount":"2353643","createdFrameCount":"10344702","renderedFrameCount":"9733366","failedFrameCount":"1096394","reservedCores":252,"reservedGpus":0},"defaultMinGpus":100,"defaultMaxGpus":100000}} +{"show":{"id":"00000000-0000-0000-0000-000000000000","name":"testshow","defaultMinCores":1,"defaultMaxCores":10,"commentEmail":"middle-tier@company.com","bookingEnabled":true,"dispatchEnabled":true,"active":true,"showStats":{"runningFrames":75,"deadFrames":14,"pendingFrames":1814,"pendingJobs":175,"createdJobCount":"2353643","createdFrameCount":"10344702","renderedFrameCount":"9733366","failedFrameCount":"1096394","reservedCores":252,"reservedGpus":0},"defaultMinGpus":100,"defaultMaxGpus":100000}} ``` ### Example (getting frames for a job): @@ -143,11 +144,11 @@ message FrameSeq { repeated Frame frames = 1; } ``` -request (gateway running on `http://opencue-gateway.apps.com`): +request (gateway running on `OPENCUE_REST_GATEWAY_URL`): Note: it is important to include 'page' and 'limit' when getting frames for a job. ```bash -curl -i -X POST http://opencue-gateway.apps.com/job.JobInterface/GetFrames -d '{"job":{"id":"9999999999-b8d7-9999-a29c-99999999999999"}, "req": {"include_finished":true,"page":1,"limit":100}}' +curl -i -H "Authorization: Bearer jwtToken" -X POST OPENCUE_REST_GATEWAY_URL/job.JobInterface/GetFrames -d '{"job":{"id":"00000000-0000-0000-0000-000000000001", "req": {"include_finished":true,"page":1,"limit":100}}' ``` response ```bash @@ -157,7 +158,17 @@ grpc-metadata-content-type: application/grpc grpc-metadata-grpc-accept-encoding: gzip date: Tue, 13 Feb 2024 17:15:49 GMT transfer-encoding: chunked -set-cookie: 3d3a38cc45d028e42e93031e0ccc9b1e=534d34fde72242856a7fdadc27260929; path=/; HttpOnly +set-cookie: 01234567890123456789012345678901234567890123456789012345678901234; path=/; HttpOnly + + +{"frames":{"frames":[{"id":"00000000-0000-0000-0000-000000000002", "name":"0001-bty_tp_3d_123456", "layerName":"bty_tp_3d_123456", "number":1, "state":"WAITING", "retryCount":0, "exitStatus":-1, "dispatchOrder":0, "startTime":0, "stopTime":0, "maxRss":"0", "usedMemory":"0", "reservedMemory":"0", "reservedGpuMemory":"0", "lastResource":"/0.00/0", "checkpointState":"DISABLED", "checkpointCount":0, "totalCoreTime":0, "lluTime":1707842141, "totalGpuTime":0, "maxGpuMemory":"0", "usedGpuMemory":"0", "frameStateDisplayOverride":null}, {"id":"00000000-0000-0000-0000-000000000003", "name":"0002-bty_tp_3d_123456", "layerName":"bty_tp_3d_123456", "number":2, "state":"WAITING", "retryCount":0, "exitStatus":-1, "dispatchOrder":1, "startTime":0, "stopTime":0, "maxRss":"0", "usedMemory":"0", "reservedMemory":"0", "reservedGpuMemory":"0", "lastResource":"/0.00/0", "checkpointState":"DISABLED", "checkpointCount":0, "totalCoreTime":0, "lluTime":1707842141, "totalGpuTime":0, "maxGpuMemory":"0", "usedGpuMemory":"0", "frameStateDisplayOverride":null}, {"id":"00000000-0000-0000-0000-000000000004", "name":"0003-bty_tp_3d_083540", "layerName":"bty_tp_3d_123456", "number":3, "state":"WAITING", "retryCount":0, "exitStatus":-1, "dispatchOrder":2, "startTime":0, "stopTime":0, "maxRss":"0", "usedMemory":"0", "reservedMemory":"0", "reservedGpuMemory":"0", "lastResource":"/0.00/0", "checkpointState":"DISABLED", "checkpointCount":0, "totalCoreTime":0, "lluTime":1707842141, "totalGpuTime":0, "maxGpuMemory":"0", "usedGpuMemory":"0", "frameStateDisplayOverride":null}, {"id":"00000000-0000-0000-0000-000000000005", "name":"0004-bty_tp_3d_083540", "layerName":"bty_tp_3d_123456", "number":4, "state":"WAITING", "retryCount":0, "exitStatus":-1, "dispatchOrder":3, "startTime":0, "stopTime":0, "maxRss":"0", "usedMemory":"0", "reservedMemory":"0", "reservedGpuMemory":"0", "lastResource":"/0.00/0", "checkpointState":"DISABLED", "checkpointCount":0, "totalCoreTime":0, "lluTime":1707842141, "totalGpuTime":0, "maxGpuMemory":"0", "usedGpuMemory":"0", "frameStateDisplayOverride":null}, {"id":"00000000-0000-0000-0000-000000000006", "name":"0005-bty_tp_3d_083540", "layerName":"bty_tp_3d_123456", "number":5, "state":"WAITING", "retryCount":0, "exitStatus":-1, "dispatchOrder":4, "startTime":0, "stopTime":0, "maxRss":"0", "usedMemory":"0", "reservedMemory":"0", "reservedGpuMemory":"0", "lastResource":"/0.00/0", "checkpointState":"DISABLED", "checkpointCount":0, "totalCoreTime":0, "lluTime":1707842141, "totalGpuTime":0, "maxGpuMemory":"0", "usedGpuMemory":"0", "frameStateDisplayOverride":null}]}} +``` + +## Unit testing and system logs +Unit tests for the gRPC REST gateway can be run by uncommenting `RUN go test -v` in the Dockerfile. Unit tests currently cover the following cases for jwtMiddleware (used for authentication): +- valid tokens +- missing tokens +- invalid tokens +- expired tokens -{"frames":{"frames":[{"id":"9999999", "name":"0001-some_frame_0999990", "layerName":"h", "number":1, "state":"WAITING", "retryCount":0, "exitStatus":-1, "dispatchOrder":0, "startTime":0, "stopTime":0, "maxRss":"0", "usedMemory":"0", "reservedMemory":"0", "reservedGpuMemory":"0", "lastResource":"/0.00/0", "checkpointState":"DISABLED", "checkpointCount":0, "totalCoreTime":0, "lluTime":1707842141, "totalGpuTime":0, "maxGpuMemory":"0", "usedGpuMemory":"0", "frameStateDisplayOverride":null}, {"id":"10fa17d4-9313-4924-86f5-380c5b2a25d8", "name":"0002-some_frame_0999990", "layerName":"some_frame_0999990", "number":2, "state":"WAITING", "retryCount":0, "exitStatus":-1, "dispatchOrder":1, "startTime":0, "stopTime":0, "maxRss":"0", "usedMemory":"0", "reservedMemory":"0", "reservedGpuMemory":"0", "lastResource":"/0.00/0", "checkpointState":"DISABLED", "checkpointCount":0, "totalCoreTime":0, "lluTime":1707842141, "totalGpuTime":0, "maxGpuMemory":"0", "usedGpuMemory":"0", "frameStateDisplayOverride":null}, {"id":"8d39c602-0b27-4b1e-a09b-4fa35db40e55", "name":"0003-some_frame_0999990", "layerName":"some_frame_0999990", "number":3, "state":"WAITING", "retryCount":0, "exitStatus":-1, "dispatchOrder":2, "startTime":0, "stopTime":0, "maxRss":"0", "usedMemory":"0", "reservedMemory":"0", "reservedGpuMemory":"0", "lastResource":"/0.00/0", "checkpointState":"DISABLED", "checkpointCount":0, "totalCoreTime":0, "lluTime":1707842141, "totalGpuTime":0, "maxGpuMemory":"0", "usedGpuMemory":"0", "frameStateDisplayOverride":null}, {"id":"d418a837-e974-4716-9105-296f495bc407", "name":"0004-some_frame_0999990", "layerName":"some_frame_0999990", "number":4, "state":"WAITING", "retryCount":0, "exitStatus":-1, "dispatchOrder":3, "startTime":0, "stopTime":0, "maxRss":"0", "usedMemory":"0", "reservedMemory":"0", "reservedGpuMemory":"0", "lastResource":"/0.00/0", "checkpointState":"DISABLED", "checkpointCount":0, "totalCoreTime":0, "lluTime":1707842141, "totalGpuTime":0, "maxGpuMemory":"0", "usedGpuMemory":"0", "frameStateDisplayOverride":null}, {"id":"d2113372-99999-4c05-8100-9999999", "name":"0005-some_frame_0999990", "layerName":"some_frame_0999990", "number":5, "state":"WAITING", "retryCount":0, "exitStatus":-1, "dispatchOrder":4, "startTime":0, "stopTime":0, "maxRss":"0", "usedMemory":"0", "reservedMemory":"0", "reservedGpuMemory":"0", "lastResource":"/0.00/0", "checkpointState":"DISABLED", "checkpointCount":0, "totalCoreTime":0, "lluTime":1707842141, "totalGpuTime":0, "maxGpuMemory":"0", "usedGpuMemory":"0", "frameStateDisplayOverride":null}]}} -``` \ No newline at end of file +System logs are available in /logs and require mounting to be properly tracked. All Stdout are output to both the console and /logs. Here is an example Docker run command that includes addding an environment file and volume mounting: `docker run --env-file ./rest_gateway/.env -v PATH_TO_REST_GATEWAY/logs:/logs -p 8448:8448 restgateway`. \ No newline at end of file diff --git a/rest_gateway/opencue_gateway/main.go b/rest_gateway/opencue_gateway/main.go index 55f8355a3..974b04a7d 100644 --- a/rest_gateway/opencue_gateway/main.go +++ b/rest_gateway/opencue_gateway/main.go @@ -2,9 +2,14 @@ package main import ( "context" "flag" + "fmt" + "io" + "log" "net/http" "os" - + "strings" + + "github.com/golang-jwt/jwt/v5" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -13,16 +18,68 @@ import ( gw "opencue_gateway/gen/go" // Update ) -func getEnv(key, fallback string) string { +func getEnv(key string) string { + // Return the value of the environment variable if it's found if value, ok := os.LookupEnv(key); ok { return value + } else { + // If the environment variable is not found, output an error and exit the program + log.Fatal(fmt.Sprintf("Error: environment variable '%v' not found", key)) } - return fallback + return "" +} + +// Parse and validate the JWT token string +func validateJWTToken(tokenString string, jwtSecret []byte) (*jwt.Token, error) { + return jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + // Ensure that the token's signing method is HMAC + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + errorString := fmt.Sprintf("Unexpected signing method: %v", token.Header["alg"]) + log.Printf(errorString) + return nil, fmt.Errorf(errorString) + } + // Return the secret key for validation + return jwtSecret, nil + }) +} + +// Middleware to handle token authorization +func jwtMiddleware(next http.Handler, jwtSecret []byte) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Get the authorization header and return 401 if there is no header + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + errorString := "Authorization header required" + log.Printf(errorString) + http.Error(w, errorString, http.StatusUnauthorized) + return + } + + // Get the token from the header and validate it + tokenString := strings.TrimPrefix(authHeader, "Bearer ") + token, err := validateJWTToken(tokenString, jwtSecret) + if err!=nil { + errorString := fmt.Sprintf("Token validation error: %v", err) + log.Printf(errorString) + http.Error(w, errorString, http.StatusUnauthorized) + return + } + if !token.Valid { + errorString := "Invalid token" + log.Printf(errorString) + http.Error(w, errorString, http.StatusUnauthorized) + return + } + + // If token is valid, pass it to the next handler + next.ServeHTTP(w, r) + }) } func run() error { - grpcServerEndpoint := getEnv("CUEBOT_ENDPOINT", "opencuetest01.your.test.server:8443") - port := getEnv("REST_PORT", "8448") + grpcServerEndpoint := getEnv("CUEBOT_ENDPOINT") + port := getEnv("REST_PORT") + jwtSecret := []byte(getEnv("JWT_AUTH_SECRET")) ctx := context.Background() ctx, cancel := context.WithCancel(ctx) @@ -73,13 +130,26 @@ func run() error { if errProc != nil { return errProc } - + + // Create a new HTTP ServeMux with middleware jwtMiddleware to protect the mux + httpMux := http.NewServeMux() + httpMux.Handle("/", jwtMiddleware(mux, jwtSecret)) // Start HTTP server (and proxy calls to gRPC server endpoint) - return http.ListenAndServe(":" + port, mux) + return http.ListenAndServe(":" + port, httpMux) } func main() { + // Set up file to capture all log outputs + f, err := os.OpenFile("/logs/opencue_gateway.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) + if err != nil { + log.Fatal(err) + } + // Enable output to both Stdout and the log file + mw := io.MultiWriter(os.Stdout, f) + defer f.Close() + log.SetOutput(mw) + flag.Parse() if err := run(); err != nil { diff --git a/rest_gateway/opencue_gateway/main_test.go b/rest_gateway/opencue_gateway/main_test.go new file mode 100644 index 000000000..18878dec8 --- /dev/null +++ b/rest_gateway/opencue_gateway/main_test.go @@ -0,0 +1,88 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/stretchr/testify/assert" +) + +func TestJwtMiddleware(t *testing.T) { + jwtSecret := []byte("test_secret") + + // Set up a sample handler to use with the middleware + sampleHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + }) + + // Create a test server with the middleware + ts := httptest.NewServer(jwtMiddleware(sampleHandler, jwtSecret)) + defer ts.Close() + + t.Run("Valid Token", func(t *testing.T) { + // Generate a valid token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "sub": "test_user", + "exp": time.Now().Add(time.Hour).Unix(), + }) + tokenString, err := token.SignedString(jwtSecret) + assert.NoError(t, err) + + // Create a request with the valid token + req, err := http.NewRequest("GET", ts.URL, nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenString) + + // Perform the request + res, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + }) + + t.Run("Missing Token", func(t *testing.T) { + // Create a request without a token + req, err := http.NewRequest("GET", ts.URL, nil) + assert.NoError(t, err) + + // Perform the request + res, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, res.StatusCode) + }) + + t.Run("Invalid Token", func(t *testing.T) { + // Create a request with an invalid token + req, err := http.NewRequest("GET", ts.URL, nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer invalid_token") + + // Perform the request + res, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, res.StatusCode) + }) + + t.Run("Expired Token", func(t *testing.T) { + // Generate an expired token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "sub": "test_user", + "exp": time.Now().Add(-time.Hour).Unix(), + }) + tokenString, err := token.SignedString(jwtSecret) + assert.NoError(t, err) + + // Create a request with the expired token + req, err := http.NewRequest("GET", ts.URL, nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenString) + + // Perform the request + res, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, res.StatusCode) + }) +} diff --git a/rest_gateway/opencue_gateway/tools/gateway.go b/rest_gateway/opencue_gateway/tools/gateway.go new file mode 100644 index 000000000..86d3c81ce --- /dev/null +++ b/rest_gateway/opencue_gateway/tools/gateway.go @@ -0,0 +1,8 @@ +package tools + +import ( + _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway" + _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2" + _ "google.golang.org/grpc/cmd/protoc-gen-go-grpc" + _ "google.golang.org/protobuf/cmd/protoc-gen-go" +) \ No newline at end of file From 1f3229599a58af64aa9c7feda7657f8fece9c97d Mon Sep 17 00:00:00 2001 From: Ramon Figueiredo Date: Wed, 14 Aug 2024 13:14:19 -0700 Subject: [PATCH 3/4] [rest_gateway] Enhance README.md with a new overview, JWT authentication, and examples (#1470) - Improved Introduction for better clarity on the Rest Gateway's purpose. - A detailed guide on JWT authentication and its usage was added. - Included a table of contents for easy navigation. - Provided clearer examples of how to interact with the REST interface. - Updated section on unit testing. Co-authored-by: Ramon Figueiredo Co-authored-by: Zachary Fong --------- Co-authored-by: Zachary Fong --- rest_gateway/README.md | 204 +++++++++++++++--- .../k8s/kustomize/base/rest-gateway.yaml | 54 ----- 2 files changed, 175 insertions(+), 83 deletions(-) delete mode 100644 spideploy/k8s/kustomize/base/rest-gateway.yaml diff --git a/rest_gateway/README.md b/rest_gateway/README.md index 5b97453eb..ca78b0e2b 100644 --- a/rest_gateway/README.md +++ b/rest_gateway/README.md @@ -1,41 +1,90 @@ # Opencue Rest Gateway -A gateway to provide a REST endpoint to opencue gRPC API. +A gateway to provide a REST endpoint to the Opencue gRPC API. -## How does it work -This is a go serviced based on the official [grpc-gateway project](https://github.com/grpc-ecosystem/grpc-gateway) -that compiles opencue's proto files into a go service that provides a REST interface and redirect calls to the -grpc endpoint. All API calls over the REST interface requires an authentication header with a json web token as the bearer. +## Contents + +1. [Introduction](#introduction) +2. [How does it work](#how-does-it-work) +3. [REST interface](#rest-interface) + - [Example: Getting a show](#example-getting-a-show) + - [Example: Getting frames for a job](#example-getting-frames-for-a-job) +4. [Authentication](#authentication) + - [What are JSON Web Tokens?](#what-are-json-web-tokens) + - [JSON Web Tokens in a web system and the Rest Gateway](#json-web-tokens-in-a-web-system-and-the-rest-gateway) + - [Web system](#web-system) + - [Rest Gateway](#rest-gateway) +5. [Rest Gateway unit testing](#rest-gateway-unit-testing) + + +## Introduction + +The Opencue Rest Gateway is a crucial component that bridges the gap between Opencue's high-performance gRPC API and the widespread, web-friendly RESTful interface. Designed with scalability and integration flexibility in mind, this gateway allows developers and systems to interact with Opencue through standard HTTP methods, making it easier to integrate Opencue into a variety of environments, including web applications, automation scripts, and third-party services. + +By leveraging REST, the Opencue Rest Gateway opens up Opencue's advanced rendering and job management capabilities to a broader audience, enabling access through familiar, widely-adopted web technologies. This gateway not only simplifies interaction with Opencue but also ensures that all communications are secure, thanks to its implementation of [JSON Web Token (JWT)](https://jwt.io/) authentication. + +This documentation provides detailed instructions on how to set up and use the Opencue Rest Gateway, including configuration tips, examples of API calls, and insights into the security mechanisms that protect your data and operations. + +Go back to [Contents](#contents). + + +## How Does It Work + +The Opencue Rest Gateway operates as a translator and secure access point between the RESTful world and the gRPC services provided by Opencue. Built on top of Go and the [grpc-gateway project](https://github.com/grpc-ecosystem/grpc-gateway), the gateway automatically converts Opencue's protocol buffer (proto) definitions into REST endpoints. + +Here’s a step-by-step breakdown of how it works: + +1. **Request Conversion**: When a client sends an HTTP request to the gateway, the request is matched against the predefined RESTful routes generated from the proto files. The gateway then converts this HTTP request into the corresponding gRPC call. + +2. **gRPC Communication**: The converted request is sent to the appropriate Opencue gRPC service, where it is processed just like any other gRPC request. + +3. **Response Handling**: After the gRPC service processes the request, the response is returned to the gateway, which then converts the gRPC response into a JSON format suitable for HTTP. + +4. **Security Enforcement**: Before any request is processed, the gateway enforces security by requiring a JSON Web Token (JWT) in the `Authorization header`. This token is validated to ensure that the request is authenticated and authorized to access the requested resources. + +5. **Final Response**: The formatted JSON response is sent back to the client via HTTP, completing the request-response cycle. + +This seamless conversion and security process allows the Opencue Rest Gateway to provide a robust, secure, and user-friendly interface to Opencue's gRPC services, making it accessible to a wide range of clients and applications. **Note:** In the examples below, the REST gateway is available at OPENCUE_REST_GATEWAY_URL. Remember to replace OPENCUE_REST_GATEWAY_URL with the appropriate URL. +Go back to [Contents](#contents). + + ## REST interface -All service rpc calls are accessible: - * HTTP method is POST - * URI path is built from the service’s name and method: // (e.g.: /show.ShowInterface/FindShow) - * HTTP header must have an authorization with a jwt token as the bearer. e.g: - ```headers: { +All service RPC calls are accessible via the REST interface: + + * **HTTP method:** POST + * **URI path:** Built from the service's name and method: `//` (e.g., `/show.ShowInterface/FindShow`) + * **Authorization header:** Must include a JWT token as the bearer. + ```json + headers: { "Authorization": `Bearer ${jwtToken}`, }, ``` - * HTTP body is a JSON with the request object: e.g.: + * **HTTP body:** A JSON object with the request data. ```proto message ShowFindShowRequest { string name = 1; } ``` - becomes: + Becomes: ```json { "name": "value for name" } ``` - * HTTP response is a JSON object with the formatted response + * **HTTP response:** A JSON object with the formatted response. + +Go back to [Contents](#contents). + + +### Example: Getting a show + +Given the following proto definition in `show.proto`: -### Example (getting a show): -show.proto: ```proto service ShowInterface { // Find a show with the specified name. @@ -62,11 +111,15 @@ message Show { float default_max_gpus = 11; } ``` -request (gateway running on `OPENCUE_REST_GATEWAY_URL`): + +You can send a request to the REST gateway running on `OPENCUE_REST_GATEWAY_URL`: + ```bash curl -i -H "Authorization: Bearer jwtToken" -X POST OPENCUE_REST_GATEWAY_URL/show.ShowInterface/FindShow -d '{"name": "testshow"}` ``` -response + +The response might look like this: + ```bash HTTP/1.1 200 OK Content-Type: application/json @@ -78,8 +131,13 @@ Content-Length: 501 {"show":{"id":"00000000-0000-0000-0000-000000000000","name":"testshow","defaultMinCores":1,"defaultMaxCores":10,"commentEmail":"middle-tier@company.com","bookingEnabled":true,"dispatchEnabled":true,"active":true,"showStats":{"runningFrames":75,"deadFrames":14,"pendingFrames":1814,"pendingJobs":175,"createdJobCount":"2353643","createdFrameCount":"10344702","renderedFrameCount":"9733366","failedFrameCount":"1096394","reservedCores":252,"reservedGpus":0},"defaultMinGpus":100,"defaultMaxGpus":100000}} ``` -### Example (getting frames for a job): -job.proto: +Go back to [Contents](#contents). + + +### Example: Getting frames for a job + +Given the following proto definition in `job.proto`: + ```proto service JobInterface { // Returns all frame objects that match FrameSearchCriteria @@ -144,13 +202,17 @@ message FrameSeq { repeated Frame frames = 1; } ``` -request (gateway running on `OPENCUE_REST_GATEWAY_URL`): -Note: it is important to include 'page' and 'limit' when getting frames for a job. +You can send a request to the REST gateway running on `OPENCUE_REST_GATEWAY_URL`: + +**Note:** It is important to include 'page' and 'limit' when getting frames for a job. + ```bash curl -i -H "Authorization: Bearer jwtToken" -X POST OPENCUE_REST_GATEWAY_URL/job.JobInterface/GetFrames -d '{"job":{"id":"00000000-0000-0000-0000-000000000001", "req": {"include_finished":true,"page":1,"limit":100}}' ``` -response + +The response might look like this: + ```bash HTTP/1.1 200 OK content-type: application/json @@ -164,11 +226,95 @@ set-cookie: 01234567890123456789012345678901234567890123456789012345678901234; p {"frames":{"frames":[{"id":"00000000-0000-0000-0000-000000000002", "name":"0001-bty_tp_3d_123456", "layerName":"bty_tp_3d_123456", "number":1, "state":"WAITING", "retryCount":0, "exitStatus":-1, "dispatchOrder":0, "startTime":0, "stopTime":0, "maxRss":"0", "usedMemory":"0", "reservedMemory":"0", "reservedGpuMemory":"0", "lastResource":"/0.00/0", "checkpointState":"DISABLED", "checkpointCount":0, "totalCoreTime":0, "lluTime":1707842141, "totalGpuTime":0, "maxGpuMemory":"0", "usedGpuMemory":"0", "frameStateDisplayOverride":null}, {"id":"00000000-0000-0000-0000-000000000003", "name":"0002-bty_tp_3d_123456", "layerName":"bty_tp_3d_123456", "number":2, "state":"WAITING", "retryCount":0, "exitStatus":-1, "dispatchOrder":1, "startTime":0, "stopTime":0, "maxRss":"0", "usedMemory":"0", "reservedMemory":"0", "reservedGpuMemory":"0", "lastResource":"/0.00/0", "checkpointState":"DISABLED", "checkpointCount":0, "totalCoreTime":0, "lluTime":1707842141, "totalGpuTime":0, "maxGpuMemory":"0", "usedGpuMemory":"0", "frameStateDisplayOverride":null}, {"id":"00000000-0000-0000-0000-000000000004", "name":"0003-bty_tp_3d_083540", "layerName":"bty_tp_3d_123456", "number":3, "state":"WAITING", "retryCount":0, "exitStatus":-1, "dispatchOrder":2, "startTime":0, "stopTime":0, "maxRss":"0", "usedMemory":"0", "reservedMemory":"0", "reservedGpuMemory":"0", "lastResource":"/0.00/0", "checkpointState":"DISABLED", "checkpointCount":0, "totalCoreTime":0, "lluTime":1707842141, "totalGpuTime":0, "maxGpuMemory":"0", "usedGpuMemory":"0", "frameStateDisplayOverride":null}, {"id":"00000000-0000-0000-0000-000000000005", "name":"0004-bty_tp_3d_083540", "layerName":"bty_tp_3d_123456", "number":4, "state":"WAITING", "retryCount":0, "exitStatus":-1, "dispatchOrder":3, "startTime":0, "stopTime":0, "maxRss":"0", "usedMemory":"0", "reservedMemory":"0", "reservedGpuMemory":"0", "lastResource":"/0.00/0", "checkpointState":"DISABLED", "checkpointCount":0, "totalCoreTime":0, "lluTime":1707842141, "totalGpuTime":0, "maxGpuMemory":"0", "usedGpuMemory":"0", "frameStateDisplayOverride":null}, {"id":"00000000-0000-0000-0000-000000000006", "name":"0005-bty_tp_3d_083540", "layerName":"bty_tp_3d_123456", "number":5, "state":"WAITING", "retryCount":0, "exitStatus":-1, "dispatchOrder":4, "startTime":0, "stopTime":0, "maxRss":"0", "usedMemory":"0", "reservedMemory":"0", "reservedGpuMemory":"0", "lastResource":"/0.00/0", "checkpointState":"DISABLED", "checkpointCount":0, "totalCoreTime":0, "lluTime":1707842141, "totalGpuTime":0, "maxGpuMemory":"0", "usedGpuMemory":"0", "frameStateDisplayOverride":null}]}} ``` -## Unit testing and system logs -Unit tests for the gRPC REST gateway can be run by uncommenting `RUN go test -v` in the Dockerfile. Unit tests currently cover the following cases for jwtMiddleware (used for authentication): -- valid tokens -- missing tokens -- invalid tokens -- expired tokens +Go back to [Contents](#contents). + + +## Authentication + +Go back to [Contents](#contents). + + +### What are JSON Web Tokens? + +The gRPC REST gateway implements JSON Web Tokens (JWT) for authentication. JWTs are a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object and are digitally signed for verification and security. In the gRPC REST gateway, all JWTs are signed using a secret. A JWT consists of the following three parts separated by dots: + +1. **Header:** Contains the type of token (usually `JWT`) and the signing algorithm (like `SHA256`) + +- Example: + +```json +{ + "alg": "HS256", + "typ": "JWT" +} +``` + +2. **Payload:** Contains the claims, which are statements about an entity (user) and additional data. + +- Example: + +```json +{ + "sub": "user-id-123", + "role": "admin", + "iat": 1609459200, // Example timestamp (Jan 1, 2021) + "exp": 1609462800 // Example expiration (1 hour later) +} + +``` +3. **Signature:** Created from the encoded header, the encoded payload, a secret, the algorithm specified in the header, and signed. + +- The signature also verifies that the original message was not altered and can verify that the sender of the JWT is who they say they are (when signed with a private key). + +- Example: + +``` +HMACSHA256( + base64UrlEncode(header) + "." + + base64UrlEncode(payload), + secret +) +``` +Together, these three parts form a token like `xxxxx.yyyyy.zzzzz`, which is three Base64-URL strings separated by dots that can be passed in HTML environments. + +Go back to [Contents](#contents). + + +### JSON Web Tokens in a web system and the Rest Gateway + +In a web system and Rest Gateway, the secret for the JWT must be defined and match. In Rest Gateway, the secret is defined as an environment variable called `JWT_AUTH_SECRET`. + +Go back to [Contents](#contents). + + +#### Web system + +When a web system accesses the gRPC REST gateway, `fetchObjectFromRestGateway()` will be called, which initializes a JWT with an expiration time (e.g. 1 hour). This JWT is then passed on every request to the gRPC REST gateway as the authorization bearer in the header. If this JWT is successfully authenticated by the Rest Gateway, the gRPC endpoint will be reached. If the JWT is invalid, an error will be returned, and the gRPC endpoint will not be reached. + +Go back to [Contents](#contents). + + +#### Rest Gateway + +When the gRPC REST gateway receives a request, it must first verify and authenticate it using middleware (`jwtMiddleware()`). The following requirements are checked before the gRPC REST gateway complies with the request: +- The request contains an `Authorization header` with a `Bearer token`. +- The token's signing method is Hash-based message authentication code (or HMAC). +- The token is valid. +- The token is not expired. +- The token's secret matches the Rest Gateway's secret. + +Go back to [Contents](#contents). + + +## Rest Gateway unit testing + +Unit tests for the gRPC REST gateway can be run with `go test`. To run the Rest Gateway unit testing using the Dockerfile, uncomment `RUN go test -v` in the Dockerfile. + +Unit tests currently cover the following cases for `jwtMiddleware` (used for authentication): + +- Valid tokens +- Missing tokens +- Invalid tokens +- Expired tokens -System logs are available in /logs and require mounting to be properly tracked. All Stdout are output to both the console and /logs. Here is an example Docker run command that includes addding an environment file and volume mounting: `docker run --env-file ./rest_gateway/.env -v PATH_TO_REST_GATEWAY/logs:/logs -p 8448:8448 restgateway`. \ No newline at end of file +Go back to [Contents](#contents). diff --git a/spideploy/k8s/kustomize/base/rest-gateway.yaml b/spideploy/k8s/kustomize/base/rest-gateway.yaml deleted file mode 100644 index bdb099654..000000000 --- a/spideploy/k8s/kustomize/base/rest-gateway.yaml +++ /dev/null @@ -1,54 +0,0 @@ -kind: Deployment -apiVersion: apps/v1 -metadata: - name: opencue-rest-gateway - namespace: opencue - labels: - app: opencue - component: rest-gateway -spec: - replicas: 1 - selector: - matchLabels: - app: opencue - component: rest-gateway - template: - metadata: - creationTimestamp: null - labels: - app: opencue - component: rest-gateway - spec: - containers: - - name: rest-gateway - imagePullPolicy: Always - terminationMessagePolicy: File - ports: - - containerPort: 8448 - image: docker-local.artifactory.spimageworks.com/gitlab/spi/dev/infrastructure/api/opencue/rest_gateway:latest - env: - - name: CUEBOT_ENDPOINT - values: cuebot-grpc.opencue.svc.cluster.local:8443 - resources: - limits: - cpu: "1000m" - memory: "2Gi" - requests: - cpu: "200m" - memory: "1Gi" - restartPolicy: Always - terminationGracePeriodSeconds: 10 ---- -apiVersion: v1 -kind: Service -metadata: - name: rest-gateway-service - namespace: opencue -spec: - selector: - app: opencue - component: rest-gateway - ports: - - protocol: TCP - port: 8448 - targetPort: 8448 \ No newline at end of file From bf5f79862e3663f88b6169acf0f4ce2cd546c7c3 Mon Sep 17 00:00:00 2001 From: Diego Tavares Date: Wed, 11 Sep 2024 15:31:11 -0700 Subject: [PATCH 4/4] Version up --- VERSION.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION.in b/VERSION.in index c74e8a041..2037dfa6c 100644 --- a/VERSION.in +++ b/VERSION.in @@ -1 +1 @@ -0.35 +0.36