diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..778d4d54 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +workers/dotnet/**/obj/ +workers/dotnet/**/bin/ +workers/dotnet/**/build/ +workers/*/projects/tests/project-build-*/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc91c44f..92d9567a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -180,6 +180,62 @@ jobs: annotate_only: true skip_annotations: true + build-project-worker: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - dockerfile: go-project.Dockerfile + project-dir: workers/go/projects/tests/helloworld + name: go-helloworld + - dockerfile: dotnet-project.Dockerfile + project-dir: workers/dotnet/projects/tests/HelloWorld + name: dotnet-helloworld + - dockerfile: dotnet-project.Dockerfile + project-dir: workers/dotnet/projects/tests/NexusSimpleWorkflow + name: dotnet-nexus + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + submodules: true + - uses: actions/setup-go@v4 + with: + go-version-file: "go.mod" + - name: Build ${{ matrix.name }} project image + run: | + docker build \ + -f dockerfiles/${{ matrix.dockerfile }} \ + --build-arg PROJECT_DIR=${{ matrix.project-dir }} \ + -t omes-project:${{ matrix.name }} . + - name: Install Temporal CLI + uses: temporalio/setup-temporal@v0 + - name: Start dev server + run: | + temporal server start-dev --port 7233 --headless --log-level error & + timeout 30 bash -c 'until nc -z 127.0.0.1 7233; do sleep 1; done' + - name: Start ${{ matrix.name }} worker + run: | + docker run --rm -d --network host \ + --name omes-project-worker \ + omes-project:${{ matrix.name }} worker \ + --task-queue omes-project-${{ matrix.name }} \ + --server-address 127.0.0.1:7233 \ + --namespace default + - name: Run project scenario + run: | + docker run --rm --network host \ + omes-project:${{ matrix.name }} run-scenario \ + --scenario project \ + --run-id project-${{ matrix.name }} \ + --iterations 1 \ + --server-address 127.0.0.1:7233 \ + --namespace default + - name: Stop worker + if: always() + run: docker stop omes-project-worker || true + build-ks-gen-and-ensure-protos-up-to-date: runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index b95f7192..a01dcd12 100644 --- a/.gitignore +++ b/.gitignore @@ -15,15 +15,18 @@ workers/java/build /loadgen/kitchen-sink-gen/target/ omes.sln -/workers/dotnet/obj/ -/workers/dotnet/bin/ +workers/dotnet/**/obj/ +workers/dotnet/**/bin/ +workers/dotnet/**/build/ /last_fuzz_run.proto workers/dotnet/Temporalio.Omes.temp.csproj workers/python/**/__pycache__/ workers/*/omes-temp-*/ workers/*/prepared/ +workers/*/projects/tests/project-build-*/ tsconfig.tsbuildinfo temporal-omes +.gocache/ diff --git a/README.md b/README.md index 755df0a0..627a42d2 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,155 @@ If version is not specified, the SDK version specified in `versions.env` will be Publishing images is done via CI, using the `build-push-worker-image` command. See the GHA workflows for more information. +## Project Tests + +Project tests are self-contained test programs that exercise specific SDK features (e.g., Nexus operations) +independently of the standard scenario/executor framework. Each project brings its own SDK version and +test logic, while a shared harness handles client creation, worker lifecycle, and gRPC orchestration. + +### Structure + +``` +workers//projects/ + harness/ # Shared harness (gRPC server, client factory, worker management) + tests/ + HelloWorld/ # Simple example project + NexusSimpleWorkflow/ # Nexus operation test +``` + +Each project binary supports two subcommands: +- `worker` — starts a Temporal worker +- `project-server` — starts a gRPC server that accepts `Init` and `Execute` RPCs + +The `Init` RPC initializing the load test, providing client connection parameters, load test metadata +(i.e. RunID), and optional project-specific data. + +The `Execute` RPC is called for each iteration of the executor running against your project test. This is +principally what drives load. + +### Docker + +Project Dockerfiles (`-project.Dockerfile` in `dockerfiles/`) produce images that can run both the project worker and the scenario runner. +The entrypoint script routes commands automatically: `worker` and `project-server` run the project +binary, while `run-scenario` and other commands run the omes CLI. + +```sh +# Build +docker build -f dockerfiles/go-project.Dockerfile \ + --build-arg PROJECT_DIR=workers/go/projects/tests/helloworld \ + -t omes-go-project-helloworld . + +# Run worker (one container) +docker run --rm -d omes-go-project-helloworld worker \ + --task-queue omes-my-run --server-address host.docker.internal:7233 --namespace default + +# Run scenario (another container, same image) +docker run --rm omes-go-project-helloworld run-scenario \ + --scenario project \ + --run-id my-run --iterations 1 \ + --server-address host.docker.internal:7233 --namespace default +``` + +### Running locally + +The easiest way to run a project test locally is `run-scenario-with-worker`, which starts an embedded +Temporal server, builds the project, starts the worker, and runs the scenario: + +```sh +# Go +go run ./cmd run-scenario-with-worker \ + --scenario project --project-dir workers/go/projects/tests/helloworld \ + --option language=go --option project-dir=workers/go/projects/tests/helloworld \ + --language go --embedded-server --iterations 1 +``` + +This can be done with any other implemented language by swapping out the `language` value and +pointing to the respective `project-dir`. + +**Note**: +There's a peculiarity in this usage. You have to specify `--project-dir ...` and also `--option project-dir=...`. +This is because we are building both the worker and the runner. The worker expects the top-level +`--project-dir` while the runner expects the `--option`. This allows separate usage of `run-worker` and `run-scenario`. + +You can also run the worker and scenario separately against an existing server: + +```sh +# Terminal 1: start worker +go run ./cmd run-worker --language go --run-id my-run \ + --project-dir workers/go/projects/tests/helloworld + +# Terminal 2: run scenario +go run ./cmd run-scenario --scenario project --run-id my-run --iterations 1 \ + --option language=go --option project-dir=workers/go/projects/tests/helloworld +``` + +To build against a local SDK repo, add `--version ../sdk-go` (or `../sdk-dotnet`). + +### Writing a new project test + +The best starting point is the `HelloWorld` sample in your language (`workers//projects/tests/HelloWorld/`). + +1. Create a directory under `workers//projects/tests//` +2. Register callbacks with the harness and call `Run()`: + +```go +package main + +import ( + harness "github.com/temporalio/omes/workers/go/projects/harness" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/worker" +) + +func main() { + h := harness.New() + + // RegisterClient creates a Temporal client. Called by both the worker and project-server. + // Can customize the client options as you like, in addition to the provided options. + h.RegisterClient(func(opts client.Options, _ harness.ClientConfig) (client.Client, error) { + return client.Dial(opts) + }) + + // OnInit runs project-specific setup during the Init RPC (i.e. creating Nexus endpoints) + // Optional. + h.OnInit(func(ctx context.Context, c client.Client, config harness.InitConfig) error { + return nil + }) + + // RegisterWorker starts the Temporal worker with your registered workflows/activities etc. + // Can customize worker options as you like, in addition to the config provided. + h.RegisterWorker(func(c client.Client, config harness.WorkerConfig) error { + w := worker.New(c, config.TaskQueue, worker.Options{}) + w.RegisterWorkflow(MyWorkflow) + return w.Run(worker.InterruptCh()) + }) + + // OnExecute runs once per iteration - create your load (per-iteration) here (i.e. start a workflow) + h.OnExecute(func(ctx context.Context, c client.Client, info harness.ExecuteInfo) error { + run, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + TaskQueue: info.TaskQueue, + }, MyWorkflow, "input") + if err != nil { + return err + } + var result string + return run.Get(ctx, &result) + }) + + h.Run() +} +``` + +3. Add a test function in `scenarios/project/project_test.go` + +### Running project tests +There are some basic tests that run a project against a test server at `scenarios/project/project_test.go`. +You can run one via: + +```sh +go test ./scenarios/project/ -run TestGoHelloWorld -count=1 +``` + ## Specific Scenarios ### ThroughputStress (Go only) diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 6b110412..9953b8da 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -5,7 +5,8 @@ import ( "os" "github.com/spf13/cobra" - _ "github.com/temporalio/omes/scenarios" // Register scenarios (side-effect) + _ "github.com/temporalio/omes/scenarios" // Register scenarios (side-effect) + _ "github.com/temporalio/omes/scenarios/project" // Register project scenario ) func Main() { diff --git a/cmd/cli/prepare_worker.go b/cmd/cli/prepare_worker.go index 0023db64..6720d7eb 100644 --- a/cmd/cli/prepare_worker.go +++ b/cmd/cli/prepare_worker.go @@ -6,6 +6,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/temporalio/omes/cmd/clioptions" + project "github.com/temporalio/omes/scenarios/project" "github.com/temporalio/omes/workers" ) @@ -22,9 +23,20 @@ func prepareWorkerCmd() *cobra.Command { if err != nil { b.Logger.Fatal(fmt.Errorf("failed to get root directory: %w", err)) } - baseDir := workers.BaseDir(repoDir, b.SdkOptions.Language) - if _, err := b.Build(cmd.Context(), baseDir); err != nil { - b.Logger.Fatal(err) + if b.ProjectDir != "" { + if _, err := project.Build(cmd.Context(), project.BuildOptions{ + Language: b.SdkOptions.Language, + ProjectDir: b.ProjectDir, + Version: b.SdkOptions.Version, + Logger: b.Logger, + }); err != nil { + b.Logger.Fatal(err) + } + } else { + baseDir := workers.BaseDir(repoDir, b.SdkOptions.Language) + if _, err := b.Build(cmd.Context(), baseDir); err != nil { + b.Logger.Fatal(err) + } } }, } @@ -41,6 +53,7 @@ type workerBuilder struct { func (b *workerBuilder) addCLIFlags(fs *pflag.FlagSet) { fs.StringVar(&b.DirName, "dir-name", "", "Directory name for prepared worker") + fs.StringVar(&b.ProjectDir, "project-dir", "", "Path to project directory (builds a project test instead of standard worker)") b.SdkOptions.AddCLIFlags(fs) fs.AddFlagSet(b.loggingOptions.FlagSet()) } diff --git a/cmd/cli/run_scenario.go b/cmd/cli/run_scenario.go index ff85dc77..7026c97e 100644 --- a/cmd/cli/run_scenario.go +++ b/cmd/cli/run_scenario.go @@ -162,6 +162,7 @@ func (r *scenarioRunner) run(ctx context.Context) error { Logger: r.logger, MetricsHandler: metrics.NewHandler(), Client: client, + ClientOptions: r.clientOptions, Configuration: loadgen.RunConfiguration{ Iterations: r.iterations, Duration: r.duration, diff --git a/dockerfiles/dotnet-project.Dockerfile b/dockerfiles/dotnet-project.Dockerfile new file mode 100644 index 00000000..13d22ea2 --- /dev/null +++ b/dockerfiles/dotnet-project.Dockerfile @@ -0,0 +1,61 @@ +# Build in a full featured container +ARG TARGETARCH +FROM --platform=linux/$TARGETARCH mcr.microsoft.com/dotnet/sdk:8.0-jammy AS build + +# Install protobuf compiler and build tools +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive \ + apt-get install --no-install-recommends --assume-yes \ + protobuf-compiler=3.12.4* libprotobuf-dev=3.12.4* build-essential=12.* + +# Get go compiler +ARG TARGETARCH +RUN wget -q https://go.dev/dl/go1.21.12.linux-${TARGETARCH}.tar.gz \ + && tar -C /usr/local -xzf go1.21.12.linux-${TARGETARCH}.tar.gz + +# Install Rust for compiling the core bridge - only required for installation from a repo but is cheap enough to install +# in the "build" container (-y is for non-interactive install) +# hadolint ignore=DL4006 +RUN wget -q -O - https://sh.rustup.rs | sh -s -- -y + +ENV PATH="$PATH:/root/.cargo/bin:/usr/local/go/bin" + +WORKDIR /app + +# Copy CLI build dependencies +COPY cmd ./cmd +COPY loadgen ./loadgen +COPY scenarios ./scenarios +COPY metrics ./metrics +COPY workers/*.go ./workers/ +COPY workers/go/projects/api ./workers/go/projects/api +COPY go.mod go.sum ./ + +# Build the CLI +RUN CGO_ENABLED=0 /usr/local/go/bin/go build -o temporal-omes ./cmd + +ARG SDK_VERSION + +# Optional SDK dir to copy, defaults to unimportant file +ARG SDK_DIR=.gitignore +COPY ${SDK_DIR} ./repo + +# Copy the worker + project files +COPY workers/dotnet ./workers/dotnet +COPY workers/proto ./workers/proto + +# Build the project +ARG PROJECT_DIR +RUN ./temporal-omes prepare-worker --language cs --dir-name project-prepared --project-dir "$PROJECT_DIR" --version "$SDK_VERSION" + +# Runtime container with ASP.NET for gRPC server support +FROM --platform=linux/$TARGETARCH mcr.microsoft.com/dotnet/aspnet:8.0-jammy + +ENV OMES_PROJECT_LANGUAGE=cs +ENV OMES_PROJECT_BINARY=/app/prebuilt-project/build/program + +COPY --from=build /app/temporal-omes /app/temporal-omes +COPY --from=build /app/workers/dotnet/projects/tests/project-build-*/. /app/prebuilt-project/ +COPY dockerfiles/project-entrypoint.sh /app/entrypoint.sh + +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/dockerfiles/dotnet.Dockerfile b/dockerfiles/dotnet.Dockerfile index 10fed5f7..e5bcc1f7 100644 --- a/dockerfiles/dotnet.Dockerfile +++ b/dockerfiles/dotnet.Dockerfile @@ -28,6 +28,7 @@ COPY loadgen ./loadgen COPY scenarios ./scenarios COPY metrics ./metrics COPY workers/*.go ./workers/ +COPY workers/go/projects/api ./workers/go/projects/api COPY go.mod go.sum ./ # Build the CLI @@ -41,6 +42,7 @@ COPY ${SDK_DIR} ./repo # Copy the worker files COPY workers/dotnet ./workers/dotnet +COPY workers/proto ./workers/proto # Prepare the worker RUN CGO_ENABLED=0 ./temporal-omes prepare-worker --language cs --dir-name prepared --version "$SDK_VERSION" diff --git a/dockerfiles/go-project.Dockerfile b/dockerfiles/go-project.Dockerfile new file mode 100644 index 00000000..cebd5bb4 --- /dev/null +++ b/dockerfiles/go-project.Dockerfile @@ -0,0 +1,43 @@ +# Build in a full featured container +ARG TARGETARCH +FROM --platform=linux/$TARGETARCH golang:1.25 AS build + +WORKDIR /app + +# Copy CLI build dependencies +COPY cmd ./cmd +COPY loadgen ./loadgen +COPY scenarios ./scenarios +COPY metrics ./metrics +COPY workers/*.go ./workers/ +COPY workers/go/projects/api ./workers/go/projects/api +COPY go.mod go.sum ./ + +# Build the CLI +RUN CGO_ENABLED=0 go build -o temporal-omes ./cmd + +ARG SDK_VERSION + +# Optional SDK dir to copy, defaults to unimportant file +ARG SDK_DIR=.gitignore +COPY ${SDK_DIR} ./repo + +# Copy the worker + project files +COPY workers/go ./workers/go +COPY workers/proto ./workers/proto + +# Build the project +ARG PROJECT_DIR +RUN CGO_ENABLED=0 ./temporal-omes prepare-worker --language go --dir-name project-prepared --project-dir "$PROJECT_DIR" --version "$SDK_VERSION" + +# Runtime container +FROM --platform=linux/$TARGETARCH alpine:3 + +ENV OMES_PROJECT_LANGUAGE=go +ENV OMES_PROJECT_BINARY=/app/prebuilt-project/program + +COPY --from=build /app/temporal-omes /app/temporal-omes +COPY --from=build /app/workers/go/projects/tests/project-build-*/. /app/prebuilt-project/ +COPY dockerfiles/project-entrypoint.sh /app/entrypoint.sh + +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/dockerfiles/go.Dockerfile b/dockerfiles/go.Dockerfile index 0f618292..9c6c942d 100644 --- a/dockerfiles/go.Dockerfile +++ b/dockerfiles/go.Dockerfile @@ -10,6 +10,7 @@ COPY loadgen ./loadgen COPY scenarios ./scenarios COPY metrics ./metrics COPY workers/*.go ./workers/ +COPY workers/go/projects/api ./workers/go/projects/api COPY go.mod go.sum ./ # Build the CLI @@ -23,6 +24,7 @@ COPY ${SDK_DIR} ./repo # Copy the worker files COPY workers/go ./workers/go +COPY workers/proto ./workers/proto # Build the worker RUN CGO_ENABLED=0 ./temporal-omes prepare-worker --language go --dir-name prepared --version "$SDK_VERSION" diff --git a/dockerfiles/java.Dockerfile b/dockerfiles/java.Dockerfile index 2a507efa..b6618a36 100644 --- a/dockerfiles/java.Dockerfile +++ b/dockerfiles/java.Dockerfile @@ -21,6 +21,7 @@ COPY loadgen ./loadgen COPY metrics ./metrics COPY scenarios ./scenarios COPY workers/*.go ./workers/ +COPY workers/go/projects/api ./workers/go/projects/api COPY go.mod go.sum ./ # Build the CLI diff --git a/dockerfiles/project-entrypoint.sh b/dockerfiles/project-entrypoint.sh new file mode 100755 index 00000000..bab62180 --- /dev/null +++ b/dockerfiles/project-entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh +case "$1" in + worker|project-server) + exec ${OMES_PROJECT_BINARY} "$@" ;; + run-scenario) + exec /app/temporal-omes "$@" \ + --option "language=${OMES_PROJECT_LANGUAGE}" \ + --option "prebuilt-project-dir=/app/prebuilt-project" ;; + *) + exec /app/temporal-omes "$@" ;; +esac diff --git a/dockerfiles/python.Dockerfile b/dockerfiles/python.Dockerfile index fbefed9a..a2e945ac 100644 --- a/dockerfiles/python.Dockerfile +++ b/dockerfiles/python.Dockerfile @@ -31,6 +31,7 @@ COPY loadgen ./loadgen COPY scenarios ./scenarios COPY metrics ./metrics COPY workers/*.go ./workers/ +COPY workers/go/projects/api ./workers/go/projects/api COPY go.mod go.sum ./ # Build the CLI diff --git a/dockerfiles/ruby.Dockerfile b/dockerfiles/ruby.Dockerfile index 29333f59..286025c6 100644 --- a/dockerfiles/ruby.Dockerfile +++ b/dockerfiles/ruby.Dockerfile @@ -15,6 +15,7 @@ COPY loadgen ./loadgen COPY scenarios ./scenarios COPY metrics ./metrics COPY workers/*.go ./workers/ +COPY workers/go/projects/api ./workers/go/projects/api COPY go.mod go.sum ./ # Build the CLI diff --git a/dockerfiles/typescript.Dockerfile b/dockerfiles/typescript.Dockerfile index 9d495329..c230e2ef 100644 --- a/dockerfiles/typescript.Dockerfile +++ b/dockerfiles/typescript.Dockerfile @@ -26,6 +26,7 @@ COPY loadgen ./loadgen COPY scenarios ./scenarios COPY metrics ./metrics COPY workers/*.go ./workers/ +COPY workers/go/projects/api ./workers/go/projects/api COPY go.mod go.sum ./ # Build the CLI diff --git a/go.mod b/go.mod index 2bacffb2..5d13b2e3 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.11.1 github.com/temporalio/features v0.0.0-20260324215619-e5868d9ba03f + github.com/temporalio/omes/workers/go/projects/api v0.0.0-00010101000000-000000000000 go.temporal.io/api v1.62.1 go.temporal.io/sdk v1.40.0 go.uber.org/zap v1.27.0 @@ -70,4 +71,5 @@ require ( replace ( github.com/temporalio/features/features => github.com/temporalio/features/features v0.0.0-20260324215619-e5868d9ba03f github.com/temporalio/features/harness/go => github.com/temporalio/features/harness/go v0.0.0-20260324215619-e5868d9ba03f + github.com/temporalio/omes/workers/go/projects/api => ./workers/go/projects/api ) diff --git a/loadgen/scenario.go b/loadgen/scenario.go index 7c66c93e..24a78fd4 100644 --- a/loadgen/scenario.go +++ b/loadgen/scenario.go @@ -22,6 +22,7 @@ import ( "go.temporal.io/sdk/temporal" "go.uber.org/zap" + "github.com/temporalio/omes/cmd/clioptions" "github.com/temporalio/omes/loadgen/kitchensink" ) @@ -132,6 +133,9 @@ type ScenarioInfo struct { RootPath string // ExportOptions contains export-related configuration ExportOptions ExportOptions + // ClientOptions carries the raw connection options. Scenarios that spawn + // external client processes use this to pass connection info to those processes. + ClientOptions clioptions.ClientOptions } // ExportOptions contains configuration for exporting scenario data. diff --git a/scenarios/project/build.go b/scenarios/project/build.go new file mode 100644 index 00000000..c84027f4 --- /dev/null +++ b/scenarios/project/build.go @@ -0,0 +1,177 @@ +package project + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/temporalio/features/sdkbuild" + "github.com/temporalio/omes/cmd/clioptions" + "go.uber.org/zap" +) + +// BuildOptions configures a project build. +type BuildOptions struct { + // Language for the project. + Language clioptions.Language + // ProjectDir is the absolute or relative path to the project directory. + // Build output is placed in the parent directory of ProjectDir. + ProjectDir string + // Version is the SDK version to use (empty = auto). + Version string + // Logger for build output. + Logger *zap.SugaredLogger +} + +// Build builds a project test binary for the given language. +func Build(ctx context.Context, opts BuildOptions) (sdkbuild.Program, error) { + switch opts.Language { + case clioptions.LangGo: + return buildGo(ctx, opts) + case clioptions.LangDotNet: + return buildDotNet(ctx, opts) + default: + return nil, fmt.Errorf("unsupported language for project builds: %s", opts.Language) + } +} + +func buildGo(ctx context.Context, opts BuildOptions) (sdkbuild.Program, error) { + absProjectDir, err := filepath.Abs(opts.ProjectDir) + if err != nil { + return nil, fmt.Errorf("failed to resolve project dir: %w", err) + } + + baseDir := filepath.Dir(absProjectDir) + projectName := filepath.Base(absProjectDir) + pkgName := strings.ReplaceAll(projectName, "-", "") + testModule := fmt.Sprintf("github.com/temporalio/omes/workers/go/projects/tests/%s", projectName) + harnessModule := "github.com/temporalio/omes/workers/go/projects/harness" + apiModule := "github.com/temporalio/omes/workers/go/projects/api" + + absHarness, _ := filepath.Abs(filepath.Join(absProjectDir, "..", "..", "harness")) + absAPI, _ := filepath.Abs(filepath.Join(absProjectDir, "..", "..", "api")) + + goMod := fmt.Sprintf(`module github.com/temporalio/omes/build + +go 1.22 + +require %s v0.0.0 + +replace %s => %s +replace %s => %s +replace %s => %s +`, testModule, + testModule, absProjectDir, + harnessModule, absHarness, + apiModule, absAPI) + + goMain := fmt.Sprintf(`package main + +import %s "%s" + +func main() { + %s.Main() +} +`, pkgName, testModule, pkgName) + + dirName := fmt.Sprintf("project-build-%s", projectName) + buildDir := filepath.Join(baseDir, dirName) + if err := os.MkdirAll(buildDir, 0755); err != nil { + return nil, fmt.Errorf("failed creating build dir: %w", err) + } + + buildOpts := sdkbuild.BuildGoProgramOptions{ + BaseDir: baseDir, + DirName: dirName, + Version: opts.Version, + GoModContents: goMod, + GoMainContents: goMain, + } + if opts.Logger != nil { + buildOpts.Stdout = &logWriter{logger: opts.Logger} + buildOpts.Stderr = &logWriter{logger: opts.Logger} + opts.Logger.Infof("Building project at %s", baseDir) + } + + prog, err := sdkbuild.BuildGoProgram(ctx, buildOpts) + if err != nil { + return nil, fmt.Errorf("failed to build project: %w", err) + } + return prog, nil +} + +func buildDotNet(ctx context.Context, opts BuildOptions) (sdkbuild.Program, error) { + absProjectDir, err := filepath.Abs(opts.ProjectDir) + if err != nil { + return nil, fmt.Errorf("failed to resolve project dir: %w", err) + } + + if opts.Logger != nil { + opts.Logger.Infof("Building .NET project at %s", absProjectDir) + } + + // Find the project's csproj file (.csproj) + projectName := filepath.Base(absProjectDir) + absCsprojFile := filepath.Join(absProjectDir, projectName+".csproj") + if _, err := os.Stat(absCsprojFile); err != nil { + return nil, fmt.Errorf("cannot find %s.csproj in %s: %w", projectName, absProjectDir, err) + } + + baseDir := filepath.Dir(absProjectDir) + dirName := fmt.Sprintf("project-build-%s", projectName) + buildDir := filepath.Join(baseDir, dirName) + if err := os.MkdirAll(buildDir, 0755); err != nil { + return nil, fmt.Errorf("failed creating build dir: %w", err) + } + + buildOpts := sdkbuild.BuildDotNetProgramOptions{ + BaseDir: baseDir, + DirName: dirName, + Version: opts.Version, + Configuration: "Release", + ProgramContents: fmt.Sprintf("return await %s.RunAsync(args);", projectName), + CsprojContents: ` + + Exe + net8.0 + + + + + `, + } + if opts.Logger != nil { + buildOpts.Stdout = &logWriter{logger: opts.Logger} + buildOpts.Stderr = &logWriter{logger: opts.Logger} + } + + prog, err := sdkbuild.BuildDotNetProgram(ctx, buildOpts) + if err != nil { + return nil, fmt.Errorf("failed to build .NET project: %w", err) + } + return prog, nil +} + +// LoadPrebuilt loads an already-built project binary from the given directory. +// This is used in Docker containers where the binary was built during image creation. +func LoadPrebuilt(dir string, lang clioptions.Language) (sdkbuild.Program, error) { + switch lang { + case clioptions.LangGo: + return sdkbuild.GoProgramFromDir(dir) + case clioptions.LangDotNet: + return sdkbuild.DotNetProgramFromDir(dir) + default: + return nil, fmt.Errorf("prebuilt projects not supported for language: %s", lang) + } +} + +type logWriter struct { + logger *zap.SugaredLogger +} + +func (w *logWriter) Write(p []byte) (int, error) { + w.logger.Debug(strings.TrimSpace(string(p))) + return len(p), nil +} diff --git a/scenarios/project/handle.go b/scenarios/project/handle.go new file mode 100644 index 00000000..64accf55 --- /dev/null +++ b/scenarios/project/handle.go @@ -0,0 +1,107 @@ +package project + +import ( + "context" + "fmt" + "net" + "time" + + "github.com/temporalio/omes/loadgen" + "github.com/temporalio/omes/workers/go/projects/api" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +const ( + defaultClientHost = "localhost" + defaultClientReadyTimeout = 3 * time.Second + defaultClientReadyCheckInterval = 100 * time.Millisecond +) + +// ProjectHandle is a gRPC client for calling project service endpoints. +type ProjectHandle struct { + address string + conn *grpc.ClientConn + client api.ProjectServiceClient +} + +func NewProjectHandle(ctx context.Context, port int, req *api.InitRequest) (ProjectHandle, error) { + address := fmt.Sprintf("%s:%d", defaultClientHost, port) + c := ProjectHandle{address: address} + + // Wait for the gRPC server to be ready + deadline := time.Now().Add(defaultClientReadyTimeout) + var err error + for time.Now().Before(deadline) { + conn, dialErr := net.Dial("tcp", c.address) + if dialErr == nil { + conn.Close() + err = nil + break + } + err = dialErr + select { + case <-ctx.Done(): + return ProjectHandle{}, ctx.Err() + case <-time.After(defaultClientReadyCheckInterval): + } + } + if err != nil { + return ProjectHandle{}, fmt.Errorf("project server not ready after %v: %w", defaultClientReadyTimeout, err) + } + + conn, err := grpc.NewClient( + c.address, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + return ProjectHandle{}, fmt.Errorf("failed to connect project service %s: %w", c.address, err) + } + + c.conn = conn + c.client = api.NewProjectServiceClient(conn) + + if err := c.init(ctx, req); err != nil { + return ProjectHandle{}, fmt.Errorf("project init failed: %w", err) + } + return c, nil +} + +func (c *ProjectHandle) Close() error { + if c.conn == nil { + return nil + } + err := c.conn.Close() + c.conn = nil + c.client = nil + return err +} + +func (c *ProjectHandle) init(ctx context.Context, req *api.InitRequest) error { + _, err := c.client.Init(ctx, req) + if err != nil { + return fmt.Errorf("init request failed: %w", err) + } + return nil +} + +// Execute calls ProjectService.Execute for a single iteration. +func (c *ProjectHandle) Execute(ctx context.Context, req *api.ExecuteRequest) (*api.ExecuteResponse, error) { + resp, err := c.client.Execute(ctx, req) + if err != nil { + return nil, fmt.Errorf("execute request failed: %w", err) + } + return resp, nil +} + +// NewSteadyRateExecutor creates an executor that calls Execute once per iteration. +func NewSteadyRateExecutor(c *ProjectHandle) loadgen.Executor { + return &loadgen.GenericExecutor{ + Execute: func(ctx context.Context, run *loadgen.Run) error { + _, err := c.Execute(ctx, &api.ExecuteRequest{ + Iteration: int64(run.Iteration), + }) + return err + }, + } +} diff --git a/scenarios/project/project.go b/scenarios/project/project.go new file mode 100644 index 00000000..1a6af9c4 --- /dev/null +++ b/scenarios/project/project.go @@ -0,0 +1,189 @@ +package project + +import ( + "context" + "fmt" + "net" + "os" + "os/exec" + "path/filepath" + "strconv" + + "github.com/temporalio/features/sdkbuild" + "github.com/temporalio/omes/cmd/clioptions" + "github.com/temporalio/omes/loadgen" + "github.com/temporalio/omes/workers/go/projects/api" + "go.uber.org/zap" +) + +func init() { + loadgen.MustRegisterScenario(loadgen.Scenario{ + Description: `Run a self-contained project test. Builds (or loads) the project binary, spawns a project-server, and drives iterations via gRPC. + Required: --option language= + One of: --option project-dir= (build from source) + --option prebuilt-project-dir= (use pre-built binary, e.g. in Docker) + Optional: --option project-config-file= (project-specific JSON config)`, + ExecutorFn: func() loadgen.Executor { + return &projectScenarioExecutor{} + }, + }) +} + +type projectScenarioOptions struct { + language clioptions.Language + projectDir string + prebuiltDir string + configJSON []byte +} + +// projectScenarioExecutor wraps the full project test lifecycle: +// build (or load) project binary, spawn processes, gRPC init, execute iterations, cleanup. +type projectScenarioExecutor struct{} + +func (e *projectScenarioExecutor) Run(ctx context.Context, info loadgen.ScenarioInfo) error { + opts, err := e.validate(info) + if err != nil { + return err + } + + var prog sdkbuild.Program + if opts.prebuiltDir != "" { + info.Logger.Infof("Loading prebuilt project from %s", opts.prebuiltDir) + prog, err = LoadPrebuilt(opts.prebuiltDir, opts.language) + } else { + info.Logger.Infof("Building project %s", filepath.Base(opts.projectDir)) + prog, err = Build(ctx, BuildOptions{ + Language: opts.language, + ProjectDir: opts.projectDir, + Logger: info.Logger, + }) + } + if err != nil { + return fmt.Errorf("failed to prepare project: %w", err) + } + + port, err := findAvailablePort() + if err != nil { + return fmt.Errorf("failed to allocate port: %w", err) + } + + taskQueue := loadgen.TaskQueueForRun(info.RunID) + + // Spawn project server + serverCtx, serverCancel := context.WithCancel(ctx) + defer serverCancel() + + serverCmd, err := startProjectProcess(serverCtx, prog, info.Logger, []string{ + "project-server", + "--port", strconv.Itoa(port), + }) + if err != nil { + return fmt.Errorf("failed to spawn project server: %w", err) + } + defer stopProjectProcess("project-server", serverCancel, serverCmd, info.Logger) + + // Connect and Init + co := info.ClientOptions + handle, err := NewProjectHandle(ctx, port, &api.InitRequest{ + ExecutionId: info.ExecutionID, + RunId: info.RunID, + TaskQueue: taskQueue, + ConnectOptions: &api.ConnectOptions{ + Namespace: co.Namespace, + ServerAddress: co.Address, + AuthHeader: co.AuthHeader, + EnableTls: co.EnableTLS, + TlsCertPath: co.ClientCertPath, + TlsKeyPath: co.ClientKeyPath, + TlsServerName: co.TLSServerName, + DisableHostVerification: co.DisableHostVerification, + }, + ConfigJson: opts.configJSON, + RegisterSearchAttributes: !info.Configuration.DoNotRegisterSearchAttributes, + }) + if err != nil { + return fmt.Errorf("failed to init project: %w", err) + } + defer handle.Close() + + executor := NewSteadyRateExecutor(&handle) + return executor.Run(ctx, info) +} + +func (e *projectScenarioExecutor) validate(info loadgen.ScenarioInfo) (projectScenarioOptions, error) { + var opts projectScenarioOptions + + lang := info.ScenarioOptions["language"] + if lang == "" { + return opts, fmt.Errorf("--option language= is required") + } + if err := opts.language.Set(lang); err != nil { + return opts, fmt.Errorf("unrecognized language: %s", lang) + } + + projectDir := info.ScenarioOptions["project-dir"] + prebuiltDir := info.ScenarioOptions["prebuilt-project-dir"] + if projectDir == "" && prebuiltDir == "" { + return opts, fmt.Errorf("either --option project-dir or --option prebuilt-project-dir is required") + } + if projectDir != "" && prebuiltDir != "" { + return opts, fmt.Errorf("cannot specify both project-dir and prebuilt-project-dir") + } + + if projectDir != "" { + abs, err := filepath.Abs(projectDir) + if err != nil { + return opts, fmt.Errorf("failed to resolve project-dir: %w", err) + } + opts.projectDir = abs + } else { + abs, err := filepath.Abs(prebuiltDir) + if err != nil { + return opts, fmt.Errorf("failed to resolve prebuilt-project-dir: %w", err) + } + opts.prebuiltDir = abs + } + + if configPath := info.ScenarioOptions["project-config-file"]; configPath != "" { + data, err := os.ReadFile(configPath) + if err != nil { + return opts, fmt.Errorf("failed to read config file %s: %w", configPath, err) + } + opts.configJSON = data + } + + return opts, nil +} + +func findAvailablePort() (int, error) { + listener, err := net.Listen("tcp", ":0") + if err != nil { + return 0, err + } + port := listener.Addr().(*net.TCPAddr).Port + listener.Close() + return port, nil +} + +func startProjectProcess(ctx context.Context, prog sdkbuild.Program, logger *zap.SugaredLogger, args []string) (*exec.Cmd, error) { + cmd, err := prog.NewCommand(ctx, args...) + if err != nil { + return nil, err + } + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Start(); err != nil { + return nil, err + } + logger.Infof("Started process (PID %d): %v", cmd.Process.Pid, args) + return cmd, nil +} + +func stopProjectProcess(name string, cancel context.CancelFunc, cmd *exec.Cmd, logger *zap.SugaredLogger) { + cancel() + if cmd != nil { + if err := cmd.Wait(); err != nil { + logger.Debugf("Process %s exited: %v", name, err) + } + } +} diff --git a/scenarios/project/project_test.go b/scenarios/project/project_test.go new file mode 100644 index 00000000..6887632f --- /dev/null +++ b/scenarios/project/project_test.go @@ -0,0 +1,123 @@ +package project + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/temporalio/features/sdkbuild" + "github.com/temporalio/omes/cmd/clioptions" + "github.com/temporalio/omes/workers/go/projects/api" + "go.temporal.io/sdk/testsuite" + "go.uber.org/zap" +) + +func TestGoHelloWorld(t *testing.T) { + runProjectTest(t, "go", "helloworld", nil) +} + +func TestDotNetHelloWorld(t *testing.T) { + runProjectTest(t, "dotnet", "HelloWorld", nil) +} + +func TestDotNetNexusSimpleWorkflow(t *testing.T) { + runProjectTest(t, "dotnet", "NexusSimpleWorkflow", nil) +} + +func runProjectTest(t *testing.T, lang, testName string, config []byte) { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + server, err := testsuite.StartDevServer(ctx, testsuite.DevServerOptions{ + LogLevel: "error", + Stdout: os.Stderr, + Stderr: os.Stderr, + }) + require.NoError(t, err) + defer server.Stop() + + hostPort := server.FrontendHostPort() + namespace := "default" + taskQueue := fmt.Sprintf("test-%s-%s-%d", lang, testName, time.Now().UnixNano()) + + repoRoot, err := filepath.Abs("../..") + require.NoError(t, err) + projectDir := filepath.Join(repoRoot, "workers", lang, "projects/tests", testName) + + logger, _ := zap.NewDevelopment() + sugar := logger.Sugar() + + var language clioptions.Language + language.Set(lang) + prog, err := Build(ctx, BuildOptions{ + Language: language, + ProjectDir: projectDir, + Logger: sugar, + }) + require.NoError(t, err, "failed to build project %s/%s", lang, testName) + + workerCtx, workerCancel := context.WithCancel(ctx) + defer workerCancel() + + workerCmd, err := startTestProcess(workerCtx, prog, []string{ + "worker", + "--task-queue", taskQueue, + "--server-address", hostPort, + "--namespace", namespace, + }) + require.NoError(t, err, "failed to start worker") + defer func() { + workerCancel() + workerCmd.Wait() + }() + + serverPort, err := findAvailablePort() + require.NoError(t, err) + + serverCtx, serverCancel := context.WithCancel(ctx) + defer serverCancel() + + serverCmd, err := startTestProcess(serverCtx, prog, []string{ + "project-server", + "--port", strconv.Itoa(serverPort), + }) + require.NoError(t, err, "failed to start project server") + defer func() { + serverCancel() + serverCmd.Wait() + }() + + handle, err := NewProjectHandle(ctx, serverPort, &api.InitRequest{ + ExecutionId: fmt.Sprintf("test-%d", time.Now().UnixNano()), + RunId: "test-run", + TaskQueue: taskQueue, + ConnectOptions: &api.ConnectOptions{Namespace: namespace, ServerAddress: hostPort}, + RegisterSearchAttributes: true, + ConfigJson: config, + }) + require.NoError(t, err, "failed to init project handle") + defer handle.Close() + + _, err = handle.Execute(ctx, &api.ExecuteRequest{Iteration: 1}) + require.NoError(t, err, "execute failed for project %s/%s", lang, testName) +} + +func startTestProcess(ctx context.Context, prog sdkbuild.Program, args []string) (*exec.Cmd, error) { + cmd, err := prog.NewCommand(ctx, args...) + if err != nil { + return nil, err + } + cmd.Stdout = os.Stderr + cmd.Stderr = os.Stderr + if err := cmd.Start(); err != nil { + return nil, err + } + return cmd, nil +} diff --git a/workers/build.go b/workers/build.go index 2d69a183..f3677237 100644 --- a/workers/build.go +++ b/workers/build.go @@ -18,6 +18,7 @@ import ( type Builder struct { DirName string SdkOptions clioptions.SdkOptions + ProjectDir string Logger *zap.SugaredLogger stdout io.Writer stderr io.Writer @@ -76,7 +77,10 @@ require github.com/temporalio/omes v1.0.0 require github.com/temporalio/omes/workers/go v1.0.0 replace github.com/temporalio/omes => ../../../ -replace github.com/temporalio/omes/workers/go => ../`, +replace github.com/temporalio/omes/workers/go => ../ +// Required because the root omes module transitively depends on this unpublished +// local module via scenarios/project -> workers/go/projects/api. +replace github.com/temporalio/omes/workers/go/projects/api => ../projects/api`, GoMainContents: `package main import "github.com/temporalio/omes/workers/go/worker" diff --git a/workers/dotnet/Temporalio.Omes.csproj b/workers/dotnet/Temporalio.Omes.csproj index 39957069..f1b58b20 100644 --- a/workers/dotnet/Temporalio.Omes.csproj +++ b/workers/dotnet/Temporalio.Omes.csproj @@ -16,6 +16,10 @@ Library + + + + diff --git a/workers/dotnet/projects/harness/Harness.cs b/workers/dotnet/projects/harness/Harness.cs new file mode 100644 index 00000000..e3b6e947 --- /dev/null +++ b/workers/dotnet/projects/harness/Harness.cs @@ -0,0 +1,312 @@ +using System.CommandLine; +using System.CommandLine.Invocation; +using Google.Protobuf; +using Grpc.Core; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.DependencyInjection; +using Temporalio.Client; +using Temporal.Omes.Projects.V1; + +namespace Temporalio.Omes.Projects.Harness; + +/// +/// Minimal config passed to ClientFunc for client creation. +/// +public record ClientConfig( + string TaskQueue +); + +/// +/// Run-level config passed to InitFunc for project-specific setup. +/// +public record InitConfig( + byte[]? ConfigJson, + string TaskQueue, + string RunId, + string ExecutionId +); + +/// +/// Per-iteration data passed to ExecuteFunc. +/// +public record ExecuteInfo( + string TaskQueue, + string ExecutionId, + string RunId, + long Iteration, + byte[]? Payload +); + +/// +/// Worker configuration passed to WorkerFunc. +/// +public record WorkerConfig( + string TaskQueue, + string? PromListenAddress = null +); + +/// +/// Bridges the omes CLI with .NET project test code via gRPC. +/// +public partial class ProjectHarness +{ + public const string OmesSearchAttributeKey = "OmesExecutionID"; + + public delegate Task ClientFunc( + TemporalClientConnectOptions opts, ClientConfig config); + + public delegate Task InitFunc( + ITemporalClient client, InitConfig config); + + public delegate Task ExecuteFunc( + ITemporalClient client, ExecuteInfo info); + + public delegate Task WorkerFunc( + ITemporalClient client, WorkerConfig config); + + private ClientFunc? _clientFn; + private InitFunc? _initFn; + private WorkerFunc? _workerFn; + private ExecuteFunc? _executeFn; + private ITemporalClient? _client; + private RunContext? _runCtx; + + public void RegisterClient(ClientFunc fn) + { + if (_clientFn != null) + throw new InvalidOperationException("Client factory already registered"); + _clientFn = fn; + } + + public void OnInit(InitFunc fn) + { + if (_initFn != null) + throw new InvalidOperationException("Init handler already registered"); + _initFn = fn; + } + + public void RegisterWorker(WorkerFunc fn) + { + if (_workerFn != null) + throw new InvalidOperationException("Worker already registered"); + _workerFn = fn; + } + + public void OnExecute(ExecuteFunc fn) + { + if (_executeFn != null) + throw new InvalidOperationException("Execute handler already registered"); + _executeFn = fn; + } + + public async Task RunAsync(string[] args) + { + if (_clientFn == null) + throw new InvalidOperationException("No client factory registered; call RegisterClient before RunAsync"); + + var root = new RootCommand("Project test harness"); + root.Add(BuildWorkerCommand()); + + // Project-server subcommand + var portOption = new Option("--port", () => 8080, "gRPC server port"); + var serverCmd = new Command("project-server", "Run the project gRPC server"); + serverCmd.Add(portOption); + serverCmd.SetHandler(async (InvocationContext ctx) => + { + if (_executeFn == null) + throw new InvalidOperationException("No execute handler registered"); + if (_workerFn == null) + throw new InvalidOperationException("No worker handler registered"); + + var port = ctx.ParseResult.GetValueForOption(portOption); + await StartGrpcServerAsync(port); + }); + root.Add(serverCmd); + + return await root.InvokeAsync(args); + } + + private async Task StartGrpcServerAsync(int port) + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.ConfigureKestrel(options => + { + options.ListenAnyIP(port, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + }); + }); + builder.Services.AddGrpc(); + builder.Services.AddSingleton(this); + + var app = builder.Build(); + app.MapGrpcService(); + + await app.RunAsync(); + } + + internal async Task HandleInit(InitRequest req) + { + ValidateInit(req); + + var co = req.ConnectOptions; + var opts = await BuildClientConnectOptions( + co.ServerAddress, + co.Namespace, + co.AuthHeader, + co.EnableTls, + co.TlsCertPath, + co.TlsKeyPath, + co.TlsServerName); + + _client = await _clientFn!(opts, new ClientConfig(req.TaskQueue)); + + if (_initFn != null) + { + await _initFn(_client, new InitConfig( + req.ConfigJson?.ToByteArray(), + req.TaskQueue, + req.RunId, + req.ExecutionId + )); + } + + if (req.RegisterSearchAttributes) + { + await RegisterSearchAttributes(co.Namespace); + } + + _runCtx = new RunContext(req.TaskQueue, req.ExecutionId, req.RunId); + return new InitResponse(); + } + + internal async Task HandleExecute(ExecuteRequest req) + { + if (req == null) + throw new RpcException(new Status(StatusCode.InvalidArgument, "execute request is nil")); + if (_executeFn == null) + throw new RpcException(new Status(StatusCode.FailedPrecondition, "no execute handler")); + if (_runCtx == null) + throw new RpcException(new Status(StatusCode.FailedPrecondition, "not initialized")); + + await _executeFn(_client!, new ExecuteInfo( + _runCtx.TaskQueue, + _runCtx.ExecutionId, + _runCtx.RunId, + req.Iteration, + req.Payload?.ToByteArray() + )); + + return new ExecuteResponse(); + } + + // --- Internal helpers --- + + private static void ValidateInit(InitRequest req) + { + if (req == null) + throw new RpcException(new Status(StatusCode.InvalidArgument, "request: required")); + if (string.IsNullOrEmpty(req.ExecutionId)) + throw new RpcException(new Status(StatusCode.InvalidArgument, "execution_id: required")); + if (string.IsNullOrEmpty(req.RunId)) + throw new RpcException(new Status(StatusCode.InvalidArgument, "run_id: required")); + if (string.IsNullOrEmpty(req.TaskQueue)) + throw new RpcException(new Status(StatusCode.InvalidArgument, "task_queue: required")); + var co = req.ConnectOptions; + if (co == null) + throw new RpcException(new Status(StatusCode.InvalidArgument, "connect_options: required")); + if (string.IsNullOrEmpty(co.Namespace)) + throw new RpcException(new Status(StatusCode.InvalidArgument, "connect_options.namespace: required")); + if (string.IsNullOrEmpty(co.ServerAddress)) + throw new RpcException(new Status(StatusCode.InvalidArgument, "connect_options.server_address: required")); + } + + internal static async Task BuildClientConnectOptions( + string serverAddress, + string ns, + string? authHeader, + bool enableTls, + string? tlsCertPath, + string? tlsKeyPath, + string? tlsServerName) + { + var opts = new TemporalClientConnectOptions(serverAddress) { Namespace = ns }; + + if (!string.IsNullOrEmpty(authHeader)) + { + opts.RpcMetadata = new Dictionary + { + ["Authorization"] = authHeader + }; + } + + if (enableTls || !string.IsNullOrEmpty(tlsCertPath)) + { + opts.Tls = new TlsOptions(); + if (!string.IsNullOrEmpty(tlsServerName)) + { + opts.Tls.Domain = tlsServerName; + } + if (!string.IsNullOrEmpty(tlsCertPath) && !string.IsNullOrEmpty(tlsKeyPath)) + { + opts.Tls.ClientCert = await File.ReadAllBytesAsync(tlsCertPath); + opts.Tls.ClientPrivateKey = await File.ReadAllBytesAsync(tlsKeyPath); + } + } + + return opts; + } + + private async Task RegisterSearchAttributes(string ns) + { + try + { + await _client!.Connection.OperatorService.AddSearchAttributesAsync( + new Temporalio.Api.OperatorService.V1.AddSearchAttributesRequest + { + Namespace = ns, + SearchAttributes = + { + { "KS_Keyword", Temporalio.Api.Enums.V1.IndexedValueType.Keyword }, + { "KS_Int", Temporalio.Api.Enums.V1.IndexedValueType.Int }, + { OmesSearchAttributeKey, Temporalio.Api.Enums.V1.IndexedValueType.Keyword }, + } + }); + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.AlreadyExists) + { + // Already registered, that's fine + } + catch (RpcException ex) when (ex.Message.Contains("attributes mapping unavailable")) + { + // Also fine + } + } + + private record RunContext(string TaskQueue, string ExecutionId, string RunId); +} + +/// +/// gRPC service implementation that delegates to the harness. +/// +public class ProjectServiceImpl : ProjectService.ProjectServiceBase +{ + private readonly ProjectHarness _harness; + + public ProjectServiceImpl(ProjectHarness harness) + { + _harness = harness; + } + + public override async Task Init(InitRequest request, ServerCallContext context) + { + return await _harness.HandleInit(request); + } + + public override async Task Execute(ExecuteRequest request, ServerCallContext context) + { + return await _harness.HandleExecute(request); + } +} diff --git a/workers/dotnet/projects/harness/ProjectHarness.csproj b/workers/dotnet/projects/harness/ProjectHarness.csproj new file mode 100644 index 00000000..3eb69e35 --- /dev/null +++ b/workers/dotnet/projects/harness/ProjectHarness.csproj @@ -0,0 +1,20 @@ + + + net8.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/workers/dotnet/projects/harness/Worker.cs b/workers/dotnet/projects/harness/Worker.cs new file mode 100644 index 00000000..0265f570 --- /dev/null +++ b/workers/dotnet/projects/harness/Worker.cs @@ -0,0 +1,94 @@ +using System.CommandLine; +using System.CommandLine.Invocation; +using Temporalio.Client; +using Temporalio.Runtime; + +namespace Temporalio.Omes.Projects.Harness; + +public partial class ProjectHarness +{ + private static readonly Option ServerAddressOption = new( + "--server-address", () => "localhost:7233", "Temporal server address"); + private static readonly Option NamespaceOption = new( + "--namespace", () => "default", "Temporal namespace"); + private static readonly Option TaskQueueOption = new( + "--task-queue", "Task queue name (required)"); + private static readonly Option AuthHeaderOption = new( + "--auth-header", "Authorization header value"); + private static readonly Option TlsOption = new( + "--tls", "Enable TLS"); + private static readonly Option TlsCertPathOption = new( + "--tls-cert-path", "Path to client TLS certificate"); + private static readonly Option TlsKeyPathOption = new( + "--tls-key-path", "Path to client TLS private key"); + private static readonly Option TlsServerNameOption = new( + "--tls-server-name", "TLS target server name"); + private static readonly Option DisableHostVerificationOption = new( + "--disable-tls-host-verification", "Disable TLS host verification (not supported in .NET SDK)"); + private static readonly Option PromListenAddressOption = new( + "--prom-listen-address", "Prometheus metrics address"); + + private Command BuildWorkerCommand() + { + var cmd = new Command("worker", "Run the project worker"); + cmd.Add(TaskQueueOption); + cmd.Add(ServerAddressOption); + cmd.Add(NamespaceOption); + cmd.Add(AuthHeaderOption); + cmd.Add(TlsOption); + cmd.Add(TlsCertPathOption); + cmd.Add(TlsKeyPathOption); + cmd.Add(TlsServerNameOption); + cmd.Add(DisableHostVerificationOption); + cmd.Add(PromListenAddressOption); + cmd.SetHandler(StartWorkerAsync); + return cmd; + } + + private async Task StartWorkerAsync(InvocationContext ctx) + { + if (_workerFn == null) + throw new InvalidOperationException("No worker registered"); + + var taskQueue = ctx.ParseResult.GetValueForOption(TaskQueueOption)!; + if (string.IsNullOrEmpty(taskQueue)) + { + Console.Error.WriteLine("error: --task-queue is required"); + Environment.Exit(1); + } + + var opts = await BuildClientConnectOptions( + ctx.ParseResult.GetValueForOption(ServerAddressOption)!, + ctx.ParseResult.GetValueForOption(NamespaceOption)!, + ctx.ParseResult.GetValueForOption(AuthHeaderOption), + ctx.ParseResult.GetValueForOption(TlsOption), + ctx.ParseResult.GetValueForOption(TlsCertPathOption), + ctx.ParseResult.GetValueForOption(TlsKeyPathOption), + ctx.ParseResult.GetValueForOption(TlsServerNameOption)); + + // Set up Prometheus metrics if address provided + var promListenAddress = ctx.ParseResult.GetValueForOption(PromListenAddressOption); + if (!string.IsNullOrEmpty(promListenAddress)) + { + var runtime = new TemporalRuntime(new() + { + Telemetry = new TelemetryOptions + { + Metrics = new MetricsOptions + { + Prometheus = new PrometheusOptions(promListenAddress) + { + UseSecondsForDuration = true + } + } + } + }); + opts.Runtime = runtime; + } + + var client = await _clientFn!(opts, new ClientConfig(taskQueue)); + + Console.WriteLine($"Worker starting on task queue: {taskQueue}"); + await _workerFn(client, new WorkerConfig(taskQueue, promListenAddress)); + } +} diff --git a/workers/dotnet/projects/tests/HelloWorld/HelloWorld.cs b/workers/dotnet/projects/tests/HelloWorld/HelloWorld.cs new file mode 100644 index 00000000..d4f85dc3 --- /dev/null +++ b/workers/dotnet/projects/tests/HelloWorld/HelloWorld.cs @@ -0,0 +1,41 @@ +using Temporalio.Client; +using Temporalio.Common; +using Temporalio.Omes.Projects.Harness; + +public static class HelloWorld +{ + public static Task RunAsync(string[] args) + { + var harness = new ProjectHarness(); + harness.RegisterClient(async (opts, config) => + { + return await TemporalClient.ConnectAsync(opts); + }); + + harness.RegisterWorker(async (client, config) => + { + using var worker = new Temporalio.Worker.TemporalWorker(client, new Temporalio.Worker.TemporalWorkerOptions(config.TaskQueue) + .AddWorkflow()); + using var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; + await worker.ExecuteAsync(cts.Token); + }); + + harness.OnExecute(async (client, info) => + { + var handle = await client.StartWorkflowAsync( + (HelloWorldWorkflow wf) => wf.RunAsync("World"), + new WorkflowOptions(id: $"helloworld-{info.Iteration}", taskQueue: info.TaskQueue) + { + TypedSearchAttributes = new SearchAttributeCollection.Builder() + .Set(SearchAttributeKey.CreateKeyword(ProjectHarness.OmesSearchAttributeKey), info.ExecutionId) + .ToSearchAttributeCollection() + }); + + var result = await handle.GetResultAsync(); + Console.WriteLine($"Workflow result: {result}"); + }); + + return harness.RunAsync(args); + } +} diff --git a/workers/dotnet/projects/tests/HelloWorld/HelloWorld.csproj b/workers/dotnet/projects/tests/HelloWorld/HelloWorld.csproj new file mode 100644 index 00000000..fe3d52e6 --- /dev/null +++ b/workers/dotnet/projects/tests/HelloWorld/HelloWorld.csproj @@ -0,0 +1,13 @@ + + + net8.0 + enable + enable + + + + + + + + diff --git a/workers/dotnet/projects/tests/HelloWorld/HelloWorldWorkflow.cs b/workers/dotnet/projects/tests/HelloWorld/HelloWorldWorkflow.cs new file mode 100644 index 00000000..d333df95 --- /dev/null +++ b/workers/dotnet/projects/tests/HelloWorld/HelloWorldWorkflow.cs @@ -0,0 +1,11 @@ +using Temporalio.Workflows; + +[Workflow] +public class HelloWorldWorkflow +{ + [WorkflowRun] + public Task RunAsync(string name) + { + return Task.FromResult($"Hello {name}"); + } +} diff --git a/workers/dotnet/projects/tests/NexusSimpleWorkflow/CallerWorkflow.cs b/workers/dotnet/projects/tests/NexusSimpleWorkflow/CallerWorkflow.cs new file mode 100644 index 00000000..806cbbb9 --- /dev/null +++ b/workers/dotnet/projects/tests/NexusSimpleWorkflow/CallerWorkflow.cs @@ -0,0 +1,15 @@ +using Temporalio.Nexus; +using Temporalio.Workflows; + +namespace NexusSimpleWorkflowProject; + +[Workflow] +public class CallerWorkflow +{ + [WorkflowRun] + public async Task RunAsync(string endpointName, string input) + { + return await Workflow.CreateNexusWorkflowClient(endpointName) + .ExecuteNexusOperationAsync(svc => svc.DoSomething(input)); + } +} diff --git a/workers/dotnet/projects/tests/NexusSimpleWorkflow/HandlerWorkflow.cs b/workers/dotnet/projects/tests/NexusSimpleWorkflow/HandlerWorkflow.cs new file mode 100644 index 00000000..a29b5340 --- /dev/null +++ b/workers/dotnet/projects/tests/NexusSimpleWorkflow/HandlerWorkflow.cs @@ -0,0 +1,10 @@ +using Temporalio.Workflows; + +namespace NexusSimpleWorkflowProject; + +[Workflow] +public class HandlerWorkflow +{ + [WorkflowRun] + public Task RunAsync(string name) => Task.FromResult($"Hello from workflow, {name}"); +} diff --git a/workers/dotnet/projects/tests/NexusSimpleWorkflow/NexusService.cs b/workers/dotnet/projects/tests/NexusSimpleWorkflow/NexusService.cs new file mode 100644 index 00000000..24e83c6f --- /dev/null +++ b/workers/dotnet/projects/tests/NexusSimpleWorkflow/NexusService.cs @@ -0,0 +1,26 @@ +using NexusRpc; +using NexusRpc.Handlers; +using Temporalio.Nexus; + +namespace NexusSimpleWorkflowProject; + +[NexusService] +public interface IStringService +{ + [NexusOperation] + string DoSomething(string name); +} + +[NexusServiceHandler(typeof(IStringService))] +public class StringServiceHandler +{ + [NexusOperationHandler] + public IOperationHandler DoSomething() => + WorkflowRunOperationHandler.FromHandleFactory( + async (WorkflowRunOperationContext context, string input) => + { + return await context.StartWorkflowAsync( + (HandlerWorkflow wf) => wf.RunAsync(input), + new() { Id = $"nexus-handler-{Guid.NewGuid()}" }); + }); +} diff --git a/workers/dotnet/projects/tests/NexusSimpleWorkflow/NexusSimpleWorkflow.cs b/workers/dotnet/projects/tests/NexusSimpleWorkflow/NexusSimpleWorkflow.cs new file mode 100644 index 00000000..85adbd9a --- /dev/null +++ b/workers/dotnet/projects/tests/NexusSimpleWorkflow/NexusSimpleWorkflow.cs @@ -0,0 +1,75 @@ +using Temporalio.Client; +using Temporalio.Common; +using Temporalio.Omes.Projects.Harness; +using NexusSimpleWorkflowProject; + +public static class NexusSimpleWorkflow +{ + public static Task RunAsync(string[] args) + { + string? nexusEndpointName = null; + + var harness = new ProjectHarness(); + + harness.RegisterClient(async (opts, config) => + { + return await TemporalClient.ConnectAsync(opts); + }); + + harness.OnInit(async (client, config) => + { + nexusEndpointName = $"nexus-endpoint-{config.TaskQueue}"; + await client.Connection.OperatorService.CreateNexusEndpointAsync( + new Temporalio.Api.OperatorService.V1.CreateNexusEndpointRequest + { + Spec = new Temporalio.Api.Nexus.V1.EndpointSpec + { + Name = nexusEndpointName, + Target = new Temporalio.Api.Nexus.V1.EndpointTarget + { + Worker = new Temporalio.Api.Nexus.V1.EndpointTarget.Types.Worker + { + Namespace = client.Options.Namespace, + TaskQueue = config.TaskQueue, + }, + }, + }, + }); + Console.WriteLine($"Created Nexus endpoint: {nexusEndpointName}"); + }); + + harness.RegisterWorker(async (client, config) => + { + using var worker = new Temporalio.Worker.TemporalWorker(client, + new Temporalio.Worker.TemporalWorkerOptions(config.TaskQueue) + .AddWorkflow() + .AddWorkflow() + .AddNexusService(new StringServiceHandler())); + using var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; + await worker.ExecuteAsync(cts.Token); + }); + + harness.OnExecute(async (client, info) => + { + var handle = await client.StartWorkflowAsync( + (CallerWorkflow wf) => wf.RunAsync(nexusEndpointName!, "some-name"), + new WorkflowOptions(id: $"nexus-caller-{info.Iteration}", taskQueue: info.TaskQueue) + { + TypedSearchAttributes = new SearchAttributeCollection.Builder() + .Set(SearchAttributeKey.CreateKeyword(ProjectHarness.OmesSearchAttributeKey), info.ExecutionId) + .ToSearchAttributeCollection() + }); + + var result = await handle.GetResultAsync(); + Console.WriteLine($"Nexus workflow result: {result}"); + + if (result != "Hello from workflow, some-name") + { + throw new Exception($"unexpected result: {result}"); + } + }); + + return harness.RunAsync(args); + } +} diff --git a/workers/dotnet/projects/tests/NexusSimpleWorkflow/NexusSimpleWorkflow.csproj b/workers/dotnet/projects/tests/NexusSimpleWorkflow/NexusSimpleWorkflow.csproj new file mode 100644 index 00000000..7c22d18d --- /dev/null +++ b/workers/dotnet/projects/tests/NexusSimpleWorkflow/NexusSimpleWorkflow.csproj @@ -0,0 +1,13 @@ + + + net8.0 + enable + enable + + + + + + + + diff --git a/workers/go/go.mod b/workers/go/go.mod index 442b263e..da467599 100644 --- a/workers/go/go.mod +++ b/workers/go/go.mod @@ -62,3 +62,7 @@ require ( ) replace github.com/temporalio/omes => ../../ + +// Required because the root omes module transitively depends on this unpublished +// local module via scenarios/project -> workers/go/projects/api. +replace github.com/temporalio/omes/workers/go/projects/api => ./projects/api diff --git a/workers/go/projects/api/api.pb.go b/workers/go/projects/api/api.pb.go new file mode 100644 index 00000000..ee9c0573 --- /dev/null +++ b/workers/go/projects/api/api.pb.go @@ -0,0 +1,444 @@ +// Protocol for the project test framework. +// +// Each project test compiles into a single binary with two subcommands: +// - "worker": connects to Temporal and polls for tasks +// - "project-server": runs a gRPC server implementing ProjectService +// +// The omes CLI orchestrates the test by: +// 1. Starting the worker (separate process or container) +// 2. Starting the project-server +// 3. Sending Init (creates client, optional project setup like Nexus endpoints) +// 4. Sending Execute for each iteration (starts workflows, verifies results) + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: api.proto + +package api + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +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) +) + +// Connection options passed from the CLI to the project-server during Init. +// The project-server uses these to create its own Temporal client. +type ConnectOptions struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + ServerAddress string `protobuf:"bytes,2,opt,name=server_address,json=serverAddress,proto3" json:"server_address,omitempty"` + AuthHeader string `protobuf:"bytes,3,opt,name=auth_header,json=authHeader,proto3" json:"auth_header,omitempty"` + EnableTls bool `protobuf:"varint,4,opt,name=enable_tls,json=enableTls,proto3" json:"enable_tls,omitempty"` + TlsCertPath string `protobuf:"bytes,5,opt,name=tls_cert_path,json=tlsCertPath,proto3" json:"tls_cert_path,omitempty"` + TlsKeyPath string `protobuf:"bytes,6,opt,name=tls_key_path,json=tlsKeyPath,proto3" json:"tls_key_path,omitempty"` + TlsServerName string `protobuf:"bytes,7,opt,name=tls_server_name,json=tlsServerName,proto3" json:"tls_server_name,omitempty"` + DisableHostVerification bool `protobuf:"varint,8,opt,name=disable_host_verification,json=disableHostVerification,proto3" json:"disable_host_verification,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConnectOptions) Reset() { + *x = ConnectOptions{} + mi := &file_api_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConnectOptions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConnectOptions) ProtoMessage() {} + +func (x *ConnectOptions) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConnectOptions.ProtoReflect.Descriptor instead. +func (*ConnectOptions) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{0} +} + +func (x *ConnectOptions) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *ConnectOptions) GetServerAddress() string { + if x != nil { + return x.ServerAddress + } + return "" +} + +func (x *ConnectOptions) GetAuthHeader() string { + if x != nil { + return x.AuthHeader + } + return "" +} + +func (x *ConnectOptions) GetEnableTls() bool { + if x != nil { + return x.EnableTls + } + return false +} + +func (x *ConnectOptions) GetTlsCertPath() string { + if x != nil { + return x.TlsCertPath + } + return "" +} + +func (x *ConnectOptions) GetTlsKeyPath() string { + if x != nil { + return x.TlsKeyPath + } + return "" +} + +func (x *ConnectOptions) GetTlsServerName() string { + if x != nil { + return x.TlsServerName + } + return "" +} + +func (x *ConnectOptions) GetDisableHostVerification() bool { + if x != nil { + return x.DisableHostVerification + } + return false +} + +// Sent once at the start of a test run to initialize the project-server. +type InitRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ExecutionId string `protobuf:"bytes,1,opt,name=execution_id,json=executionId,proto3" json:"execution_id,omitempty"` + RunId string `protobuf:"bytes,2,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` + TaskQueue string `protobuf:"bytes,3,opt,name=task_queue,json=taskQueue,proto3" json:"task_queue,omitempty"` + ConnectOptions *ConnectOptions `protobuf:"bytes,4,opt,name=connect_options,json=connectOptions,proto3" json:"connect_options,omitempty"` + // Optional JSON project configuration, passed from --option project-config-file + // to the project's OnInit callback. Schema is project-defined. + ConfigJson []byte `protobuf:"bytes,5,opt,name=config_json,json=configJson,proto3" json:"config_json,omitempty"` + // When true, the harness registers standard omes search attributes + // (OmesExecutionID, KS_Keyword, KS_Int) on the namespace. + RegisterSearchAttributes bool `protobuf:"varint,6,opt,name=register_search_attributes,json=registerSearchAttributes,proto3" json:"register_search_attributes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InitRequest) Reset() { + *x = InitRequest{} + mi := &file_api_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InitRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InitRequest) ProtoMessage() {} + +func (x *InitRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InitRequest.ProtoReflect.Descriptor instead. +func (*InitRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{1} +} + +func (x *InitRequest) GetExecutionId() string { + if x != nil { + return x.ExecutionId + } + return "" +} + +func (x *InitRequest) GetRunId() string { + if x != nil { + return x.RunId + } + return "" +} + +func (x *InitRequest) GetTaskQueue() string { + if x != nil { + return x.TaskQueue + } + return "" +} + +func (x *InitRequest) GetConnectOptions() *ConnectOptions { + if x != nil { + return x.ConnectOptions + } + return nil +} + +func (x *InitRequest) GetConfigJson() []byte { + if x != nil { + return x.ConfigJson + } + return nil +} + +func (x *InitRequest) GetRegisterSearchAttributes() bool { + if x != nil { + return x.RegisterSearchAttributes + } + return false +} + +type InitResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InitResponse) Reset() { + *x = InitResponse{} + mi := &file_api_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InitResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InitResponse) ProtoMessage() {} + +func (x *InitResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InitResponse.ProtoReflect.Descriptor instead. +func (*InitResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{2} +} + +// Sent once per iteration. The project-server starts workflows and +// verifies results within the Execute handler. +type ExecuteRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Iteration int64 `protobuf:"varint,1,opt,name=iteration,proto3" json:"iteration,omitempty"` + // Reserved for future executor-specific per-iteration data. + Payload []byte `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecuteRequest) Reset() { + *x = ExecuteRequest{} + mi := &file_api_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecuteRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecuteRequest) ProtoMessage() {} + +func (x *ExecuteRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecuteRequest.ProtoReflect.Descriptor instead. +func (*ExecuteRequest) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{3} +} + +func (x *ExecuteRequest) GetIteration() int64 { + if x != nil { + return x.Iteration + } + return 0 +} + +func (x *ExecuteRequest) GetPayload() []byte { + if x != nil { + return x.Payload + } + return nil +} + +type ExecuteResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecuteResponse) Reset() { + *x = ExecuteResponse{} + mi := &file_api_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecuteResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecuteResponse) ProtoMessage() {} + +func (x *ExecuteResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecuteResponse.ProtoReflect.Descriptor instead. +func (*ExecuteResponse) Descriptor() ([]byte, []int) { + return file_api_proto_rawDescGZIP(), []int{4} +} + +var File_api_proto protoreflect.FileDescriptor + +const file_api_proto_rawDesc = "" + + "\n" + + "\tapi.proto\x12\x19temporal.omes.projects.v1\"\xbf\x02\n" + + "\x0eConnectOptions\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12%\n" + + "\x0eserver_address\x18\x02 \x01(\tR\rserverAddress\x12\x1f\n" + + "\vauth_header\x18\x03 \x01(\tR\n" + + "authHeader\x12\x1d\n" + + "\n" + + "enable_tls\x18\x04 \x01(\bR\tenableTls\x12\"\n" + + "\rtls_cert_path\x18\x05 \x01(\tR\vtlsCertPath\x12 \n" + + "\ftls_key_path\x18\x06 \x01(\tR\n" + + "tlsKeyPath\x12&\n" + + "\x0ftls_server_name\x18\a \x01(\tR\rtlsServerName\x12:\n" + + "\x19disable_host_verification\x18\b \x01(\bR\x17disableHostVerification\"\x99\x02\n" + + "\vInitRequest\x12!\n" + + "\fexecution_id\x18\x01 \x01(\tR\vexecutionId\x12\x15\n" + + "\x06run_id\x18\x02 \x01(\tR\x05runId\x12\x1d\n" + + "\n" + + "task_queue\x18\x03 \x01(\tR\ttaskQueue\x12R\n" + + "\x0fconnect_options\x18\x04 \x01(\v2).temporal.omes.projects.v1.ConnectOptionsR\x0econnectOptions\x12\x1f\n" + + "\vconfig_json\x18\x05 \x01(\fR\n" + + "configJson\x12<\n" + + "\x1aregister_search_attributes\x18\x06 \x01(\bR\x18registerSearchAttributes\"\x0e\n" + + "\fInitResponse\"H\n" + + "\x0eExecuteRequest\x12\x1c\n" + + "\titeration\x18\x01 \x01(\x03R\titeration\x12\x18\n" + + "\apayload\x18\x02 \x01(\fR\apayload\"\x11\n" + + "\x0fExecuteResponse2\xcf\x01\n" + + "\x0eProjectService\x12Y\n" + + "\x04Init\x12&.temporal.omes.projects.v1.InitRequest\x1a'.temporal.omes.projects.v1.InitResponse\"\x00\x12b\n" + + "\aExecute\x12).temporal.omes.projects.v1.ExecuteRequest\x1a*.temporal.omes.projects.v1.ExecuteResponse\"\x00B4Z2github.com/temporalio/omes/workers/go/projects/apib\x06proto3" + +var ( + file_api_proto_rawDescOnce sync.Once + file_api_proto_rawDescData []byte +) + +func file_api_proto_rawDescGZIP() []byte { + file_api_proto_rawDescOnce.Do(func() { + file_api_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_proto_rawDesc), len(file_api_proto_rawDesc))) + }) + return file_api_proto_rawDescData +} + +var file_api_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_api_proto_goTypes = []any{ + (*ConnectOptions)(nil), // 0: temporal.omes.projects.v1.ConnectOptions + (*InitRequest)(nil), // 1: temporal.omes.projects.v1.InitRequest + (*InitResponse)(nil), // 2: temporal.omes.projects.v1.InitResponse + (*ExecuteRequest)(nil), // 3: temporal.omes.projects.v1.ExecuteRequest + (*ExecuteResponse)(nil), // 4: temporal.omes.projects.v1.ExecuteResponse +} +var file_api_proto_depIdxs = []int32{ + 0, // 0: temporal.omes.projects.v1.InitRequest.connect_options:type_name -> temporal.omes.projects.v1.ConnectOptions + 1, // 1: temporal.omes.projects.v1.ProjectService.Init:input_type -> temporal.omes.projects.v1.InitRequest + 3, // 2: temporal.omes.projects.v1.ProjectService.Execute:input_type -> temporal.omes.projects.v1.ExecuteRequest + 2, // 3: temporal.omes.projects.v1.ProjectService.Init:output_type -> temporal.omes.projects.v1.InitResponse + 4, // 4: temporal.omes.projects.v1.ProjectService.Execute:output_type -> temporal.omes.projects.v1.ExecuteResponse + 3, // [3:5] is the sub-list for method output_type + 1, // [1:3] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_api_proto_init() } +func file_api_proto_init() { + if File_api_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_proto_rawDesc), len(file_api_proto_rawDesc)), + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_api_proto_goTypes, + DependencyIndexes: file_api_proto_depIdxs, + MessageInfos: file_api_proto_msgTypes, + }.Build() + File_api_proto = out.File + file_api_proto_goTypes = nil + file_api_proto_depIdxs = nil +} diff --git a/workers/go/projects/api/api_grpc.pb.go b/workers/go/projects/api/api_grpc.pb.go new file mode 100644 index 00000000..845210c9 --- /dev/null +++ b/workers/go/projects/api/api_grpc.pb.go @@ -0,0 +1,179 @@ +// Protocol for the project test framework. +// +// Each project test compiles into a single binary with two subcommands: +// - "worker": connects to Temporal and polls for tasks +// - "project-server": runs a gRPC server implementing ProjectService +// +// The omes CLI orchestrates the test by: +// 1. Starting the worker (separate process or container) +// 2. Starting the project-server +// 3. Sending Init (creates client, optional project setup like Nexus endpoints) +// 4. Sending Execute for each iteration (starts workflows, verifies results) + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: api.proto + +package api + +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.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ProjectService_Init_FullMethodName = "/temporal.omes.projects.v1.ProjectService/Init" + ProjectService_Execute_FullMethodName = "/temporal.omes.projects.v1.ProjectService/Execute" +) + +// ProjectServiceClient is the client API for ProjectService 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. +// +// ProjectService is the gRPC interface between the omes CLI and a project test binary. +type ProjectServiceClient interface { + // Init creates a Temporal client, runs project-specific setup, and prepares for execution. + Init(ctx context.Context, in *InitRequest, opts ...grpc.CallOption) (*InitResponse, error) + // Execute runs a single test iteration (e.g., start a workflow and verify the result). + Execute(ctx context.Context, in *ExecuteRequest, opts ...grpc.CallOption) (*ExecuteResponse, error) +} + +type projectServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewProjectServiceClient(cc grpc.ClientConnInterface) ProjectServiceClient { + return &projectServiceClient{cc} +} + +func (c *projectServiceClient) Init(ctx context.Context, in *InitRequest, opts ...grpc.CallOption) (*InitResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(InitResponse) + err := c.cc.Invoke(ctx, ProjectService_Init_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *projectServiceClient) Execute(ctx context.Context, in *ExecuteRequest, opts ...grpc.CallOption) (*ExecuteResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ExecuteResponse) + err := c.cc.Invoke(ctx, ProjectService_Execute_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ProjectServiceServer is the server API for ProjectService service. +// All implementations must embed UnimplementedProjectServiceServer +// for forward compatibility. +// +// ProjectService is the gRPC interface between the omes CLI and a project test binary. +type ProjectServiceServer interface { + // Init creates a Temporal client, runs project-specific setup, and prepares for execution. + Init(context.Context, *InitRequest) (*InitResponse, error) + // Execute runs a single test iteration (e.g., start a workflow and verify the result). + Execute(context.Context, *ExecuteRequest) (*ExecuteResponse, error) + mustEmbedUnimplementedProjectServiceServer() +} + +// UnimplementedProjectServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedProjectServiceServer struct{} + +func (UnimplementedProjectServiceServer) Init(context.Context, *InitRequest) (*InitResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Init not implemented") +} +func (UnimplementedProjectServiceServer) Execute(context.Context, *ExecuteRequest) (*ExecuteResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Execute not implemented") +} +func (UnimplementedProjectServiceServer) mustEmbedUnimplementedProjectServiceServer() {} +func (UnimplementedProjectServiceServer) testEmbeddedByValue() {} + +// UnsafeProjectServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ProjectServiceServer will +// result in compilation errors. +type UnsafeProjectServiceServer interface { + mustEmbedUnimplementedProjectServiceServer() +} + +func RegisterProjectServiceServer(s grpc.ServiceRegistrar, srv ProjectServiceServer) { + // If the following call panics, it indicates UnimplementedProjectServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ProjectService_ServiceDesc, srv) +} + +func _ProjectService_Init_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(InitRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProjectServiceServer).Init(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ProjectService_Init_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProjectServiceServer).Init(ctx, req.(*InitRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ProjectService_Execute_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ExecuteRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProjectServiceServer).Execute(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ProjectService_Execute_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProjectServiceServer).Execute(ctx, req.(*ExecuteRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ProjectService_ServiceDesc is the grpc.ServiceDesc for ProjectService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ProjectService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "temporal.omes.projects.v1.ProjectService", + HandlerType: (*ProjectServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Init", + Handler: _ProjectService_Init_Handler, + }, + { + MethodName: "Execute", + Handler: _ProjectService_Execute_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "api.proto", +} diff --git a/workers/go/projects/api/go.mod b/workers/go/projects/api/go.mod new file mode 100644 index 00000000..b9eff004 --- /dev/null +++ b/workers/go/projects/api/go.mod @@ -0,0 +1,15 @@ +module github.com/temporalio/omes/workers/go/projects/api + +go 1.22 + +require ( + google.golang.org/grpc v1.66.0 + google.golang.org/protobuf v1.34.2 +) + +require ( + golang.org/x/net v0.26.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect +) diff --git a/workers/go/projects/api/go.sum b/workers/go/projects/api/go.sum new file mode 100644 index 00000000..a7a0b603 --- /dev/null +++ b/workers/go/projects/api/go.sum @@ -0,0 +1,14 @@ +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= +google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= diff --git a/workers/go/projects/harness/go.mod b/workers/go/projects/harness/go.mod new file mode 100644 index 00000000..90eac3ff --- /dev/null +++ b/workers/go/projects/harness/go.mod @@ -0,0 +1,46 @@ +module github.com/temporalio/omes/workers/go/projects/harness + +go 1.24.0 + +require ( + github.com/prometheus/client_golang v1.16.0 + github.com/temporalio/omes/workers/go/projects/api v0.0.0 + go.temporal.io/api v1.43.0 + go.temporal.io/sdk v1.31.0 + google.golang.org/grpc v1.79.3 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/nexus-rpc/sdk-go v0.1.0 // indirect + github.com/pborman/uuid v1.2.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.10.1 // indirect + github.com/robfig/cron v1.2.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.10.0 // indirect + golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/time v0.3.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/temporalio/omes/workers/go/projects/api => ../api diff --git a/workers/go/projects/harness/go.sum b/workers/go/projects/harness/go.sum new file mode 100644 index 00000000..749ec2bc --- /dev/null +++ b/workers/go/projects/harness/go.sum @@ -0,0 +1,213 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/nexus-rpc/sdk-go v0.1.0 h1:PUL/0vEY1//WnqyEHT5ao4LBRQ6MeNUihmnNGn0xMWY= +github.com/nexus-rpc/sdk-go v0.1.0/go.mod h1:TpfkM2Cw0Rlk9drGkoiSMpFqflKTiQLWUNyKJjF8mKQ= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= +github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.temporal.io/api v1.43.0 h1:lBhq+u5qFJqGMXwWsmg/i8qn1UA/3LCwVc88l2xUMHg= +go.temporal.io/api v1.43.0/go.mod h1:1WwYUMo6lao8yl0371xWUm13paHExN5ATYT/B7QtFis= +go.temporal.io/sdk v1.31.0 h1:CLYiP0R5Sdj0gq8LyYKDDz4ccGOdJPR8wNGJU0JGwj8= +go.temporal.io/sdk v1.31.0/go.mod h1:8U8H7rF9u4Hyb4Ry9yiEls5716DHPNvVITPNkgWUwE8= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= +golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/workers/go/projects/harness/harness.go b/workers/go/projects/harness/harness.go new file mode 100644 index 00000000..f71e976d --- /dev/null +++ b/workers/go/projects/harness/harness.go @@ -0,0 +1,359 @@ +package harness + +import ( + "context" + "crypto/tls" + "errors" + "flag" + "fmt" + "log" + "net" + "os" + "strings" + + "github.com/temporalio/omes/workers/go/projects/api" + "go.temporal.io/api/enums/v1" + "go.temporal.io/api/operatorservice/v1" + "go.temporal.io/sdk/client" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const ( + OmesSearchAttributeKey = "OmesExecutionID" + defaultHost = "0.0.0.0" +) + +// ClientFunc creates a Temporal client from the given options. Called by both +// the worker subprocess and the project-server Init handler. +type ClientFunc func(client.Options, ClientConfig) (client.Client, error) + +// ClientConfig is passed to ClientFunc with minimal connection-time data. +type ClientConfig struct { + TaskQueue string +} + +// InitFunc performs project-specific setup after the client is created. +// Only called by the project-server Init gRPC handler, not by the worker. +type InitFunc func(context.Context, client.Client, InitConfig) error + +// InitConfig is passed to InitFunc with run-level configuration. +type InitConfig struct { + ConfigJSON []byte // from --config-file, project-specific schema + TaskQueue string + RunID string + ExecutionID string +} + +// ExecuteFunc is called once per Execute RPC (one per iteration). +type ExecuteFunc func(context.Context, client.Client, ExecuteInfo) error + +// ExecuteInfo is passed to ExecuteFunc for each iteration. +type ExecuteInfo struct { + TaskQueue string + ExecutionID string + RunID string + Iteration int64 + Payload []byte // from executor, executor-specific schema +} + +// WorkerFunc runs the worker. +type WorkerFunc func(client.Client, WorkerConfig) error + +// WorkerConfig is passed to WorkerFunc. +type WorkerConfig struct { + TaskQueue string + PromListenAddress string +} + +// Harness bridges the omes CLI with project test code. +type Harness struct { + runCtx *runContext + client client.Client + clientFunc ClientFunc + initFunc InitFunc + workerFunc WorkerFunc + executeFunc ExecuteFunc + api.UnsafeProjectServiceServer +} + +func New() *Harness { + return &Harness{} +} + +func (h *Harness) RegisterClient(fn ClientFunc) { + if h.clientFunc != nil { + log.Fatalf("client factory already registered") + } + h.clientFunc = fn +} + +func (h *Harness) OnInit(fn InitFunc) { + if h.initFunc != nil { + log.Fatalf("init handler already registered") + } + h.initFunc = fn +} + +func (h *Harness) RegisterWorker(fn WorkerFunc) { + if h.workerFunc != nil { + log.Fatalf("worker already registered") + } + h.workerFunc = fn +} + +func (h *Harness) OnExecute(fn ExecuteFunc) { + if h.executeFunc != nil { + log.Fatalf("execute handler already registered") + } + h.executeFunc = fn +} + +func (h *Harness) Run() { + if h.clientFunc == nil { + log.Fatalf("no client factory registered; call RegisterClient before Run") + } + if len(os.Args) < 2 { + fmt.Fprintln(os.Stderr, "usage: [project-server|worker] ...") + os.Exit(1) + } + cmd := os.Args[1] + os.Args = append(os.Args[:1], os.Args[2:]...) + + switch cmd { + case "project-server": + h.startGrpcServer() + case "worker": + h.startWorker() + default: + fmt.Fprintf(os.Stderr, "unknown command: %s\n", cmd) + os.Exit(1) + } +} + +// Init handles the Init gRPC call from the omes CLI. +func (s *Harness) Init(ctx context.Context, req *api.InitRequest) (*api.InitResponse, error) { + if err := validateInit(req); err != nil { + return nil, err + } + + co := req.GetConnectOptions() + opts, err := buildClientOptions( + co.GetServerAddress(), + co.GetNamespace(), + co.GetAuthHeader(), + tlsOptions{ + EnableTLS: co.GetEnableTls(), + CertPath: co.GetTlsCertPath(), + KeyPath: co.GetTlsKeyPath(), + ServerName: co.GetTlsServerName(), + DisableHostVerification: co.GetDisableHostVerification(), + }, + ) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + c, err := s.clientFunc(opts, ClientConfig{ + TaskQueue: req.GetTaskQueue(), + }) + if err != nil { + return nil, status.Error(codes.Internal, fmt.Sprintf("client creation failed: %v", err)) + } + s.client = c + + if s.initFunc != nil { + if err := s.initFunc(ctx, c, InitConfig{ + ConfigJSON: req.GetConfigJson(), + TaskQueue: req.GetTaskQueue(), + RunID: req.GetRunId(), + ExecutionID: req.GetExecutionId(), + }); err != nil { + return nil, status.Error(codes.Internal, fmt.Sprintf("init failed: %v", err)) + } + } + + if req.RegisterSearchAttributes { + if err := s.registerSearchAttributes(ctx, co.GetNamespace()); err != nil { + return nil, status.Error(codes.FailedPrecondition, err.Error()) + } + } + + s.runCtx = &runContext{ + TaskQueue: req.GetTaskQueue(), + ExecutionID: req.GetExecutionId(), + RunID: req.GetRunId(), + } + + return &api.InitResponse{}, nil +} + +// Execute handles the Execute gRPC call from the omes CLI. +func (s *Harness) Execute(ctx context.Context, req *api.ExecuteRequest) (*api.ExecuteResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "execute request is nil") + } + if s.executeFunc == nil { + return nil, status.Error(codes.FailedPrecondition, "no execute handler registered") + } + if s.client == nil { + return nil, status.Error(codes.FailedPrecondition, "must initialize harness before calling execute") + } + + if err := s.executeFunc(ctx, s.client, ExecuteInfo{ + TaskQueue: s.runCtx.TaskQueue, + ExecutionID: s.runCtx.ExecutionID, + RunID: s.runCtx.RunID, + Iteration: req.Iteration, + Payload: req.Payload, + }); err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &api.ExecuteResponse{}, nil +} + +// --- Internal helpers --- + +type runContext struct { + TaskQueue string + ExecutionID string + RunID string +} + +func (h *Harness) startGrpcServer() { + if h.executeFunc == nil { + log.Fatalf("Attempted to start server, but no execute handler was registered") + } + if h.workerFunc == nil { + log.Fatalf("Attempted to start server, but no worker handler was registered") + } + + fs := flag.NewFlagSet("client", flag.ExitOnError) + port := fs.Int("port", 8080, "HTTP port") + + if err := fs.Parse(os.Args[1:]); err != nil { + log.Fatalf("Failed to parse flags: %v", err) + } + + lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", defaultHost, *port)) + if err != nil { + log.Fatalf("Failed to listen: %v", err) + } + grpcServer := grpc.NewServer() + api.RegisterProjectServiceServer(grpcServer, h) + if err := grpcServer.Serve(lis); err != nil && !errors.Is(err, grpc.ErrServerStopped) { + log.Fatalf("gRPC server failed: %v", err) + } +} + +func validateInit(req *api.InitRequest) error { + if req == nil { + return status.Error(codes.InvalidArgument, "request: required") + } + if req.GetExecutionId() == "" { + return status.Error(codes.InvalidArgument, "execution_id: required") + } + if req.GetRunId() == "" { + return status.Error(codes.InvalidArgument, "run_id: required") + } + if req.GetTaskQueue() == "" { + return status.Error(codes.InvalidArgument, "task_queue: required") + } + co := req.GetConnectOptions() + if co == nil { + return status.Error(codes.InvalidArgument, "connect_options: required") + } + if co.GetNamespace() == "" { + return status.Error(codes.InvalidArgument, "connect_options.namespace: required") + } + if co.GetServerAddress() == "" { + return status.Error(codes.InvalidArgument, "connect_options.server_address: required") + } + return nil +} + +type staticHeadersProvider map[string]string + +func (s staticHeadersProvider) GetHeaders(context.Context) (map[string]string, error) { + return s, nil +} + +type tlsOptions struct { + EnableTLS bool + CertPath string + KeyPath string + ServerName string + DisableHostVerification bool +} + +func buildClientOptions( + serverAddress string, + namespace string, + authHeader string, + tlsOpts tlsOptions, +) (client.Options, error) { + opts := client.Options{ + HostPort: serverAddress, + Namespace: namespace, + } + if authHeader != "" { + opts.HeadersProvider = staticHeadersProvider{ + "Authorization": authHeader, + } + } + tlsCfg, err := loadTLSConfig(tlsOpts) + if err != nil { + return client.Options{}, fmt.Errorf("failed to load TLS config: %w", err) + } + if tlsCfg != nil { + opts.ConnectionOptions.TLS = tlsCfg + } + return opts, nil +} + +func loadTLSConfig(opts tlsOptions) (*tls.Config, error) { + tlsConfig := &tls.Config{ + InsecureSkipVerify: opts.DisableHostVerification, + ServerName: opts.ServerName, + MinVersion: tls.VersionTLS13, + } + if opts.CertPath != "" { + if opts.KeyPath == "" { + return nil, errors.New("got TLS cert with no key") + } + cert, err := tls.LoadX509KeyPair(opts.CertPath, opts.KeyPath) + if err != nil { + return nil, fmt.Errorf("failed to load certs: %w", err) + } + tlsConfig.Certificates = []tls.Certificate{cert} + return tlsConfig, nil + } else if opts.KeyPath != "" { + return nil, errors.New("got TLS key with no cert") + } + if opts.EnableTLS { + return tlsConfig, nil + } + return nil, nil +} + +func (s *Harness) registerSearchAttributes(ctx context.Context, namespace string) error { + _, err := s.client.OperatorService().AddSearchAttributes(ctx, &operatorservice.AddSearchAttributesRequest{ + SearchAttributes: map[string]enums.IndexedValueType{ + "KS_Keyword": enums.INDEXED_VALUE_TYPE_KEYWORD, + "KS_Int": enums.INDEXED_VALUE_TYPE_INT, + OmesSearchAttributeKey: enums.INDEXED_VALUE_TYPE_KEYWORD, + }, + Namespace: namespace, + }) + if err != nil { + if status.Code(err) == codes.AlreadyExists { + return nil + } + if strings.Contains(err.Error(), "attributes mapping unavailable") { + return nil + } + return fmt.Errorf("failed to register search attributes: %w", err) + } + return nil +} diff --git a/workers/go/projects/harness/metrics.go b/workers/go/projects/harness/metrics.go new file mode 100644 index 00000000..11057994 --- /dev/null +++ b/workers/go/projects/harness/metrics.go @@ -0,0 +1,149 @@ +// This file is a standalone implementation of client.MetricsHandler backed by +// Prometheus. It duplicates the implementation in metrics/metrics.go because the +// project harness is a separate Go module — importing metrics/ would pull in the +// full omes dependency tree and lock project tests to the main module's SDK version. +// Project tests need to pin their own SDK versions independently. + +package harness + +import ( + "fmt" + "sort" + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" + "go.temporal.io/sdk/client" +) + +type promMetricsState struct { + mu sync.Mutex + registry *prometheus.Registry + cache map[string]any +} + +type promMetricsHandler struct { + state *promMetricsState + labels []string + values []string +} + +func newPromMetricsHandler(registry *prometheus.Registry) client.MetricsHandler { + return &promMetricsHandler{ + state: &promMetricsState{ + registry: registry, + cache: make(map[string]any), + }, + } +} + +func (h *promMetricsHandler) WithTags(tags map[string]string) client.MetricsHandler { + merged := make(map[string]string, len(h.labels)+len(tags)) + for i, label := range h.labels { + merged[label] = h.values[i] + } + for k, v := range tags { + merged[k] = v + } + + labels := make([]string, 0, len(merged)) + for k := range merged { + labels = append(labels, k) + } + sort.Strings(labels) + + values := make([]string, len(labels)) + for i, label := range labels { + values[i] = merged[label] + } + + return &promMetricsHandler{ + state: h.state, + labels: labels, + values: values, + } +} + +func (h *promMetricsHandler) Counter(name string) client.MetricsCounter { + h.state.mu.Lock() + defer h.state.mu.Unlock() + + var ctr *prometheus.CounterVec + if c, ok := h.state.cache[name]; ok { + var okType bool + ctr, okType = c.(*prometheus.CounterVec) + if !okType { + panic(fmt.Errorf("duplicate metric with different type: %s", name)) + } + } else { + ctr = prometheus.NewCounterVec(prometheus.CounterOpts{Name: name}, h.labels) + h.state.registry.MustRegister(ctr) + h.state.cache[name] = ctr + } + + return promMetricsCounter{prom: ctr.WithLabelValues(h.values...)} +} + +func (h *promMetricsHandler) Gauge(name string) client.MetricsGauge { + h.state.mu.Lock() + defer h.state.mu.Unlock() + + var gauge *prometheus.GaugeVec + if c, ok := h.state.cache[name]; ok { + var okType bool + gauge, okType = c.(*prometheus.GaugeVec) + if !okType { + panic(fmt.Errorf("duplicate metric with different type: %s", name)) + } + } else { + gauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{Name: name}, h.labels) + h.state.registry.MustRegister(gauge) + h.state.cache[name] = gauge + } + + return promMetricsGauge{prom: gauge.WithLabelValues(h.values...)} +} + +func (h *promMetricsHandler) Timer(name string) client.MetricsTimer { + h.state.mu.Lock() + defer h.state.mu.Unlock() + + var timer *prometheus.HistogramVec + if c, ok := h.state.cache[name]; ok { + var okType bool + timer, okType = c.(*prometheus.HistogramVec) + if !okType { + panic(fmt.Errorf("duplicate metric with different type: %s", name)) + } + } else { + timer = prometheus.NewHistogramVec(prometheus.HistogramOpts{Name: name}, h.labels) + h.state.registry.MustRegister(timer) + h.state.cache[name] = timer + } + + return promMetricsTimer{prom: timer.WithLabelValues(h.values...)} +} + +type promMetricsCounter struct { + prom prometheus.Counter +} + +func (m promMetricsCounter) Inc(incr int64) { + m.prom.Add(float64(incr)) +} + +type promMetricsGauge struct { + prom prometheus.Gauge +} + +func (m promMetricsGauge) Update(x float64) { + m.prom.Set(x) +} + +type promMetricsTimer struct { + prom prometheus.Observer +} + +func (m promMetricsTimer) Record(duration time.Duration) { + m.prom.Observe(duration.Seconds()) +} diff --git a/workers/go/projects/harness/worker.go b/workers/go/projects/harness/worker.go new file mode 100644 index 00000000..9536d858 --- /dev/null +++ b/workers/go/projects/harness/worker.go @@ -0,0 +1,91 @@ +package harness + +import ( + "flag" + "fmt" + "log" + "net" + "net/http" + "os" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +// startWorker parses CLI flags, creates a client via InitFunc, and runs the worker. +func (h *Harness) startWorker() { + if h.workerFunc == nil { + log.Fatalf("Attempted to start worker, but a worker was not registered") + } + + fs := flag.NewFlagSet("worker", flag.ContinueOnError) + taskQueue := fs.String("task-queue", "", "Task queue name (required)") + serverAddress := fs.String("server-address", "localhost:7233", "Temporal server address") + namespace := fs.String("namespace", "default", "Temporal namespace") + authHeader := fs.String("auth-header", "", "Authorization header value") + promListenAddress := fs.String("prom-listen-address", "", "Prometheus metrics address") + enableTLS := fs.Bool("tls", false, "Enable TLS") + tlsCertPath := fs.String("tls-cert-path", "", "Path to client TLS certificate") + tlsKeyPath := fs.String("tls-key-path", "", "Path to client TLS private key") + tlsServerName := fs.String("tls-server-name", "", "TLS target server name") + disableHostVerification := fs.Bool("disable-tls-host-verification", false, "Disable TLS host verification") + + _ = fs.Parse(os.Args[1:]) + + if *taskQueue == "" { + fmt.Fprintln(os.Stderr, "error: --task-queue is required") + os.Exit(1) + } + + opts, err := buildClientOptions( + *serverAddress, + *namespace, + *authHeader, + tlsOptions{ + EnableTLS: *enableTLS, + CertPath: *tlsCertPath, + KeyPath: *tlsKeyPath, + ServerName: *tlsServerName, + DisableHostVerification: *disableHostVerification, + }, + ) + if err != nil { + log.Fatalf("Failed to build client options: %v", err) + } + + // Set up Prometheus metrics if address provided + if *promListenAddress != "" { + registry := prometheus.NewRegistry() + opts.MetricsHandler = newPromMetricsHandler(registry) + + mux := http.NewServeMux() + mux.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{})) + listener, err := net.Listen("tcp", *promListenAddress) + if err != nil { + log.Fatalf("Failed to listen on %s: %v", *promListenAddress, err) + } + server := &http.Server{Handler: mux} + go func() { + if err := server.Serve(listener); err != nil && err != http.ErrServerClosed { + log.Printf("Metrics server error: %v", err) + } + }() + } + + // Create client via ClientFunc (no project-specific init for worker) + c, err := h.clientFunc(opts, ClientConfig{ + TaskQueue: *taskQueue, + }) + if err != nil { + log.Fatalf("Failed to create client: %v", err) + } + + fmt.Printf("Worker starting on task queue: %s\n", *taskQueue) + + if err := h.workerFunc(c, WorkerConfig{ + TaskQueue: *taskQueue, + PromListenAddress: *promListenAddress, + }); err != nil { + log.Fatalf("Worker error: %v", err) + } +} diff --git a/workers/go/projects/tests/helloworld/client.go b/workers/go/projects/tests/helloworld/client.go new file mode 100644 index 00000000..0fb10f7e --- /dev/null +++ b/workers/go/projects/tests/helloworld/client.go @@ -0,0 +1,31 @@ +package helloworld + +import ( + "context" + "log" + + harness "github.com/temporalio/omes/workers/go/projects/harness" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/temporal" +) + +func clientMain(ctx context.Context, c client.Client, iter harness.ExecuteInfo) error { + opts := client.StartWorkflowOptions{ + TaskQueue: iter.TaskQueue, + TypedSearchAttributes: temporal.NewSearchAttributes( + temporal.NewSearchAttributeKeyString(harness.OmesSearchAttributeKey).ValueSet(iter.ExecutionID), + ), + } + + wf, err := c.ExecuteWorkflow(ctx, opts, Workflow, "World") + if err != nil { + return err + } + + var result string + if err := wf.Get(ctx, &result); err != nil { + return err + } + log.Printf("Workflow result: %s", result) + return nil +} diff --git a/workers/go/projects/tests/helloworld/go.mod b/workers/go/projects/tests/helloworld/go.mod new file mode 100644 index 00000000..b4139acd --- /dev/null +++ b/workers/go/projects/tests/helloworld/go.mod @@ -0,0 +1,49 @@ +module github.com/temporalio/omes/workers/go/projects/tests/helloworld + +go 1.24.0 + +require ( + github.com/temporalio/omes/workers/go/projects/harness v0.0.0 + go.temporal.io/sdk v1.31.0 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/mock v1.6.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/nexus-rpc/sdk-go v0.1.0 // indirect + github.com/pborman/uuid v1.2.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.16.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.10.1 // indirect + github.com/robfig/cron v1.2.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.10.0 // indirect + github.com/temporalio/omes/workers/go/projects/api v0.0.0 // indirect + go.temporal.io/api v1.43.0 // indirect + golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/time v0.3.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/grpc v1.79.3 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/temporalio/omes/workers/go/projects/harness => ../../harness + +replace github.com/temporalio/omes/workers/go/projects/api => ../../api diff --git a/workers/go/projects/tests/helloworld/go.sum b/workers/go/projects/tests/helloworld/go.sum new file mode 100644 index 00000000..749ec2bc --- /dev/null +++ b/workers/go/projects/tests/helloworld/go.sum @@ -0,0 +1,213 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/nexus-rpc/sdk-go v0.1.0 h1:PUL/0vEY1//WnqyEHT5ao4LBRQ6MeNUihmnNGn0xMWY= +github.com/nexus-rpc/sdk-go v0.1.0/go.mod h1:TpfkM2Cw0Rlk9drGkoiSMpFqflKTiQLWUNyKJjF8mKQ= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= +github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.temporal.io/api v1.43.0 h1:lBhq+u5qFJqGMXwWsmg/i8qn1UA/3LCwVc88l2xUMHg= +go.temporal.io/api v1.43.0/go.mod h1:1WwYUMo6lao8yl0371xWUm13paHExN5ATYT/B7QtFis= +go.temporal.io/sdk v1.31.0 h1:CLYiP0R5Sdj0gq8LyYKDDz4ccGOdJPR8wNGJU0JGwj8= +go.temporal.io/sdk v1.31.0/go.mod h1:8U8H7rF9u4Hyb4Ry9yiEls5716DHPNvVITPNkgWUwE8= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= +golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/workers/go/projects/tests/helloworld/helloworld.go b/workers/go/projects/tests/helloworld/helloworld.go new file mode 100644 index 00000000..5bba4ba9 --- /dev/null +++ b/workers/go/projects/tests/helloworld/helloworld.go @@ -0,0 +1,16 @@ +package helloworld + +import ( + harness "github.com/temporalio/omes/workers/go/projects/harness" + "go.temporal.io/sdk/client" +) + +func Main() { + h := harness.New() + h.RegisterClient(func(opts client.Options, _ harness.ClientConfig) (client.Client, error) { + return client.Dial(opts) + }) + h.RegisterWorker(workerMain) + h.OnExecute(clientMain) + h.Run() +} diff --git a/workers/go/projects/tests/helloworld/worker.go b/workers/go/projects/tests/helloworld/worker.go new file mode 100644 index 00000000..b9b809af --- /dev/null +++ b/workers/go/projects/tests/helloworld/worker.go @@ -0,0 +1,13 @@ +package helloworld + +import ( + harness "github.com/temporalio/omes/workers/go/projects/harness" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/worker" +) + +func workerMain(c client.Client, config harness.WorkerConfig) error { + w := worker.New(c, config.TaskQueue, worker.Options{}) + w.RegisterWorkflow(Workflow) + return w.Run(worker.InterruptCh()) +} diff --git a/workers/go/projects/tests/helloworld/workflow.go b/workers/go/projects/tests/helloworld/workflow.go new file mode 100644 index 00000000..16f001c7 --- /dev/null +++ b/workers/go/projects/tests/helloworld/workflow.go @@ -0,0 +1,8 @@ +package helloworld + +import "go.temporal.io/sdk/workflow" + +// Workflow is a simple workflow that returns a greeting. +func Workflow(ctx workflow.Context, name string) (string, error) { + return "Hello " + name, nil +} diff --git a/workers/proto/projects/api.proto b/workers/proto/projects/api.proto new file mode 100644 index 00000000..95c0d418 --- /dev/null +++ b/workers/proto/projects/api.proto @@ -0,0 +1,66 @@ +// Protocol for the project test framework. +// +// Each project test compiles into a single binary with two subcommands: +// - "worker": connects to Temporal and polls for tasks +// - "project-server": runs a gRPC server implementing ProjectService +// +// The omes CLI orchestrates the test by: +// 1. Starting the worker (separate process or container) +// 2. Starting the project-server +// 3. Sending Init (creates client, optional project setup like Nexus endpoints) +// 4. Sending Execute for each iteration (starts workflows, verifies results) + +syntax = "proto3"; +package temporal.omes.projects.v1; + +option go_package = "github.com/temporalio/omes/workers/go/projects/api"; + +// Connection options passed from the CLI to the project-server during Init. +// The project-server uses these to create its own Temporal client. +message ConnectOptions { + string namespace = 1; + string server_address = 2; + string auth_header = 3; + bool enable_tls = 4; + string tls_cert_path = 5; + string tls_key_path = 6; + string tls_server_name = 7; + bool disable_host_verification = 8; +} + +// Sent once at the start of a test run to initialize the project-server. +message InitRequest { + string execution_id = 1; + string run_id = 2; + string task_queue = 3; + ConnectOptions connect_options = 4; + + // Optional JSON project configuration, passed from --option project-config-file + // to the project's OnInit callback. Schema is project-defined. + bytes config_json = 5; + + // When true, the harness registers standard omes search attributes + // (OmesExecutionID, KS_Keyword, KS_Int) on the namespace. + bool register_search_attributes = 6; +} + +message InitResponse {} + +// Sent once per iteration. The project-server starts workflows and +// verifies results within the Execute handler. +message ExecuteRequest { + int64 iteration = 1; + + // Reserved for future executor-specific per-iteration data. + bytes payload = 2; +} + +message ExecuteResponse {} + +// ProjectService is the gRPC interface between the omes CLI and a project test binary. +service ProjectService { + // Init creates a Temporal client, runs project-specific setup, and prepares for execution. + rpc Init(InitRequest) returns (InitResponse) {} + // Execute runs a single test iteration (e.g., start a workflow and verify the result). + rpc Execute(ExecuteRequest) returns (ExecuteResponse) {} +} diff --git a/workers/proto/projects/buf.gen.yaml b/workers/proto/projects/buf.gen.yaml new file mode 100644 index 00000000..f233fc6c --- /dev/null +++ b/workers/proto/projects/buf.gen.yaml @@ -0,0 +1,10 @@ +version: v2 +plugins: + # Go + - remote: buf.build/protocolbuffers/go + out: ../../go/projects/api + opt: paths=source_relative + - remote: buf.build/grpc/go + out: ../../go/projects/api + opt: + - paths=source_relative diff --git a/workers/proto/projects/buf.yaml b/workers/proto/projects/buf.yaml new file mode 100644 index 00000000..4ea3c491 --- /dev/null +++ b/workers/proto/projects/buf.yaml @@ -0,0 +1,9 @@ +version: v2 +lint: + use: + - STANDARD + except: + - PACKAGE_DIRECTORY_MATCH +breaking: + use: + - FILE diff --git a/workers/run.go b/workers/run.go index c6738311..3dfc4b05 100644 --- a/workers/run.go +++ b/workers/run.go @@ -15,6 +15,7 @@ import ( "github.com/spf13/pflag" "github.com/temporalio/features/sdkbuild" "github.com/temporalio/omes/cmd/clioptions" + "github.com/temporalio/omes/scenarios/project" "go.temporal.io/sdk/client" "go.temporal.io/sdk/testsuite" ) @@ -72,10 +73,20 @@ func (r *Runner) Run(ctx context.Context, baseDir string) error { }() } - // If there is not a prepared dir, we must build a temporary one and perform - // the prep. Otherwise we reload the command from the directory. + // Build the program. Project builds use projectbuild; standard workers use Builder. var prog sdkbuild.Program - if r.DirName == "" { + if r.ProjectDir != "" { + var err error + prog, err = project.Build(ctx, project.BuildOptions{ + Language: r.SdkOptions.Language, + ProjectDir: r.ProjectDir, + Version: r.SdkOptions.Version, + Logger: r.Logger, + }) + if err != nil { + return fmt.Errorf("failed building project worker: %w", err) + } + } else if r.DirName == "" { // Create temp dir tempDir, err := os.MkdirTemp(baseDir, "omes-temp-") if err != nil { @@ -116,7 +127,9 @@ func (r *Runner) Run(ctx context.Context, baseDir string) error { // Build command args var args []string - if r.SdkOptions.Language == clioptions.LangPython { + if r.ProjectDir != "" { + args = append(args, "worker") + } else if r.SdkOptions.Language == clioptions.LangPython { // Python needs module name first args = append(args, "main") } else if r.SdkOptions.Language == clioptions.LangTypeScript {