diff --git a/.github/workflows/build-and-publish-docker.yml b/.github/workflows/build-and-publish-docker.yml new file mode 100644 index 000000000..00bd416d8 --- /dev/null +++ b/.github/workflows/build-and-publish-docker.yml @@ -0,0 +1,123 @@ +name: Build and Publish Docker Image + +on: + workflow_dispatch: + inputs: + version: + description: 'Version tag for the Docker image (e.g., 1.2.3 or v1.2.3)' + required: true + type: string + publish: + description: 'Push the image to Docker Hub' + required: false + type: boolean + default: true + tag_latest: + description: 'Also tag this image as latest' + required: false + type: boolean + default: false + +permissions: + contents: read + +jobs: + build-and-publish: + name: Build and Publish Docker Image + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Get build metadata from release + id: meta + env: + INPUT_VERSION: ${{ inputs.version }} + INPUT_TAG_LATEST: ${{ inputs.tag_latest }} + uses: actions/github-script@v8 + with: + script: | + const inputVersion = process.env.INPUT_VERSION; + const inputTagLatest = process.env.INPUT_TAG_LATEST; + + const version = inputVersion.startsWith('v') ? inputVersion.slice(1) : inputVersion; + const releaseTag = inputVersion.startsWith('v') ? inputVersion : `v${inputVersion}`; + + const { data: release } = await github.rest.repos.getReleaseByTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag: releaseTag + }); + + const cliSha = release.target_commitish; + const imageShaTag = cliSha.substring(0, 7); + + core.setOutput('cli_sha', cliSha); + core.setOutput('image_sha_tag', imageShaTag); + core.setOutput('version', version); + core.setOutput('release_tag', releaseTag); + core.setOutput('tag_latest', inputTagLatest === 'true'); + + - name: Download release assets + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ steps.meta.outputs.release_tag }} + run: | + echo "Downloading assets from release ${RELEASE_TAG}..." + gh release download "${RELEASE_TAG}" --pattern "temporal_*_linux_*.tar.gz" + + echo "Extracting and organizing binaries..." + mkdir -p dist/amd64 dist/arm64 + + tar -xzf temporal_*_linux_amd64.tar.gz + mv temporal dist/amd64/temporal + + tar -xzf temporal_*_linux_arm64.tar.gz + mv temporal dist/arm64/temporal + + echo "Verifying binaries..." + ls -lh dist/amd64/temporal + ls -lh dist/arm64/temporal + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + if: inputs.publish + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker image + if: inputs.publish + run: | + docker buildx bake \ + --file docker-bake.hcl \ + --push \ + cli + env: + CLI_SHA: ${{ steps.meta.outputs.cli_sha }} + IMAGE_SHA_TAG: ${{ steps.meta.outputs.image_sha_tag }} + VERSION: ${{ steps.meta.outputs.version }} + TAG_LATEST: ${{ steps.meta.outputs.tag_latest }} + IMAGE_NAMESPACE: temporalio + IMAGE_NAME: temporal + GITHUB_REPOSITORY: ${{ github.repository }} + + - name: Build Docker image (no push) + if: ${{ !inputs.publish }} + run: | + docker buildx bake \ + --file docker-bake.hcl \ + cli + env: + CLI_SHA: ${{ steps.meta.outputs.cli_sha }} + IMAGE_SHA_TAG: ${{ steps.meta.outputs.image_sha_tag }} + VERSION: ${{ steps.meta.outputs.version }} + TAG_LATEST: ${{ steps.meta.outputs.tag_latest }} + IMAGE_NAMESPACE: temporalio + IMAGE_NAME: temporal + GITHUB_REPOSITORY: ${{ github.repository }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bf2750297..a8cc5a6bc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,10 +10,17 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, macos-13, windows-latest, ubuntu-arm] + os: + [ + ubuntu-latest, + macos-latest, + macos-15-intel, + windows-latest, + ubuntu-arm, + ] include: - os: ubuntu-latest - checkGenCodeTarget: true + checkGenCommands: true cloudTestTarget: true - os: ubuntu-arm runsOn: buildjet-4vcpu-ubuntu-2204-arm @@ -31,7 +38,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version-file: 'go.mod' + go-version-file: go.mod - name: Install gotestsum run: go install gotest.tools/gotestsum@latest @@ -42,7 +49,7 @@ jobs: - name: Test run: gotestsum --junitfile junit-xml/${{matrix.os}}.xml -- ./... - - name: 'Upload junit-xml artifacts' + - name: Upload junit-xml artifacts uses: actions/upload-artifact@v4 if: always() with: @@ -51,11 +58,16 @@ jobs: retention-days: 14 - name: Regen code, confirm unchanged - if: ${{ matrix.checkGenCodeTarget }} + if: ${{ matrix.checkGenCommands }} run: | - go run ./temporalcli/internal/cmd/gen-commands + go run ./cmd/gen-commands -input internal/temporalcli/commands.yaml -pkg temporalcli -context "*CommandContext" > internal/temporalcli/commands.gen.go git diff --exit-code + - name: Generate docs, confirm working + if: ${{ matrix.checkGenCommands }} + run: | + go run ./cmd/gen-docs -input internal/temporalcli/commands.yaml -output dist/docs + - name: Test cloud mTLS if: ${{ matrix.cloudTestTarget && env.HAS_SECRETS == 'true' }} env: diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/release.yml similarity index 58% rename from .github/workflows/goreleaser.yml rename to .github/workflows/release.yml index 3ef1d0b13..0bcd5d666 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: goreleaser +name: Release on: workflow_dispatch: @@ -6,45 +6,50 @@ on: types: - published +permissions: + contents: write + jobs: - goreleaser: + release: + name: Release runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4.0.1 + uses: actions/setup-go@v6 with: go-version-file: "go.mod" check-latest: true + cache: true - name: Get build date id: date - run: echo "::set-output name=date::$(date '+%F-%T')" + run: echo "date=$(date '+%F-%T')" >> "$GITHUB_OUTPUT" - name: Get build unix timestamp id: timestamp - run: echo "::set-output name=timestamp::$(date '+%s')" + run: echo "timestamp=$(date '+%s')" >> "$GITHUB_OUTPUT" - name: Get git branch id: branch - run: echo "::set-output name=branch::$(git rev-parse --abbrev-ref HEAD)" + run: echo "branch=$(git rev-parse --abbrev-ref HEAD)" >> "$GITHUB_OUTPUT" - name: Get build platform id: platform - run: echo "::set-output name=platform::$(go version | cut -d ' ' -f 4)" + run: echo "platform=$(go version | cut -d ' ' -f 4)" >> "$GITHUB_OUTPUT" - name: Get Go version id: go - run: echo "::set-output name=go::$(go version | cut -d ' ' -f 3)" + run: echo "go=$(go version | cut -d ' ' -f 3)" >> "$GITHUB_OUTPUT" - name: Run GoReleaser - uses: goreleaser/goreleaser-action@336e29918d653399e599bfca99fadc1d7ffbc9f7 # v4.3.0 + uses: goreleaser/goreleaser-action@v6 with: - version: v1.26.2 + version: v2.12.7 args: release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/trigger-docs.yml b/.github/workflows/trigger-docs.yml index 8b62778bf..cfa6349a8 100644 --- a/.github/workflows/trigger-docs.yml +++ b/.github/workflows/trigger-docs.yml @@ -3,8 +3,13 @@ on: workflow_dispatch: release: types: [published] + +permissions: + contents: read + jobs: update: + if: github.repository == 'temporalio/cli' runs-on: ubuntu-latest defaults: run: @@ -12,32 +17,36 @@ jobs: steps: - name: Get user info from GitHub API id: get_user + env: + GITHUB_ACTOR: ${{ github.actor }} run: | - echo "GitHub actor: ${{ github.actor }}" + echo "GitHub actor: ${GITHUB_ACTOR}" # Query the GitHub API for the user's details. curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ - https://api.github.com/users/${{ github.actor }} > user.json - + "https://api.github.com/users/${GITHUB_ACTOR}" > user.json + # Extract the user's full name if available, default to the username otherwise. git_name=$(jq -r '.name // empty' user.json) if [ -z "$git_name" ]; then - git_name="${{ github.actor }}" + git_name="${GITHUB_ACTOR}" fi - - git_email="${{ github.actor }}@users.noreply.github.com" - + + git_email="${GITHUB_ACTOR}@users.noreply.github.com" + # Set the outputs for subsequent steps. - echo "GIT_NAME=$git_name" >> $GITHUB_OUTPUT - echo "GIT_EMAIL=$git_email" >> $GITHUB_OUTPUT + echo "GIT_NAME=$git_name" >> "$GITHUB_OUTPUT" + echo "GIT_EMAIL=$git_email" >> "$GITHUB_OUTPUT" - name: Generate token id: generate_token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v2 with: app-id: ${{ secrets.TEMPORAL_CICD_APP_ID }} private-key: ${{ secrets.TEMPORAL_CICD_PRIVATE_KEY }} owner: ${{ github.repository_owner }} - repositories: documentation # generate a token with permissions to trigger GHA in documentation repo + # Generate a token with permissions to trigger GHA in documentation repo. + repositories: | + documentation - name: Trigger Documentation Workflow env: diff --git a/.github/workflows/trigger-publish.yml b/.github/workflows/trigger-publish.yml deleted file mode 100644 index 04eee3192..000000000 --- a/.github/workflows/trigger-publish.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: 'Trigger Docker image build' - -on: - workflow_dispatch: - release: - types: [published] - -jobs: - trigger: - if: ${{ ! contains(github.ref, '-rc.') }} - name: 'trigger Docker image build' - runs-on: ubuntu-latest - - defaults: - run: - shell: bash - - steps: - - name: Generate a token - id: generate_token - uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 - with: - app_id: ${{ secrets.TEMPORAL_CICD_APP_ID }} - private_key: ${{ secrets.TEMPORAL_CICD_PRIVATE_KEY }} - - - name: Dispatch docker builds Github Action - env: - PAT: ${{ steps.generate_token.outputs.token }} - PARENT_REPO: temporalio/docker-builds - PARENT_BRANCH: ${{ toJSON('main') }} - WORKFLOW_ID: update-submodules.yml - REPO: ${{ toJSON('cli') }} - BRANCH: ${{ toJSON('main') }} - COMMIT: ${{ toJSON(github.sha) }} - run: | - curl -fL -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token $PAT" "https://api.github.com/repos/$PARENT_REPO/actions/workflows/$WORKFLOW_ID/dispatches" -d '{"ref":'"$PARENT_BRANCH"', "inputs": { "repo":'"$REPO"', "branch":'"$BRANCH"', "commit": '"$COMMIT"' }}' diff --git a/.goreleaser.yml b/.goreleaser.yml index caa2005d1..f3e29ced2 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,3 +1,5 @@ +version: 2 + before: hooks: - go mod download @@ -11,24 +13,26 @@ archives: - <<: &archive_defaults name_template: "temporal_cli_{{ .Version }}_{{ .Os }}_{{ .Arch }}" id: nix - builds: + ids: - nix - format: tar.gz + formats: + - tar.gz files: - LICENSE - <<: *archive_defaults id: windows-zip - builds: + ids: - windows - format: zip + formats: + - zip files: - LICENSE # used by SDKs as zip cannot be used by rust https://github.com/zip-rs/zip/issues/108 - <<: *archive_defaults id: windows-targz - builds: + ids: - windows files: - LICENSE @@ -38,7 +42,7 @@ builds: dir: cmd/temporal binary: temporal ldflags: - - -s -w -X github.com/temporalio/cli/temporalcli.Version={{.Version}} + - -s -w -X github.com/temporalio/cli/internal.Version={{.Version}} goarch: - amd64 - arm64 @@ -61,7 +65,7 @@ checksum: algorithm: sha256 changelog: - skip: true + disable: true announce: skip: "true" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 122c7140f..f0630164e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,10 +20,10 @@ Example to run a single test case: ## Adding/updating commands -First, update [commands.yml](temporalcli/commandsgen/commands.yml) following the rules in that file. Then to regenerate the -[commands.gen.go](temporalcli/commands.gen.go) file from code, simply run: +First, update [commands.yaml](internal/temporalcli/commands.yaml) following the rules in that file. Then to regenerate the +[commands.gen.go](internal/temporalcli/commands.gen.go) file from code, run: - go run ./temporalcli/internal/cmd/gen-commands + go run ./cmd/gen-commands -input internal/temporalcli/commands.yaml -pkg temporalcli -context "*CommandContext" > internal/temporalcli/commands.gen.go This will expect every non-parent command to have a `run` method, so for new commands developers will have to implement `run` on the new command in a separate file before it will compile. @@ -31,21 +31,21 @@ This will expect every non-parent command to have a `run` method, so for new com Once a command is updated, the CI will automatically generate new docs and create a PR in the Documentation repo with the corresponding updates. To generate these docs locally, run: - go run ./temporalcli/internal/cmd/gen-docs + go run ./cmd/gen-docs -input internal/temporalcli/commands.yaml -output dist/docs -This will auto-generate a new set of docs to `temporalcli/docs/`. If a new root command is added, a new file will be automatically generated, like `temporal activity` and `activity.mdx`. +This will auto-generate a new set of docs to `dist/docs/`. If a new root command is added, a new file will be automatically generated, like `temporal activity` and `activity.mdx`. ## Inject additional build-time information To add build-time information to the version string printed by the binary, use - go build -ldflags "-X github.com/temporalio/cli/temporalcli.buildInfo=" + go build -ldflags "-X github.com/temporalio/cli/internal.buildInfo=" This can be useful if, for example, you've used a `replace` statement in go.mod pointing to a local directory. Note that inclusion of space characters in the value supplied via `-ldflags` is tricky. Here's an example that adds branch info from a local repo to the version string, and includes a space character: - go build -ldflags "-X 'github.com/temporalio/cli/temporalcli.buildInfo=ServerBranch $(git -C ../temporal rev-parse --abbrev-ref HEAD)'" -o temporal ./cmd/temporal/main.go + go build -ldflags "-X 'github.com/temporalio/cli/internal.buildInfo=ServerBranch $(git -C ../temporal rev-parse --abbrev-ref HEAD)'" -o temporal ./cmd/temporal/main.go ## Building Docker image diff --git a/Dockerfile b/Dockerfile index 05c3938a3..ca9d986f6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,9 @@ -FROM --platform=$BUILDARCH scratch AS dist -COPY ./dist/nix_linux_amd64_v1/temporal /dist/amd64/temporal -COPY ./dist/nix_linux_arm64/temporal /dist/arm64/temporal +FROM alpine:3.22@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412 -FROM alpine:3.22 ARG TARGETARCH -RUN apk add --no-cache ca-certificates -COPY --from=dist /dist/$TARGETARCH/temporal /usr/local/bin/temporal + +RUN apk add --no-cache ca-certificates tzdata +COPY dist/${TARGETARCH}/temporal /usr/local/bin/temporal RUN adduser -u 1000 -D temporal USER temporal diff --git a/Makefile b/Makefile index 90a87893f..ca21efd74 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,14 @@ -.PHONY: all gen build +.PHONY: all gen gen-docs build all: gen build -gen: temporalcli/commands.gen.go +gen: internal/temporalcli/commands.gen.go -temporalcli/commands.gen.go: temporalcli/commandsgen/commands.yml - go run ./temporalcli/internal/cmd/gen-commands +internal/temporalcli/commands.gen.go: internal/temporalcli/commands.yaml + go run ./cmd/gen-commands -input $< -pkg temporalcli -context "*CommandContext" > $@ + +gen-docs: internal/temporalcli/commands.yaml + go run ./cmd/gen-docs -input $< -output dist/docs build: go build ./cmd/temporal diff --git a/cmd/gen-commands/README.md b/cmd/gen-commands/README.md new file mode 100644 index 000000000..28c3bd956 --- /dev/null +++ b/cmd/gen-commands/README.md @@ -0,0 +1,4 @@ +This package provides a CLI command generator for Temporal CLI applications. +It is designed specifically for the needs of Temporal CLIs, and not general-purpose command-line tools. + +Backwards-compatibility is not guaranteed. diff --git a/cmd/gen-commands/main.go b/cmd/gen-commands/main.go new file mode 100644 index 000000000..5297a59be --- /dev/null +++ b/cmd/gen-commands/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + + "github.com/temporalio/cli/internal/commandsgen" +) + +func main() { + if err := run(); err != nil { + log.Fatal(err) + } +} + +func run() error { + var ( + pkg string + contextType string + inputFile string + ) + + flag.StringVar(&pkg, "pkg", "main", "Package name for generated code") + flag.StringVar(&contextType, "context", "*CommandContext", "Context type for generated code") + flag.StringVar(&inputFile, "input", "", "Input YAML file (required)") + flag.Parse() + + // Read input from file + if inputFile == "" { + return fmt.Errorf("-input flag is required") + } + yamlBytes, err := os.ReadFile(inputFile) + if err != nil { + return fmt.Errorf("failed reading input: %w", err) + } + + // Parse YAML + cmds, err := commandsgen.ParseCommands(yamlBytes) + if err != nil { + return fmt.Errorf("failed parsing YAML: %w", err) + } + + // Generate code + b, err := commandsgen.GenerateCommandsCode(pkg, contextType, cmds) + if err != nil { + return fmt.Errorf("failed generating code: %w", err) + } + + // Write output to stdout + if _, err = os.Stdout.Write(b); err != nil { + return fmt.Errorf("failed writing output: %w", err) + } + + return nil +} diff --git a/cmd/gen-docs/README.md b/cmd/gen-docs/README.md new file mode 100644 index 000000000..c3f13ba25 --- /dev/null +++ b/cmd/gen-docs/README.md @@ -0,0 +1,4 @@ +This package provides a CLI command documentation generator for Temporal CLI applications. +It is designed specifically for the needs of Temporal CLIs, and not general-purpose command-line tools. + +Backwards-compatibility is not guaranteed. diff --git a/cmd/gen-docs/main.go b/cmd/gen-docs/main.go new file mode 100644 index 000000000..72f4ecdec --- /dev/null +++ b/cmd/gen-docs/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "path/filepath" + + "github.com/temporalio/cli/internal/commandsgen" +) + +func main() { + if err := run(); err != nil { + log.Fatal(err) + } +} + +func run() error { + var ( + outputDir string + inputFile string + ) + + flag.StringVar(&inputFile, "input", "", "Input YAML file (required)") + flag.StringVar(&outputDir, "output", ".", "Output directory for docs") + flag.Parse() + + // Read input from file + if inputFile == "" { + return fmt.Errorf("-input flag is required") + } + yamlBytes, err := os.ReadFile(inputFile) + if err != nil { + return fmt.Errorf("failed reading input: %w", err) + } + + // Create output directory + if err := os.MkdirAll(outputDir, 0755); err != nil { + return fmt.Errorf("failed creating output directory: %w", err) + } + + // Parse YAML + cmds, err := commandsgen.ParseCommands(yamlBytes) + if err != nil { + return fmt.Errorf("failed parsing YAML: %w", err) + } + + // Generate docs + docs, err := commandsgen.GenerateDocsFiles(cmds) + if err != nil { + return fmt.Errorf("failed generating docs: %w", err) + } + + // Write files + for filename, content := range docs { + filePath := filepath.Join(outputDir, filename+".mdx") + if err := os.WriteFile(filePath, content, 0644); err != nil { + return fmt.Errorf("failed writing %s: %w", filePath, err) + } + } + + return nil +} diff --git a/cmd/temporal/main.go b/cmd/temporal/main.go index 82203bf0c..0c482a6ed 100644 --- a/cmd/temporal/main.go +++ b/cmd/temporal/main.go @@ -3,7 +3,7 @@ package main import ( "context" - "github.com/temporalio/cli/temporalcli" + "github.com/temporalio/cli/internal/temporalcli" // Prevent the pinned version of sqlite driver from unintentionally changing // until https://gitlab.com/cznic/sqlite/-/issues/196 is resolved. diff --git a/docker-bake.hcl b/docker-bake.hcl new file mode 100644 index 000000000..66156cf2e --- /dev/null +++ b/docker-bake.hcl @@ -0,0 +1,45 @@ +variable "IMAGE_NAMESPACE" { + default = "" +} + +variable "IMAGE_NAME" { + default = "temporal" +} + +variable "GITHUB_REPOSITORY" { + default = "temporalio/cli" +} + +variable "IMAGE_SHA_TAG" {} + +variable "CLI_SHA" { + default = "" +} + +variable "VERSION" { + default = "dev" +} + +variable "TAG_LATEST" { + default = false +} + +target "cli" { + dockerfile = "Dockerfile" + context = "." + tags = compact([ + "${IMAGE_NAMESPACE}/${IMAGE_NAME}:${IMAGE_SHA_TAG}", + "${IMAGE_NAMESPACE}/${IMAGE_NAME}:${VERSION}", + TAG_LATEST ? "${IMAGE_NAMESPACE}/${IMAGE_NAME}:latest" : "", + ]) + platforms = ["linux/amd64", "linux/arm64"] + labels = { + "org.opencontainers.image.title" = "temporal" + "org.opencontainers.image.description" = "Temporal CLI" + "org.opencontainers.image.url" = "https://github.com/${GITHUB_REPOSITORY}" + "org.opencontainers.image.source" = "https://github.com/${GITHUB_REPOSITORY}" + "org.opencontainers.image.licenses" = "MIT" + "org.opencontainers.image.revision" = "${CLI_SHA}" + "org.opencontainers.image.created" = timestamp() + } +} diff --git a/go.mod b/go.mod index c27bb75ab..6bbf21854 100644 --- a/go.mod +++ b/go.mod @@ -15,11 +15,13 @@ require ( github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.6 github.com/stretchr/testify v1.10.0 - github.com/temporalio/ui-server/v2 v2.39.0 + github.com/temporalio/ui-server/v2 v2.42.1 go.temporal.io/api v1.59.1-0.20251203230651-7773526824c5 - go.temporal.io/sdk v1.36.0 + go.temporal.io/sdk v1.37.0 go.temporal.io/sdk/contrib/envconfig v0.1.0 go.temporal.io/server v1.29.0-135.0.0.20251210204203-56cbfb9a643c + golang.org/x/term v0.38.0 + golang.org/x/tools v0.40.0 google.golang.org/grpc v1.72.2 google.golang.org/protobuf v1.36.6 gopkg.in/yaml.v3 v3.0.1 @@ -149,13 +151,14 @@ require ( go.uber.org/mock v0.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.38.0 // indirect + golang.org/x/crypto v0.46.0 // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect - golang.org/x/net v0.40.0 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/text v0.25.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.11.0 // indirect google.golang.org/api v0.228.0 // indirect google.golang.org/genproto v0.0.0-20250324211829-b45e905df463 // indirect diff --git a/go.sum b/go.sum index 6b8443142..24cdc7424 100644 --- a/go.sum +++ b/go.sum @@ -322,8 +322,8 @@ github.com/temporalio/sqlparser v0.0.0-20231115171017-f4060bcfa6cb/go.mod h1:143 github.com/temporalio/tchannel-go v1.22.1-0.20220818200552-1be8d8cffa5b/go.mod h1:c+V9Z/ZgkzAdyGvHrvC5AsXgN+M9Qwey04cBdKYzV7U= github.com/temporalio/tchannel-go v1.22.1-0.20240528171429-1db37fdea938 h1:sEJGhmDo+0FaPWM6f0v8Tjia0H5pR6/Baj6+kS78B+M= github.com/temporalio/tchannel-go v1.22.1-0.20240528171429-1db37fdea938/go.mod h1:ezRQRwu9KQXy8Wuuv1aaFFxoCNz5CeNbVOOkh3xctbY= -github.com/temporalio/ui-server/v2 v2.39.0 h1:ZC6yoSCi74qKtDy3ly/tsgwYv3fSP+a748nADUSl3qo= -github.com/temporalio/ui-server/v2 v2.39.0/go.mod h1:f64k+N/ByniY9nI4c7cxbaiybwz10BkhapeIUDY3qIQ= +github.com/temporalio/ui-server/v2 v2.42.1 h1:ajeOxqCnUiCRQQhQYLxaT7wUgF/slqZJtdW4pLjVqCs= +github.com/temporalio/ui-server/v2 v2.42.1/go.mod h1:lKTnn50t8yQvcrarxAOjX33YcfkomkiNB5BH06wQwEE= github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= github.com/uber-common/bark v1.0.0/go.mod h1:g0ZuPcD7XiExKHynr93Q742G/sbrdVQkghrqLGOoFuY= @@ -380,8 +380,8 @@ go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.temporal.io/api v1.59.1-0.20251203230651-7773526824c5 h1:7lFIrLVM+NPVcqFMrEwv5d8D9meA7n/Xl9GtCl8Gyhc= go.temporal.io/api v1.59.1-0.20251203230651-7773526824c5/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= -go.temporal.io/sdk v1.36.0 h1:WO9zetpybBNK7xsQth4Z+3Zzw1zSaM9MOUGrnnUjZMo= -go.temporal.io/sdk v1.36.0/go.mod h1:8BxGRF0LcQlfQrLLGkgVajbsKUp/PY7280XTdcKc18Y= +go.temporal.io/sdk v1.37.0 h1:RbwCkUQuqY4rfCzdrDZF9lgT7QWG/pHlxfZFq0NPpDQ= +go.temporal.io/sdk v1.37.0/go.mod h1:tOy6vGonfAjrpCl6Bbw/8slTgQMiqvoyegRv2ZHPm5M= go.temporal.io/sdk/contrib/envconfig v0.1.0 h1:s+G/Ujph+Xl2jzLiiIm2T1vuijDkUL4Kse49dgDVGBE= go.temporal.io/sdk/contrib/envconfig v0.1.0/go.mod h1:FQEO3C56h9C7M6sDgSanB8HnBTmopw9qgVx4F1S6pJk= go.temporal.io/server v1.29.0-135.0.0.20251210204203-56cbfb9a643c h1:UC4KqFNh9Gx5w7asy/qv4CN2HmZBMebG3edO7X7axaY= @@ -412,8 +412,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -433,8 +433,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= 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= @@ -446,8 +446,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +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.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -456,8 +456,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -476,13 +476,15 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +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/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -490,8 +492,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +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.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -508,8 +510,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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= diff --git a/temporalcli/commandsgen/code.go b/internal/commandsgen/code.go similarity index 86% rename from temporalcli/commandsgen/code.go rename to internal/commandsgen/code.go index 6c67082ba..9d4134a3b 100644 --- a/temporalcli/commandsgen/code.go +++ b/internal/commandsgen/code.go @@ -2,18 +2,27 @@ package commandsgen import ( "bytes" + "embed" "fmt" + "go/ast" "go/format" + "go/parser" + "go/token" + "io/fs" "path" "regexp" "sort" "strings" - "go.temporal.io/server/common/primitives/timestamp" + "github.com/temporalio/cli/internal/commandsgen/types" ) -func GenerateCommandsCode(pkg string, commands Commands) ([]byte, error) { - w := &codeWriter{allCommands: commands.CommandList, OptionSets: commands.OptionSets} +//go:embed types/*.go +var typesFS embed.FS + +func GenerateCommandsCode(pkg string, contextType string, commands Commands) ([]byte, error) { + w := &codeWriter{allCommands: commands.CommandList, OptionSets: commands.OptionSets, contextType: contextType} + // Put terminal check at top w.writeLinef("var hasHighlighting = %v.IsTerminal(%v.Stdout.Fd())", w.importIsatty(), w.importPkg("os")) @@ -24,13 +33,28 @@ func GenerateCommandsCode(pkg string, commands Commands) ([]byte, error) { } } - // Write all commands, then come back and write package and imports + // Write all commands for _, cmd := range commands.CommandList { if err := cmd.writeCode(w); err != nil { return nil, fmt.Errorf("failed writing command %v: %w", cmd.FullName, err) } } + // Append embedded Go files from types/ (parse imports with go/ast, write code after imports) + err := fs.WalkDir(typesFS, "types", func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() || strings.Contains(path, "_test.go") { + return err + } + src, err := typesFS.ReadFile(path) + if err != nil { + return err + } + return w.appendGoSource(string(src)) + }) + if err != nil { + return nil, err + } + // Write package and imports to final buf var finalBuf bytes.Buffer finalBuf.WriteString("// Code generated. DO NOT EDIT.\n\n") @@ -59,6 +83,7 @@ type codeWriter struct { buf bytes.Buffer allCommands []Command OptionSets []OptionSets + contextType string // Key is short ref, value is full imports map[string]string } @@ -98,6 +123,36 @@ func (c *codeWriter) importPkg(pkg string) string { func (c *codeWriter) importCobra() string { return c.importPkg("github.com/spf13/cobra") } +// appendGoSource parses a Go source file, registers its imports, and appends +// everything after the import block to the output buffer. +func (c *codeWriter) appendGoSource(src string) error { + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "", src, parser.ImportsOnly) + if err != nil { + return fmt.Errorf("failed to parse embedded source: %w", err) + } + + // Register imports + for _, imp := range f.Imports { + // imp.Path.Value includes quotes, so trim them + c.importPkg(strings.Trim(imp.Path.Value, `"`)) + } + + // Find end of imports and append the rest + var lastImportEnd token.Pos + for _, decl := range f.Decls { + if genDecl, ok := decl.(*ast.GenDecl); ok && genDecl.Tok == token.IMPORT { + if genDecl.End() > lastImportEnd { + lastImportEnd = genDecl.End() + } + } + } + + // Write everything after imports + c.buf.WriteString(src[fset.Position(lastImportEnd).Offset:]) + return nil +} + func (c *codeWriter) importPflag() string { return c.importPkg("github.com/spf13/pflag") } func (c *codeWriter) importIsatty() string { return c.importPkg("github.com/mattn/go-isatty") } @@ -120,8 +175,8 @@ func (o *OptionSets) writeCode(w *codeWriter) error { w.writeLinef("}\n") // write flags - w.writeLinef("func (v *%v) buildFlags(cctx *CommandContext, f *%v.FlagSet) {", - o.setStructName(), w.importPflag()) + w.writeLinef("func (v *%v) buildFlags(cctx %s, f *%v.FlagSet) {", + o.setStructName(), w.contextType, w.importPflag()) o.writeFlagBuilding("v", "f", w) w.writeLinef("}\n") @@ -164,10 +219,10 @@ func (c *Command) writeCode(w *codeWriter) error { // Constructor builds the struct and sets the flags if hasParent { - w.writeLinef("func New%v(cctx *CommandContext, parent *%v) *%v {", - c.structName(), parent.structName(), c.structName()) + w.writeLinef("func New%v(cctx %s, parent *%v) *%v {", + c.structName(), w.contextType, parent.structName(), c.structName()) } else { - w.writeLinef("func New%v(cctx *CommandContext) *%v {", c.structName(), c.structName()) + w.writeLinef("func New%v(cctx %s) *%v {", c.structName(), w.contextType, c.structName()) } w.writeLinef("var s %v", c.structName()) if hasParent { @@ -328,7 +383,7 @@ func (o *Option) writeFlagBuilding(selfVar, flagVar string, w *codeWriter) error case "duration": flagMeth, setDefault = "Var", "0" if o.Default != "" { - dur, err := timestamp.ParseDuration(o.Default) + dur, err := types.ParseDuration(o.Default) if err != nil { return fmt.Errorf("invalid default: %w", err) } diff --git a/internal/commandsgen/docs.go b/internal/commandsgen/docs.go new file mode 100644 index 000000000..b3857503e --- /dev/null +++ b/internal/commandsgen/docs.go @@ -0,0 +1,264 @@ +package commandsgen + +import ( + "bytes" + "fmt" + "regexp" + "sort" + "strings" +) + +func GenerateDocsFiles(commands Commands) (map[string][]byte, error) { + optionSetMap := make(map[string]OptionSets) + for i, optionSet := range commands.OptionSets { + optionSetMap[optionSet.Name] = commands.OptionSets[i] + } + + w := &docWriter{ + fileMap: make(map[string]*bytes.Buffer), + optionSetMap: optionSetMap, + allCommands: commands.CommandList, + globalFlagsMap: make(map[string]map[string]Option), + } + + // sorted ascending by full name of command (activity complete, batch list, etc) + for _, cmd := range commands.CommandList { + if err := cmd.writeDoc(w); err != nil { + return nil, fmt.Errorf("failed writing docs for command %s: %w", cmd.FullName, err) + } + } + + // Write global flags section once at the end of each file + w.writeGlobalFlagsSections() + + // Format and return + var finalMap = make(map[string][]byte) + for key, buf := range w.fileMap { + finalMap[key] = buf.Bytes() + } + return finalMap, nil +} + +type docWriter struct { + allCommands []Command + fileMap map[string]*bytes.Buffer + optionSetMap map[string]OptionSets + optionsStack [][]Option + globalFlagsMap map[string]map[string]Option // fileName -> optionName -> Option +} + +func (c *Command) writeDoc(w *docWriter) error { + w.processOptions(c) + + // If this is a root command, write a new file + depth := c.depth() + if depth == 1 { + w.writeCommand(c) + } else if depth > 1 { + w.writeSubcommand(c) + } + return nil +} + +func (w *docWriter) writeCommand(c *Command) { + fileName := c.fileName() + w.fileMap[fileName] = &bytes.Buffer{} + w.fileMap[fileName].WriteString("---\n") + w.fileMap[fileName].WriteString("id: " + fileName + "\n") + w.fileMap[fileName].WriteString("title: Temporal CLI " + fileName + " command reference\n") + w.fileMap[fileName].WriteString("sidebar_label: " + fileName + "\n") + w.fileMap[fileName].WriteString("description: " + c.Docs.DescriptionHeader + "\n") + w.fileMap[fileName].WriteString("toc_max_heading_level: 4\n") + + w.fileMap[fileName].WriteString("keywords:\n") + for _, keyword := range c.Docs.Keywords { + w.fileMap[fileName].WriteString(" - " + keyword + "\n") + } + w.fileMap[fileName].WriteString("tags:\n") + for _, tag := range c.Docs.Tags { + w.fileMap[fileName].WriteString(" - " + tag + "\n") + } + w.fileMap[fileName].WriteString("---") + w.fileMap[fileName].WriteString("\n\n") + w.fileMap[fileName].WriteString("{/* NOTE: This is an auto-generated file. Any edit to this file will be overwritten.\n") + w.fileMap[fileName].WriteString("This file is generated from https://github.com/temporalio/cli/blob/main/internal/commandsgen/commands.yml via internal/cmd/gen-docs */}\n\n") + // Add introductory paragraph + w.fileMap[fileName].WriteString(fmt.Sprintf("This page provides a reference for the `temporal` CLI `%s` command. ", fileName)) + w.fileMap[fileName].WriteString("The flags applicable to each subcommand are presented in a table within the heading for the subcommand. ") + w.fileMap[fileName].WriteString("Refer to [Global Flags](#global-flags) for flags that you can use with every subcommand.\n\n") +} + +func (w *docWriter) writeSubcommand(c *Command) { + fileName := c.fileName() + prefix := strings.Repeat("#", c.depth()) + w.fileMap[fileName].WriteString(prefix + " " + c.leafName() + "\n\n") + w.fileMap[fileName].WriteString(c.Description + "\n\n") + + if w.isLeafCommand(c) { + // gather options from command and all options available from parent commands + var options = make([]Option, 0) + var globalOptions = make([]Option, 0) + for i, o := range w.optionsStack { + if i == len(w.optionsStack)-1 { + options = append(options, o...) + } else { + globalOptions = append(globalOptions, o...) + } + } + + // alphabetize options + sort.Slice(options, func(i, j int) bool { + return options[i].Name < options[j].Name + }) + + // Write command-specific flags or global flags message + if len(options) > 0 { + w.fileMap[fileName].WriteString("Use the following options to change the behavior of this command. ") + w.fileMap[fileName].WriteString("You can also use any of the [global flags](#global-flags) that apply to all subcommands.\n\n") + w.writeOptionsTable(options, c) + } else { + w.fileMap[fileName].WriteString("Use [global flags](#global-flags) to customize the connection to the Temporal Service for this command.\n\n") + } + + // Collect global flags for later (deduplicated) + w.collectGlobalFlags(fileName, globalOptions) + } +} + +func (w *docWriter) writeOptionsTable(options []Option, c *Command) { + if len(options) == 0 { + return + } + + fileName := c.fileName() + buf := w.fileMap[fileName] + + // Command-specific flags: 3 columns (no Default) + buf.WriteString("| Flag | Required | Description |\n") + buf.WriteString("|------|----------|-------------|\n") + + for _, o := range options { + w.writeOptionRow(buf, o, false) + } + buf.WriteString("\n") +} + +func (w *docWriter) writeOptionRow(buf *bytes.Buffer, o Option, includeDefault bool) { + // Flag name column + flagName := fmt.Sprintf("`--%s`", o.Name) + if len(o.Short) > 0 { + flagName += fmt.Sprintf(", `-%s`", o.Short) + } + + // Required column + required := "No" + if o.Required { + required = "Yes" + } + + // Description column - starts with data type + optionType := o.Type + if o.DisplayType != "" { + optionType = o.DisplayType + } + description := fmt.Sprintf("**%s** %s", optionType, encodeJSONExample(o.Description)) + if len(o.EnumValues) > 0 { + description += fmt.Sprintf(" Accepted values: %s.", strings.Join(o.EnumValues, ", ")) + } + if o.Experimental { + description += " _(Experimental)_" + } + // Escape pipes in description for table compatibility + description = strings.ReplaceAll(description, "|", "\\|") + + if includeDefault { + // Default column + defaultVal := "" + if len(o.Default) > 0 { + defaultVal = fmt.Sprintf("`%s`", o.Default) + } + buf.WriteString(fmt.Sprintf("| %s | %s | %s | %s |\n", flagName, required, description, defaultVal)) + } else { + buf.WriteString(fmt.Sprintf("| %s | %s | %s |\n", flagName, required, description)) + } +} + +func (w *docWriter) collectGlobalFlags(fileName string, options []Option) { + if w.globalFlagsMap[fileName] == nil { + w.globalFlagsMap[fileName] = make(map[string]Option) + } + for _, o := range options { + // Only add if not already present (deduplication) + if _, exists := w.globalFlagsMap[fileName][o.Name]; !exists { + w.globalFlagsMap[fileName][o.Name] = o + } + } +} + +func (w *docWriter) writeGlobalFlagsSections() { + for fileName, optionsMap := range w.globalFlagsMap { + if len(optionsMap) == 0 { + continue + } + + // Convert map to slice and sort + options := make([]Option, 0, len(optionsMap)) + for _, o := range optionsMap { + options = append(options, o) + } + sort.Slice(options, func(i, j int) bool { + return options[i].Name < options[j].Name + }) + + buf := w.fileMap[fileName] + buf.WriteString("## Global Flags\n\n") + buf.WriteString("The following options can be used with any command.\n\n") + // Global flags: 4 columns (with Default) + buf.WriteString("| Flag | Required | Description | Default |\n") + buf.WriteString("|------|----------|-------------|--------|\n") + + for _, o := range options { + w.writeOptionRow(buf, o, true) + } + buf.WriteString("\n") + } +} + +func (w *docWriter) processOptions(c *Command) { + // Pop options from stack if we are moving up a level + if len(w.optionsStack) >= len(strings.Split(c.FullName, " ")) { + w.optionsStack = w.optionsStack[:len(w.optionsStack)-1] + } + var options []Option + options = append(options, c.Options...) + + // Maintain stack of options available from parent commands + for _, set := range c.OptionSets { + optionSet, ok := w.optionSetMap[set] + if !ok { + panic(fmt.Sprintf("invalid option set %v used", set)) + } + optionSetOptions := optionSet.Options + options = append(options, optionSetOptions...) + } + + w.optionsStack = append(w.optionsStack, options) +} + +func (w *docWriter) isLeafCommand(c *Command) bool { + for _, maybeSubCmd := range w.allCommands { + if maybeSubCmd.isSubCommand(c) { + return false + } + } + return true +} + +func encodeJSONExample(v string) string { + // example: 'YourKey={"your": "value"}' + // results in an mdx acorn rendering error + // and wrapping in backticks lets it render + re := regexp.MustCompile(`('[a-zA-Z0-9]*={.*}')`) + v = re.ReplaceAllString(v, "`$1`") + return v +} diff --git a/temporalcli/commandsgen/parse.go b/internal/commandsgen/parse.go similarity index 96% rename from temporalcli/commandsgen/parse.go rename to internal/commandsgen/parse.go index 7945e1c7e..6ace2c004 100644 --- a/temporalcli/commandsgen/parse.go +++ b/internal/commandsgen/parse.go @@ -1,10 +1,8 @@ -// Package commandsgen is built to read the YAML format described in -// temporalcli/commandsgen/commands.yml and generate code from it. +// Package commandsgen reads YAML command definitions and generates code from them. package commandsgen import ( "bytes" - _ "embed" "fmt" "regexp" "slices" @@ -14,9 +12,6 @@ import ( "gopkg.in/yaml.v3" ) -//go:embed commands.yml -var CommandsYAML []byte - type ( // Option represents the structure of an option within option sets. Option struct { @@ -76,9 +71,10 @@ type ( } ) -func ParseCommands() (Commands, error) { +// ParseCommands parses command definitions from YAML bytes. +func ParseCommands(yamlData []byte) (Commands, error) { // Fix CRLF - md := bytes.ReplaceAll(CommandsYAML, []byte("\r\n"), []byte("\n")) + md := bytes.ReplaceAll(yamlData, []byte("\r\n"), []byte("\n")) var m Commands err := yaml.Unmarshal(md, &m) diff --git a/internal/commandsgen/types/duration.go b/internal/commandsgen/types/duration.go new file mode 100644 index 000000000..4ed8f080b --- /dev/null +++ b/internal/commandsgen/types/duration.go @@ -0,0 +1,49 @@ +// NOTE: this file is embedded inside the generated commands.gen.go output + +package types + +import ( + "fmt" + "regexp" + "strconv" + "strings" + "time" +) + +var reDays = regexp.MustCompile(`(\d+(\.\d*)?|(\.\d+))d`) + +type Duration time.Duration + +// ParseDuration is like time.ParseDuration, but supports unit "d" for days +// (always interpreted as exactly 24 hours). +func ParseDuration(s string) (time.Duration, error) { + s = reDays.ReplaceAllStringFunc(s, func(v string) string { + fv, err := strconv.ParseFloat(strings.TrimSuffix(v, "d"), 64) + if err != nil { + return v // will cause time.ParseDuration to return an error + } + return fmt.Sprintf("%fh", 24*fv) + }) + return time.ParseDuration(s) +} + +func (d Duration) Duration() time.Duration { + return time.Duration(d) +} + +func (d *Duration) String() string { + return d.Duration().String() +} + +func (d *Duration) Set(s string) error { + p, err := ParseDuration(s) + if err != nil { + return err + } + *d = Duration(p) + return nil +} + +func (d *Duration) Type() string { + return "duration" +} diff --git a/internal/commandsgen/types/duration_test.go b/internal/commandsgen/types/duration_test.go new file mode 100644 index 000000000..38277190c --- /dev/null +++ b/internal/commandsgen/types/duration_test.go @@ -0,0 +1,45 @@ +package types_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/suite" + "github.com/temporalio/cli/internal/commandsgen/types" +) + +type ParseDurationSuite struct { + suite.Suite +} + +func TestParseDurationSuite(t *testing.T) { + suite.Run(t, new(ParseDurationSuite)) +} + +func (s *ParseDurationSuite) TestParseDuration() { + for _, c := range []struct { + input string + expected time.Duration // -1 means error + }{ + {"1h", time.Hour}, + {"3m30s", 3*time.Minute + 30*time.Second}, + {"1d", 24 * time.Hour}, + {"3d", 3 * 24 * time.Hour}, + {"5d6h15m", 5*24*time.Hour + 6*time.Hour + 15*time.Minute}, + {"5.25d15m", 5*24*time.Hour + 6*time.Hour + 15*time.Minute}, + {".5d", 12 * time.Hour}, + {"-10d12.25h", -(10*24*time.Hour + 12*time.Hour + 15*time.Minute)}, + {"3m2h1d", 3*time.Minute + 2*time.Hour + 1*24*time.Hour}, + {"8m7h6d5d4h3m", 8*time.Minute + 7*time.Hour + 6*24*time.Hour + 5*24*time.Hour + 4*time.Hour + 3*time.Minute}, + {"7", -1}, // error + {"", -1}, // error + {"10000000h", -1}, // error out of bounds + } { + got, err := types.ParseDuration(c.input) + if c.expected == -1 { + s.Error(err) + } else { + s.Equal(c.expected, got) + } + } +} diff --git a/internal/commandsgen/types/stringenum.go b/internal/commandsgen/types/stringenum.go new file mode 100644 index 000000000..2f0f7f437 --- /dev/null +++ b/internal/commandsgen/types/stringenum.go @@ -0,0 +1,63 @@ +// NOTE: this file is embedded inside the generated commands.gen.go output + +package types + +import ( + "fmt" + "strings" +) + +type StringEnum struct { + Allowed []string + Value string + ChangedFromDefault bool +} + +func NewStringEnum(allowed []string, value string) StringEnum { + return StringEnum{Allowed: allowed, Value: value} +} + +func (s *StringEnum) String() string { return s.Value } + +func (s *StringEnum) Set(p string) error { + for _, allowed := range s.Allowed { + if p == allowed { + s.Value = p + s.ChangedFromDefault = true + return nil + } + } + return fmt.Errorf("%v is not one of required values of %v", p, strings.Join(s.Allowed, ", ")) +} + +func (*StringEnum) Type() string { return "string" } + +type StringEnumArray struct { + Allowed map[string]string + Values []string +} + +func NewStringEnumArray(allowed []string, values []string) StringEnumArray { + var allowedMap = make(map[string]string) + for _, str := range allowed { + allowedMap[strings.ToLower(str)] = str + } + return StringEnumArray{Allowed: allowedMap, Values: values} +} + +func (s *StringEnumArray) String() string { return strings.Join(s.Values, ",") } + +func (s *StringEnumArray) Set(p string) error { + val, ok := s.Allowed[strings.ToLower(p)] + if !ok { + values := make([]string, 0, len(s.Allowed)) + for _, v := range s.Allowed { + values = append(values, v) + } + return fmt.Errorf("invalid value: %s, allowed values are: %s", p, strings.Join(values, ", ")) + } + s.Values = append(s.Values, val) + return nil +} + +func (*StringEnumArray) Type() string { return "string" } diff --git a/temporalcli/timestamp.go b/internal/commandsgen/types/timestamp.go similarity index 81% rename from temporalcli/timestamp.go rename to internal/commandsgen/types/timestamp.go index 82aac30a4..4adf4fcfc 100644 --- a/temporalcli/timestamp.go +++ b/internal/commandsgen/types/timestamp.go @@ -1,4 +1,6 @@ -package temporalcli +// NOTE: this file is embedded inside the generated commands.gen.go output + +package types import "time" diff --git a/temporalcli/devserver/freeport.go b/internal/devserver/freeport.go similarity index 100% rename from temporalcli/devserver/freeport.go rename to internal/devserver/freeport.go diff --git a/temporalcli/devserver/freeport_test.go b/internal/devserver/freeport_test.go similarity index 97% rename from temporalcli/devserver/freeport_test.go rename to internal/devserver/freeport_test.go index b6cf57703..5839cef65 100644 --- a/temporalcli/devserver/freeport_test.go +++ b/internal/devserver/freeport_test.go @@ -5,7 +5,7 @@ import ( "net" "testing" - "github.com/temporalio/cli/temporalcli/devserver" + "github.com/temporalio/cli/internal/devserver" ) func TestFreePort_NoDouble(t *testing.T) { diff --git a/temporalcli/devserver/log.go b/internal/devserver/log.go similarity index 100% rename from temporalcli/devserver/log.go rename to internal/devserver/log.go diff --git a/temporalcli/devserver/server.go b/internal/devserver/server.go similarity index 100% rename from temporalcli/devserver/server.go rename to internal/devserver/server.go diff --git a/temporalcli/internal/printer/printer.go b/internal/printer/printer.go similarity index 100% rename from temporalcli/internal/printer/printer.go rename to internal/printer/printer.go diff --git a/temporalcli/internal/printer/printer_test.go b/internal/printer/printer_test.go similarity index 98% rename from temporalcli/internal/printer/printer_test.go rename to internal/printer/printer_test.go index 8965ac60b..054332043 100644 --- a/temporalcli/internal/printer/printer_test.go +++ b/internal/printer/printer_test.go @@ -10,7 +10,7 @@ import ( "unicode" "github.com/stretchr/testify/require" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" ) // TODO(cretz): Test: diff --git a/temporalcli/internal/printer/test/main.go b/internal/printer/test/main.go similarity index 90% rename from temporalcli/internal/printer/test/main.go rename to internal/printer/test/main.go index 80bce277b..48615483c 100644 --- a/temporalcli/internal/printer/test/main.go +++ b/internal/printer/test/main.go @@ -3,7 +3,7 @@ package main import ( "os" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" ) // This main function is used to test that the printer package don't panic if diff --git a/temporalcli/client.go b/internal/temporalcli/client.go similarity index 89% rename from temporalcli/client.go rename to internal/temporalcli/client.go index cc8c73018..9299d4fc7 100644 --- a/temporalcli/client.go +++ b/internal/temporalcli/client.go @@ -13,6 +13,7 @@ import ( "go.temporal.io/sdk/contrib/envconfig" "go.temporal.io/sdk/converter" "go.temporal.io/sdk/log" + "go.temporal.io/sdk/workflow" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) @@ -229,6 +230,8 @@ func (c *ClientOptions) dialClient(cctx *CommandContext) (client.Client, error) return client.DialContext(ctxWithTimeout, clientOptions) } + clientOptions.ContextPropagators = append(clientOptions.ContextPropagators, headerPropagator{}) + return client.DialContext(cctx, clientOptions) } @@ -314,3 +317,43 @@ func (rawValuePayloadConverter) Encoding() string { // Should never be used return "raw-value-encoding" } + +type headerPropagator struct{} + +type cliHeaderContextKey struct{} + +func (headerPropagator) Inject(ctx context.Context, writer workflow.HeaderWriter) error { + if headers, ok := ctx.Value(cliHeaderContextKey{}).(map[string]any); ok { + for k, v := range headers { + p, err := converter.GetDefaultDataConverter().ToPayload(v) + if err != nil { + return err + } + writer.Set(k, p) + } + } + return nil +} + +func (headerPropagator) InjectFromWorkflow(ctx workflow.Context, writer workflow.HeaderWriter) error { + return nil +} + +func (headerPropagator) Extract(ctx context.Context, _ workflow.HeaderReader) (context.Context, error) { + return ctx, nil +} + +func (headerPropagator) ExtractToWorkflow(ctx workflow.Context, _ workflow.HeaderReader) (workflow.Context, error) { + return ctx, nil +} + +func contextWithHeaders(ctx context.Context, headers []string) (context.Context, error) { + if len(headers) == 0 { + return ctx, nil + } + out, err := stringKeysJSONValues(headers, false) + if err != nil { + return ctx, err + } + return context.WithValue(ctx, cliHeaderContextKey{}, out), nil +} diff --git a/temporalcli/commands.activity.go b/internal/temporalcli/commands.activity.go similarity index 99% rename from temporalcli/commands.activity.go rename to internal/temporalcli/commands.activity.go index 0d297a4a2..503a5e1e1 100644 --- a/temporalcli/commands.activity.go +++ b/internal/temporalcli/commands.activity.go @@ -4,7 +4,7 @@ import ( "fmt" "time" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" activitypb "go.temporal.io/api/activity/v1" "go.temporal.io/api/batch/v1" "go.temporal.io/api/common/v1" diff --git a/temporalcli/commands.activity_test.go b/internal/temporalcli/commands.activity_test.go similarity index 99% rename from temporalcli/commands.activity_test.go rename to internal/temporalcli/commands.activity_test.go index 6ce214472..28ff7a13b 100644 --- a/temporalcli/commands.activity_test.go +++ b/internal/temporalcli/commands.activity_test.go @@ -436,7 +436,6 @@ func (s *SharedServerSuite) TestUnpauseActivity_BatchSuccess() { failActivity.Store(false) } - func (s *SharedServerSuite) TestResetActivity_BatchSuccess() { var failActivity atomic.Bool failActivity.Store(true) diff --git a/temporalcli/commands.batch.go b/internal/temporalcli/commands.batch.go similarity index 98% rename from temporalcli/commands.batch.go rename to internal/temporalcli/commands.batch.go index 93686732c..c96990143 100644 --- a/temporalcli/commands.batch.go +++ b/internal/temporalcli/commands.batch.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/api/serviceerror" "go.temporal.io/api/workflowservice/v1" "go.temporal.io/sdk/client" diff --git a/temporalcli/commands.batch_test.go b/internal/temporalcli/commands.batch_test.go similarity index 100% rename from temporalcli/commands.batch_test.go rename to internal/temporalcli/commands.batch_test.go diff --git a/temporalcli/commands.config.go b/internal/temporalcli/commands.config.go similarity index 99% rename from temporalcli/commands.config.go rename to internal/temporalcli/commands.config.go index a4028ff46..79467328d 100644 --- a/temporalcli/commands.config.go +++ b/internal/temporalcli/commands.config.go @@ -9,7 +9,7 @@ import ( "strings" "github.com/BurntSushi/toml" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/sdk/contrib/envconfig" ) diff --git a/temporalcli/commands.config_test.go b/internal/temporalcli/commands.config_test.go similarity index 100% rename from temporalcli/commands.config_test.go rename to internal/temporalcli/commands.config_test.go diff --git a/temporalcli/commands.env.go b/internal/temporalcli/commands.env.go similarity index 98% rename from temporalcli/commands.env.go rename to internal/temporalcli/commands.env.go index c39921689..d98c06f04 100644 --- a/temporalcli/commands.env.go +++ b/internal/temporalcli/commands.env.go @@ -7,7 +7,7 @@ import ( "sort" "strings" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "gopkg.in/yaml.v3" ) diff --git a/temporalcli/commands.env_test.go b/internal/temporalcli/commands.env_test.go similarity index 100% rename from temporalcli/commands.env_test.go rename to internal/temporalcli/commands.env_test.go diff --git a/internal/temporalcli/commands.extension.go b/internal/temporalcli/commands.extension.go new file mode 100644 index 000000000..870573034 --- /dev/null +++ b/internal/temporalcli/commands.extension.go @@ -0,0 +1,179 @@ +package temporalcli + +import ( + "context" + "fmt" + "os/exec" + "slices" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +const ( + extensionPrefix = "temporal-" + extensionSeparator = "-" // separates command parts in extension name + argDashReplacement = "_" // dashes in args are replaced to avoid ambiguity +) + +// cliArgsToParseForExtension lists CLI flags that should be parsed (validated). +var cliArgsToParseForExtension = map[string]bool{ + "command-timeout": true, +} + +// tryExecuteExtension tries to execute an extension command if the command is not a built-in command. +// It returns an error if the extension command fails, and a boolean indicating whether an extension was executed. +func tryExecuteExtension(cctx *CommandContext, tcmd *TemporalCommand) (error, bool) { + // Find the deepest matching built-in command and remaining args. + foundCmd, remainingArgs, findErr := tcmd.Command.Find(cctx.Options.Args) + + // Check if remaining args include positional args (not just flags). + // If not, a built-in command fully handles this - no extension needed. + hasPosArgs := slices.ContainsFunc(remainingArgs, isPosArg) + if findErr == nil && !hasPosArgs { + return nil, false + } + + // Group args into these lists: + // - cliParseArgs: args to validate (subset of cliPassArgs) + // - cliPassArgs: known CLI args to pass to extension + // - extArgs: args to pass to extension and use for extension lookup + cliParseArgs, cliPassArgs, extArgs := groupArgs(foundCmd, remainingArgs) + + // Search for an extension executable. + cmdPrefix := strings.Split(foundCmd.CommandPath(), " ")[1:] + extPath, extArgs := lookupExtension(cmdPrefix, extArgs) + + // Parse CLI args that need validation. + if len(cliParseArgs) > 0 { + if err := foundCmd.Flags().Parse(cliParseArgs); err != nil { + return err, false + } + } + + if extPath == "" { + return nil, false + } + + // Apply --command-timeout if set. + ctx := cctx.Context + if timeout := tcmd.CommandTimeout.Duration(); timeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, timeout) + defer cancel() + } + + cmd := exec.CommandContext(ctx, extPath, append(cliPassArgs, extArgs...)...) + cmd.Stdin, cmd.Stdout, cmd.Stderr = cctx.Options.Stdin, cctx.Options.Stdout, cctx.Options.Stderr + if err := cmd.Run(); err != nil { + if ctx.Err() != nil { + return fmt.Errorf("program interrupted"), true + } + if _, ok := err.(*exec.ExitError); ok { + return nil, true + } + return fmt.Errorf("extension %s failed: %w", extPath, err), true + } + + return nil, true +} + +func groupArgs(foundCmd *cobra.Command, args []string) (cliParseArgs, cliPassArgs, extArgs []string) { + seenPos := false + for i := 0; i < len(args); i++ { + arg := args[i] + + if isPosArg(arg) { + seenPos = true + extArgs = append(extArgs, arg) + continue + } + + name, hasInline := parseFlagArg(arg) + if f, takesValue := lookupFlag(foundCmd, name); f != nil { + // Known CLI flag: goes to cliPassArgs. + // Flags in cliArgsToParseForExtension also go to cliParseArgs. + shouldParse := cliArgsToParseForExtension[f.Name] + cliPassArgs = append(cliPassArgs, arg) + if shouldParse { + cliParseArgs = append(cliParseArgs, arg) + } + if takesValue && !hasInline && i+1 < len(args) { + i++ + cliPassArgs = append(cliPassArgs, args[i]) + if shouldParse { + cliParseArgs = append(cliParseArgs, args[i]) + } + } + } else { + // Unknown flag: before first positional goes to cliParseArgs (to fail validation), + // after first positional goes to extArgs (passed to extension). + if seenPos { + extArgs = append(extArgs, arg) + } else { + cliParseArgs = append(cliParseArgs, arg) + } + } + } + return +} + +func isPosArg(arg string) bool { + return !strings.HasPrefix(arg, "-") +} + +// parseFlagArg extracts the flag name from a flag argument. +// Handles both --flag=value and --flag forms, returning the name and whether it has an inline value. +func parseFlagArg(arg string) (name string, hasInline bool) { + name, _, hasInline = strings.Cut(strings.TrimLeft(arg, "-"), "=") + return +} + +// lookupFlag finds a flag by name on cmd and all parents. +// It resolves aliases and considers shorthand flags. +func lookupFlag(cmd *cobra.Command, name string) (*pflag.Flag, bool) { + if normalize := cmd.Flags().GetNormalizeFunc(); normalize != nil { + name = string(normalize(cmd.Flags(), name)) + } + for c := cmd; c != nil; c = c.Parent() { + if f := c.Flags().Lookup(name); f != nil { + return f, f.NoOptDefVal == "" + } + if len(name) == 1 { + if f := c.Flags().ShorthandLookup(name); f != nil { + return f, f.NoOptDefVal == "" + } + } + } + return nil, false +} + +// lookupExtension finds an extension executable and returns its path along with +// extArgs with matched positional args removed. +func lookupExtension(cmdPrefix, extArgs []string) (string, []string) { + // Extract positional args from extArgs until we hit an unknown flag. + // We stop at unknown flags because we can't tell if subsequent args are flag values or positionals. + var posArgs []string + for _, arg := range extArgs { + if !isPosArg(arg) { + break + } + // Dashes are converted to underscores so "foo bar-baz" finds "temporal-foo-bar_baz". + posArgs = append(posArgs, strings.ReplaceAll(arg, extensionSeparator, argDashReplacement)) + } + + // Try most-specific to least-specific. + parts := append(cmdPrefix, posArgs...) + for n := len(parts); n > len(cmdPrefix); n-- { + path, err := exec.LookPath(extensionPrefix + strings.Join(parts[:n], extensionSeparator)) + if err != nil { + continue + } + // Remove matched positionals from extArgs (they come first). + matched := n - len(cmdPrefix) + return path, extArgs[matched:] + } + + return "", extArgs +} diff --git a/internal/temporalcli/commands.extension_test.go b/internal/temporalcli/commands.extension_test.go new file mode 100644 index 000000000..46ab15c49 --- /dev/null +++ b/internal/temporalcli/commands.extension_test.go @@ -0,0 +1,300 @@ +package temporalcli_test + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/tools/imports" +) + +// Go code snippets for cross-platform test extensions. +var ( + codeEchoArgs = `fmt.Println("Args:", strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe"), strings.Join(os.Args[1:], " "))` + codeEchoStderr = func(msg string) string { + return fmt.Sprintf(`fmt.Fprintln(os.Stderr, %q)`, msg) + } + codeExit = func(code int) string { + return fmt.Sprintf(`os.Exit(%d)`, code) + } + codeSleep = func(d time.Duration) string { + return fmt.Sprintf(`time.Sleep(%d)`, d) + } + codeCat = `io.Copy(os.Stdout, os.Stdin)` + codeEchoEnv = func(name string) string { + return fmt.Sprintf(`fmt.Println(os.Getenv(%q))`, name) + } +) + +func TestExtension_InvokesRootExtension(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo", codeEchoArgs) + + res := h.Execute("foo") + + assert.Equal(t, "Args: temporal-foo \n", res.Stdout.String()) +} + +func TestExtension_InvokesSubcommandExtension(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo-bar", codeEchoArgs) + + res := h.Execute("foo", "bar") + + assert.Equal(t, "Args: temporal-foo-bar \n", res.Stdout.String()) +} + +func TestExtension_PrefersMostSpecificExtension(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo", codeEchoArgs) + h.createExtension("temporal-foo-bar", codeEchoArgs) + + res := h.Execute("foo", "bar") + + assert.Equal(t, "Args: temporal-foo-bar \n", res.Stdout.String()) +} + +func TestExtension_ConvertsDashToUnderscoreInLookup(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo-bar_baz", codeEchoArgs) + + // Dash in arg is converted to underscore when looking up extension. + res := h.Execute("foo", "bar-baz") + assert.Equal(t, "Args: temporal-foo-bar_baz \n", res.Stdout.String()) + + // Underscore in arg stays as underscore. + res = h.Execute("foo", "bar_baz") + assert.Equal(t, "Args: temporal-foo-bar_baz \n", res.Stdout.String()) +} + +func TestExtension_DoesNotOverrideBuiltinCommand(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-workflow", codeEchoArgs) + h.createExtension("temporal-workflow-list", codeEchoArgs) + + t.Run("root command", func(t *testing.T) { + res := h.Execute("workflow", "--help") + assert.Contains(t, res.Stdout.String(), "Workflow commands perform operations on Workflow Executions") + }) + + t.Run("subcommand", func(t *testing.T) { + res := h.Execute("workflow", "list", "--help") + assert.Contains(t, res.Stdout.String(), "List Workflow Executions") + }) +} + +func TestExtension_Flags(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo", codeEchoArgs) + h.createExtension("temporal-foo-bar", codeEchoArgs) // should never be called + h.createExtension("temporal-foo-json", codeEchoArgs) // should never be called + h.createExtension("temporal-workflow-diagram", codeEchoArgs) + h.createExtension("temporal-workflow-diagram-foo", codeEchoArgs) // should never be called + h.createExtension("temporal-workflow-diagram-json", codeEchoArgs) // should never be called + + cases := []struct { + args string + want string + err string + }{ + // Root extension + + {args: "--no-json-shorthand-payloads foo", want: "temporal-foo --no-json-shorthand-payloads"}, // boolean flag + {args: "--output json foo", want: "temporal-foo --output json"}, + {args: "--output=json foo", want: "temporal-foo --output=json"}, + {args: "-o json foo", want: "temporal-foo -o json"}, // shorthand + {args: "-o=json foo", want: "temporal-foo -o=json"}, + {args: "--unknown-flag value foo", err: "unknown flag"}, // unknown flags before extension fail + {args: "--output invalid foo", want: "temporal-foo --output invalid"}, // invalid value passed through + {args: "--command-timeout 1s foo", want: "temporal-foo --command-timeout 1s"}, // --command-timeout passed through + {args: "--command-timeout invalid foo", err: "invalid argument"}, // --command-timeout invalid + + {args: "foo --output json", want: "temporal-foo --output json"}, // not temporal-foo-json + {args: "foo --output=json", want: "temporal-foo --output=json"}, + {args: "foo -o json", want: "temporal-foo -o json"}, + {args: "foo -o=json", want: "temporal-foo -o=json"}, + {args: "foo -x bar", want: "temporal-foo -x bar"}, // not temporal-foo-x + {args: "foo --output invalid", want: "temporal-foo --output invalid"}, // invalid value passed through + {args: "foo arg1 -x value arg2", want: "temporal-foo arg1 -x value arg2"}, // order preserved + + // Subcommand extension + + {args: "--output json workflow diagram", want: "temporal-workflow-diagram --output json"}, + {args: "--output=json workflow diagram", want: "temporal-workflow-diagram --output=json"}, + {args: "-o json workflow diagram", want: "temporal-workflow-diagram -o json"}, // shorthand + {args: "-o=json workflow diagram", want: "temporal-workflow-diagram -o=json"}, + {args: "--unknown-flag value workflow diagram", err: "unknown flag"}, // unknown flags before extension fail + + {args: "workflow --tls diagram", want: "temporal-workflow-diagram --tls"}, // boolean flag + {args: "workflow --namespace my-ns diagram", want: "temporal-workflow-diagram --namespace my-ns"}, + {args: "workflow --namespace=my-ns diagram", want: "temporal-workflow-diagram --namespace=my-ns"}, + {args: "workflow -n my-ns diagram", want: "temporal-workflow-diagram -n my-ns"}, // shorthand + {args: "workflow -n=my-ns diagram", want: "temporal-workflow-diagram -n=my-ns"}, + {args: "workflow --unknown-flag diagram", err: "unknown flag"}, // unknown flags before extension fail + {args: "workflow --output invalid diagram", want: "temporal-workflow-diagram --output invalid"}, // invalid value passed through + + {args: "workflow diagram --output json", want: "temporal-workflow-diagram --output json"}, // not temporal-workflow-diagram-json + {args: "workflow diagram --output=json", want: "temporal-workflow-diagram --output=json"}, + {args: "workflow diagram -o json", want: "temporal-workflow-diagram -o json"}, // shorthand + {args: "workflow diagram -o=json", want: "temporal-workflow-diagram -o=json"}, + {args: "workflow diagram -x foo", want: "temporal-workflow-diagram -x foo"}, // not temporal-workflow-diagram-foo + {args: "workflow diagram arg1 -x value arg2", want: "temporal-workflow-diagram arg1 -x value arg2"}, // order preserved + {args: "workflow diagram foo --flag value", want: "temporal-workflow-diagram-foo --flag value"}, // nested commands + {args: "workflow diagram --output invalid", want: "temporal-workflow-diagram --output invalid"}, // invalid value passed through + + // Note: Flag aliases are already implicitly tested via other command-specific tests. + } + + for _, c := range cases { + res := h.Execute(strings.Split(c.args, " ")...) + if c.err != "" { + assert.ErrorContains(t, res.Err, c.err) + } else { + assert.Equal(t, "Args: "+c.want+"\n", res.Stdout.String()) + assert.NoError(t, res.Err) + } + } +} + +func TestExtension_PassesStdin(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo", codeCat) + h.Stdin.WriteString("hello from stdin") + + res := h.Execute("foo") + + assert.Equal(t, "hello from stdin", res.Stdout.String()) +} + +func TestExtension_InheritsEnvironmentVariables(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo", codeEchoEnv("TEST_EXT_VAR")) + os.Setenv("TEST_EXT_VAR", "test_value_123") + t.Cleanup(func() { os.Unsetenv("TEST_EXT_VAR") }) + + res := h.Execute("foo") + + assert.Equal(t, "test_value_123\n", res.Stdout.String()) +} + +func TestExtension_RelaysStderr(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo", codeEchoStderr("stderr output")) + + res := h.Execute("foo") + + assert.Empty(t, res.Stdout.String()) + assert.Equal(t, "stderr output\n", res.Stderr.String()) +} + +func TestExtension_RelaysStdoutAndStderr(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo", ` + fmt.Fprintln(os.Stdout, "stdout line") + fmt.Fprintln(os.Stderr, "stderr line") + `) + + res := h.Execute("foo") + + assert.Equal(t, "stdout line\n", res.Stdout.String()) + assert.Equal(t, "stderr line\n", res.Stderr.String()) +} + +func TestExtension_FailsOnNonExecutableCommand(t *testing.T) { + h := newExtensionHarness(t) + // Create file without execute permission. + path := filepath.Join(h.binDir, "temporal-foo") + err := os.WriteFile(path, []byte("a text file"), 0644) + require.NoError(t, err) + + res := h.Execute("foo") + + assert.Contains(t, res.Stdout.String(), "Usage:") // help text is shown + assert.EqualError(t, res.Err, "unknown command") +} + +func TestExtension_PassesThroughNonZeroExit(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo", codeEchoArgs, codeExit(42)) + + res := h.Execute("foo") + + assert.Equal(t, "Args: temporal-foo \n", res.Stdout.String()) + assert.NoError(t, res.Err) +} + +func TestExtension_FailsOnCommandTimeout(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo", codeSleep(10*time.Second)) + + res := h.Execute("foo", "--command-timeout", "100ms") + assert.EqualError(t, res.Err, "program interrupted") + + res = h.Execute("foo", "--command-timeout", "invalid") + assert.ErrorContains(t, res.Err, "invalid argument \"invalid\"") +} + +func TestExtension_FailsOnCommandCancellation(t *testing.T) { + h := newExtensionHarness(t) + h.createExtension("temporal-foo", codeSleep(10*time.Second)) + go func() { + time.Sleep(100 * time.Millisecond) + h.CancelContext() + }() + + res := h.Execute("foo") + + assert.EqualError(t, res.Err, "program interrupted") +} + +type extensionHarness struct { + *CommandHarness + binDir string +} + +func newExtensionHarness(t *testing.T) *extensionHarness { + t.Helper() + + binDir := t.TempDir() + oldPath := os.Getenv("PATH") + os.Setenv("PATH", binDir+string(os.PathListSeparator)+oldPath) + t.Cleanup(func() { os.Setenv("PATH", oldPath) }) + + return &extensionHarness{ + CommandHarness: NewCommandHarness(t), + binDir: binDir, + } +} + +func (h *extensionHarness) createExtension(name string, code ...string) string { + h.t.Helper() + + // Wrap code in main function. + source := fmt.Sprintf("package main\n\nfunc main() {\n%s\n}\n", strings.Join(code, "\n")) + + // Run goimports to resolve imports. + formatted, err := imports.Process("main.go", []byte(source), nil) + require.NoError(h.t, err, "Failed to process imports for %s:\n%s", name, source) + + // Write source file. + srcPath := filepath.Join(h.binDir, name+".go") + require.NoError(h.t, os.WriteFile(srcPath, formatted, 0644)) + + // Build executable. + binPath := filepath.Join(h.binDir, name) + if runtime.GOOS == "windows" { + binPath += ".exe" + } + cmd := exec.Command("go", "build", "-o", binPath, srcPath) + output, err := cmd.CombinedOutput() + require.NoError(h.t, err, "Failed to compile %s: %s\nSource:\n%s", name, output, formatted) + + return binPath +} diff --git a/temporalcli/commands.gen.go b/internal/temporalcli/commands.gen.go similarity index 92% rename from temporalcli/commands.gen.go rename to internal/temporalcli/commands.gen.go index d91ccf192..cb16aa439 100644 --- a/temporalcli/commands.gen.go +++ b/internal/temporalcli/commands.gen.go @@ -3,6 +3,8 @@ package temporalcli import ( + "fmt" + "github.com/mattn/go-isatty" "github.com/spf13/cobra" @@ -11,6 +13,12 @@ import ( "os" + "regexp" + + "strconv" + + "strings" + "time" ) @@ -176,6 +184,7 @@ type SingleWorkflowOrBatchOptions struct { Reason string Yes bool Rps float32 + Headers []string } func (v *SingleWorkflowOrBatchOptions) buildFlags(cctx *CommandContext, f *pflag.FlagSet) { @@ -185,6 +194,7 @@ func (v *SingleWorkflowOrBatchOptions) buildFlags(cctx *CommandContext, f *pflag f.StringVar(&v.Reason, "reason", "", "Reason for batch operation. Only use with --query. Defaults to user name.") f.BoolVarP(&v.Yes, "yes", "y", false, "Don't prompt to confirm signaling. Only allowed when --query is present.") f.Float32Var(&v.Rps, "rps", 0, "Limit batch's requests per second. Only allowed if query is present.") + f.StringArrayVar(&v.Headers, "headers", nil, "Temporal workflow headers in 'KEY=VALUE' format. Keys must be identifiers, and values must be JSON values. May be passed multiple times to set multiple Temporal headers. Note: These are workflow headers, not gRPC headers.") } type SharedWorkflowStartOptions struct { @@ -195,6 +205,7 @@ type SharedWorkflowStartOptions struct { ExecutionTimeout Duration TaskTimeout Duration SearchAttribute []string + Headers []string Memo []string StaticSummary string StaticDetails string @@ -216,6 +227,7 @@ func (v *SharedWorkflowStartOptions) buildFlags(cctx *CommandContext, f *pflag.F v.TaskTimeout = Duration(10000 * time.Millisecond) f.Var(&v.TaskTimeout, "task-timeout", "Fail a Workflow Task if it lasts longer than `DURATION`. This is the Start-to-close timeout for a Workflow Task.") f.StringArrayVar(&v.SearchAttribute, "search-attribute", nil, "Search Attribute in `KEY=VALUE` format. Keys must be identifiers, and values must be JSON values. For example: 'YourKey={\"your\": \"value\"}'. Can be passed multiple times.") + f.StringArrayVar(&v.Headers, "headers", nil, "Temporal workflow headers in 'KEY=VALUE' format. Keys must be identifiers, and values must be JSON values. May be passed multiple times to set multiple Temporal headers. Note: These are workflow headers, not gRPC headers.") f.StringArrayVar(&v.Memo, "memo", nil, "Memo using 'KEY=\"VALUE\"' pairs. Use JSON values.") f.StringVar(&v.StaticSummary, "static-summary", "", "Static Workflow summary for human consumption in UIs. Uses Temporal Markdown formatting, should be a single line. EXPERIMENTAL.") f.StringVar(&v.StaticDetails, "static-details", "", "Static Workflow details for human consumption in UIs. Uses Temporal Markdown formatting, may be multiple lines. EXPERIMENTAL.") @@ -264,6 +276,7 @@ type UpdateStartingOptions struct { WorkflowId string UpdateId string RunId string + Headers []string } func (v *UpdateStartingOptions) buildFlags(cctx *CommandContext, f *pflag.FlagSet) { @@ -274,6 +287,7 @@ func (v *UpdateStartingOptions) buildFlags(cctx *CommandContext, f *pflag.FlagSe _ = cobra.MarkFlagRequired(f, "workflow-id") f.StringVar(&v.UpdateId, "update-id", "", "Update ID. If unset, defaults to a UUID.") f.StringVarP(&v.RunId, "run-id", "r", "", "Run ID. If unset, looks for an Update against the currently-running Workflow Execution.") + f.StringArrayVar(&v.Headers, "headers", nil, "Temporal workflow headers in 'KEY=VALUE' format. Keys must be identifiers, and values must be JSON values. May be passed multiple times to set multiple Temporal headers. Note: These are workflow headers, not gRPC headers.") } type UpdateTargetingOptions struct { @@ -317,11 +331,13 @@ func (v *NexusEndpointConfigOptions) buildFlags(cctx *CommandContext, f *pflag.F type QueryModifiersOptions struct { RejectCondition StringEnum + Headers []string } func (v *QueryModifiersOptions) buildFlags(cctx *CommandContext, f *pflag.FlagSet) { v.RejectCondition = NewStringEnum([]string{"not_open", "not_completed_cleanly"}, "") f.Var(&v.RejectCondition, "reject-condition", "Optional flag for rejecting Queries based on Workflow state. Accepted values: not_open, not_completed_cleanly.") + f.StringArrayVar(&v.Headers, "headers", nil, "Temporal workflow headers in 'KEY=VALUE' format. Keys must be identifiers, and values must be JSON values. May be passed multiple times to set multiple Temporal headers. Note: These are workflow headers, not gRPC headers.") } type WorkflowUpdateOptionsOptions struct { @@ -379,7 +395,7 @@ func NewTemporalCommand(cctx *CommandContext) *TemporalCommand { s.Command.PersistentFlags().StringVar(&s.Env, "env", "default", "Active environment name (`ENV`).") cctx.BindFlagEnvVar(s.Command.PersistentFlags().Lookup("env"), "TEMPORAL_ENV") s.Command.PersistentFlags().StringVar(&s.EnvFile, "env-file", "", "Path to environment settings file. Defaults to `$HOME/.config/temporalio/temporal.yaml`.") - s.Command.PersistentFlags().StringVar(&s.ConfigFile, "config-file", "", "File path to read TOML config from, defaults to `$CONFIG_PATH/temporal/temporal.toml` where `$CONFIG_PATH` is defined as `$HOME/.config` on Unix, \"$HOME/Library/Application Support\" on macOS, and %AppData% on Windows. EXPERIMENTAL.") + s.Command.PersistentFlags().StringVar(&s.ConfigFile, "config-file", "", "File path to read TOML config from, defaults to `$CONFIG_PATH/temporal/temporal.toml` where `$CONFIG_PATH` is defined as `$HOME/.config` on Unix, `$HOME/Library/Application Support` on macOS, and `%AppData%` on Windows. EXPERIMENTAL.") s.Command.PersistentFlags().StringVar(&s.Profile, "profile", "", "Profile to use for config file. EXPERIMENTAL.") s.Command.PersistentFlags().BoolVar(&s.DisableConfigFile, "disable-config-file", false, "If set, disables loading environment config from config file. EXPERIMENTAL.") s.Command.PersistentFlags().BoolVar(&s.DisableConfigEnv, "disable-config-env", false, "If set, disables loading environment config from environment variables. EXPERIMENTAL.") @@ -819,9 +835,9 @@ func NewTemporalConfigDeleteCommand(cctx *CommandContext, parent *TemporalConfig s.Command.Use = "delete [flags]" s.Command.Short = "Delete a config file property (EXPERIMENTAL)\n" if hasHighlighting { - s.Command.Long = "Remove a property within a profile.\n\n\x1b[1mtemporal env delete \\\n --prop tls.client_cert_path\x1b[0m" + s.Command.Long = "Remove a property within a profile.\n\n\x1b[1mtemporal config delete \\\n --prop tls.client_cert_path\x1b[0m" } else { - s.Command.Long = "Remove a property within a profile.\n\n```\ntemporal env delete \\\n --prop tls.client_cert_path\n```" + s.Command.Long = "Remove a property within a profile.\n\n```\ntemporal config delete \\\n --prop tls.client_cert_path\n```" } s.Command.Args = cobra.NoArgs s.Command.Flags().StringVarP(&s.Prop, "prop", "p", "", "Specific property to delete. If unset, deletes entire profile. Required.") @@ -846,9 +862,9 @@ func NewTemporalConfigDeleteProfileCommand(cctx *CommandContext, parent *Tempora s.Command.Use = "delete-profile [flags]" s.Command.Short = "Delete an entire config profile (EXPERIMENTAL)\n" if hasHighlighting { - s.Command.Long = "Remove a full profile entirely. The \x1b[1m--profile\x1b[0m must be set explicitly.\n\n\x1b[1mtemporal env delete-profile \\\n --profile my-profile\x1b[0m" + s.Command.Long = "Remove a full profile entirely. The \x1b[1m--profile\x1b[0m must be set explicitly.\n\n\x1b[1mtemporal config delete-profile \\\n --profile my-profile\x1b[0m" } else { - s.Command.Long = "Remove a full profile entirely. The `--profile` must be set explicitly.\n\n```\ntemporal env delete-profile \\\n --profile my-profile\n```" + s.Command.Long = "Remove a full profile entirely. The `--profile` must be set explicitly.\n\n```\ntemporal config delete-profile \\\n --profile my-profile\n```" } s.Command.Args = cobra.NoArgs s.Command.Run = func(c *cobra.Command, args []string) { @@ -1334,14 +1350,14 @@ func NewTemporalOperatorNamespaceCreateCommand(cctx *CommandContext, parent *Tem s.Command.Use = "create [flags]" s.Command.Short = "Register a new Namespace" if hasHighlighting { - s.Command.Long = "Create a new Namespace on the Temporal Service:\n\n\x1b[1mtemporal operator namespace create \\\n --namespace YourNewNamespaceName \\\n [options]\x1b[0m`\n\nCreate a Namespace with multi-region data replication:\n\n\x1b[1mtemporal operator namespace create \\\n --global \\\n --namespace YourNewNamespaceName\x1b[0m\n\nConfigure settings like retention and Visibility Archival State as needed.\nFor example, the Visibility Archive can be set on a separate URI:\n\n\x1b[1mtemporal operator namespace create \\\n --retention 5d \\\n --visibility-archival-state enabled \\\n --visibility-uri YourURI \\\n --namespace YourNewNamespaceName\x1b[0m\n\nNote: URI values for archival states can't be changed once enabled." + s.Command.Long = "Create a new Namespace on the Temporal Service:\n\n\x1b[1mtemporal operator namespace create \\\n --namespace YourNewNamespaceName \\\n [options]\x1b[0m\n\nCreate a Namespace with multi-region data replication:\n\n\x1b[1mtemporal operator namespace create \\\n --global \\\n --namespace YourNewNamespaceName\x1b[0m\n\nConfigure settings like retention and Visibility Archival State as needed.\nFor example, the Visibility Archive can be set on a separate URI:\n\n\x1b[1mtemporal operator namespace create \\\n --retention 5d \\\n --visibility-archival-state enabled \\\n --visibility-uri YourURI \\\n --namespace YourNewNamespaceName\x1b[0m\n\nNote: URI values for archival states can't be changed once enabled." } else { - s.Command.Long = "Create a new Namespace on the Temporal Service:\n\n```\ntemporal operator namespace create \\\n --namespace YourNewNamespaceName \\\n [options]\n````\n\nCreate a Namespace with multi-region data replication:\n\n```\ntemporal operator namespace create \\\n --global \\\n --namespace YourNewNamespaceName\n```\n\nConfigure settings like retention and Visibility Archival State as needed.\nFor example, the Visibility Archive can be set on a separate URI:\n\n```\ntemporal operator namespace create \\\n --retention 5d \\\n --visibility-archival-state enabled \\\n --visibility-uri YourURI \\\n --namespace YourNewNamespaceName\n```\n\nNote: URI values for archival states can't be changed once enabled." + s.Command.Long = "Create a new Namespace on the Temporal Service:\n\n```\ntemporal operator namespace create \\\n --namespace YourNewNamespaceName \\\n [options]\n```\n\nCreate a Namespace with multi-region data replication:\n\n```\ntemporal operator namespace create \\\n --global \\\n --namespace YourNewNamespaceName\n```\n\nConfigure settings like retention and Visibility Archival State as needed.\nFor example, the Visibility Archive can be set on a separate URI:\n\n```\ntemporal operator namespace create \\\n --retention 5d \\\n --visibility-archival-state enabled \\\n --visibility-uri YourURI \\\n --namespace YourNewNamespaceName\n```\n\nNote: URI values for archival states can't be changed once enabled." } s.Command.Args = cobra.MaximumNArgs(1) s.Command.Flags().StringVar(&s.ActiveCluster, "active-cluster", "", "Active Cluster (Service) name.") s.Command.Flags().StringArrayVar(&s.Cluster, "cluster", nil, "Cluster (Service) names for Namespace creation. Can be passed multiple times.") - s.Command.Flags().StringArrayVar(&s.Data, "data", nil, "Namespace data as `KEY=VALUE` pairs. Keys must be identifiers, and values must be JSON values. For example: 'YourKey={\"your\": \"value\"}'. Can be passed multiple times.") + s.Command.Flags().StringArrayVar(&s.Data, "data", nil, "Namespace data as `KEY=VALUE` pairs. Keys must be identifiers, and values must be JSON values. For example: `YourKey={\"your\": \"value\"}` Can be passed multiple times.") s.Command.Flags().StringVar(&s.Description, "description", "", "Namespace description.") s.Command.Flags().StringVar(&s.Email, "email", "", "Owner email.") s.Command.Flags().BoolVar(&s.Global, "global", false, "Enable multi-region data replication.") @@ -1471,7 +1487,7 @@ func NewTemporalOperatorNamespaceUpdateCommand(cctx *CommandContext, parent *Tem s.Command.Args = cobra.MaximumNArgs(1) s.Command.Flags().StringVar(&s.ActiveCluster, "active-cluster", "", "Active Cluster (Service) name.") s.Command.Flags().StringArrayVar(&s.Cluster, "cluster", nil, "Cluster (Service) names.") - s.Command.Flags().StringArrayVar(&s.Data, "data", nil, "Namespace data as `KEY=VALUE` pairs. Keys must be identifiers, and values must be JSON values. For example: 'YourKey={\"your\": \"value\"}'. Can be passed multiple times.") + s.Command.Flags().StringArrayVar(&s.Data, "data", nil, "Namespace data as `KEY=VALUE` pairs. Keys must be identifiers, and values must be JSON values. For example: `YourKey={\"your\": \"value\"}` Can be passed multiple times.") s.Command.Flags().StringVar(&s.Description, "description", "", "Namespace description.") s.Command.Flags().StringVar(&s.Email, "email", "", "Owner email.") s.Command.Flags().BoolVar(&s.PromoteGlobal, "promote-global", false, "Enable multi-region data replication.") @@ -2138,7 +2154,7 @@ func NewTemporalServerStartDevCommand(cctx *CommandContext, parent *TemporalServ s.Command.Flags().StringVar(&s.UiAssetPath, "ui-asset-path", "", "UI custom assets path.") s.Command.Flags().StringVar(&s.UiCodecEndpoint, "ui-codec-endpoint", "", "UI remote codec HTTP endpoint.") s.Command.Flags().StringArrayVar(&s.SqlitePragma, "sqlite-pragma", nil, "SQLite pragma statements in \"PRAGMA=VALUE\" format.") - s.Command.Flags().StringArrayVar(&s.DynamicConfigValue, "dynamic-config-value", nil, "Dynamic configuration value using `KEY=VALUE` pairs. Keys must be identifiers, and values must be JSON values. For example: 'YourKey=\"YourString\"'. Can be passed multiple times.") + s.Command.Flags().StringArrayVar(&s.DynamicConfigValue, "dynamic-config-value", nil, "Dynamic configuration value using `KEY=VALUE` pairs. Keys must be identifiers, and values must be JSON values. For example: `YourKey=\"YourString\"` Can be passed multiple times.") s.Command.Flags().BoolVar(&s.LogConfig, "log-config", false, "Log the server config to stderr.") s.Command.Flags().StringArrayVar(&s.SearchAttribute, "search-attribute", nil, "Search attributes to register using `KEY=VALUE` pairs. Keys must be identifiers, and values must be the search attribute type, which is one of the following: Text, Keyword, Int, Double, Bool, Datetime, KeywordList.") s.Command.Run = func(c *cobra.Command, args []string) { @@ -2295,9 +2311,9 @@ func NewTemporalTaskQueueDescribeCommand(cctx *CommandContext, parent *TemporalT s.Command.Use = "describe [flags]" s.Command.Short = "Show active Workers" if hasHighlighting { - s.Command.Long = "Display a list of active Workers that have recently polled a Task Queue. The\nTemporal Server records each poll request time. A \x1b[1mLastAccessTime\x1b[0m over one\nminute may indicate the Worker is at capacity or has shut down. Temporal\nWorkers are removed if 5 minutes have passed since the last poll request.\n\n\x1b[1mtemporal task-queue describe \\\n --task-queue YourTaskQueue\x1b[0m\n\nThis command provides poller information for a given Task Queue.\nWorkflow and Activity polling use separate Task Queues:\n\n\x1b[1mtemporal task-queue describe \\\n --task-queue YourTaskQueue \\\n --task-queue-type \"activity\"\x1b[0m\n\nThis command provides the following task queue statistics:\n- \x1b[1mApproximateBacklogCount\x1b[0m: The approximate number of tasks backlogged in this\n task queue. May count expired tasks but eventually converges to the right\n value.\n- \x1b[1mApproximateBacklogAge\x1b[0m: Approximate age of the oldest task in the backlog,\n based on its creation time, measured in seconds.\n- \x1b[1mTasksAddRate\x1b[0m: Approximate rate at which tasks are being added to the task\n queue, measured in tasks per second, averaged over the last 30 seconds.\n Includes tasks dispatched immediately without going to the backlog\n (sync-matched tasks), as well as tasks added to the backlog. (See note below.)\n- \x1b[1mTasksDispatchRate\x1b[0m: Approximate rate at which tasks are being dispatched from\n the task queue, measured in tasks per second, averaged over the last 30\n seconds. Includes tasks dispatched immediately without going to the backlog\n (sync-matched tasks), as well as tasks added to the backlog. (See note below.)\n- \x1b[1mBacklogIncreaseRate\x1b[0m: Approximate rate at which the backlog size is\n increasing (if positive) or decreasing (if negative), measured in tasks per\n second, averaged over the last 30 seconds. This is roughly equivalent to:\n \x1b[1mTasksAddRate\x1b[0m - \x1b[1mTasksDispatchRate\x1b[0m.\n\nNOTE: The \x1b[1mTasksAddRate\x1b[0m and \x1b[1mTasksDispatchRate\x1b[0m metrics may differ from the\nactual rate of add/dispatch, because tasks may be dispatched eagerly to an\navailable worker, or may apply only to specific workers (they are \"sticky\").\nSuch tasks are not counted by these metrics. Despite the inaccuracy of\nthese two metrics, the derived metric of \x1b[1mBacklogIncreaseRate\x1b[0m is accurate\nfor backlogs older than a few seconds.\n\nSafely retire Workers assigned a Build ID by checking reachability across\nall task types. Use the flag \x1b[1m--report-reachability\x1b[0m:\n\n\x1b[1mtemporal task-queue describe \\\n --task-queue YourTaskQueue \\\n --build-id \"YourBuildId\" \\\n --report-reachability\x1b[0m\n\nTask reachability information is returned for the requested versions and all\ntask types, which can be used to safely retire Workers with old code versions,\nprovided that they were assigned a Build ID.\n\nNote that task reachability status is experimental and may significantly change\nor be removed in a future release. Also, determining task reachability incurs a\nnon-trivial computing cost.\n\nTask reachability states are reported per build ID. The state may be one of the\nfollowing:\n\n- \x1b[1mReachable\x1b[0m: using the current versioning rules, the Build ID may be used\n by new Workflow Executions or Activities OR there are currently open\n Workflow or backlogged Activity tasks assigned to the queue.\n- \x1b[1mClosedWorkflowsOnly\x1b[0m: the Build ID does not have open Workflow Executions\n and can't be reached by new Workflow Executions. It MAY have closed\n Workflow Executions within the Namespace retention period.\n- \x1b[1mUnreachable\x1b[0m: this Build ID is not used for new Workflow Executions and\n isn't used by any existing Workflow Execution within the retention period.\n\nTask reachability is eventually consistent. You may experience a delay until\nreachability converges to the most accurate value. This is designed to act\nin the most conservative way until convergence. For example, \x1b[1mReachable\x1b[0m is\nmore conservative than \x1b[1mClosedWorkflowsOnly\x1b[0m." + s.Command.Long = "Display a list of active Workers that have recently polled a Task Queue. The\nTemporal Server records each poll request time. A \x1b[1mLastAccessTime\x1b[0m over one\nminute may indicate the Worker is at capacity or has shut down. Temporal\nWorkers are removed if 5 minutes have passed since the last poll request.\n\n\x1b[1mtemporal task-queue describe \\\n --task-queue YourTaskQueue\x1b[0m\n\nThis command provides poller information for a given Task Queue.\nWorkflow and Activity polling use separate Task Queues:\n\n\x1b[1mtemporal task-queue describe \\\n --task-queue YourTaskQueue \\\n --task-queue-type \"activity\"\x1b[0m\n\nThis command provides the following task queue statistics:\n- \x1b[1mApproximateBacklogCount\x1b[0m: The approximate number of tasks backlogged in this\n task queue. May count expired tasks but eventually converges to the right\n value.\n- \x1b[1mApproximateBacklogAge\x1b[0m: Approximate age of the oldest task in the backlog,\n based on its creation time, measured in seconds.\n- \x1b[1mTasksAddRate\x1b[0m: Approximate rate at which tasks are being added to the task\n queue, measured in tasks per second, averaged over the last 30 seconds.\n Includes tasks dispatched immediately without going to the backlog\n (sync-matched tasks), as well as tasks added to the backlog. (See note below.)\n- \x1b[1mTasksDispatchRate\x1b[0m: Approximate rate at which tasks are being dispatched from\n the task queue, measured in tasks per second, averaged over the last 30\n seconds. Includes tasks dispatched immediately without going to the backlog\n (sync-matched tasks), as well as tasks added to the backlog. (See note below.)\n- \x1b[1mBacklogIncreaseRate\x1b[0m: Approximate rate at which the backlog size is\n increasing (if positive) or decreasing (if negative), measured in tasks per\n second, averaged over the last 30 seconds. This is roughly equivalent to:\n \x1b[1mTasksAddRate\x1b[0m - \x1b[1mTasksDispatchRate\x1b[0m.\n\nNOTE: The \x1b[1mTasksAddRate\x1b[0m and \x1b[1mTasksDispatchRate\x1b[0m metrics may differ from the\nactual rate of add/dispatch, because tasks may be dispatched eagerly to an\navailable worker, or may apply only to specific workers (they are \"sticky\").\nSuch tasks are not counted by these metrics. Despite the inaccuracy of\nthese two metrics, the derived metric of \x1b[1mBacklogIncreaseRate\x1b[0m is accurate\nfor backlogs older than a few seconds.\n\nSafely retire Workers assigned a Build ID by checking reachability across\nall task types. Use the flag \x1b[1m--report-reachability\x1b[0m:\n\n\x1b[1mtemporal task-queue describe \\\n --task-queue YourTaskQueue \\\n --select-build-id \"YourBuildId\" \\\n --report-reachability\x1b[0m\n\nTask reachability information is returned for the requested versions and all\ntask types, which can be used to safely retire Workers with old code versions,\nprovided that they were assigned a Build ID.\n\nNote that task reachability status is experimental and may significantly change\nor be removed in a future release. Also, determining task reachability incurs a\nnon-trivial computing cost.\n\nTask reachability states are reported per build ID. The state may be one of the\nfollowing:\n\n- \x1b[1mReachable\x1b[0m: using the current versioning rules, the Build ID may be used\n by new Workflow Executions or Activities OR there are currently open\n Workflow or backlogged Activity tasks assigned to the queue.\n- \x1b[1mClosedWorkflowsOnly\x1b[0m: the Build ID does not have open Workflow Executions\n and can't be reached by new Workflow Executions. It MAY have closed\n Workflow Executions within the Namespace retention period.\n- \x1b[1mUnreachable\x1b[0m: this Build ID is not used for new Workflow Executions and\n isn't used by any existing Workflow Execution within the retention period.\n\nTask reachability is eventually consistent. You may experience a delay until\nreachability converges to the most accurate value. This is designed to act\nin the most conservative way until convergence. For example, \x1b[1mReachable\x1b[0m is\nmore conservative than \x1b[1mClosedWorkflowsOnly\x1b[0m." } else { - s.Command.Long = "Display a list of active Workers that have recently polled a Task Queue. The\nTemporal Server records each poll request time. A `LastAccessTime` over one\nminute may indicate the Worker is at capacity or has shut down. Temporal\nWorkers are removed if 5 minutes have passed since the last poll request.\n\n```\ntemporal task-queue describe \\\n --task-queue YourTaskQueue\n```\n\nThis command provides poller information for a given Task Queue.\nWorkflow and Activity polling use separate Task Queues:\n\n```\ntemporal task-queue describe \\\n --task-queue YourTaskQueue \\\n --task-queue-type \"activity\"\n```\n\nThis command provides the following task queue statistics:\n- `ApproximateBacklogCount`: The approximate number of tasks backlogged in this\n task queue. May count expired tasks but eventually converges to the right\n value.\n- `ApproximateBacklogAge`: Approximate age of the oldest task in the backlog,\n based on its creation time, measured in seconds.\n- `TasksAddRate`: Approximate rate at which tasks are being added to the task\n queue, measured in tasks per second, averaged over the last 30 seconds.\n Includes tasks dispatched immediately without going to the backlog\n (sync-matched tasks), as well as tasks added to the backlog. (See note below.)\n- `TasksDispatchRate`: Approximate rate at which tasks are being dispatched from\n the task queue, measured in tasks per second, averaged over the last 30\n seconds. Includes tasks dispatched immediately without going to the backlog\n (sync-matched tasks), as well as tasks added to the backlog. (See note below.)\n- `BacklogIncreaseRate`: Approximate rate at which the backlog size is\n increasing (if positive) or decreasing (if negative), measured in tasks per\n second, averaged over the last 30 seconds. This is roughly equivalent to:\n `TasksAddRate` - `TasksDispatchRate`.\n\nNOTE: The `TasksAddRate` and `TasksDispatchRate` metrics may differ from the\nactual rate of add/dispatch, because tasks may be dispatched eagerly to an\navailable worker, or may apply only to specific workers (they are \"sticky\").\nSuch tasks are not counted by these metrics. Despite the inaccuracy of\nthese two metrics, the derived metric of `BacklogIncreaseRate` is accurate\nfor backlogs older than a few seconds.\n\nSafely retire Workers assigned a Build ID by checking reachability across\nall task types. Use the flag `--report-reachability`:\n\n```\ntemporal task-queue describe \\\n --task-queue YourTaskQueue \\\n --build-id \"YourBuildId\" \\\n --report-reachability\n```\n\nTask reachability information is returned for the requested versions and all\ntask types, which can be used to safely retire Workers with old code versions,\nprovided that they were assigned a Build ID.\n\nNote that task reachability status is experimental and may significantly change\nor be removed in a future release. Also, determining task reachability incurs a\nnon-trivial computing cost.\n\nTask reachability states are reported per build ID. The state may be one of the\nfollowing:\n\n- `Reachable`: using the current versioning rules, the Build ID may be used\n by new Workflow Executions or Activities OR there are currently open\n Workflow or backlogged Activity tasks assigned to the queue.\n- `ClosedWorkflowsOnly`: the Build ID does not have open Workflow Executions\n and can't be reached by new Workflow Executions. It MAY have closed\n Workflow Executions within the Namespace retention period.\n- `Unreachable`: this Build ID is not used for new Workflow Executions and\n isn't used by any existing Workflow Execution within the retention period.\n\nTask reachability is eventually consistent. You may experience a delay until\nreachability converges to the most accurate value. This is designed to act\nin the most conservative way until convergence. For example, `Reachable` is\nmore conservative than `ClosedWorkflowsOnly`." + s.Command.Long = "Display a list of active Workers that have recently polled a Task Queue. The\nTemporal Server records each poll request time. A `LastAccessTime` over one\nminute may indicate the Worker is at capacity or has shut down. Temporal\nWorkers are removed if 5 minutes have passed since the last poll request.\n\n```\ntemporal task-queue describe \\\n --task-queue YourTaskQueue\n```\n\nThis command provides poller information for a given Task Queue.\nWorkflow and Activity polling use separate Task Queues:\n\n```\ntemporal task-queue describe \\\n --task-queue YourTaskQueue \\\n --task-queue-type \"activity\"\n```\n\nThis command provides the following task queue statistics:\n- `ApproximateBacklogCount`: The approximate number of tasks backlogged in this\n task queue. May count expired tasks but eventually converges to the right\n value.\n- `ApproximateBacklogAge`: Approximate age of the oldest task in the backlog,\n based on its creation time, measured in seconds.\n- `TasksAddRate`: Approximate rate at which tasks are being added to the task\n queue, measured in tasks per second, averaged over the last 30 seconds.\n Includes tasks dispatched immediately without going to the backlog\n (sync-matched tasks), as well as tasks added to the backlog. (See note below.)\n- `TasksDispatchRate`: Approximate rate at which tasks are being dispatched from\n the task queue, measured in tasks per second, averaged over the last 30\n seconds. Includes tasks dispatched immediately without going to the backlog\n (sync-matched tasks), as well as tasks added to the backlog. (See note below.)\n- `BacklogIncreaseRate`: Approximate rate at which the backlog size is\n increasing (if positive) or decreasing (if negative), measured in tasks per\n second, averaged over the last 30 seconds. This is roughly equivalent to:\n `TasksAddRate` - `TasksDispatchRate`.\n\nNOTE: The `TasksAddRate` and `TasksDispatchRate` metrics may differ from the\nactual rate of add/dispatch, because tasks may be dispatched eagerly to an\navailable worker, or may apply only to specific workers (they are \"sticky\").\nSuch tasks are not counted by these metrics. Despite the inaccuracy of\nthese two metrics, the derived metric of `BacklogIncreaseRate` is accurate\nfor backlogs older than a few seconds.\n\nSafely retire Workers assigned a Build ID by checking reachability across\nall task types. Use the flag `--report-reachability`:\n\n```\ntemporal task-queue describe \\\n --task-queue YourTaskQueue \\\n --select-build-id \"YourBuildId\" \\\n --report-reachability\n```\n\nTask reachability information is returned for the requested versions and all\ntask types, which can be used to safely retire Workers with old code versions,\nprovided that they were assigned a Build ID.\n\nNote that task reachability status is experimental and may significantly change\nor be removed in a future release. Also, determining task reachability incurs a\nnon-trivial computing cost.\n\nTask reachability states are reported per build ID. The state may be one of the\nfollowing:\n\n- `Reachable`: using the current versioning rules, the Build ID may be used\n by new Workflow Executions or Activities OR there are currently open\n Workflow or backlogged Activity tasks assigned to the queue.\n- `ClosedWorkflowsOnly`: the Build ID does not have open Workflow Executions\n and can't be reached by new Workflow Executions. It MAY have closed\n Workflow Executions within the Namespace retention period.\n- `Unreachable`: this Build ID is not used for new Workflow Executions and\n isn't used by any existing Workflow Execution within the retention period.\n\nTask reachability is eventually consistent. You may experience a delay until\nreachability converges to the most accurate value. This is designed to act\nin the most conservative way until convergence. For example, `Reachable` is\nmore conservative than `ClosedWorkflowsOnly`." } s.Command.Args = cobra.NoArgs s.Command.Flags().StringVarP(&s.TaskQueue, "task-queue", "t", "", "Task Queue name. Required.") @@ -2868,6 +2884,8 @@ func NewTemporalWorkerCommand(cctx *CommandContext, parent *TemporalCommand) *Te } s.Command.Args = cobra.NoArgs s.Command.AddCommand(&NewTemporalWorkerDeploymentCommand(cctx, &s).Command) + s.Command.AddCommand(&NewTemporalWorkerDescribeCommand(cctx, &s).Command) + s.Command.AddCommand(&NewTemporalWorkerListCommand(cctx, &s).Command) s.ClientOptions.buildFlags(cctx, s.Command.PersistentFlags()) return &s } @@ -2893,6 +2911,7 @@ func NewTemporalWorkerDeploymentCommand(cctx *CommandContext, parent *TemporalWo s.Command.AddCommand(&NewTemporalWorkerDeploymentDescribeCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalWorkerDeploymentDescribeVersionCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalWorkerDeploymentListCommand(cctx, &s).Command) + s.Command.AddCommand(&NewTemporalWorkerDeploymentManagerIdentityCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalWorkerDeploymentSetCurrentVersionCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalWorkerDeploymentSetRampingVersionCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalWorkerDeploymentUpdateMetadataVersionCommand(cctx, &s).Command) @@ -3034,11 +3053,95 @@ func NewTemporalWorkerDeploymentListCommand(cctx *CommandContext, parent *Tempor return &s } +type TemporalWorkerDeploymentManagerIdentityCommand struct { + Parent *TemporalWorkerDeploymentCommand + Command cobra.Command +} + +func NewTemporalWorkerDeploymentManagerIdentityCommand(cctx *CommandContext, parent *TemporalWorkerDeploymentCommand) *TemporalWorkerDeploymentManagerIdentityCommand { + var s TemporalWorkerDeploymentManagerIdentityCommand + s.Parent = parent + s.Command.Use = "manager-identity" + s.Command.Short = "Manager Identity commands change the `ManagerIdentity` of a Worker Deployment" + if hasHighlighting { + s.Command.Long = "\x1b[1m+---------------------------------------------------------------------+\n| CAUTION: Worker Deployment is experimental. Deployment commands are |\n| subject to change. |\n+---------------------------------------------------------------------+\x1b[0m\n\nManager Identity commands change the \x1b[1mManagerIdentity\x1b[0m of a Worker Deployment:\n\n\x1b[1mtemporal worker deployment manager-identity [command] [options]\x1b[0m\n\nWhen present, \x1b[1mManagerIdentity\x1b[0m is the identity of the user that has the \nexclusive right to make changes to this Worker Deployment. Empty by default.\nWhen set, users whose identity does not match the \x1b[1mManagerIdentity\x1b[0m will not\nbe able to change the Worker Deployment.\n\nThis is especially useful in environments where multiple users (such as CLI\nusers and automated controllers) may interact with the same Worker Deployment.\n\x1b[1mManagerIdentity\x1b[0m allows different users to communicate with one another about\nwho is expected to make changes to the Worker Deployment.\n\nThe current Manager Identity is returned with \x1b[1mdescribe\x1b[0m:\n\x1b[1m temporal worker deployment describe \\\n --deployment-name YourDeploymentName\x1b[0m" + } else { + s.Command.Long = "```\n+---------------------------------------------------------------------+\n| CAUTION: Worker Deployment is experimental. Deployment commands are |\n| subject to change. |\n+---------------------------------------------------------------------+\n```\n\nManager Identity commands change the `ManagerIdentity` of a Worker Deployment:\n\n```\ntemporal worker deployment manager-identity [command] [options]\n```\n\nWhen present, `ManagerIdentity` is the identity of the user that has the \nexclusive right to make changes to this Worker Deployment. Empty by default.\nWhen set, users whose identity does not match the `ManagerIdentity` will not\nbe able to change the Worker Deployment.\n\nThis is especially useful in environments where multiple users (such as CLI\nusers and automated controllers) may interact with the same Worker Deployment.\n`ManagerIdentity` allows different users to communicate with one another about\nwho is expected to make changes to the Worker Deployment.\n\nThe current Manager Identity is returned with `describe`:\n```\n temporal worker deployment describe \\\n --deployment-name YourDeploymentName\n```" + } + s.Command.Args = cobra.NoArgs + s.Command.AddCommand(&NewTemporalWorkerDeploymentManagerIdentitySetCommand(cctx, &s).Command) + s.Command.AddCommand(&NewTemporalWorkerDeploymentManagerIdentityUnsetCommand(cctx, &s).Command) + return &s +} + +type TemporalWorkerDeploymentManagerIdentitySetCommand struct { + Parent *TemporalWorkerDeploymentManagerIdentityCommand + Command cobra.Command + ManagerIdentity string + Self bool + DeploymentName string + Yes bool +} + +func NewTemporalWorkerDeploymentManagerIdentitySetCommand(cctx *CommandContext, parent *TemporalWorkerDeploymentManagerIdentityCommand) *TemporalWorkerDeploymentManagerIdentitySetCommand { + var s TemporalWorkerDeploymentManagerIdentitySetCommand + s.Parent = parent + s.Command.DisableFlagsInUseLine = true + s.Command.Use = "set [flags]" + s.Command.Short = "Set the Manager Identity of a Worker Deployment" + if hasHighlighting { + s.Command.Long = "\x1b[1m+---------------------------------------------------------------------+\n| CAUTION: Worker Deployment is experimental. Deployment commands are |\n| subject to change. |\n+---------------------------------------------------------------------+\x1b[0m\n\nSet the \x1b[1mManagerIdentity\x1b[0m of a Worker Deployment given its Deployment Name.\n\nWhen present, \x1b[1mManagerIdentity\x1b[0m is the identity of the user that has the \nexclusive right to make changes to this Worker Deployment. Empty by default.\nWhen set, users whose identity does not match the \x1b[1mManagerIdentity\x1b[0m will not\nbe able to change the Worker Deployment.\n\nThis is especially useful in environments where multiple users (such as CLI\nusers and automated controllers) may interact with the same Worker Deployment.\n\x1b[1mManagerIdentity\x1b[0m allows different users to communicate with one another about\nwho is expected to make changes to the Worker Deployment.\n\n\x1b[1mtemporal worker deployment manager-identity set [options]\x1b[0m\n\nFor example:\n\n\x1b[1mtemporal worker deployment manager-identity set \\\n --deployment-name DeploymentName \\\n --self \\\n --identity YourUserIdentity # optional, populated by CLI if not provided\x1b[0m\n\nSets the Manager Identity of the Deployment to the identity of the user making \nthis request. If you don't specifically pass an identity field, the CLI will \ngenerate your identity for you.\n\nFor example:\n\x1b[1mtemporal worker deployment manager-identity set \\\n --deployment-name DeploymentName \\\n --manager-identity NewManagerIdentity\x1b[0m\n\nSets the Manager Identity of the Deployment to any string." + } else { + s.Command.Long = "```\n+---------------------------------------------------------------------+\n| CAUTION: Worker Deployment is experimental. Deployment commands are |\n| subject to change. |\n+---------------------------------------------------------------------+\n```\n\nSet the `ManagerIdentity` of a Worker Deployment given its Deployment Name.\n\nWhen present, `ManagerIdentity` is the identity of the user that has the \nexclusive right to make changes to this Worker Deployment. Empty by default.\nWhen set, users whose identity does not match the `ManagerIdentity` will not\nbe able to change the Worker Deployment.\n\nThis is especially useful in environments where multiple users (such as CLI\nusers and automated controllers) may interact with the same Worker Deployment.\n`ManagerIdentity` allows different users to communicate with one another about\nwho is expected to make changes to the Worker Deployment.\n\n```\ntemporal worker deployment manager-identity set [options]\n```\n\nFor example:\n\n```\ntemporal worker deployment manager-identity set \\\n --deployment-name DeploymentName \\\n --self \\\n --identity YourUserIdentity # optional, populated by CLI if not provided\n```\n\nSets the Manager Identity of the Deployment to the identity of the user making \nthis request. If you don't specifically pass an identity field, the CLI will \ngenerate your identity for you.\n\nFor example:\n```\ntemporal worker deployment manager-identity set \\\n --deployment-name DeploymentName \\\n --manager-identity NewManagerIdentity\n```\n\nSets the Manager Identity of the Deployment to any string." + } + s.Command.Args = cobra.NoArgs + s.Command.Flags().StringVar(&s.ManagerIdentity, "manager-identity", "", "New Manager Identity. Required unless --self is specified.") + s.Command.Flags().BoolVar(&s.Self, "self", false, "Set Manager Identity to the identity of the user submitting this request. Required unless --manager-identity is specified.") + s.Command.Flags().StringVar(&s.DeploymentName, "deployment-name", "", "Name for a Worker Deployment. Required.") + s.Command.Flags().BoolVarP(&s.Yes, "yes", "y", false, "Don't prompt to confirm set Manager Identity.") + s.Command.Run = func(c *cobra.Command, args []string) { + if err := s.run(cctx, args); err != nil { + cctx.Options.Fail(err) + } + } + return &s +} + +type TemporalWorkerDeploymentManagerIdentityUnsetCommand struct { + Parent *TemporalWorkerDeploymentManagerIdentityCommand + Command cobra.Command + DeploymentName string + Yes bool +} + +func NewTemporalWorkerDeploymentManagerIdentityUnsetCommand(cctx *CommandContext, parent *TemporalWorkerDeploymentManagerIdentityCommand) *TemporalWorkerDeploymentManagerIdentityUnsetCommand { + var s TemporalWorkerDeploymentManagerIdentityUnsetCommand + s.Parent = parent + s.Command.DisableFlagsInUseLine = true + s.Command.Use = "unset [flags]" + s.Command.Short = "Unset the Manager Identity of a Worker Deployment" + if hasHighlighting { + s.Command.Long = "\x1b[1m+---------------------------------------------------------------------+\n| CAUTION: Worker Deployment is experimental. Deployment commands are |\n| subject to change. |\n+---------------------------------------------------------------------+\x1b[0m\n\nUnset the \x1b[1mManagerIdentity\x1b[0m of a Worker Deployment given its Deployment Name.\n\nWhen present, \x1b[1mManagerIdentity\x1b[0m is the identity of the user that has the \nexclusive right to make changes to this Worker Deployment. Empty by default.\nWhen set, users whose identity does not match the \x1b[1mManagerIdentity\x1b[0m will not\nbe able to change the Worker Deployment.\n\nThis is especially useful in environments where multiple users (such as CLI\nusers and automated controllers) may interact with the same Worker Deployment.\n\x1b[1mManagerIdentity\x1b[0m allows different users to communicate with one another about\nwho is expected to make changes to the Worker Deployment.\n\n\x1b[1mtemporal worker deployment manager-identity unset [options]\x1b[0m\n\nFor example:\n\n\x1b[1mtemporal worker deployment manager-identity unset \\\n --deployment-name YourDeploymentName\x1b[0m\n\nClears the Manager Identity field for a given Deployment." + } else { + s.Command.Long = "```\n+---------------------------------------------------------------------+\n| CAUTION: Worker Deployment is experimental. Deployment commands are |\n| subject to change. |\n+---------------------------------------------------------------------+\n```\n\nUnset the `ManagerIdentity` of a Worker Deployment given its Deployment Name.\n\nWhen present, `ManagerIdentity` is the identity of the user that has the \nexclusive right to make changes to this Worker Deployment. Empty by default.\nWhen set, users whose identity does not match the `ManagerIdentity` will not\nbe able to change the Worker Deployment.\n\nThis is especially useful in environments where multiple users (such as CLI\nusers and automated controllers) may interact with the same Worker Deployment.\n`ManagerIdentity` allows different users to communicate with one another about\nwho is expected to make changes to the Worker Deployment.\n\n```\ntemporal worker deployment manager-identity unset [options]\n```\n\nFor example:\n\n```\ntemporal worker deployment manager-identity unset \\\n --deployment-name YourDeploymentName\n```\n\nClears the Manager Identity field for a given Deployment." + } + s.Command.Args = cobra.NoArgs + s.Command.Flags().StringVar(&s.DeploymentName, "deployment-name", "", "Name for a Worker Deployment. Required.") + s.Command.Flags().BoolVarP(&s.Yes, "yes", "y", false, "Don't prompt to confirm unset Manager Identity.") + s.Command.Run = func(c *cobra.Command, args []string) { + if err := s.run(cctx, args); err != nil { + cctx.Options.Fail(err) + } + } + return &s +} + type TemporalWorkerDeploymentSetCurrentVersionCommand struct { Parent *TemporalWorkerDeploymentCommand Command cobra.Command DeploymentVersionOrUnversionedOptions IgnoreMissingTaskQueues bool + AllowNoPollers bool Yes bool } @@ -3055,6 +3158,7 @@ func NewTemporalWorkerDeploymentSetCurrentVersionCommand(cctx *CommandContext, p } s.Command.Args = cobra.NoArgs s.Command.Flags().BoolVar(&s.IgnoreMissingTaskQueues, "ignore-missing-task-queues", false, "Override protection to accidentally remove task queues.") + s.Command.Flags().BoolVar(&s.AllowNoPollers, "allow-no-pollers", false, "Override protection and set version as current even if it has no pollers.") s.Command.Flags().BoolVarP(&s.Yes, "yes", "y", false, "Don't prompt to confirm set Current Version.") s.DeploymentVersionOrUnversionedOptions.buildFlags(cctx, s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { @@ -3072,6 +3176,7 @@ type TemporalWorkerDeploymentSetRampingVersionCommand struct { Percentage float32 Delete bool IgnoreMissingTaskQueues bool + AllowNoPollers bool Yes bool } @@ -3090,6 +3195,7 @@ func NewTemporalWorkerDeploymentSetRampingVersionCommand(cctx *CommandContext, p s.Command.Flags().Float32Var(&s.Percentage, "percentage", 0, "Percentage of tasks redirected to the Ramping Version. Valid range [0,100].") s.Command.Flags().BoolVar(&s.Delete, "delete", false, "Delete the Ramping Version.") s.Command.Flags().BoolVar(&s.IgnoreMissingTaskQueues, "ignore-missing-task-queues", false, "Override protection to accidentally remove task queues.") + s.Command.Flags().BoolVar(&s.AllowNoPollers, "allow-no-pollers", false, "Override protection and set version as ramping even if it has no pollers.") s.Command.Flags().BoolVarP(&s.Yes, "yes", "y", false, "Don't prompt to confirm set Ramping Version.") s.DeploymentVersionOrUnversionedOptions.buildFlags(cctx, s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { @@ -3120,7 +3226,7 @@ func NewTemporalWorkerDeploymentUpdateMetadataVersionCommand(cctx *CommandContex s.Command.Long = "```\n+---------------------------------------------------------------------+\n| CAUTION: Worker Deployment is experimental. Deployment commands are |\n| subject to change. |\n+---------------------------------------------------------------------+\n```\n\nUpdate metadata associated with a Worker Deployment Version.\n\nFor example:\n\n```\n temporal worker deployment update-metadata-version \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n --metadata bar=1 \\\n --metadata foo=true\n```\n\nThe current metadata is also returned with `describe-version`:\n```\n temporal worker deployment describe-version \\\n --deployment-name YourDeploymentName --build-id YourBuildID \\\n```" } s.Command.Args = cobra.NoArgs - s.Command.Flags().StringArrayVar(&s.Metadata, "metadata", nil, "Set deployment metadata using `KEY=\"VALUE\"` pairs. Keys must be identifiers, and values must be JSON values. For example: 'YourKey={\"your\": \"value\"}'. Can be passed multiple times.") + s.Command.Flags().StringArrayVar(&s.Metadata, "metadata", nil, "Set deployment metadata using `KEY=\"VALUE\"` pairs. Keys must be identifiers, and values must be JSON values. For example: `YourKey={\"your\": \"value\"}` Can be passed multiple times.") s.Command.Flags().StringArrayVar(&s.RemoveEntries, "remove-entries", nil, "Keys of entries to be deleted from metadata. Can be passed multiple times.") s.DeploymentVersionOptions.buildFlags(cctx, s.Command.Flags()) s.Command.Run = func(c *cobra.Command, args []string) { @@ -3131,6 +3237,63 @@ func NewTemporalWorkerDeploymentUpdateMetadataVersionCommand(cctx *CommandContex return &s } +type TemporalWorkerDescribeCommand struct { + Parent *TemporalWorkerCommand + Command cobra.Command + WorkerInstanceKey string +} + +func NewTemporalWorkerDescribeCommand(cctx *CommandContext, parent *TemporalWorkerCommand) *TemporalWorkerDescribeCommand { + var s TemporalWorkerDescribeCommand + s.Parent = parent + s.Command.DisableFlagsInUseLine = true + s.Command.Use = "describe [flags]" + s.Command.Short = "Returns information about a specific worker (EXPERIMENTAL)" + if hasHighlighting { + s.Command.Long = "Look up information of a specific worker.\n\n\x1b[1mtemporal worker describe --namespace YourNamespace --worker-instance-key YourKey\x1b[0m" + } else { + s.Command.Long = "Look up information of a specific worker.\n\n```\ntemporal worker describe --namespace YourNamespace --worker-instance-key YourKey\n```" + } + s.Command.Args = cobra.NoArgs + s.Command.Flags().StringVar(&s.WorkerInstanceKey, "worker-instance-key", "", "Worker instance key to describe. Required.") + _ = cobra.MarkFlagRequired(s.Command.Flags(), "worker-instance-key") + s.Command.Run = func(c *cobra.Command, args []string) { + if err := s.run(cctx, args); err != nil { + cctx.Options.Fail(err) + } + } + return &s +} + +type TemporalWorkerListCommand struct { + Parent *TemporalWorkerCommand + Command cobra.Command + Query string + Limit int +} + +func NewTemporalWorkerListCommand(cctx *CommandContext, parent *TemporalWorkerCommand) *TemporalWorkerListCommand { + var s TemporalWorkerListCommand + s.Parent = parent + s.Command.DisableFlagsInUseLine = true + s.Command.Use = "list [flags]" + s.Command.Short = "List worker status information in a specific namespace (EXPERIMENTAL)" + if hasHighlighting { + s.Command.Long = "Get a list of workers to the specified namespace.\n\n\x1b[1mtemporal worker list --namespace YourNamespace --query 'TaskQueue=\"YourTaskQueue\"'\x1b[0m" + } else { + s.Command.Long = "Get a list of workers to the specified namespace.\n\n```\ntemporal worker list --namespace YourNamespace --query 'TaskQueue=\"YourTaskQueue\"'\n```" + } + s.Command.Args = cobra.NoArgs + s.Command.Flags().StringVarP(&s.Query, "query", "q", "", "Content for an SQL-like `QUERY` List Filter.") + s.Command.Flags().IntVar(&s.Limit, "limit", 0, "Maximum number of workers to display.") + s.Command.Run = func(c *cobra.Command, args []string) { + if err := s.run(cctx, args); err != nil { + cctx.Options.Fail(err) + } + } + return &s +} + type TemporalWorkflowCommand struct { Parent *TemporalCommand Command cobra.Command @@ -3422,9 +3585,9 @@ func NewTemporalWorkflowListCommand(cctx *CommandContext, parent *TemporalWorkfl s.Command.Use = "list [flags]" s.Command.Short = "Show Workflow Executions" if hasHighlighting { - s.Command.Long = "List Workflow Executions. The optional \x1b[1m--query\x1b[0m limits the output to\nWorkflows matching a Query:\n\n\x1b[1mtemporal workflow list \\\n --query YourQuery\x1b[1m\x1b[0m\n\nVisit https://docs.temporal.io/visibility to read more about Search Attributes\nand Query creation. See \x1b[0mtemporal batch --help` for a quick reference.\n\nView a list of archived Workflow Executions:\n\n\x1b[1mtemporal workflow list \\\n --archived\x1b[0m" + s.Command.Long = "List Workflow Executions. The optional \x1b[1m--query\x1b[0m limits the output to\nWorkflows matching a Query:\n\n\x1b[1mtemporal workflow list \\\n --query YourQuery\x1b[0m\n\nVisit https://docs.temporal.io/visibility to read more about Search Attributes\nand Query creation. See \x1b[1mtemporal batch --help\x1b[0m for a quick reference.\n\nView a list of archived Workflow Executions:\n\n\x1b[1mtemporal workflow list \\\n --archived\x1b[0m" } else { - s.Command.Long = "List Workflow Executions. The optional `--query` limits the output to\nWorkflows matching a Query:\n\n```\ntemporal workflow list \\\n --query YourQuery`\n```\n\nVisit https://docs.temporal.io/visibility to read more about Search Attributes\nand Query creation. See `temporal batch --help` for a quick reference.\n\nView a list of archived Workflow Executions:\n\n```\ntemporal workflow list \\\n --archived\n```" + s.Command.Long = "List Workflow Executions. The optional `--query` limits the output to\nWorkflows matching a Query:\n\n```\ntemporal workflow list \\\n --query YourQuery\n```\n\nVisit https://docs.temporal.io/visibility to read more about Search Attributes\nand Query creation. See `temporal batch --help` for a quick reference.\n\nView a list of archived Workflow Executions:\n\n```\ntemporal workflow list \\\n --archived\n```" } s.Command.Args = cobra.NoArgs s.Command.Flags().StringVarP(&s.Query, "query", "q", "", "Content for an SQL-like `QUERY` List Filter.") @@ -4134,3 +4297,119 @@ func NewTemporalWorkflowUpdateOptionsCommand(cctx *CommandContext, parent *Tempo } return &s } + +var reDays = regexp.MustCompile(`(\d+(\.\d*)?|(\.\d+))d`) + +type Duration time.Duration + +// ParseDuration is like time.ParseDuration, but supports unit "d" for days +// (always interpreted as exactly 24 hours). +func ParseDuration(s string) (time.Duration, error) { + s = reDays.ReplaceAllStringFunc(s, func(v string) string { + fv, err := strconv.ParseFloat(strings.TrimSuffix(v, "d"), 64) + if err != nil { + return v // will cause time.ParseDuration to return an error + } + return fmt.Sprintf("%fh", 24*fv) + }) + return time.ParseDuration(s) +} + +func (d Duration) Duration() time.Duration { + return time.Duration(d) +} + +func (d *Duration) String() string { + return d.Duration().String() +} + +func (d *Duration) Set(s string) error { + p, err := ParseDuration(s) + if err != nil { + return err + } + *d = Duration(p) + return nil +} + +func (d *Duration) Type() string { + return "duration" +} + +type StringEnum struct { + Allowed []string + Value string + ChangedFromDefault bool +} + +func NewStringEnum(allowed []string, value string) StringEnum { + return StringEnum{Allowed: allowed, Value: value} +} + +func (s *StringEnum) String() string { return s.Value } + +func (s *StringEnum) Set(p string) error { + for _, allowed := range s.Allowed { + if p == allowed { + s.Value = p + s.ChangedFromDefault = true + return nil + } + } + return fmt.Errorf("%v is not one of required values of %v", p, strings.Join(s.Allowed, ", ")) +} + +func (*StringEnum) Type() string { return "string" } + +type StringEnumArray struct { + Allowed map[string]string + Values []string +} + +func NewStringEnumArray(allowed []string, values []string) StringEnumArray { + var allowedMap = make(map[string]string) + for _, str := range allowed { + allowedMap[strings.ToLower(str)] = str + } + return StringEnumArray{Allowed: allowedMap, Values: values} +} + +func (s *StringEnumArray) String() string { return strings.Join(s.Values, ",") } + +func (s *StringEnumArray) Set(p string) error { + val, ok := s.Allowed[strings.ToLower(p)] + if !ok { + values := make([]string, 0, len(s.Allowed)) + for _, v := range s.Allowed { + values = append(values, v) + } + return fmt.Errorf("invalid value: %s, allowed values are: %s", p, strings.Join(values, ", ")) + } + s.Values = append(s.Values, val) + return nil +} + +func (*StringEnumArray) Type() string { return "string" } + +type Timestamp time.Time + +func (t Timestamp) Time() time.Time { + return time.Time(t) +} + +func (t *Timestamp) String() string { + return t.Time().Format(time.RFC3339) +} + +func (t *Timestamp) Set(s string) error { + p, err := time.Parse(time.RFC3339, s) + if err != nil { + return err + } + *t = Timestamp(p) + return nil +} + +func (t *Timestamp) Type() string { + return "timestamp" +} diff --git a/temporalcli/commands.go b/internal/temporalcli/commands.go similarity index 87% rename from temporalcli/commands.go rename to internal/temporalcli/commands.go index a549c80b5..acf98142b 100644 --- a/temporalcli/commands.go +++ b/internal/temporalcli/commands.go @@ -19,7 +19,7 @@ import ( "github.com/fatih/color" "github.com/spf13/cobra" "github.com/spf13/pflag" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "github.com/temporalio/ui-server/v2/server/version" "go.temporal.io/api/common/v1" commonpb "go.temporal.io/api/common/v1" @@ -29,6 +29,7 @@ import ( "go.temporal.io/sdk/converter" "go.temporal.io/sdk/temporal" "go.temporal.io/server/common/headers" + "golang.org/x/term" "google.golang.org/grpc" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" @@ -60,7 +61,16 @@ type CommandContext struct { CurrentCommand *cobra.Command } +type IOStreams struct { + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer +} + type CommandOptions struct { + // IOStreams defaults to OS values. + IOStreams + // If empty, assumed to be os.Args[1:] Args []string // Deprecated `--env` and `--env-file` approach @@ -68,11 +78,6 @@ type CommandOptions struct { // If nil, [envconfig.EnvLookupOS] is used. EnvLookup envconfig.EnvLookup - // These three fields below default to OS values - Stdin io.Reader - Stdout io.Writer - Stderr io.Writer - // Defaults to logging error then os.Exit(1) Fail func(error) @@ -343,10 +348,22 @@ func Execute(ctx context.Context, options CommandOptions) { defer cancel() if err == nil { - // We have a context; let's actually run the command. cmd := NewTemporalCommand(cctx) cmd.Command.SetArgs(cctx.Options.Args) - err = cmd.Command.ExecuteContext(cctx) + cmd.Command.SetOut(cctx.Options.Stdout) + cmd.Command.SetErr(cctx.Options.Stderr) + + // Try extension first. + err, cctx.ActuallyRanCommand = tryExecuteExtension(cctx, cmd) + if err != nil { + cctx.Options.Fail(err) + return + } + + // Run builtin command if no extension handled the command. + if !cctx.ActuallyRanCommand { + err = cmd.Command.ExecuteContext(cctx) + } } if err != nil { @@ -375,8 +392,57 @@ func Execute(ctx context.Context, options CommandOptions) { } } +// getUsageTemplate returns a custom usage template with proper flag wrapping +// The default template can be found here: https://github.com/spf13/cobra/blob/v1.9.1/command.go#L1937-L1966 +func getUsageTemplate() string { + // Get terminal width, default to 80 if unable to determine + width := 80 + if w, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil && w > 0 { + width = w + } + + // Use width - 1 for wrapping to avoid edge cases + flagWidth := width - 1 + + // Custom template that uses FlagUsagesWrapped for proper indentation + return fmt.Sprintf(`Usage:{{if .Runnable}} + {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} + {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} + +Aliases: + {{.NameAndAliases}}{{end}}{{if .HasExample}} + +Examples: +{{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}} + +Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}} + +{{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}} + +Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} + +Flags: +{{.LocalFlags.FlagUsagesWrapped %d | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} + +Global Flags: +{{.InheritedFlags.FlagUsagesWrapped %d | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} + +Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} + {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} + +Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} +`, flagWidth, flagWidth) +} + func (c *TemporalCommand) initCommand(cctx *CommandContext) { c.Command.Version = VersionString() + + // Set custom usage template with proper flag wrapping + c.Command.SetUsageTemplate(getUsageTemplate()) + // Unfortunately color is a global option, so we can set in pre-run but we // must unset in post-run origNoColor := color.NoColor @@ -427,7 +493,7 @@ var buildInfo string func VersionString() string { // To add build-time information to the version string, use - // go build -ldflags "-X github.com/temporalio/cli/temporalcli.buildInfo=" + // go build -ldflags "-X github.com/temporalio/cli/internal.buildInfo=" var bi = buildInfo if bi != "" { bi = fmt.Sprintf(", %s", bi) diff --git a/temporalcli/commands.operator_cluster.go b/internal/temporalcli/commands.operator_cluster.go similarity index 99% rename from temporalcli/commands.operator_cluster.go rename to internal/temporalcli/commands.operator_cluster.go index 2cd41abf9..a31955667 100644 --- a/temporalcli/commands.operator_cluster.go +++ b/internal/temporalcli/commands.operator_cluster.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/fatih/color" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/api/operatorservice/v1" "go.temporal.io/api/workflowservice/v1" "go.temporal.io/sdk/client" diff --git a/temporalcli/commands.operator_cluster_test.go b/internal/temporalcli/commands.operator_cluster_test.go similarity index 98% rename from temporalcli/commands.operator_cluster_test.go rename to internal/temporalcli/commands.operator_cluster_test.go index 4cb3b337c..0e637e23e 100644 --- a/temporalcli/commands.operator_cluster_test.go +++ b/internal/temporalcli/commands.operator_cluster_test.go @@ -6,8 +6,8 @@ import ( "strconv" "time" - "github.com/temporalio/cli/temporalcli" - "github.com/temporalio/cli/temporalcli/devserver" + "github.com/temporalio/cli/internal/devserver" + "github.com/temporalio/cli/internal/temporalcli" "go.temporal.io/api/workflowservice/v1" ) diff --git a/temporalcli/commands.operator_namespace.go b/internal/temporalcli/commands.operator_namespace.go similarity index 99% rename from temporalcli/commands.operator_namespace.go rename to internal/temporalcli/commands.operator_namespace.go index 47a5830b7..041955e00 100644 --- a/temporalcli/commands.operator_namespace.go +++ b/internal/temporalcli/commands.operator_namespace.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/fatih/color" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/api/enums/v1" "go.temporal.io/api/namespace/v1" "go.temporal.io/api/operatorservice/v1" @@ -26,16 +26,16 @@ func (c *TemporalOperatorCommand) getNSFromFlagOrArg0(cctx *CommandContext, args } func (c *TemporalOperatorNamespaceCreateCommand) run(cctx *CommandContext, args []string) error { - nsName, err := c.Parent.Parent.getNSFromFlagOrArg0(cctx, args) + cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) if err != nil { return err } + defer cl.Close() - cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) + nsName, err := c.Parent.Parent.getNSFromFlagOrArg0(cctx, args) if err != nil { return err } - defer cl.Close() var clusters []*replication.ClusterReplicationConfig for _, clusterName := range c.Cluster { @@ -74,6 +74,12 @@ func (c *TemporalOperatorNamespaceCreateCommand) run(cctx *CommandContext, args } func (c *TemporalOperatorNamespaceDeleteCommand) run(cctx *CommandContext, args []string) error { + cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) + if err != nil { + return err + } + defer cl.Close() + nsName, err := c.Parent.Parent.getNSFromFlagOrArg0(cctx, args) if err != nil { return err @@ -91,12 +97,6 @@ func (c *TemporalOperatorNamespaceDeleteCommand) run(cctx *CommandContext, args return fmt.Errorf("user denied confirmation or mistyped the namespace name") } - cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) - if err != nil { - return err - } - defer cl.Close() - resp, err := cl.OperatorService().DeleteNamespace(cctx, &operatorservice.DeleteNamespaceRequest{ Namespace: nsName, }) @@ -114,6 +114,12 @@ func (c *TemporalOperatorNamespaceDeleteCommand) run(cctx *CommandContext, args func (c *TemporalOperatorNamespaceDescribeCommand) run(cctx *CommandContext, args []string) error { nsID := c.NamespaceId + cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) + if err != nil { + return err + } + defer cl.Close() + nsName, err := c.Parent.Parent.getNSFromFlagOrArg0(cctx, args) if err != nil { return err @@ -131,12 +137,6 @@ func (c *TemporalOperatorNamespaceDescribeCommand) run(cctx *CommandContext, arg return fmt.Errorf("provide one of --namespace-id= or -n name, but not both") } - cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) - if err != nil { - return err - } - defer cl.Close() - resp, err := cl.WorkflowService().DescribeNamespace(cctx, &workflowservice.DescribeNamespaceRequest{ Namespace: nsName, Id: nsID, @@ -193,16 +193,16 @@ func (c *TemporalOperatorNamespaceListCommand) run(cctx *CommandContext, args [] } func (c *TemporalOperatorNamespaceUpdateCommand) run(cctx *CommandContext, args []string) error { - nsName, err := c.Parent.Parent.getNSFromFlagOrArg0(cctx, args) + cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) if err != nil { return err } + defer cl.Close() - cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) + nsName, err := c.Parent.Parent.getNSFromFlagOrArg0(cctx, args) if err != nil { return err } - defer cl.Close() var updateRequest *workflowservice.UpdateNamespaceRequest diff --git a/temporalcli/commands.operator_namespace_test.go b/internal/temporalcli/commands.operator_namespace_test.go similarity index 71% rename from temporalcli/commands.operator_namespace_test.go rename to internal/temporalcli/commands.operator_namespace_test.go index 65b46cf1e..261fe07a0 100644 --- a/temporalcli/commands.operator_namespace_test.go +++ b/internal/temporalcli/commands.operator_namespace_test.go @@ -2,9 +2,10 @@ package temporalcli_test import ( "fmt" + "os" "time" - "github.com/temporalio/cli/temporalcli" + "github.com/temporalio/cli/internal/temporalcli" "go.temporal.io/api/enums/v1" "go.temporal.io/api/workflowservice/v1" ) @@ -206,3 +207,86 @@ func (s *SharedServerSuite) TestUpdateOldAndNewNSArgs() { s.Error(res.Err) s.ContainsOnSameLine(res.Err.Error(), "namespace was provided as both an argument", "and a flag") } + +func (s *SharedServerSuite) TestOperatorNamespace_EnvConfigResolution() { + // Create a test namespace to use in envconfig + testNS := "envconfig-test-namespace" + res := s.Execute( + "operator", "namespace", "create", + "--address", s.Address(), + "-n", testNS, + ) + s.NoError(res.Err) + + // Create temp config file with namespace + f, err := os.CreateTemp("", "temporal-test-*.toml") + s.NoError(err) + defer os.Remove(f.Name()) + + _, err = fmt.Fprintf(f, ` +[profile.default] +address = "%s" +namespace = "%s" +`, s.Address(), testNS) + s.NoError(err) + f.Close() + + // Set environment to use config file + s.CommandHarness.Options.EnvLookup = EnvLookupMap{ + "TEMPORAL_CONFIG_FILE": f.Name(), + } + + // Test 1: Describe should use envconfig namespace (no -n flag) + res = s.Execute( + "operator", "namespace", "describe", + "--output", "json", + ) + s.NoError(res.Err) + var descResp workflowservice.DescribeNamespaceResponse + s.NoError(temporalcli.UnmarshalProtoJSONWithOptions(res.Stdout.Bytes(), &descResp, true)) + s.Equal(testNS, descResp.NamespaceInfo.Name, "Should use namespace from envconfig") + + // Test 2: Update should use envconfig namespace + res = s.Execute( + "operator", "namespace", "update", + "--description", "Updated via envconfig", + "--output", "json", + ) + s.NoError(res.Err) + + // Verify update was applied to correct namespace + res = s.Execute( + "operator", "namespace", "describe", + "--output", "json", + ) + s.NoError(res.Err) + s.NoError(temporalcli.UnmarshalProtoJSONWithOptions(res.Stdout.Bytes(), &descResp, true)) + s.Equal("Updated via envconfig", descResp.NamespaceInfo.Description) + s.Equal(testNS, descResp.NamespaceInfo.Name) + + // Test 3: CLI flag should override envconfig + res = s.Execute( + "operator", "namespace", "describe", + "--output", "json", + "-n", "default", + ) + s.NoError(res.Err) + s.NoError(temporalcli.UnmarshalProtoJSONWithOptions(res.Stdout.Bytes(), &descResp, true)) + s.Equal("default", descResp.NamespaceInfo.Name, "Explicit -n flag should override envconfig") + + // Test 4: Delete should use envconfig namespace + res = s.Execute( + "operator", "namespace", "delete", + "--yes", + ) + s.NoError(res.Err) + + // Verify namespace was deleted + res = s.Execute( + "operator", "namespace", "describe", + "--output", "json", + "-n", testNS, + ) + s.Error(res.Err) + s.Contains(res.Err.Error(), "is not found") +} diff --git a/temporalcli/commands.operator_nexus.go b/internal/temporalcli/commands.operator_nexus.go similarity index 99% rename from temporalcli/commands.operator_nexus.go rename to internal/temporalcli/commands.operator_nexus.go index aeef936fa..03e337e6f 100644 --- a/temporalcli/commands.operator_nexus.go +++ b/internal/temporalcli/commands.operator_nexus.go @@ -7,7 +7,7 @@ import ( "os" "github.com/fatih/color" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" commonpb "go.temporal.io/api/common/v1" nexuspb "go.temporal.io/api/nexus/v1" "go.temporal.io/api/operatorservice/v1" diff --git a/temporalcli/commands.operator_nexus_test.go b/internal/temporalcli/commands.operator_nexus_test.go similarity index 99% rename from temporalcli/commands.operator_nexus_test.go rename to internal/temporalcli/commands.operator_nexus_test.go index 16bcfbd28..2cc31cf19 100644 --- a/temporalcli/commands.operator_nexus_test.go +++ b/internal/temporalcli/commands.operator_nexus_test.go @@ -11,7 +11,7 @@ import ( "time" "github.com/stretchr/testify/require" - "github.com/temporalio/cli/temporalcli" + "github.com/temporalio/cli/internal/temporalcli" "go.temporal.io/api/nexus/v1" "go.temporal.io/api/operatorservice/v1" ) diff --git a/temporalcli/commands.operator_search_attribute.go b/internal/temporalcli/commands.operator_search_attribute.go similarity index 98% rename from temporalcli/commands.operator_search_attribute.go rename to internal/temporalcli/commands.operator_search_attribute.go index afdca478c..d1187be20 100644 --- a/temporalcli/commands.operator_search_attribute.go +++ b/internal/temporalcli/commands.operator_search_attribute.go @@ -5,7 +5,7 @@ import ( "strings" "github.com/fatih/color" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/api/enums/v1" "go.temporal.io/api/operatorservice/v1" ) diff --git a/temporalcli/commands.operator_search_attribute_test.go b/internal/temporalcli/commands.operator_search_attribute_test.go similarity index 98% rename from temporalcli/commands.operator_search_attribute_test.go rename to internal/temporalcli/commands.operator_search_attribute_test.go index efed38b61..eba4ee81b 100644 --- a/temporalcli/commands.operator_search_attribute_test.go +++ b/internal/temporalcli/commands.operator_search_attribute_test.go @@ -1,7 +1,7 @@ package temporalcli_test import ( - "github.com/temporalio/cli/temporalcli" + "github.com/temporalio/cli/internal/temporalcli" "go.temporal.io/api/enums/v1" "go.temporal.io/api/operatorservice/v1" ) diff --git a/temporalcli/commands.schedule.go b/internal/temporalcli/commands.schedule.go similarity index 98% rename from temporalcli/commands.schedule.go rename to internal/temporalcli/commands.schedule.go index 7cf300e88..88bd1fd87 100644 --- a/temporalcli/commands.schedule.go +++ b/internal/temporalcli/commands.schedule.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "google.golang.org/protobuf/encoding/protojson" commonpb "go.temporal.io/api/common/v1" @@ -16,7 +16,6 @@ import ( schedpb "go.temporal.io/api/schedule/v1" "go.temporal.io/api/workflowservice/v1" "go.temporal.io/sdk/client" - "go.temporal.io/server/common/primitives/timestamp" ) type printableSchedule struct { @@ -210,11 +209,11 @@ func toIntervalSpec(str string) (client.ScheduleIntervalSpec, error) { if len(parts) > 2 { return spec, fmt.Errorf(`invalid interval: must be "" or "/"`) } else if len(parts) == 2 { - if spec.Offset, err = timestamp.ParseDuration(parts[1]); err != nil { + if spec.Offset, err = ParseDuration(parts[1]); err != nil { return spec, fmt.Errorf("invalid interval: %w", err) } } - if spec.Every, err = timestamp.ParseDuration(parts[0]); err != nil { + if spec.Every, err = ParseDuration(parts[0]); err != nil { return spec, fmt.Errorf("invalid interval: %w", err) } return spec, nil diff --git a/temporalcli/commands.schedule_test.go b/internal/temporalcli/commands.schedule_test.go similarity index 96% rename from temporalcli/commands.schedule_test.go rename to internal/temporalcli/commands.schedule_test.go index d24dc02c2..d828e55ea 100644 --- a/temporalcli/commands.schedule_test.go +++ b/internal/temporalcli/commands.schedule_test.go @@ -12,8 +12,7 @@ import ( "time" "github.com/stretchr/testify/assert" - "github.com/temporalio/cli/temporalcli" - + "github.com/temporalio/cli/internal/temporalcli" "go.temporal.io/api/enums/v1" "go.temporal.io/api/operatorservice/v1" "go.temporal.io/sdk/workflow" @@ -29,8 +28,10 @@ func (s *SharedServerSuite) createSchedule(args ...string) (schedId, schedWfId s ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() options := temporalcli.CommandOptions{ - Stdout: io.Discard, - Stderr: io.Discard, + IOStreams: temporalcli.IOStreams{ + Stdout: io.Discard, + Stderr: io.Discard, + }, Args: []string{ "schedule", "delete", "--address", s.Address(), @@ -223,14 +224,17 @@ func (s *SharedServerSuite) TestSchedule_List() { s.NoError(res.Err) // table really-long - - res = s.Execute( - "schedule", "list", - "--address", s.Address(), - "--really-long", - ) - s.NoError(res.Err) - out := res.Stdout.String() + var out string + s.EventuallyWithT(func(t *assert.CollectT) { + res = s.Execute( + "schedule", "list", + "--address", s.Address(), + "--really-long", + ) + assert.NoError(t, res.Err) + out = res.Stdout.String() + assert.Contains(t, out, schedId) + }, 10*time.Second, time.Second) s.ContainsOnSameLine(out, schedId, "DevWorkflow", "0s" /*jitter*/, "false", "nil" /*memo*/) s.ContainsOnSameLine(out, "TestSchedule_List") diff --git a/temporalcli/commands.server.go b/internal/temporalcli/commands.server.go similarity index 99% rename from temporalcli/commands.server.go rename to internal/temporalcli/commands.server.go index 5b2ffc253..91af9d801 100644 --- a/temporalcli/commands.server.go +++ b/internal/temporalcli/commands.server.go @@ -6,8 +6,9 @@ import ( "strings" "github.com/google/uuid" - "github.com/temporalio/cli/temporalcli/devserver" "go.temporal.io/api/enums/v1" + + "github.com/temporalio/cli/internal/devserver" ) var defaultDynamicConfigValues = map[string]any{ diff --git a/temporalcli/commands.server_test.go b/internal/temporalcli/commands.server_test.go similarity index 99% rename from temporalcli/commands.server_test.go rename to internal/temporalcli/commands.server_test.go index e1b4586d8..8c25558d9 100644 --- a/temporalcli/commands.server_test.go +++ b/internal/temporalcli/commands.server_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/temporalio/cli/temporalcli/devserver" + "github.com/temporalio/cli/internal/devserver" "go.temporal.io/api/operatorservice/v1" "go.temporal.io/sdk/client" "go.temporal.io/sdk/temporal" diff --git a/temporalcli/commands.taskqueue.go b/internal/temporalcli/commands.taskqueue.go similarity index 99% rename from temporalcli/commands.taskqueue.go rename to internal/temporalcli/commands.taskqueue.go index a769190a4..a11cc1793 100644 --- a/temporalcli/commands.taskqueue.go +++ b/internal/temporalcli/commands.taskqueue.go @@ -5,7 +5,7 @@ import ( "time" "github.com/fatih/color" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" commonpb "go.temporal.io/api/common/v1" "go.temporal.io/api/enums/v1" "go.temporal.io/api/taskqueue/v1" diff --git a/temporalcli/commands.taskqueue.helper.go b/internal/temporalcli/commands.taskqueue.helper.go similarity index 97% rename from temporalcli/commands.taskqueue.helper.go rename to internal/temporalcli/commands.taskqueue.helper.go index 591531c3d..f63e8b22b 100644 --- a/temporalcli/commands.taskqueue.helper.go +++ b/internal/temporalcli/commands.taskqueue.helper.go @@ -5,7 +5,7 @@ import ( "time" "github.com/fatih/color" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/api/enums/v1" "go.temporal.io/api/taskqueue/v1" ) diff --git a/temporalcli/commands.taskqueue_build_id_test.go b/internal/temporalcli/commands.taskqueue_build_id_test.go similarity index 100% rename from temporalcli/commands.taskqueue_build_id_test.go rename to internal/temporalcli/commands.taskqueue_build_id_test.go diff --git a/temporalcli/commands.taskqueue_config.go b/internal/temporalcli/commands.taskqueue_config.go similarity index 98% rename from temporalcli/commands.taskqueue_config.go rename to internal/temporalcli/commands.taskqueue_config.go index a9de915ef..18f330d51 100644 --- a/temporalcli/commands.taskqueue_config.go +++ b/internal/temporalcli/commands.taskqueue_config.go @@ -5,7 +5,7 @@ import ( "strconv" "strings" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" enums "go.temporal.io/api/enums/v1" "go.temporal.io/api/taskqueue/v1" "go.temporal.io/api/workflowservice/v1" @@ -24,17 +24,17 @@ func (c *TemporalTaskQueueConfigGetCommand) run(cctx *CommandContext, args []str return err } - namespace := c.Parent.Parent.Namespace - if namespace == "" { - return fmt.Errorf("namespace is required") - } - cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) if err != nil { return err } defer cl.Close() + namespace := c.Parent.Parent.Namespace + if namespace == "" { + return fmt.Errorf("namespace is required") + } + // Get the task queue configuration resp, err := cl.WorkflowService().DescribeTaskQueue(cctx, &workflowservice.DescribeTaskQueueRequest{ Namespace: namespace, @@ -69,11 +69,6 @@ func (c *TemporalTaskQueueConfigSetCommand) run(cctx *CommandContext, args []str return err } - namespace := c.Parent.Parent.Namespace - if namespace == "" { - return fmt.Errorf("namespace is required") - } - // Check workflow task queue restrictions if taskQueueType == enums.TASK_QUEUE_TYPE_WORKFLOW { if c.Command.Flags().Changed("queue-rps-limit") || @@ -129,6 +124,11 @@ func (c *TemporalTaskQueueConfigSetCommand) run(cctx *CommandContext, args []str } defer cl.Close() + namespace := c.Parent.Parent.Namespace + if namespace == "" { + return fmt.Errorf("namespace is required") + } + request := &workflowservice.UpdateTaskQueueConfigRequest{ Namespace: namespace, Identity: c.Parent.Parent.Identity, diff --git a/temporalcli/commands.taskqueue_config_test.go b/internal/temporalcli/commands.taskqueue_config_test.go similarity index 80% rename from temporalcli/commands.taskqueue_config_test.go rename to internal/temporalcli/commands.taskqueue_config_test.go index d6fac4b71..9939844dc 100644 --- a/temporalcli/commands.taskqueue_config_test.go +++ b/internal/temporalcli/commands.taskqueue_config_test.go @@ -2,6 +2,8 @@ package temporalcli_test import ( "encoding/json" + "fmt" + "os" ) type taskQueueConfigType struct { @@ -254,3 +256,72 @@ func (s *SharedServerSuite) TestTaskQueue_Config_Describe_With_Report_Config() { updTime, _ := md["update_time"].(map[string]any) s.NotEmpty(updTime) } + +func (s *SharedServerSuite) TestTaskQueueConfig_EnvConfigNamespace() { + // Create test namespace + testNS := "tq-config-envconfig-test" + res := s.Execute( + "operator", "namespace", "create", + "--address", s.Address(), + "-n", testNS, + ) + s.NoError(res.Err) + + // Create temp config file + f, err := os.CreateTemp("", "temporal-test-*.toml") + s.NoError(err) + defer os.Remove(f.Name()) + + _, err = fmt.Fprintf(f, ` +[profile.default] +address = "%s" +namespace = "%s" +`, s.Address(), testNS) + s.NoError(err) + f.Close() + + // Set environment + s.CommandHarness.Options.EnvLookup = EnvLookupMap{ + "TEMPORAL_CONFIG_FILE": f.Name(), + } + + taskQueue := "test-tq-envconfig-" + s.T().Name() + + // Test 1: Set config without -n flag (should use envconfig) + res = s.Execute( + "task-queue", "config", "set", + "--task-queue", taskQueue, + "--task-queue-type", "activity", + "--queue-rps-limit", "15.0", + "--queue-rps-limit-reason", "envconfig test", + ) + s.NoError(res.Err) + + // Test 2: Get config without -n flag (should use envconfig) + res = s.Execute( + "task-queue", "config", "get", + "--task-queue", taskQueue, + "--task-queue-type", "activity", + "-o", "json", + ) + s.NoError(res.Err) + + var config taskQueueConfigType + s.NoError(json.Unmarshal(res.Stdout.Bytes(), &config)) + s.NotNil(config.QueueRateLimit) + s.NotNil(config.QueueRateLimit.RateLimit) + s.Equal(float32(15.0), config.QueueRateLimit.RateLimit.RequestsPerSecond) + s.Equal("envconfig test", config.QueueRateLimit.Metadata.Reason) + + // Test 3: CLI flag should override envconfig - verify config doesn't exist in default namespace + res = s.Execute( + "task-queue", "config", "get", + "--task-queue", taskQueue, + "--task-queue-type", "activity", + "-n", "default", + ) + s.NoError(res.Err) + // In default namespace, no config was set for this task queue + s.Contains(res.Stdout.String(), "No configuration found for task queue", + "CLI flag should override envconfig and query default namespace") +} diff --git a/temporalcli/commands.taskqueue_get_build_id.go b/internal/temporalcli/commands.taskqueue_get_build_id.go similarity index 98% rename from temporalcli/commands.taskqueue_get_build_id.go rename to internal/temporalcli/commands.taskqueue_get_build_id.go index ad51ae177..56cc521b0 100644 --- a/temporalcli/commands.taskqueue_get_build_id.go +++ b/internal/temporalcli/commands.taskqueue_get_build_id.go @@ -5,7 +5,7 @@ import ( "strconv" "github.com/fatih/color" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/sdk/client" ) diff --git a/temporalcli/commands.taskqueue_test.go b/internal/temporalcli/commands.taskqueue_test.go similarity index 99% rename from temporalcli/commands.taskqueue_test.go rename to internal/temporalcli/commands.taskqueue_test.go index 346df180d..b695758b2 100644 --- a/temporalcli/commands.taskqueue_test.go +++ b/internal/temporalcli/commands.taskqueue_test.go @@ -5,13 +5,12 @@ import ( "strings" "time" - "github.com/stretchr/testify/assert" - "go.temporal.io/sdk/workflow" - "github.com/google/uuid" - "github.com/temporalio/cli/temporalcli" + "github.com/stretchr/testify/assert" + "github.com/temporalio/cli/internal/temporalcli" "go.temporal.io/api/enums/v1" "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/sdk/workflow" ) type statsRowType struct { diff --git a/temporalcli/commands.taskqueue_update_build_ids.go b/internal/temporalcli/commands.taskqueue_update_build_ids.go similarity index 100% rename from temporalcli/commands.taskqueue_update_build_ids.go rename to internal/temporalcli/commands.taskqueue_update_build_ids.go diff --git a/temporalcli/commands.taskqueue_versioning_rules.go b/internal/temporalcli/commands.taskqueue_versioning_rules.go similarity index 99% rename from temporalcli/commands.taskqueue_versioning_rules.go rename to internal/temporalcli/commands.taskqueue_versioning_rules.go index d64095e8b..4552492fc 100644 --- a/temporalcli/commands.taskqueue_versioning_rules.go +++ b/internal/temporalcli/commands.taskqueue_versioning_rules.go @@ -5,7 +5,7 @@ import ( "time" "github.com/fatih/color" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/sdk/client" ) diff --git a/temporalcli/commands.taskqueue_versioning_rules_test.go b/internal/temporalcli/commands.taskqueue_versioning_rules_test.go similarity index 100% rename from temporalcli/commands.taskqueue_versioning_rules_test.go rename to internal/temporalcli/commands.taskqueue_versioning_rules_test.go diff --git a/temporalcli/commands.worker.deployment.go b/internal/temporalcli/commands.worker.deployment.go similarity index 85% rename from temporalcli/commands.worker.deployment.go rename to internal/temporalcli/commands.worker.deployment.go index 072fd838f..532b390dd 100644 --- a/temporalcli/commands.worker.deployment.go +++ b/internal/temporalcli/commands.worker.deployment.go @@ -1,28 +1,30 @@ package temporalcli import ( + "errors" "fmt" "time" "github.com/fatih/color" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/api/common/v1" + "go.temporal.io/api/serviceerror" "go.temporal.io/sdk/client" "go.temporal.io/sdk/worker" ) type versionSummariesRowType struct { DeploymentName string `json:"deploymentName"` - BuildId string `json:"buildId"` + BuildID string `json:"BuildID"` DrainageStatus string `json:"drainageStatus"` CreateTime time.Time `json:"createTime"` } type formattedRoutingConfigType struct { CurrentVersionDeploymentName string `json:"currentVersionDeploymentName"` - CurrentVersionBuildId string `json:"currentVersionBuildId"` + CurrentVersionBuildID string `json:"currentVersionBuildID"` RampingVersionDeploymentName string `json:"rampingVersionDeploymentName"` - RampingVersionBuildId string `json:"rampingVersionBuildId"` + RampingVersionBuildID string `json:"rampingVersionBuildID"` RampingVersionPercentage float32 `json:"rampingVersionPercentage"` CurrentVersionChangedTime time.Time `json:"currentVersionChangedTime"` RampingVersionChangedTime time.Time `json:"rampingVersionChangedTime"` @@ -35,15 +37,16 @@ type formattedWorkerDeploymentInfoType struct { LastModifierIdentity string `json:"lastModifierIdentity"` RoutingConfig formattedRoutingConfigType `json:"routingConfig"` VersionSummaries []versionSummariesRowType `json:"versionSummaries"` + ManagerIdentity string `json:"managerIdentity"` } type formattedWorkerDeploymentListEntryType struct { Name string CreateTime time.Time CurrentVersionDeploymentName string `cli:",cardOmitEmpty"` - CurrentVersionBuildId string `cli:",cardOmitEmpty"` + CurrentVersionBuildID string `cli:",cardOmitEmpty"` RampingVersionDeploymentName string `cli:",cardOmitEmpty"` - RampingVersionBuildId string `cli:",cardOmitEmpty"` + RampingVersionBuildID string `cli:",cardOmitEmpty"` RampingVersionPercentage float32 `cli:",cardOmitEmpty"` } @@ -60,7 +63,7 @@ type formattedTaskQueueInfoRowType struct { type formattedWorkerDeploymentVersionInfoType struct { DeploymentName string `json:"deploymentName"` - BuildId string `json:"buildId"` + BuildID string `json:"BuildID"` CreateTime time.Time `json:"createTime"` RoutingChangedTime time.Time `json:"routingChangedTime"` CurrentSinceTime time.Time `json:"currentSinceTime"` @@ -93,7 +96,7 @@ func formatVersionSummaries(vss []client.WorkerDeploymentVersionSummary) ([]vers } vsRows = append(vsRows, versionSummariesRowType{ DeploymentName: vs.Version.DeploymentName, - BuildId: vs.Version.BuildId, + BuildID: vs.Version.BuildID, CreateTime: vs.CreateTime, DrainageStatus: drainageStr, }) @@ -108,17 +111,17 @@ func formatRoutingConfig(rc client.WorkerDeploymentRoutingConfig) (formattedRout rvbid := "" if rc.CurrentVersion != nil { cvdn = rc.CurrentVersion.DeploymentName - cvbid = rc.CurrentVersion.BuildId + cvbid = rc.CurrentVersion.BuildID } if rc.RampingVersion != nil { rvdn = rc.RampingVersion.DeploymentName - rvbid = rc.RampingVersion.BuildId + rvbid = rc.RampingVersion.BuildID } return formattedRoutingConfigType{ CurrentVersionDeploymentName: cvdn, - CurrentVersionBuildId: cvbid, + CurrentVersionBuildID: cvbid, RampingVersionDeploymentName: rvdn, - RampingVersionBuildId: rvbid, + RampingVersionBuildID: rvbid, RampingVersionPercentage: rc.RampingVersionPercentage, CurrentVersionChangedTime: rc.CurrentVersionChangedTime, RampingVersionChangedTime: rc.RampingVersionChangedTime, @@ -143,6 +146,7 @@ func workerDeploymentInfoToRows(deploymentInfo client.WorkerDeploymentInfo) (for CreateTime: deploymentInfo.CreateTime, RoutingConfig: rc, VersionSummaries: vs, + ManagerIdentity: deploymentInfo.ManagerIdentity, }, nil } @@ -161,16 +165,17 @@ func printWorkerDeploymentInfo(cctx *CommandContext, deploymentInfo client.Worke rampVerBuildId := "" if deploymentInfo.RoutingConfig.CurrentVersion != nil { curVerDepName = deploymentInfo.RoutingConfig.CurrentVersion.DeploymentName - curVerBuildId = deploymentInfo.RoutingConfig.CurrentVersion.BuildId + curVerBuildId = deploymentInfo.RoutingConfig.CurrentVersion.BuildID } if deploymentInfo.RoutingConfig.RampingVersion != nil { rampVerDepName = deploymentInfo.RoutingConfig.RampingVersion.DeploymentName - rampVerBuildId = deploymentInfo.RoutingConfig.RampingVersion.BuildId + rampVerBuildId = deploymentInfo.RoutingConfig.RampingVersion.BuildID } printMe := struct { Name string CreateTime time.Time LastModifierIdentity string `cli:",cardOmitEmpty"` + ManagerIdentity string `cli:",cardOmitEmpty"` CurrentVersionDeploymentName string `cli:",cardOmitEmpty"` CurrentVersionBuildID string `cli:",cardOmitEmpty"` RampingVersionDeploymentName string `cli:",cardOmitEmpty"` @@ -183,6 +188,7 @@ func printWorkerDeploymentInfo(cctx *CommandContext, deploymentInfo client.Worke Name: deploymentInfo.Name, CreateTime: deploymentInfo.CreateTime, LastModifierIdentity: deploymentInfo.LastModifierIdentity, + ManagerIdentity: deploymentInfo.ManagerIdentity, CurrentVersionDeploymentName: curVerDepName, CurrentVersionBuildID: curVerBuildId, RampingVersionDeploymentName: rampVerDepName, @@ -261,7 +267,7 @@ func workerDeploymentVersionInfoToRows(deploymentInfo client.WorkerDeploymentVer return formattedWorkerDeploymentVersionInfoType{ DeploymentName: deploymentInfo.Version.DeploymentName, - BuildId: deploymentInfo.Version.BuildId, + BuildID: deploymentInfo.Version.BuildID, CreateTime: deploymentInfo.CreateTime, RoutingChangedTime: deploymentInfo.RoutingChangedTime, CurrentSinceTime: deploymentInfo.CurrentSinceTime, @@ -295,7 +301,7 @@ func printWorkerDeploymentVersionInfo(cctx *CommandContext, deploymentInfo clien printMe := struct { DeploymentName string - BuildId string + BuildID string CreateTime time.Time RoutingChangedTime time.Time `cli:",cardOmitEmpty"` CurrentSinceTime time.Time `cli:",cardOmitEmpty"` @@ -307,7 +313,7 @@ func printWorkerDeploymentVersionInfo(cctx *CommandContext, deploymentInfo clien Metadata map[string]*common.Payload `cli:",cardOmitEmpty"` }{ DeploymentName: deploymentInfo.Version.DeploymentName, - BuildId: deploymentInfo.Version.BuildId, + BuildID: deploymentInfo.Version.BuildID, CreateTime: deploymentInfo.CreateTime, RoutingChangedTime: deploymentInfo.RoutingChangedTime, CurrentSinceTime: deploymentInfo.CurrentSinceTime, @@ -471,9 +477,9 @@ func (c *TemporalWorkerDeploymentListCommand) run(cctx *CommandContext, args []s Name: listEntry.Name, CreateTime: listEntry.CreateTime, CurrentVersionDeploymentName: listEntry.RoutingConfig.CurrentVersionDeploymentName, - CurrentVersionBuildId: listEntry.RoutingConfig.CurrentVersionBuildId, + CurrentVersionBuildID: listEntry.RoutingConfig.CurrentVersionBuildID, RampingVersionDeploymentName: listEntry.RoutingConfig.RampingVersionDeploymentName, - RampingVersionBuildId: listEntry.RoutingConfig.RampingVersionBuildId, + RampingVersionBuildID: listEntry.RoutingConfig.RampingVersionBuildID, RampingVersionPercentage: listEntry.RoutingConfig.RampingVersionPercentage, }) if len(page) == cap(page) { @@ -492,6 +498,72 @@ func (c *TemporalWorkerDeploymentListCommand) run(cctx *CommandContext, args []s return nil } +func (c *TemporalWorkerDeploymentManagerIdentitySetCommand) run(cctx *CommandContext, args []string) error { + cl, err := c.Parent.Parent.Parent.dialClient(cctx) + if err != nil { + return err + } + defer cl.Close() + + token, err := c.Parent.Parent.getConflictToken(cctx, &getDeploymentConflictTokenOptions{ + safeMode: !c.Yes, + safeModeMessage: "ManagerIdentity", + deploymentName: c.DeploymentName, + }) + if err != nil { + return err + } + + newManagerIdentity := c.ManagerIdentity + if c.Self { + newManagerIdentity = c.Parent.Parent.Parent.Identity + } + + dHandle := cl.WorkerDeploymentClient().GetHandle(c.DeploymentName) + resp, err := dHandle.SetManagerIdentity(cctx, client.WorkerDeploymentSetManagerIdentityOptions{ + Identity: c.Parent.Parent.Parent.Identity, + ConflictToken: token, + Self: c.Self, + ManagerIdentity: c.ManagerIdentity, + }) + if err != nil { + return fmt.Errorf("error setting the manager identity: %w", err) + } + + cctx.Printer.Printlnf("Successfully set manager identity to '%s', was previously '%s'", newManagerIdentity, resp.PreviousManagerIdentity) + return nil +} + +func (c *TemporalWorkerDeploymentManagerIdentityUnsetCommand) run(cctx *CommandContext, args []string) error { + cl, err := c.Parent.Parent.Parent.dialClient(cctx) + if err != nil { + return err + } + defer cl.Close() + + token, err := c.Parent.Parent.getConflictToken(cctx, &getDeploymentConflictTokenOptions{ + safeMode: !c.Yes, + safeModeMessage: "ManagerIdentity", + deploymentName: c.DeploymentName, + }) + if err != nil { + return err + } + + dHandle := cl.WorkerDeploymentClient().GetHandle(c.DeploymentName) + resp, err := dHandle.SetManagerIdentity(cctx, client.WorkerDeploymentSetManagerIdentityOptions{ + Identity: c.Parent.Parent.Parent.Identity, + ConflictToken: token, + ManagerIdentity: "", + }) + if err != nil { + return fmt.Errorf("error unsetting the manager identity: %w", err) + } + + cctx.Printer.Printlnf("Successfully unset manager identity, was previously '%s'", resp.PreviousManagerIdentity) + return nil +} + func (c *TemporalWorkerDeploymentDeleteVersionCommand) run(cctx *CommandContext, args []string) error { cl, err := c.Parent.Parent.ClientOptions.dialClient(cctx) if err != nil { @@ -549,7 +621,7 @@ func (c *TemporalWorkerDeploymentSetCurrentVersionCommand) run(cctx *CommandCont safeModeMessage: "Current", deploymentName: c.DeploymentName, }) - if err != nil { + if err != nil && !(errors.As(err, new(*serviceerror.NotFound)) && c.AllowNoPollers) { return err } @@ -558,6 +630,7 @@ func (c *TemporalWorkerDeploymentSetCurrentVersionCommand) run(cctx *CommandCont BuildID: c.BuildId, Identity: c.Parent.Parent.Identity, IgnoreMissingTaskQueues: c.IgnoreMissingTaskQueues, + AllowNoPollers: c.AllowNoPollers, ConflictToken: token, }) if err != nil { @@ -580,7 +653,7 @@ func (c *TemporalWorkerDeploymentSetRampingVersionCommand) run(cctx *CommandCont safeModeMessage: "Ramping", deploymentName: c.DeploymentName, }) - if err != nil { + if err != nil && !(errors.As(err, new(*serviceerror.NotFound)) && c.AllowNoPollers) { return err } @@ -596,6 +669,7 @@ func (c *TemporalWorkerDeploymentSetRampingVersionCommand) run(cctx *CommandCont ConflictToken: token, Identity: c.Parent.Parent.Identity, IgnoreMissingTaskQueues: c.IgnoreMissingTaskQueues, + AllowNoPollers: c.AllowNoPollers, }) if err != nil { return fmt.Errorf("error setting the ramping worker deployment version: %w", err) @@ -620,7 +694,7 @@ func (c *TemporalWorkerDeploymentUpdateMetadataVersionCommand) run(cctx *Command dHandle := cl.WorkerDeploymentClient().GetHandle(c.DeploymentName) response, err := dHandle.UpdateVersionMetadata(cctx, client.WorkerDeploymentUpdateVersionMetadataOptions{ Version: worker.WorkerDeploymentVersion{ - BuildId: c.BuildId, + BuildID: c.BuildId, DeploymentName: c.DeploymentName, }, MetadataUpdate: client.WorkerDeploymentMetadataUpdate{ diff --git a/temporalcli/commands.worker.deployment_test.go b/internal/temporalcli/commands.worker.deployment_test.go similarity index 70% rename from temporalcli/commands.worker.deployment_test.go rename to internal/temporalcli/commands.worker.deployment_test.go index f72b8b40a..3ceb4c18c 100644 --- a/temporalcli/commands.worker.deployment_test.go +++ b/internal/temporalcli/commands.worker.deployment_test.go @@ -17,16 +17,16 @@ import ( type jsonVersionSummariesRowType struct { DeploymentName string `json:"deploymentName"` - BuildID string `json:"buildId"` + BuildID string `json:"BuildID"` DrainageStatus string `json:"drainageStatus"` CreateTime time.Time `json:"createTime"` } type jsonRoutingConfigType struct { CurrentVersionDeploymentName string `json:"currentVersionDeploymentName"` - CurrentVersionBuildID string `json:"currentVersionBuildId"` + CurrentVersionBuildID string `json:"currentVersionBuildID"` RampingVersionDeploymentName string `json:"rampingVersionDeploymentName"` - RampingVersionBuildID string `json:"rampingVersionBuildId"` + RampingVersionBuildID string `json:"rampingVersionBuildID"` RampingVersionPercentage float32 `json:"rampingVersionPercentage"` CurrentVersionChangedTime time.Time `json:"currentVersionChangedTime"` RampingVersionChangedTime time.Time `json:"rampingVersionChangedTime"` @@ -39,6 +39,7 @@ type jsonDeploymentInfoType struct { LastModifierIdentity string `json:"lastModifierIdentity"` RoutingConfig jsonRoutingConfigType `json:"routingConfig"` VersionSummaries []jsonVersionSummariesRowType `json:"versionSummaries"` + ManagerIdentity string `json:"managerIdentity"` } type jsonDrainageInfo struct { @@ -69,7 +70,7 @@ func (s *SharedServerSuite) TestDeployment_Set_Current_Version() { buildId := uuid.NewString() version := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName, - BuildId: buildId, + BuildID: buildId, } w := s.DevServer.StartDevWorker(s.Suite.T(), DevWorkerOptions{ Worker: worker.Options{ @@ -95,7 +96,7 @@ func (s *SharedServerSuite) TestDeployment_Set_Current_Version() { res := s.Execute( "worker", "deployment", "describe-version", "--address", s.Address(), - "--deployment-name", version.DeploymentName, "--build-id", version.BuildId, + "--deployment-name", version.DeploymentName, "--build-id", version.BuildID, ) assert.NoError(t, res.Err) }, 30*time.Second, 100*time.Millisecond) @@ -103,7 +104,7 @@ func (s *SharedServerSuite) TestDeployment_Set_Current_Version() { res := s.Execute( "worker", "deployment", "set-current-version", "--address", s.Address(), - "--deployment-name", version.DeploymentName, "--build-id", version.BuildId, + "--deployment-name", version.DeploymentName, "--build-id", version.BuildID, "--yes", ) s.NoError(res.Err) @@ -117,7 +118,7 @@ func (s *SharedServerSuite) TestDeployment_Set_Current_Version() { s.ContainsOnSameLine(res.Stdout.String(), "Name", deploymentName) s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionDeploymentName", version.DeploymentName) - s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionBuildID", version.BuildId) + s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionBuildID", version.BuildID) // json res = s.Execute( @@ -132,13 +133,13 @@ func (s *SharedServerSuite) TestDeployment_Set_Current_Version() { s.NoError(json.Unmarshal(res.Stdout.Bytes(), &jsonOut)) s.Equal(deploymentName, jsonOut.Name) s.Equal(version.DeploymentName, jsonOut.RoutingConfig.CurrentVersionDeploymentName) - s.Equal(version.BuildId, jsonOut.RoutingConfig.CurrentVersionBuildID) + s.Equal(version.BuildID, jsonOut.RoutingConfig.CurrentVersionBuildID) // set metadata res = s.Execute( "worker", "deployment", "update-metadata-version", "--address", s.Address(), - "--deployment-name", version.DeploymentName, "--build-id", version.BuildId, + "--deployment-name", version.DeploymentName, "--build-id", version.BuildID, "--metadata", "bar=1", "--output", "json", ) @@ -155,7 +156,7 @@ func (s *SharedServerSuite) TestDeployment_Set_Current_Version() { res = s.Execute( "worker", "deployment", "update-metadata-version", "--address", s.Address(), - "--deployment-name", version.DeploymentName, "--build-id", version.BuildId, + "--deployment-name", version.DeploymentName, "--build-id", version.BuildID, "--remove-entries", "bar", "--output", "json", ) @@ -164,7 +165,7 @@ func (s *SharedServerSuite) TestDeployment_Set_Current_Version() { res = s.Execute( "worker", "deployment", "describe-version", "--address", s.Address(), - "--deployment-name", version.DeploymentName, "--build-id", version.BuildId, + "--deployment-name", version.DeploymentName, "--build-id", version.BuildID, "--output", "json", ) s.NoError(res.Err) @@ -172,6 +173,133 @@ func (s *SharedServerSuite) TestDeployment_Set_Current_Version() { s.Nil(jsonVersionOut.Metadata) } +func (s *SharedServerSuite) TestDeployment_Set_Current_Version_AllowNoPollers() { + deploymentName := uuid.NewString() + buildId := uuid.NewString() + version := worker.WorkerDeploymentVersion{ + DeploymentName: deploymentName, + BuildID: buildId, + } + + // with --allow-no-pollers, no need to have a worker polling on this version + res := s.Execute( + "worker", "deployment", "set-current-version", + "--address", s.Address(), + "--deployment-name", version.DeploymentName, "--build-id", version.BuildID, + "--allow-no-pollers", + "--yes", + ) + s.NoError(res.Err) + + s.EventuallyWithT(func(t *assert.CollectT) { + res := s.Execute( + "worker", "deployment", "list", + "--address", s.Address(), + ) + assert.NoError(t, res.Err) + assert.Contains(t, res.Stdout.String(), deploymentName) + }, 30*time.Second, 100*time.Millisecond) + + s.EventuallyWithT(func(t *assert.CollectT) { + res := s.Execute( + "worker", "deployment", "describe-version", + "--address", s.Address(), + "--deployment-name", version.DeploymentName, "--build-id", version.BuildID, + ) + assert.NoError(t, res.Err) + }, 30*time.Second, 100*time.Millisecond) + + res = s.Execute( + "worker", "deployment", "describe", + "--address", s.Address(), + "--name", deploymentName, + ) + s.NoError(res.Err) + + s.ContainsOnSameLine(res.Stdout.String(), "Name", deploymentName) + s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionDeploymentName", version.DeploymentName) + s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionBuildID", version.BuildID) + + // json + res = s.Execute( + "worker", "deployment", "describe", + "--address", s.Address(), + "--name", deploymentName, + "--output", "json", + ) + s.NoError(res.Err) + + var jsonOut jsonDeploymentInfoType + s.NoError(json.Unmarshal(res.Stdout.Bytes(), &jsonOut)) + s.Equal(deploymentName, jsonOut.Name) + s.Equal(version.DeploymentName, jsonOut.RoutingConfig.CurrentVersionDeploymentName) + s.Equal(version.BuildID, jsonOut.RoutingConfig.CurrentVersionBuildID) +} + +func (s *SharedServerSuite) TestDeployment_Set_Ramping_Version_AllowNoPollers() { + deploymentName := uuid.NewString() + buildId := uuid.NewString() + version := worker.WorkerDeploymentVersion{ + DeploymentName: deploymentName, + BuildID: buildId, + } + + // with --allow-no-pollers, no need to have a worker polling on this version + res := s.Execute( + "worker", "deployment", "set-ramping-version", + "--address", s.Address(), + "--deployment-name", version.DeploymentName, "--build-id", version.BuildID, + "--percentage", "5", + "--allow-no-pollers", + "--yes", + ) + s.NoError(res.Err) + + s.EventuallyWithT(func(t *assert.CollectT) { + res := s.Execute( + "worker", "deployment", "list", + "--address", s.Address(), + ) + assert.NoError(t, res.Err) + assert.Contains(t, res.Stdout.String(), deploymentName) + }, 30*time.Second, 100*time.Millisecond) + + s.EventuallyWithT(func(t *assert.CollectT) { + res := s.Execute( + "worker", "deployment", "describe-version", + "--address", s.Address(), + "--deployment-name", version.DeploymentName, "--build-id", version.BuildID, + ) + assert.NoError(t, res.Err) + }, 30*time.Second, 100*time.Millisecond) + + res = s.Execute( + "worker", "deployment", "describe", + "--address", s.Address(), + "--name", deploymentName, + ) + s.NoError(res.Err) + + s.ContainsOnSameLine(res.Stdout.String(), "Name", deploymentName) + s.ContainsOnSameLine(res.Stdout.String(), "RampingVersionDeploymentName", version.DeploymentName) + s.ContainsOnSameLine(res.Stdout.String(), "RampingVersionBuildID", version.BuildID) + + // json + res = s.Execute( + "worker", "deployment", "describe", + "--address", s.Address(), + "--name", deploymentName, + "--output", "json", + ) + s.NoError(res.Err) + + var jsonOut jsonDeploymentInfoType + s.NoError(json.Unmarshal(res.Stdout.Bytes(), &jsonOut)) + s.Equal(deploymentName, jsonOut.Name) + s.Equal(version.DeploymentName, jsonOut.RoutingConfig.RampingVersionDeploymentName) + s.Equal(version.BuildID, jsonOut.RoutingConfig.RampingVersionBuildID) +} + func filterByNamePrefix(jsonOut []jsonDeploymentInfoType, prefix string) []jsonDeploymentInfoType { result := []jsonDeploymentInfoType{} for i := range jsonOut { @@ -190,11 +318,11 @@ func (s *SharedServerSuite) TestDeployment_List() { buildId2 := uuid.NewString() version1 := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName1, - BuildId: buildId1, + BuildID: buildId1, } version2 := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName2, - BuildId: buildId2, + BuildID: buildId2, } w1 := s.DevServer.StartDevWorker(s.Suite.T(), DevWorkerOptions{ @@ -233,13 +361,13 @@ func (s *SharedServerSuite) TestDeployment_List() { res := s.Execute( "worker", "deployment", "describe-version", "--address", s.Address(), - "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildId, + "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildID, ) assert.NoError(t, res.Err) res = s.Execute( "worker", "deployment", "describe-version", "--address", s.Address(), - "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildId, + "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildID, ) assert.NoError(t, res.Err) }, 30*time.Second, 100*time.Millisecond) @@ -247,7 +375,7 @@ func (s *SharedServerSuite) TestDeployment_List() { res := s.Execute( "worker", "deployment", "set-current-version", "--address", s.Address(), - "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildId, + "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildID, "--yes", ) s.NoError(res.Err) @@ -255,7 +383,7 @@ func (s *SharedServerSuite) TestDeployment_List() { res = s.Execute( "worker", "deployment", "set-current-version", "--address", s.Address(), - "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildId, + "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildID, "--yes", ) s.NoError(res.Err) @@ -265,11 +393,13 @@ func (s *SharedServerSuite) TestDeployment_List() { "worker", "deployment", "list", "--address", s.Address(), ) - s.NoError(res.Err) + assert.NoError(t, res.Err) + assert.Contains(t, res.Stdout.String(), version1.BuildID) + assert.Contains(t, res.Stdout.String(), version2.BuildID) }, 10*time.Second, 100*time.Millisecond) - s.ContainsOnSameLine(res.Stdout.String(), deploymentName1, version1.BuildId) - s.ContainsOnSameLine(res.Stdout.String(), deploymentName2, version2.BuildId) + s.ContainsOnSameLine(res.Stdout.String(), deploymentName1, version1.BuildID) + s.ContainsOnSameLine(res.Stdout.String(), deploymentName2, version2.BuildID) // json res = s.Execute( @@ -288,10 +418,10 @@ func (s *SharedServerSuite) TestDeployment_List() { s.Equal(2, len(jsonOut)) s.Equal(deploymentName1, jsonOut[0].Name) s.Equal(version1.DeploymentName, jsonOut[0].RoutingConfig.CurrentVersionDeploymentName) - s.Equal(version1.BuildId, jsonOut[0].RoutingConfig.CurrentVersionBuildID) + s.Equal(version1.BuildID, jsonOut[0].RoutingConfig.CurrentVersionBuildID) s.Equal(deploymentName2, jsonOut[1].Name) s.Equal(version2.DeploymentName, jsonOut[1].RoutingConfig.CurrentVersionDeploymentName) - s.Equal(version2.BuildId, jsonOut[1].RoutingConfig.CurrentVersionBuildID) + s.Equal(version2.BuildID, jsonOut[1].RoutingConfig.CurrentVersionBuildID) } func (s *SharedServerSuite) TestDeployment_Describe_Drainage() { @@ -300,11 +430,11 @@ func (s *SharedServerSuite) TestDeployment_Describe_Drainage() { buildId2 := "b" + uuid.NewString() version1 := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName, - BuildId: buildId1, + BuildID: buildId1, } version2 := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName, - BuildId: buildId2, + BuildID: buildId2, } w1 := s.DevServer.StartDevWorker(s.Suite.T(), DevWorkerOptions{ @@ -342,13 +472,13 @@ func (s *SharedServerSuite) TestDeployment_Describe_Drainage() { res := s.Execute( "worker", "deployment", "describe-version", "--address", s.Address(), - "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildId, + "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildID, ) assert.NoError(t, res.Err) res = s.Execute( "worker", "deployment", "describe-version", "--address", s.Address(), - "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildId, + "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildID, ) assert.NoError(t, res.Err) }, 30*time.Second, 100*time.Millisecond) @@ -356,7 +486,7 @@ func (s *SharedServerSuite) TestDeployment_Describe_Drainage() { res := s.Execute( "worker", "deployment", "set-current-version", "--address", s.Address(), - "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildId, + "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildID, "--yes", ) s.NoError(res.Err) @@ -368,13 +498,13 @@ func (s *SharedServerSuite) TestDeployment_Describe_Drainage() { ) s.NoError(res.Err) s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionDeploymentName", version1.DeploymentName) - s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionBuildID", version1.BuildId) + s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionBuildID", version1.BuildID) fmt.Print("hello") res = s.Execute( "worker", "deployment", "set-current-version", "--address", s.Address(), - "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildId, + "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildID, "--yes", ) s.NoError(res.Err) @@ -387,7 +517,7 @@ func (s *SharedServerSuite) TestDeployment_Describe_Drainage() { s.NoError(res.Err) s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionDeploymentName", version2.DeploymentName) - s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionBuildID", version2.BuildId) + s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionBuildID", version2.BuildID) s.ContainsOnSameLine(res.Stdout.String(), version1.DeploymentName, "draining") s.ContainsOnSameLine(res.Stdout.String(), version2.DeploymentName, "unspecified") @@ -409,9 +539,9 @@ func (s *SharedServerSuite) TestDeployment_Describe_Drainage() { s.Equal(2, len(jsonOut.VersionSummaries)) s.Equal("draining", jsonOut.VersionSummaries[0].DrainageStatus) - s.Equal(version1.BuildId, jsonOut.VersionSummaries[0].BuildID) + s.Equal(version1.BuildID, jsonOut.VersionSummaries[0].BuildID) s.Equal("unspecified", jsonOut.VersionSummaries[1].DrainageStatus) - s.Equal(version2.BuildId, jsonOut.VersionSummaries[1].BuildID) + s.Equal(version2.BuildID, jsonOut.VersionSummaries[1].BuildID) } func (s *SharedServerSuite) TestDeployment_Ramping() { @@ -420,11 +550,11 @@ func (s *SharedServerSuite) TestDeployment_Ramping() { buildId2 := "b" + uuid.NewString() version1 := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName, - BuildId: buildId1, + BuildID: buildId1, } version2 := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName, - BuildId: buildId2, + BuildID: buildId2, } w1 := s.DevServer.StartDevWorker(s.Suite.T(), DevWorkerOptions{ @@ -462,13 +592,13 @@ func (s *SharedServerSuite) TestDeployment_Ramping() { res := s.Execute( "worker", "deployment", "describe-version", "--address", s.Address(), - "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildId, + "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildID, ) assert.NoError(t, res.Err) res = s.Execute( "worker", "deployment", "describe-version", "--address", s.Address(), - "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildId, + "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildID, ) assert.NoError(t, res.Err) }, 30*time.Second, 100*time.Millisecond) @@ -476,7 +606,7 @@ func (s *SharedServerSuite) TestDeployment_Ramping() { res := s.Execute( "worker", "deployment", "set-current-version", "--address", s.Address(), - "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildId, + "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildID, "--yes", ) s.NoError(res.Err) @@ -484,7 +614,7 @@ func (s *SharedServerSuite) TestDeployment_Ramping() { res = s.Execute( "worker", "deployment", "set-ramping-version", "--address", s.Address(), - "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildId, + "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildID, "--percentage", "12.5", "--yes", ) @@ -497,16 +627,16 @@ func (s *SharedServerSuite) TestDeployment_Ramping() { ) s.NoError(res.Err) s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionDeploymentName", version1.DeploymentName) - s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionBuildID", version1.BuildId) + s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionBuildID", version1.BuildID) s.ContainsOnSameLine(res.Stdout.String(), "RampingVersionDeploymentName", version2.DeploymentName) - s.ContainsOnSameLine(res.Stdout.String(), "RampingVersionBuildID", version2.BuildId) + s.ContainsOnSameLine(res.Stdout.String(), "RampingVersionBuildID", version2.BuildID) s.ContainsOnSameLine(res.Stdout.String(), "RampingVersionPercentage", "12.5") // setting version2 as current also removes the ramp res = s.Execute( "worker", "deployment", "set-current-version", "--address", s.Address(), - "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildId, + "--deployment-name", version2.DeploymentName, "--build-id", version2.BuildID, "--yes", ) s.NoError(res.Err) @@ -523,13 +653,13 @@ func (s *SharedServerSuite) TestDeployment_Ramping() { s.NoError(json.Unmarshal(res.Stdout.Bytes(), &jsonOut)) s.Equal(deploymentName, jsonOut.Name) s.Empty(jsonOut.RoutingConfig.RampingVersionBuildID) - s.Equal(version2.BuildId, jsonOut.RoutingConfig.CurrentVersionBuildID) + s.Equal(version2.BuildID, jsonOut.RoutingConfig.CurrentVersionBuildID) //same with explicit delete res = s.Execute( "worker", "deployment", "set-ramping-version", "--address", s.Address(), - "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildId, + "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildID, "--percentage", "10.1", "--yes", ) @@ -542,15 +672,15 @@ func (s *SharedServerSuite) TestDeployment_Ramping() { ) s.NoError(res.Err) s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionDeploymentName", version2.DeploymentName) - s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionBuildID", version2.BuildId) + s.ContainsOnSameLine(res.Stdout.String(), "CurrentVersionBuildID", version2.BuildID) s.ContainsOnSameLine(res.Stdout.String(), "RampingVersionDeploymentName", version1.DeploymentName) - s.ContainsOnSameLine(res.Stdout.String(), "RampingVersionBuildID", version1.BuildId) + s.ContainsOnSameLine(res.Stdout.String(), "RampingVersionBuildID", version1.BuildID) s.ContainsOnSameLine(res.Stdout.String(), "RampingVersionPercentage", "10.1") res = s.Execute( "worker", "deployment", "set-ramping-version", "--address", s.Address(), - "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildId, + "--deployment-name", version1.DeploymentName, "--build-id", version1.BuildID, "--delete", "--yes", ) @@ -567,5 +697,75 @@ func (s *SharedServerSuite) TestDeployment_Ramping() { s.NoError(json.Unmarshal(res.Stdout.Bytes(), &jsonOut)) s.Equal(deploymentName, jsonOut.Name) s.Equal(float32(0), jsonOut.RoutingConfig.RampingVersionPercentage) - s.Equal(version2.BuildId, jsonOut.RoutingConfig.CurrentVersionBuildID) + s.Equal(version2.BuildID, jsonOut.RoutingConfig.CurrentVersionBuildID) +} + +func (s *SharedServerSuite) TestDeployment_Set_Manager_Identity() { + deploymentName := uuid.NewString() + BuildID := uuid.NewString() + testIdentity := uuid.NewString() + version := worker.WorkerDeploymentVersion{ + DeploymentName: deploymentName, + BuildID: BuildID, + } + w := s.DevServer.StartDevWorker(s.Suite.T(), DevWorkerOptions{ + Worker: worker.Options{ + DeploymentOptions: worker.DeploymentOptions{ + UseVersioning: true, + Version: version, + DefaultVersioningBehavior: workflow.VersioningBehaviorPinned, + }, + }, + }) + defer w.Stop() + + s.EventuallyWithT(func(t *assert.CollectT) { + res := s.Execute( + "worker", "deployment", "list", + "--address", s.Address(), + ) + assert.NoError(t, res.Err) + assert.Contains(t, res.Stdout.String(), deploymentName) + }, 30*time.Second, 100*time.Millisecond) + + s.EventuallyWithT(func(t *assert.CollectT) { + res := s.Execute( + "worker", "deployment", "describe-version", + "--address", s.Address(), + "--deployment-name", version.DeploymentName, "--build-id", version.BuildID, + ) + assert.NoError(t, res.Err) + }, 30*time.Second, 100*time.Millisecond) + + res := s.Execute( + "worker", "deployment", "manager-identity", "set", + "--address", s.Address(), + "--deployment-name", version.DeploymentName, "--manager-identity", testIdentity, + "--yes", + ) + s.NoError(res.Err) + + res = s.Execute( + "worker", "deployment", "describe", + "--address", s.Address(), + "--name", deploymentName, + ) + s.NoError(res.Err) + + s.ContainsOnSameLine(res.Stdout.String(), "Name", deploymentName) + s.ContainsOnSameLine(res.Stdout.String(), "ManagerIdentity", testIdentity) + + // json + res = s.Execute( + "worker", "deployment", "describe", + "--address", s.Address(), + "--name", deploymentName, + "--output", "json", + ) + s.NoError(res.Err) + + var jsonOut jsonDeploymentInfoType + s.NoError(json.Unmarshal(res.Stdout.Bytes(), &jsonOut)) + s.Equal(deploymentName, jsonOut.Name) + s.Equal(testIdentity, jsonOut.ManagerIdentity) } diff --git a/internal/temporalcli/commands.worker.go b/internal/temporalcli/commands.worker.go new file mode 100644 index 000000000..a6d8db64a --- /dev/null +++ b/internal/temporalcli/commands.worker.go @@ -0,0 +1,381 @@ +package temporalcli + +import ( + "fmt" + "strings" + "time" + + "github.com/temporalio/cli/internal/printer" + deploymentpb "go.temporal.io/api/deployment/v1" + enumspb "go.temporal.io/api/enums/v1" + workerpb "go.temporal.io/api/worker/v1" + "go.temporal.io/api/workflowservice/v1" + "google.golang.org/protobuf/types/known/durationpb" +) + +type workerListRow struct { + WorkerInstanceKey string + Status string + TaskQueue string + WorkerIdentity string + HostName string + Deployment string + HeartbeatTime time.Time + Elapsed string +} + +type workerDeploymentVersionRef struct { + DeploymentName string + BuildId string +} + +type workerHostInfo struct { + HostName string + ProcessId string + WorkerGroupingKey string + CurrentHostCPUUsage float32 + CurrentHostMemUsage float32 +} + +type workerSlotsInfo struct { + CurrentAvailableSlots int32 + CurrentUsedSlots int32 + SlotSupplierKind string + TotalProcessedTasks int32 + TotalFailedTasks int32 + LastIntervalProcessedTasks int32 + LastIntervalFailureTasks int32 +} + +type workerPollerInfo struct { + CurrentPollers int32 + LastSuccessfulPollTime time.Time + IsAutoscaling bool +} + +type pluginInfo struct { + Name string + Version string +} + +type workerDescribeDetail struct { + WorkerInstanceKey string + WorkerIdentity string + Status string + TaskQueue string + DeploymentVersion *workerDeploymentVersionRef `cli:",cardOmitEmpty"` + SdkName string + SdkVersion string + StartTime time.Time + HeartbeatTime time.Time + ElapsedSinceLastHeartbeat string + HostInfo *workerHostInfo + WorkflowTaskSlotsInfo *workerSlotsInfo `cli:",cardOmitEmpty"` + ActivityTaskSlotsInfo *workerSlotsInfo `cli:",cardOmitEmpty"` + NexusTaskSlotsInfo *workerSlotsInfo `cli:",cardOmitEmpty"` + LocalActivityTaskSlotsInfo *workerSlotsInfo `cli:",cardOmitEmpty"` + WorkflowPollerInfo *workerPollerInfo `cli:",cardOmitEmpty"` + WorkflowStickyPollerInfo *workerPollerInfo `cli:",cardOmitEmpty"` + ActivityPollerInfo *workerPollerInfo `cli:",cardOmitEmpty"` + NexusPollerInfo *workerPollerInfo `cli:",cardOmitEmpty"` + TotalStickyCacheHit int32 + TotalStickyCacheMiss int32 + CurrentStickyCacheSize int32 + Plugins []pluginInfo `cli:",cardOmitEmpty"` +} + +func (c *TemporalWorkerDescribeCommand) run(cctx *CommandContext, args []string) error { + cl, err := c.Parent.ClientOptions.dialClient(cctx) + if err != nil { + return err + } + defer cl.Close() + + resp, err := cl.WorkflowService().DescribeWorker(cctx, &workflowservice.DescribeWorkerRequest{ + Namespace: c.Parent.Namespace, + WorkerInstanceKey: c.WorkerInstanceKey, + }) + if err != nil { + return err + } + + if cctx.JSONOutput { + return cctx.Printer.PrintStructured(resp.GetWorkerInfo(), printer.StructuredOptions{}) + } + + info := resp.GetWorkerInfo() + if info == nil { + return fmt.Errorf("worker info not found in response") + } + + hb := info.GetWorkerHeartbeat() + if hb == nil { + return fmt.Errorf("worker heartbeat not found in response") + } + + formatted := formatWorkerDescribeDetail(hb) + return cctx.Printer.PrintStructured(formatted, printer.StructuredOptions{}) +} + +func (c *TemporalWorkerListCommand) run(cctx *CommandContext, args []string) error { + cl, err := c.Parent.ClientOptions.dialClient(cctx) + if err != nil { + return err + } + defer cl.Close() + + svc := cl.WorkflowService() + + limit := c.Limit + + cctx.Printer.StartList() + defer cctx.Printer.EndList() + + printOpts := printer.StructuredOptions{Table: &printer.TableOptions{}} + page := make([]*workerListRow, 0, 100) + printed := 0 + var token []byte + + for { + req := &workflowservice.ListWorkersRequest{ + Namespace: c.Parent.Namespace, + NextPageToken: token, + Query: c.Query, + } + + resp, err := svc.ListWorkers(cctx, req) + if err != nil { + return err + } + + workers := resp.GetWorkersInfo() + if cctx.JSONOutput { + for _, info := range workers { + if limit > 0 && printed >= limit { + break + } + if info == nil { + continue + } + if err := cctx.Printer.PrintStructured(info, printer.StructuredOptions{}); err != nil { + return err + } + printed++ + } + } else { + for _, info := range workers { + if limit > 0 && printed >= limit { + break + } + if info == nil { + continue + } + hb := info.GetWorkerHeartbeat() + if hb == nil { + continue + } + row := formatWorkerListRow(hb) + page = append(page, &row) + printed++ + if len(page) == cap(page) { + if err := cctx.Printer.PrintStructured(page, printOpts); err != nil { + return err + } + page = page[:0] + printOpts.Table.NoHeader = true + } + } + } + + if limit > 0 && printed >= limit { + break + } + + token = resp.GetNextPageToken() + if len(token) == 0 { + break + } + } + + if !cctx.JSONOutput { + if err := cctx.Printer.PrintStructured(page, printOpts); err != nil { + return err + } + } + + return nil +} + +func formatWorkerListRow(hb *workerpb.WorkerHeartbeat) workerListRow { + if hb == nil { + return workerListRow{} + } + + row := workerListRow{ + WorkerInstanceKey: hb.GetWorkerInstanceKey(), + Status: workerStatusToString(hb.GetStatus()), + TaskQueue: hb.GetTaskQueue(), + WorkerIdentity: hb.GetWorkerIdentity(), + HeartbeatTime: timestampToTime(hb.GetHeartbeatTime()), + Elapsed: durationToString(hb.GetElapsedSinceLastHeartbeat()), + } + + if host := hb.GetHostInfo(); host != nil { + row.HostName = host.GetHostName() + } + if dv := hb.GetDeploymentVersion(); dv != nil { + row.Deployment = formatDeploymentVersion(dv) + } + + return row +} + +func formatWorkerDescribeDetail(hb *workerpb.WorkerHeartbeat) workerDescribeDetail { + if hb == nil { + return workerDescribeDetail{} + } + + detail := workerDescribeDetail{ + WorkerInstanceKey: hb.GetWorkerInstanceKey(), + WorkerIdentity: hb.GetWorkerIdentity(), + Status: workerStatusToString(hb.GetStatus()), + TaskQueue: hb.GetTaskQueue(), + SdkName: hb.GetSdkName(), + SdkVersion: hb.GetSdkVersion(), + StartTime: timestampToTime(hb.GetStartTime()), + HeartbeatTime: timestampToTime(hb.GetHeartbeatTime()), + ElapsedSinceLastHeartbeat: durationToString(hb.GetElapsedSinceLastHeartbeat()), + HostInfo: formatWorkerHostInfo(hb.GetHostInfo()), + WorkflowTaskSlotsInfo: formatWorkerSlots(hb.GetWorkflowTaskSlotsInfo()), + ActivityTaskSlotsInfo: formatWorkerSlots(hb.GetActivityTaskSlotsInfo()), + NexusTaskSlotsInfo: formatWorkerSlots(hb.GetNexusTaskSlotsInfo()), + LocalActivityTaskSlotsInfo: formatWorkerSlots(hb.GetLocalActivitySlotsInfo()), + WorkflowPollerInfo: formatWorkerPoller(hb.GetWorkflowPollerInfo()), + WorkflowStickyPollerInfo: formatWorkerPoller(hb.GetWorkflowStickyPollerInfo()), + ActivityPollerInfo: formatWorkerPoller(hb.GetActivityPollerInfo()), + NexusPollerInfo: formatWorkerPoller(hb.GetNexusPollerInfo()), + TotalStickyCacheHit: hb.GetTotalStickyCacheHit(), + TotalStickyCacheMiss: hb.GetTotalStickyCacheMiss(), + CurrentStickyCacheSize: hb.GetCurrentStickyCacheSize(), + Plugins: formatPlugins(hb.GetPlugins()), + } + + if dv := hb.GetDeploymentVersion(); dv != nil { + if dv.GetDeploymentName() != "" || dv.GetBuildId() != "" { + detail.DeploymentVersion = &workerDeploymentVersionRef{ + DeploymentName: dv.GetDeploymentName(), + BuildId: dv.GetBuildId(), + } + } + } + + return detail +} + +func workerStatusToString(status enumspb.WorkerStatus) string { + statusStr := status.String() + statusStr = strings.TrimPrefix(statusStr, "WORKER_STATUS_") + if statusStr == "" { + return "unspecified" + } + return statusStr +} + +func formatDeploymentVersion(dv *deploymentpb.WorkerDeploymentVersion) string { + if dv == nil { + return "" + } + depName := dv.GetDeploymentName() + buildID := dv.GetBuildId() + switch { + case depName != "" && buildID != "": + return depName + "@" + buildID + case depName != "": + return depName + case buildID != "": + return buildID + default: + return "" + } +} + +func formatWorkerHostInfo(info *workerpb.WorkerHostInfo) *workerHostInfo { + if info == nil { + return nil + } + formatted := &workerHostInfo{ + HostName: info.GetHostName(), + ProcessId: info.GetProcessId(), + WorkerGroupingKey: info.GetWorkerGroupingKey(), + CurrentHostCPUUsage: info.GetCurrentHostCpuUsage(), + CurrentHostMemUsage: info.GetCurrentHostMemUsage(), + } + if formatted.HostName == "" && formatted.ProcessId == "" && formatted.WorkerGroupingKey == "" && + formatted.CurrentHostCPUUsage == 0 && formatted.CurrentHostMemUsage == 0 { + return nil + } + return formatted +} + +func formatWorkerSlots(info *workerpb.WorkerSlotsInfo) *workerSlotsInfo { + if info == nil { + return nil + } + formatted := &workerSlotsInfo{ + CurrentAvailableSlots: info.GetCurrentAvailableSlots(), + CurrentUsedSlots: info.GetCurrentUsedSlots(), + SlotSupplierKind: info.GetSlotSupplierKind(), + TotalProcessedTasks: info.GetTotalProcessedTasks(), + TotalFailedTasks: info.GetTotalFailedTasks(), + LastIntervalProcessedTasks: info.GetLastIntervalProcessedTasks(), + LastIntervalFailureTasks: info.GetLastIntervalFailureTasks(), + } + if formatted.CurrentAvailableSlots == 0 && formatted.CurrentUsedSlots == 0 && formatted.SlotSupplierKind == "Fixed" && + formatted.TotalProcessedTasks == 0 && formatted.TotalFailedTasks == 0 && + formatted.LastIntervalProcessedTasks == 0 && formatted.LastIntervalFailureTasks == 0 { + return nil + } + return formatted +} + +func formatWorkerPoller(info *workerpb.WorkerPollerInfo) *workerPollerInfo { + if info == nil { + return nil + } + formatted := &workerPollerInfo{ + CurrentPollers: info.GetCurrentPollers(), + LastSuccessfulPollTime: timestampToTime(info.GetLastSuccessfulPollTime()), + IsAutoscaling: info.GetIsAutoscaling(), + } + if formatted.CurrentPollers == 0 && formatted.LastSuccessfulPollTime.IsZero() && !formatted.IsAutoscaling { + return nil + } + return formatted +} + +func formatPlugins(plugins []*workerpb.PluginInfo) []pluginInfo { + if len(plugins) == 0 { + return nil + } + formatted := make([]pluginInfo, 0, len(plugins)) + for _, plugin := range plugins { + if plugin == nil { + continue + } + formatted = append(formatted, pluginInfo{ + Name: plugin.GetName(), + Version: plugin.GetVersion(), + }) + } + if len(formatted) == 0 { + return nil + } + return formatted +} + +func durationToString(d *durationpb.Duration) string { + if d == nil { + return "" + } + return d.AsDuration().String() +} diff --git a/internal/temporalcli/commands.worker_test.go b/internal/temporalcli/commands.worker_test.go new file mode 100644 index 000000000..65505186f --- /dev/null +++ b/internal/temporalcli/commands.worker_test.go @@ -0,0 +1,182 @@ +package temporalcli_test + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + deploymentpb "go.temporal.io/api/deployment/v1" + enumspb "go.temporal.io/api/enums/v1" + workerpb "go.temporal.io/api/worker/v1" + "go.temporal.io/api/workflowservice/v1" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type heartbeatJSON struct { + Heartbeat workerHeartbeat `json:"workerHeartbeat"` +} + +type workerHeartbeat struct { + WorkerInstanceKey string `json:"workerInstanceKey"` + WorkerIdentity string `json:"workerIdentity"` + HostInfo hostInfo `json:"hostInfo"` + TaskQueue string `json:"taskQueue"` + DeploymentVersion deploymentVersion `json:"deploymentVersion"` + SdkName string `json:"sdkName"` + SdkVersion string `json:"sdkVersion"` + Status string `json:"status"` + StartTime time.Time `json:"startTime"` + HeartbeatTime time.Time `json:"heartbeatTime"` + ElapsedSinceLastHeartbeat string `json:"elapsedSinceLastHeartbeat"` +} +type hostInfo struct { + HostName string `json:"hostName"` +} + +type deploymentVersion struct { + DeploymentName string `json:"deploymentName"` + BuildId string `json:"buildId"` +} + +func (s *SharedServerSuite) TestWorkerHeartbeat_List() { + heartbeat, err := s.recordWorkerHeartbeat() + s.NoError(err) + + sentHeartbeatJSON := s.waitForWorkerListJSON(heartbeat.TaskQueue, heartbeat.WorkerInstanceKey) + + s.Equal(heartbeat.WorkerIdentity, sentHeartbeatJSON.Heartbeat.WorkerIdentity) + s.Equal(heartbeat.TaskQueue, sentHeartbeatJSON.Heartbeat.TaskQueue) + s.Equal(heartbeat.WorkerInstanceKey, sentHeartbeatJSON.Heartbeat.WorkerInstanceKey) + s.False(sentHeartbeatJSON.Heartbeat.HeartbeatTime.IsZero()) + + res := s.Execute( + "worker", "list", + "--address", s.Address(), + "--query", fmt.Sprintf("TaskQueue=\"%s\"", heartbeat.TaskQueue), + ) + s.NoError(res.Err) + s.ContainsOnSameLine(res.Stdout.String(), heartbeat.WorkerInstanceKey, heartbeat.TaskQueue, heartbeat.WorkerIdentity) +} + +func (s *SharedServerSuite) TestWorkerHeartbeat_Describe() { + heartbeat, err := s.recordWorkerHeartbeat() + s.NoError(err) + + listJSON := s.waitForWorkerListJSON(heartbeat.TaskQueue, heartbeat.WorkerInstanceKey) + + var detail heartbeatJSON + res := s.Execute( + "worker", "describe", + "--address", s.Address(), + "--worker-instance-key", heartbeat.WorkerInstanceKey, + "--output", "json", + ) + s.NoError(res.Err) + + var parsed heartbeatJSON + s.NoError(json.Unmarshal(res.Stdout.Bytes(), &parsed)) + + s.Equal(heartbeat.WorkerInstanceKey, parsed.Heartbeat.WorkerInstanceKey) + s.Equal(heartbeat.TaskQueue, parsed.Heartbeat.TaskQueue) + s.Equal(heartbeat.WorkerIdentity, parsed.Heartbeat.WorkerIdentity) + s.False(parsed.Heartbeat.StartTime.IsZero()) + s.False(parsed.Heartbeat.HeartbeatTime.IsZero()) + + detail = parsed + + s.Equal(heartbeat.WorkerInstanceKey, detail.Heartbeat.WorkerInstanceKey) + s.Equal(heartbeat.TaskQueue, detail.Heartbeat.TaskQueue) + s.Equal(heartbeat.WorkerIdentity, detail.Heartbeat.WorkerIdentity) + // Ensure that JSON output for list and describe are identical + s.Equal(listJSON, detail) + + res = s.Execute( + "worker", "describe", + "--address", s.Address(), + "--worker-instance-key", heartbeat.WorkerInstanceKey, + ) + s.NoError(res.Err) + s.Contains(res.Stdout.String(), heartbeat.WorkerIdentity) +} + +func (s *SharedServerSuite) recordWorkerHeartbeat() (*workerpb.WorkerHeartbeat, error) { + workerIdentity := "heartbeat-list-" + uuid.NewString() + taskQueue := "heartbeat-tq-" + uuid.NewString() + instanceKey := "heartbeat-instance-" + uuid.NewString() + hostName := "heartbeat-host-" + uuid.NewString() + deploymentName := "heartbeat-deployment-" + uuid.NewString() + buildID := "heartbeat-build-" + uuid.NewString() + status := enumspb.WORKER_STATUS_RUNNING + startTime := time.Now().UTC().Add(-1 * time.Minute).Truncate(time.Second) + heartbeatTime := time.Now().UTC().Truncate(time.Second) + + hb := &workerpb.WorkerHeartbeat{ + WorkerInstanceKey: instanceKey, + WorkerIdentity: workerIdentity, + TaskQueue: taskQueue, + DeploymentVersion: &deploymentpb.WorkerDeploymentVersion{ + DeploymentName: deploymentName, + BuildId: buildID, + }, + HostInfo: &workerpb.WorkerHostInfo{ + HostName: hostName, + }, + SdkName: "temporal-go-sdk", + SdkVersion: "v0.0.0-test", + Status: status, + StartTime: timestamppb.New(startTime), + HeartbeatTime: timestamppb.New(heartbeatTime), + ElapsedSinceLastHeartbeat: durationpb.New(5 * time.Second), + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + _, err := s.Client.WorkflowService().RecordWorkerHeartbeat(ctx, &workflowservice.RecordWorkerHeartbeatRequest{ + Namespace: s.Namespace(), + Identity: identity, + WorkerHeartbeat: []*workerpb.WorkerHeartbeat{hb}, + }) + s.NoError(err) + + return hb, nil +} + +func (s *SharedServerSuite) waitForWorkerListJSON(taskQueue, instanceKey string) heartbeatJSON { + var row heartbeatJSON + + s.EventuallyWithT(func(t *assert.CollectT) { + res := s.Execute( + "worker", "list", + "--address", s.Address(), + "--query", fmt.Sprintf("TaskQueue=\"%s\"", taskQueue), + "--output", "json", + ) + assert.NoError(t, res.Err) + + var parsed []heartbeatJSON + if !assert.NoError(t, json.Unmarshal(res.Stdout.Bytes(), &parsed)) { + return + } + if len(parsed) == 0 { + assert.Fail(t, "no workers returned yet") + return + } + + for _, candidate := range parsed { + if candidate.Heartbeat.WorkerInstanceKey == instanceKey { + row = candidate + return + } + } + + assert.Failf(t, "worker instance key not yet found", "%s", instanceKey) + }, 5*time.Second, 200*time.Millisecond) + + s.NotEmpty(row.Heartbeat.WorkerInstanceKey) + return row +} diff --git a/temporalcli/commands.workflow.go b/internal/temporalcli/commands.workflow.go similarity index 96% rename from temporalcli/commands.workflow.go rename to internal/temporalcli/commands.workflow.go index 6b9b8714b..8915f4e35 100644 --- a/temporalcli/commands.workflow.go +++ b/internal/temporalcli/commands.workflow.go @@ -12,7 +12,7 @@ import ( "github.com/fatih/color" "github.com/google/uuid" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/api/batch/v1" "go.temporal.io/api/common/v1" deploymentpb "go.temporal.io/api/deployment/v1" @@ -129,7 +129,7 @@ func (c *TemporalWorkflowUpdateOptionsCommand) run(cctx *CommandContext, args [] Value: &client.PinnedVersioningOverride{ Version: worker.WorkerDeploymentVersion{ DeploymentName: c.VersioningOverrideDeploymentName, - BuildId: c.VersioningOverrideBuildId, + BuildID: c.VersioningOverrideBuildId, }, }, } @@ -190,12 +190,12 @@ func (c *TemporalWorkflowUpdateOptionsCommand) run(cctx *CommandContext, args [] func (c *TemporalWorkflowMetadataCommand) run(cctx *CommandContext, _ []string) error { return queryHelper(cctx, c.Parent, PayloadInputOptions{}, - metadataQueryName, c.RejectCondition, c.WorkflowReferenceOptions) + metadataQueryName, nil, c.RejectCondition, c.WorkflowReferenceOptions) } func (c *TemporalWorkflowQueryCommand) run(cctx *CommandContext, args []string) error { return queryHelper(cctx, c.Parent, c.PayloadInputOptions, - c.Name, c.RejectCondition, c.WorkflowReferenceOptions) + c.Name, c.Headers, c.RejectCondition, c.WorkflowReferenceOptions) } func (c *TemporalWorkflowSignalCommand) run(cctx *CommandContext, args []string) error { @@ -210,6 +210,11 @@ func (c *TemporalWorkflowSignalCommand) run(cctx *CommandContext, args []string) return err } + cctx.Context, err = contextWithHeaders(cctx.Context, c.Headers) + if err != nil { + return err + } + exec, batchReq, err := c.workflowExecOrBatch(cctx, c.Parent.Namespace, cl, singleOrBatchOverrides{}) // Run single or batch @@ -246,7 +251,7 @@ func (c *TemporalWorkflowSignalCommand) run(cctx *CommandContext, args []string) func (c *TemporalWorkflowStackCommand) run(cctx *CommandContext, args []string) error { return queryHelper(cctx, c.Parent, PayloadInputOptions{}, - "__stack_trace", c.RejectCondition, c.WorkflowReferenceOptions) + "__stack_trace", nil, c.RejectCondition, c.WorkflowReferenceOptions) } func (c *TemporalWorkflowTerminateCommand) run(cctx *CommandContext, _ []string) error { @@ -449,6 +454,11 @@ func workflowUpdateHelper(cctx *CommandContext, WaitForStage: waitForStage, } + cctx.Context, err = contextWithHeaders(cctx.Context, updateStartOpts.Headers) + if err != nil { + return err + } + updateHandle, err := cl.UpdateWorkflow(cctx, request) if err != nil { return fmt.Errorf("unable to update workflow: %w", err) @@ -580,6 +590,7 @@ func queryHelper(cctx *CommandContext, parent *TemporalWorkflowCommand, inputOpts PayloadInputOptions, queryType string, + headers []string, rejectCondition StringEnum, execution WorkflowReferenceOptions, ) error { @@ -605,6 +616,11 @@ func queryHelper(cctx *CommandContext, return fmt.Errorf("invalid query reject condition: %v, valid values are: 'not_open', 'not_completed_cleanly'", rejectCondition) } + cctx.Context, err = contextWithHeaders(cctx.Context, headers) + if err != nil { + return err + } + result, err := cl.WorkflowService().QueryWorkflow(cctx, &workflowservice.QueryWorkflowRequest{ Namespace: parent.Namespace, Execution: &common.WorkflowExecution{WorkflowId: execution.WorkflowId, RunId: execution.RunId}, @@ -698,17 +714,17 @@ func versioningOverrideToProto(versioningOverride client.VersioningOverride) *wo case *client.PinnedVersioningOverride: return &workflowpb.VersioningOverride{ Behavior: enums.VERSIONING_BEHAVIOR_PINNED, - PinnedVersion: fmt.Sprintf("%s.%s", v.Version.DeploymentName, v.Version.BuildId), + PinnedVersion: fmt.Sprintf("%s.%s", v.Version.DeploymentName, v.Version.BuildID), Deployment: &deploymentpb.Deployment{ SeriesName: v.Version.DeploymentName, - BuildId: v.Version.BuildId, + BuildId: v.Version.BuildID, }, Override: &workflowpb.VersioningOverride_Pinned{ Pinned: &workflowpb.VersioningOverride_PinnedOverride{ Behavior: workflowpb.VersioningOverride_PINNED_OVERRIDE_BEHAVIOR_PINNED, Version: &deploymentpb.WorkerDeploymentVersion{ DeploymentName: v.Version.DeploymentName, - BuildId: v.Version.BuildId, + BuildId: v.Version.BuildID, }, }, }, diff --git a/temporalcli/commands.workflow_exec.go b/internal/temporalcli/commands.workflow_exec.go similarity index 98% rename from temporalcli/commands.workflow_exec.go rename to internal/temporalcli/commands.workflow_exec.go index fce190b25..37b2a54ad 100644 --- a/temporalcli/commands.workflow_exec.go +++ b/internal/temporalcli/commands.workflow_exec.go @@ -14,7 +14,7 @@ import ( "github.com/fatih/color" "github.com/google/uuid" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/api/common/v1" commonpb "go.temporal.io/api/common/v1" "go.temporal.io/api/enums/v1" @@ -158,6 +158,11 @@ func (c *TemporalWorkflowSignalWithStartCommand) run(cctx *CommandContext, _ []s searchAttr = &common.SearchAttributes{IndexedFields: fields} } + cctx.Context, err = contextWithHeaders(cctx.Context, c.SharedWorkflowStartOptions.Headers) + if err != nil { + return err + } + // We have to use the raw signal service call here because the Go SDK's // signal-with-start call doesn't accept multiple signal arguments. resp, err := cl.WorkflowService().SignalWithStartWorkflowExecution( @@ -363,6 +368,10 @@ func executeUpdateWithStartWorkflow( if err != nil { return nil, err } + cctx.Context, err = contextWithHeaders(cctx.Context, sharedWfOpts.Headers) + if err != nil { + return nil, err + } startOp := cl.NewWithStartWorkflowOperation( clStartWfOpts, @@ -521,6 +530,12 @@ func (c *TemporalWorkflowCommand) startWorkflow( if err != nil { return nil, err } + + cctx.Context, err = contextWithHeaders(cctx.Context, sharedWorkflowOpts.Headers) + if err != nil { + return nil, err + } + run, err := cl.ExecuteWorkflow(cctx, startOpts, sharedWorkflowOpts.Type, input...) if err != nil { return nil, fmt.Errorf("failed starting workflow: %w", err) diff --git a/temporalcli/commands.workflow_exec_test.go b/internal/temporalcli/commands.workflow_exec_test.go similarity index 97% rename from temporalcli/commands.workflow_exec_test.go rename to internal/temporalcli/commands.workflow_exec_test.go index 690b4823f..62d809d3b 100644 --- a/temporalcli/commands.workflow_exec_test.go +++ b/internal/temporalcli/commands.workflow_exec_test.go @@ -104,6 +104,33 @@ func (s *SharedServerSuite) TestWorkflow_Start_StartDelay() { ) } +func (s *SharedServerSuite) TestWorkflow_Start_With_headers() { + res := s.Execute( + "workflow", "start", + "--address", s.Address(), + "--headers", "id=123", + "--task-queue", s.Worker().Options.TaskQueue, + "--type", "DevWorkflow", + "--workflow-id", "id123", + "-i", `["val1", "val2"]`, + ) + s.NoError(res.Err) + eventIter := s.Client.GetWorkflowHistory(s.Context, "id123", "", false, enums.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT) + for eventIter.HasNext() { + event, err := eventIter.Next() + s.NoError(err) + if event.EventType == enums.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED { + headers := event.GetWorkflowExecutionStartedEventAttributes().GetHeader() + payload := headers.Fields["id"] + s.NotNil(payload) + var val int + err := converter.GetDefaultDataConverter().FromPayload(payload, &val) + s.NoError(err) + s.Equal(123, val) + } + } +} + func (s *SharedServerSuite) TestWorkflow_Execute_SimpleSuccess() { // Text s.Worker().OnDevWorkflow(func(ctx workflow.Context, input any) (any, error) { diff --git a/temporalcli/commands.workflow_fix.go b/internal/temporalcli/commands.workflow_fix.go similarity index 100% rename from temporalcli/commands.workflow_fix.go rename to internal/temporalcli/commands.workflow_fix.go diff --git a/temporalcli/commands.workflow_pause.go b/internal/temporalcli/commands.workflow_pause.go similarity index 100% rename from temporalcli/commands.workflow_pause.go rename to internal/temporalcli/commands.workflow_pause.go diff --git a/temporalcli/commands.workflow_reset.go b/internal/temporalcli/commands.workflow_reset.go similarity index 99% rename from temporalcli/commands.workflow_reset.go rename to internal/temporalcli/commands.workflow_reset.go index 9a4b6f135..2da4e0633 100644 --- a/temporalcli/commands.workflow_reset.go +++ b/internal/temporalcli/commands.workflow_reset.go @@ -7,14 +7,13 @@ import ( "strings" "github.com/google/uuid" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/api/batch/v1" "go.temporal.io/api/common/v1" "go.temporal.io/api/enums/v1" workflow "go.temporal.io/api/workflow/v1" "go.temporal.io/api/workflowservice/v1" "go.temporal.io/sdk/client" - - "github.com/temporalio/cli/temporalcli/internal/printer" ) func (c *TemporalWorkflowResetCommand) run(cctx *CommandContext, _ []string) error { diff --git a/temporalcli/commands.workflow_reset_test.go b/internal/temporalcli/commands.workflow_reset_test.go similarity index 100% rename from temporalcli/commands.workflow_reset_test.go rename to internal/temporalcli/commands.workflow_reset_test.go diff --git a/temporalcli/commands.workflow_reset_update_options.go b/internal/temporalcli/commands.workflow_reset_update_options.go similarity index 100% rename from temporalcli/commands.workflow_reset_update_options.go rename to internal/temporalcli/commands.workflow_reset_update_options.go diff --git a/temporalcli/commands.workflow_reset_update_options_test.go b/internal/temporalcli/commands.workflow_reset_update_options_test.go similarity index 100% rename from temporalcli/commands.workflow_reset_update_options_test.go rename to internal/temporalcli/commands.workflow_reset_update_options_test.go diff --git a/temporalcli/commands.workflow_test.go b/internal/temporalcli/commands.workflow_test.go similarity index 98% rename from temporalcli/commands.workflow_test.go rename to internal/temporalcli/commands.workflow_test.go index 775b7000e..44119740f 100644 --- a/temporalcli/commands.workflow_test.go +++ b/internal/temporalcli/commands.workflow_test.go @@ -13,7 +13,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/temporalio/cli/temporalcli" + "github.com/temporalio/cli/internal/temporalcli" "go.temporal.io/api/enums/v1" workflowpb "go.temporal.io/api/workflow/v1" "go.temporal.io/api/workflowservice/v1" @@ -474,11 +474,11 @@ func (s *SharedServerSuite) TestWorkflow_Batch_Update_Options_Versioning_Overrid deploymentName := uuid.NewString() version1 := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName, - BuildId: buildId1, + BuildID: buildId1, } version2 := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName, - BuildId: buildId2, + BuildID: buildId2, } // Workflow that waits to be canceled. waitingWorkflow := func(ctx workflow.Context) error { @@ -511,7 +511,7 @@ func (s *SharedServerSuite) TestWorkflow_Batch_Update_Options_Versioning_Overrid "worker", "deployment", "describe-version", "--address", s.Address(), "--deployment-name", version1.DeploymentName, - "--build-id", version1.BuildId, + "--build-id", version1.BuildID, ) assert.NoError(t, res.Err) }, 30*time.Second, 100*time.Millisecond) @@ -520,7 +520,7 @@ func (s *SharedServerSuite) TestWorkflow_Batch_Update_Options_Versioning_Overrid "worker", "deployment", "set-current-version", "--address", s.Address(), "--deployment-name", version1.DeploymentName, - "--build-id", version1.BuildId, + "--build-id", version1.BuildID, "--yes", ) s.NoError(res.Err) @@ -551,7 +551,7 @@ func (s *SharedServerSuite) TestWorkflow_Batch_Update_Options_Versioning_Overrid ) assert.NoError(t, res.Err) assert.Contains(t, res.Stdout.String(), version1.DeploymentName) - assert.Contains(t, res.Stdout.String(), version1.BuildId) + assert.Contains(t, res.Stdout.String(), version1.BuildID) assert.Contains(t, res.Stdout.String(), "Pinned") } }, 30*time.Second, 100*time.Millisecond) @@ -572,7 +572,7 @@ func (s *SharedServerSuite) TestWorkflow_Batch_Update_Options_Versioning_Overrid "--query", "CustomKeywordField = '"+searchAttr+"'", "--versioning-override-behavior", "pinned", "--versioning-override-deployment-name", version2.DeploymentName, - "--versioning-override-build-id", version2.BuildId, + "--versioning-override-build-id", version2.BuildID, ) s.NoError(res.Err) time.Sleep(10 * time.Second) @@ -602,7 +602,7 @@ func (s *SharedServerSuite) TestWorkflow_Batch_Update_Options_Versioning_Overrid require.NotNil(t, versioningInfo.VersioningOverride) asPinned := versioningInfo.VersioningOverride.Override.(*workflowpb.VersioningOverride_Pinned) require.Equal(t, version2.DeploymentName, asPinned.Pinned.Version.DeploymentName) - require.Equal(t, version2.BuildId, asPinned.Pinned.Version.BuildId) + require.Equal(t, version2.BuildID, asPinned.Pinned.Version.BuildId) require.Equal(t, enums.VERSIONING_BEHAVIOR_PINNED, versioningInfo.Behavior) } }, 10*time.Second, 100*time.Millisecond) @@ -615,11 +615,11 @@ func (s *SharedServerSuite) TestWorkflow_Update_Options_Versioning_Override() { deploymentName := uuid.NewString() version1 := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName, - BuildId: buildId1, + BuildID: buildId1, } version2 := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName, - BuildId: buildId2, + BuildID: buildId2, } // Workflow that waits to be canceled. @@ -653,7 +653,7 @@ func (s *SharedServerSuite) TestWorkflow_Update_Options_Versioning_Override() { "worker", "deployment", "describe-version", "--address", s.Address(), "--deployment-name", version1.DeploymentName, - "--build-id", version1.BuildId, + "--build-id", version1.BuildID, ) assert.NoError(t, res.Err) }, 30*time.Second, 100*time.Millisecond) @@ -662,7 +662,7 @@ func (s *SharedServerSuite) TestWorkflow_Update_Options_Versioning_Override() { "worker", "deployment", "set-current-version", "--address", s.Address(), "--deployment-name", version1.DeploymentName, - "--build-id", version1.BuildId, + "--build-id", version1.BuildID, "--yes", ) s.NoError(res.Err) @@ -683,7 +683,7 @@ func (s *SharedServerSuite) TestWorkflow_Update_Options_Versioning_Override() { ) assert.NoError(t, res.Err) assert.Contains(t, res.Stdout.String(), version1.DeploymentName) - assert.Contains(t, res.Stdout.String(), version1.BuildId) + assert.Contains(t, res.Stdout.String(), version1.BuildID) assert.Contains(t, res.Stdout.String(), "Pinned") }, 30*time.Second, 100*time.Millisecond) @@ -693,7 +693,7 @@ func (s *SharedServerSuite) TestWorkflow_Update_Options_Versioning_Override() { "-w", run.GetID(), "--versioning-override-behavior", "pinned", "--versioning-override-deployment-name", version2.DeploymentName, - "--versioning-override-build-id", version2.BuildId, + "--versioning-override-build-id", version2.BuildID, ) s.NoError(res.Err) @@ -706,7 +706,7 @@ func (s *SharedServerSuite) TestWorkflow_Update_Options_Versioning_Override() { s.ContainsOnSameLine(res.Stdout.String(), "OverrideBehavior", "Pinned") s.ContainsOnSameLine(res.Stdout.String(), "OverridePinnedVersionDeploymentName", version2.DeploymentName) - s.ContainsOnSameLine(res.Stdout.String(), "OverridePinnedVersionBuildId", version2.BuildId) + s.ContainsOnSameLine(res.Stdout.String(), "OverridePinnedVersionBuildId", version2.BuildID) // Using only build-id res = s.Execute( diff --git a/temporalcli/commands.workflow_trace.go b/internal/temporalcli/commands.workflow_trace.go similarity index 97% rename from temporalcli/commands.workflow_trace.go rename to internal/temporalcli/commands.workflow_trace.go index d6fc41a70..74062a836 100644 --- a/temporalcli/commands.workflow_trace.go +++ b/internal/temporalcli/commands.workflow_trace.go @@ -7,8 +7,8 @@ import ( "time" "github.com/fatih/color" - "github.com/temporalio/cli/temporalcli/internal/printer" - "github.com/temporalio/cli/temporalcli/internal/tracer" + "github.com/temporalio/cli/internal/printer" + "github.com/temporalio/cli/internal/tracer" "go.temporal.io/api/enums/v1" "go.temporal.io/sdk/client" ) diff --git a/temporalcli/commands.workflow_trace_test.go b/internal/temporalcli/commands.workflow_trace_test.go similarity index 100% rename from temporalcli/commands.workflow_trace_test.go rename to internal/temporalcli/commands.workflow_trace_test.go diff --git a/temporalcli/commands.workflow_view.go b/internal/temporalcli/commands.workflow_view.go similarity index 99% rename from temporalcli/commands.workflow_view.go rename to internal/temporalcli/commands.workflow_view.go index 23cdccc94..3952e7892 100644 --- a/temporalcli/commands.workflow_view.go +++ b/internal/temporalcli/commands.workflow_view.go @@ -7,7 +7,7 @@ import ( "time" "github.com/fatih/color" - "github.com/temporalio/cli/temporalcli/internal/printer" + "github.com/temporalio/cli/internal/printer" "go.temporal.io/api/common/v1" "go.temporal.io/api/enums/v1" "go.temporal.io/api/failure/v1" diff --git a/temporalcli/commands.workflow_view_test.go b/internal/temporalcli/commands.workflow_view_test.go similarity index 98% rename from temporalcli/commands.workflow_view_test.go rename to internal/temporalcli/commands.workflow_view_test.go index 1e9150755..980d4b632 100644 --- a/temporalcli/commands.workflow_view_test.go +++ b/internal/temporalcli/commands.workflow_view_test.go @@ -9,12 +9,11 @@ import ( "strings" "time" - "go.temporal.io/api/common/v1" - "github.com/google/uuid" "github.com/nexus-rpc/sdk-go/nexus" "github.com/stretchr/testify/assert" - "github.com/temporalio/cli/temporalcli" + "github.com/temporalio/cli/internal/temporalcli" + "go.temporal.io/api/common/v1" "go.temporal.io/api/enums/v1" nexuspb "go.temporal.io/api/nexus/v1" "go.temporal.io/api/operatorservice/v1" @@ -560,7 +559,7 @@ func (s *SharedServerSuite) TestWorkflow_Describe_Deployment() { } version := worker.WorkerDeploymentVersion{ DeploymentName: deploymentName, - BuildId: buildId, + BuildID: buildId, } w := s.DevServer.StartDevWorker(s.Suite.T(), DevWorkerOptions{ Worker: worker.Options{ @@ -588,7 +587,7 @@ func (s *SharedServerSuite) TestWorkflow_Describe_Deployment() { "worker", "deployment", "describe-version", "--address", s.Address(), "--deployment-name", version.DeploymentName, - "--build-id", version.BuildId, + "--build-id", version.BuildID, ) assert.NoError(t, res.Err) }, 30*time.Second, 100*time.Millisecond) @@ -597,7 +596,7 @@ func (s *SharedServerSuite) TestWorkflow_Describe_Deployment() { "worker", "deployment", "set-current-version", "--address", s.Address(), "--deployment-name", version.DeploymentName, - "--build-id", version.BuildId, + "--build-id", version.BuildID, "--yes", ) s.NoError(res.Err) @@ -618,14 +617,14 @@ func (s *SharedServerSuite) TestWorkflow_Describe_Deployment() { ) assert.NoError(t, res.Err) assert.Contains(t, res.Stdout.String(), version.DeploymentName) - assert.Contains(t, res.Stdout.String(), version.BuildId) + assert.Contains(t, res.Stdout.String(), version.BuildID) assert.Contains(t, res.Stdout.String(), "Pinned") }, 30*time.Second, 100*time.Millisecond) out := res.Stdout.String() s.ContainsOnSameLine(out, "Behavior", "Pinned") s.ContainsOnSameLine(out, "DeploymentName", version.DeploymentName) - s.ContainsOnSameLine(out, "BuildId", version.BuildId) + s.ContainsOnSameLine(out, "BuildId", version.BuildID) // json res = s.Execute( @@ -640,7 +639,7 @@ func (s *SharedServerSuite) TestWorkflow_Describe_Deployment() { s.NoError(temporalcli.UnmarshalProtoJSONWithOptions(res.Stdout.Bytes(), &jsonResp, true)) versioningInfo := jsonResp.WorkflowExecutionInfo.VersioningInfo s.Equal("Pinned", versioningInfo.Behavior.String()) - s.Equal(version.BuildId, versioningInfo.DeploymentVersion.BuildId) + s.Equal(version.BuildID, versioningInfo.DeploymentVersion.BuildId) s.Equal(version.DeploymentName, versioningInfo.DeploymentVersion.DeploymentName) s.Nil(versioningInfo.VersioningOverride) } diff --git a/temporalcli/commandsgen/commands.yml b/internal/temporalcli/commands.yaml similarity index 94% rename from temporalcli/commandsgen/commands.yml rename to internal/temporalcli/commands.yaml index 9eba88a47..4089d214b 100644 --- a/temporalcli/commandsgen/commands.yml +++ b/internal/temporalcli/commands.yaml @@ -38,7 +38,7 @@ # temporal env set \ # --env prod \ # --key tls-cert-path \ -# --value /home/my-user/certs/cluster.cert` +# --value /home/my-user/certs/cluster.cert # ``` # * No: `temporal env set prod.tls-cert-path /home/my-user/certs/cluster.cert`. # * Split invocation samples to multiple lines. @@ -113,7 +113,7 @@ # * name, summary, and descrption are required fields. All other fields are optional. # * Available option types are `bool`, `duration`, `int`, `float`, `string`, `string[]`, -# `string-enum`, string-enum[], or `timestamp`. +# `string-enum`, `string-enum[]`, or `timestamp`. # * Include a new-line after each command entry. # OPTION SET OVERVIEW @@ -166,8 +166,8 @@ commands: description: | File path to read TOML config from, defaults to `$CONFIG_PATH/temporal/temporal.toml` where `$CONFIG_PATH` is defined - as `$HOME/.config` on Unix, "$HOME/Library/Application Support" on - macOS, and %AppData% on Windows. + as `$HOME/.config` on Unix, `$HOME/Library/Application Support` on + macOS, and `%AppData%` on Windows. experimental: true implied-env: TEMPORAL_CONFIG_FILE - name: profile @@ -787,7 +787,7 @@ commands: Remove a property within a profile. ``` - temporal env delete \ + temporal config delete \ --prop tls.client_cert_path ``` options: @@ -805,7 +805,7 @@ commands: Remove a full profile entirely. The `--profile` must be set explicitly. ``` - temporal env delete-profile \ + temporal config delete-profile \ --profile my-profile ``` @@ -886,6 +886,8 @@ commands: keywords: - worker - worker deployment + - worker list + - worker describe tags: - Temporal CLI @@ -942,6 +944,7 @@ commands: - worker deployment set-ramping-version - worker deployment delete-version - worker deployment update-metadata-version + - worker deployment manager-identity - name: temporal worker deployment describe summary: Show properties of a Worker Deployment @@ -1136,6 +1139,9 @@ commands: - name: ignore-missing-task-queues type: bool description: Override protection to accidentally remove task queues. + - name: allow-no-pollers + type: bool + description: Override protection and set version as current even if it has no pollers. - name: yes short: y type: bool @@ -1203,6 +1209,9 @@ commands: - name: ignore-missing-task-queues type: bool description: Override protection to accidentally remove task queues. + - name: allow-no-pollers + type: bool + description: Override protection and set version as ramping even if it has no pollers. - name: yes short: y type: bool @@ -1242,7 +1251,7 @@ commands: description: | Set deployment metadata using `KEY="VALUE"` pairs. Keys must be identifiers, and values must be JSON values. - For example: 'YourKey={"your": "value"}'. + For example: `YourKey={"your": "value"}` Can be passed multiple times. - name: remove-entries type: string[] @@ -1250,6 +1259,184 @@ commands: Keys of entries to be deleted from metadata. Can be passed multiple times. + - name: temporal worker deployment manager-identity + summary: Manager Identity commands change the `ManagerIdentity` of a Worker Deployment + description: | + ``` + +---------------------------------------------------------------------+ + | CAUTION: Worker Deployment is experimental. Deployment commands are | + | subject to change. | + +---------------------------------------------------------------------+ + ``` + + Manager Identity commands change the `ManagerIdentity` of a Worker Deployment: + + ``` + temporal worker deployment manager-identity [command] [options] + ``` + + When present, `ManagerIdentity` is the identity of the user that has the + exclusive right to make changes to this Worker Deployment. Empty by default. + When set, users whose identity does not match the `ManagerIdentity` will not + be able to change the Worker Deployment. + + This is especially useful in environments where multiple users (such as CLI + users and automated controllers) may interact with the same Worker Deployment. + `ManagerIdentity` allows different users to communicate with one another about + who is expected to make changes to the Worker Deployment. + + The current Manager Identity is returned with `describe`: + ``` + temporal worker deployment describe \ + --deployment-name YourDeploymentName + ``` + + docs: + description-header: >- + Temporal Deployment Manager Identity commands enable set and unset + operations on the manager identity field of Worker Deployments. + keywords: + - worker deployment manager-identity set + - worker deployment manager-identity unset + + - name: temporal worker deployment manager-identity set + summary: Set the Manager Identity of a Worker Deployment + description: | + ``` + +---------------------------------------------------------------------+ + | CAUTION: Worker Deployment is experimental. Deployment commands are | + | subject to change. | + +---------------------------------------------------------------------+ + ``` + + Set the `ManagerIdentity` of a Worker Deployment given its Deployment Name. + + When present, `ManagerIdentity` is the identity of the user that has the + exclusive right to make changes to this Worker Deployment. Empty by default. + When set, users whose identity does not match the `ManagerIdentity` will not + be able to change the Worker Deployment. + + This is especially useful in environments where multiple users (such as CLI + users and automated controllers) may interact with the same Worker Deployment. + `ManagerIdentity` allows different users to communicate with one another about + who is expected to make changes to the Worker Deployment. + + ``` + temporal worker deployment manager-identity set [options] + ``` + + For example: + + ``` + temporal worker deployment manager-identity set \ + --deployment-name DeploymentName \ + --self \ + --identity YourUserIdentity # optional, populated by CLI if not provided + ``` + + Sets the Manager Identity of the Deployment to the identity of the user making + this request. If you don't specifically pass an identity field, the CLI will + generate your identity for you. + + For example: + ``` + temporal worker deployment manager-identity set \ + --deployment-name DeploymentName \ + --manager-identity NewManagerIdentity + ``` + + Sets the Manager Identity of the Deployment to any string. + + options: + - name: manager-identity + type: string + description: New Manager Identity. Required unless --self is specified. + - name: self + type: bool + description: Set Manager Identity to the identity of the user submitting this request. Required unless --manager-identity is specified. + - name: deployment-name + type: string + description: Name for a Worker Deployment. Required. + - name: yes + short: y + type: bool + description: Don't prompt to confirm set Manager Identity. + + - name: temporal worker deployment manager-identity unset + summary: Unset the Manager Identity of a Worker Deployment + description: | + ``` + +---------------------------------------------------------------------+ + | CAUTION: Worker Deployment is experimental. Deployment commands are | + | subject to change. | + +---------------------------------------------------------------------+ + ``` + + Unset the `ManagerIdentity` of a Worker Deployment given its Deployment Name. + + When present, `ManagerIdentity` is the identity of the user that has the + exclusive right to make changes to this Worker Deployment. Empty by default. + When set, users whose identity does not match the `ManagerIdentity` will not + be able to change the Worker Deployment. + + This is especially useful in environments where multiple users (such as CLI + users and automated controllers) may interact with the same Worker Deployment. + `ManagerIdentity` allows different users to communicate with one another about + who is expected to make changes to the Worker Deployment. + + ``` + temporal worker deployment manager-identity unset [options] + ``` + + For example: + + ``` + temporal worker deployment manager-identity unset \ + --deployment-name YourDeploymentName + ``` + + Clears the Manager Identity field for a given Deployment. + + options: + - name: deployment-name + type: string + description: Name for a Worker Deployment. Required. + - name: yes + short: y + type: bool + description: Don't prompt to confirm unset Manager Identity. + + - name: temporal worker list + summary: List worker status information in a specific namespace (EXPERIMENTAL) + description: | + Get a list of workers to the specified namespace. + + ``` + temporal worker list --namespace YourNamespace --query 'TaskQueue="YourTaskQueue"' + ``` + options: + - name: query + short: q + type: string + description: Content for an SQL-like `QUERY` List Filter. + - name: limit + type: int + description: Maximum number of workers to display. + + - name: temporal worker describe + summary: Returns information about a specific worker (EXPERIMENTAL) + description: | + Look up information of a specific worker. + + ``` + temporal worker describe --namespace YourNamespace --worker-instance-key YourKey + ``` + options: + - name: worker-instance-key + type: string + description: Worker instance key to describe. + required: true + - name: temporal env summary: Manage environments description: | @@ -1584,7 +1771,7 @@ commands: temporal operator namespace create \ --namespace YourNewNamespaceName \ [options] - ```` + ``` Create a Namespace with multi-region data replication: @@ -1621,7 +1808,7 @@ commands: description: | Namespace data as `KEY=VALUE` pairs. Keys must be identifiers, and values must be JSON values. - For example: 'YourKey={"your": "value"}'. + For example: `YourKey={"your": "value"}` Can be passed multiple times. - name: description type: string @@ -1761,7 +1948,7 @@ commands: description: | Namespace data as `KEY=VALUE` pairs. Keys must be identifiers, and values must be JSON values. - For example: 'YourKey={"your": "value"}'. + For example: `YourKey={"your": "value"}` Can be passed multiple times. - name: description type: string @@ -2413,7 +2600,7 @@ commands: description: | Dynamic configuration value using `KEY=VALUE` pairs. Keys must be identifiers, and values must be JSON values. - For example: 'YourKey="YourString"'. + For example: `YourKey="YourString"` Can be passed multiple times. - name: log-config type: bool @@ -2515,7 +2702,7 @@ commands: ``` temporal task-queue describe \ --task-queue YourTaskQueue \ - --build-id "YourBuildId" \ + --select-build-id "YourBuildId" \ --report-reachability ``` @@ -3514,7 +3701,7 @@ commands: ``` temporal workflow list \ - --query YourQuery` + --query YourQuery ``` Visit https://docs.temporal.io/visibility to read more about Search Attributes @@ -4564,6 +4751,13 @@ option-sets: description: | Limit batch's requests per second. Only allowed if query is present. + - name: headers + type: string[] + description: | + Temporal workflow headers in 'KEY=VALUE' format. + Keys must be identifiers, and values must be JSON values. + May be passed multiple times to set multiple Temporal headers. + Note: These are workflow headers, not gRPC headers. - name: shared-workflow-start options: @@ -4606,6 +4800,13 @@ option-sets: Keys must be identifiers, and values must be JSON values. For example: 'YourKey={"your": "value"}'. Can be passed multiple times. + - name: headers + type: string[] + description: | + Temporal workflow headers in 'KEY=VALUE' format. + Keys must be identifiers, and values must be JSON values. + May be passed multiple times to set multiple Temporal headers. + Note: These are workflow headers, not gRPC headers. - name: memo type: string[] description: | @@ -4733,6 +4934,13 @@ option-sets: description: | Run ID. If unset, looks for an Update against the currently-running Workflow Execution. + - name: headers + type: string[] + description: | + Temporal workflow headers in 'KEY=VALUE' format. + Keys must be identifiers, and values must be JSON values. + May be passed multiple times to set multiple Temporal headers. + Note: These are workflow headers, not gRPC headers. - name: update-targeting options: @@ -4796,6 +5004,13 @@ option-sets: enum-values: - not_open - not_completed_cleanly + - name: headers + type: string[] + description: | + Temporal workflow headers in 'KEY=VALUE' format. + Keys must be identifiers, and values must be JSON values. + May be passed multiple times to set multiple Temporal headers. + Note: These are workflow headers, not gRPC headers. - name: workflow-update-options options: diff --git a/temporalcli/commands_test.go b/internal/temporalcli/commands_test.go similarity index 98% rename from temporalcli/commands_test.go rename to internal/temporalcli/commands_test.go index 5d1e50289..e23eccacc 100644 --- a/temporalcli/commands_test.go +++ b/internal/temporalcli/commands_test.go @@ -18,16 +18,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "github.com/temporalio/cli/internal/devserver" + "github.com/temporalio/cli/internal/temporalcli" "go.temporal.io/api/enums/v1" "go.temporal.io/api/operatorservice/v1" "go.temporal.io/sdk/client" "go.temporal.io/sdk/temporal" "go.temporal.io/sdk/worker" "go.temporal.io/sdk/workflow" - - "github.com/temporalio/cli/temporalcli" - "github.com/temporalio/cli/temporalcli/devserver" - "google.golang.org/grpc" ) @@ -238,6 +236,9 @@ func (s *SharedServerSuite) SetupSuite() { // this is overridden since we don't want caching to be enabled // while testing DescribeTaskQueue behaviour related to versioning "matching.TaskQueueInfoByBuildIdTTL": 0 * time.Second, + // worker heartbeating + "frontend.WorkerHeartbeatsEnabled": true, + "frontend.ListWorkersEnabled": true, }, }, }) diff --git a/temporalcli/payload.go b/internal/temporalcli/payload.go similarity index 100% rename from temporalcli/payload.go rename to internal/temporalcli/payload.go diff --git a/temporalcli/strings.go b/internal/temporalcli/strings.go similarity index 50% rename from temporalcli/strings.go rename to internal/temporalcli/strings.go index 5904cb5ea..8eab2c288 100644 --- a/temporalcli/strings.go +++ b/internal/temporalcli/strings.go @@ -8,66 +8,6 @@ import ( "strings" ) -type StringEnum struct { - Allowed []string - Value string - ChangedFromDefault bool -} - -func NewStringEnum(allowed []string, value string) StringEnum { - return StringEnum{Allowed: allowed, Value: value} -} - -func (s *StringEnum) String() string { return s.Value } - -func (s *StringEnum) Set(p string) error { - for _, allowed := range s.Allowed { - if p == allowed { - s.Value = p - s.ChangedFromDefault = true - return nil - } - } - return fmt.Errorf("%v is not one of required values of %v", p, strings.Join(s.Allowed, ", ")) -} - -func (*StringEnum) Type() string { return "string" } - -type StringEnumArray struct { - // maps lower case value to original case - Allowed map[string]string - // values in original case - Values []string -} - -func NewStringEnumArray(allowed []string, values []string) StringEnumArray { - // maps lower case value to original case so we can do case insensitive comparison, - // while maintaining original case - var allowedMap = make(map[string]string) - for _, str := range allowed { - allowedMap[strings.ToLower(str)] = str - } - - return StringEnumArray{Allowed: allowedMap, Values: values} -} - -func (s *StringEnumArray) String() string { return strings.Join(s.Values, ",") } - -func (s *StringEnumArray) Set(p string) error { - val, ok := s.Allowed[strings.ToLower(p)] - if !ok { - values := make([]string, 0, len(s.Allowed)) - for _, v := range s.Allowed { - values = append(values, v) - } - return fmt.Errorf("invalid value: %s, allowed values are: %s", p, strings.Join(values, ", ")) - } - s.Values = append(s.Values, val) - return nil -} - -func (*StringEnumArray) Type() string { return "string" } - func stringToProtoEnum[T ~int32](s string, maps ...map[string]int32) (T, error) { // Go over each map looking, if not there, use first map to build set of // strings required diff --git a/temporalcli/internal/tracer/execution_icons.go b/internal/tracer/execution_icons.go similarity index 100% rename from temporalcli/internal/tracer/execution_icons.go rename to internal/tracer/execution_icons.go diff --git a/temporalcli/internal/tracer/execution_state.go b/internal/tracer/execution_state.go similarity index 100% rename from temporalcli/internal/tracer/execution_state.go rename to internal/tracer/execution_state.go diff --git a/temporalcli/internal/tracer/execution_state_test.go b/internal/tracer/execution_state_test.go similarity index 100% rename from temporalcli/internal/tracer/execution_state_test.go rename to internal/tracer/execution_state_test.go diff --git a/temporalcli/internal/tracer/execution_test.go b/internal/tracer/execution_test.go similarity index 100% rename from temporalcli/internal/tracer/execution_test.go rename to internal/tracer/execution_test.go diff --git a/temporalcli/internal/tracer/execution_tmpls.go b/internal/tracer/execution_tmpls.go similarity index 100% rename from temporalcli/internal/tracer/execution_tmpls.go rename to internal/tracer/execution_tmpls.go diff --git a/temporalcli/internal/tracer/tail_buffer.go b/internal/tracer/tail_buffer.go similarity index 100% rename from temporalcli/internal/tracer/tail_buffer.go rename to internal/tracer/tail_buffer.go diff --git a/temporalcli/internal/tracer/tail_buffer_test.go b/internal/tracer/tail_buffer_test.go similarity index 100% rename from temporalcli/internal/tracer/tail_buffer_test.go rename to internal/tracer/tail_buffer_test.go diff --git a/temporalcli/internal/tracer/templates/activity.tmpl b/internal/tracer/templates/activity.tmpl similarity index 100% rename from temporalcli/internal/tracer/templates/activity.tmpl rename to internal/tracer/templates/activity.tmpl diff --git a/temporalcli/internal/tracer/templates/common.tmpl b/internal/tracer/templates/common.tmpl similarity index 100% rename from temporalcli/internal/tracer/templates/common.tmpl rename to internal/tracer/templates/common.tmpl diff --git a/temporalcli/internal/tracer/templates/timer.tmpl b/internal/tracer/templates/timer.tmpl similarity index 100% rename from temporalcli/internal/tracer/templates/timer.tmpl rename to internal/tracer/templates/timer.tmpl diff --git a/temporalcli/internal/tracer/templates/workflow.tmpl b/internal/tracer/templates/workflow.tmpl similarity index 100% rename from temporalcli/internal/tracer/templates/workflow.tmpl rename to internal/tracer/templates/workflow.tmpl diff --git a/temporalcli/internal/tracer/term_size_unix.go b/internal/tracer/term_size_unix.go similarity index 100% rename from temporalcli/internal/tracer/term_size_unix.go rename to internal/tracer/term_size_unix.go diff --git a/temporalcli/internal/tracer/term_size_windows.go b/internal/tracer/term_size_windows.go similarity index 100% rename from temporalcli/internal/tracer/term_size_windows.go rename to internal/tracer/term_size_windows.go diff --git a/temporalcli/internal/tracer/term_writer.go b/internal/tracer/term_writer.go similarity index 100% rename from temporalcli/internal/tracer/term_writer.go rename to internal/tracer/term_writer.go diff --git a/temporalcli/internal/tracer/term_writer_test.go b/internal/tracer/term_writer_test.go similarity index 100% rename from temporalcli/internal/tracer/term_writer_test.go rename to internal/tracer/term_writer_test.go diff --git a/temporalcli/internal/tracer/workflow_execution_update.go b/internal/tracer/workflow_execution_update.go similarity index 100% rename from temporalcli/internal/tracer/workflow_execution_update.go rename to internal/tracer/workflow_execution_update.go diff --git a/temporalcli/internal/tracer/workflow_execution_update_test.go b/internal/tracer/workflow_execution_update_test.go similarity index 100% rename from temporalcli/internal/tracer/workflow_execution_update_test.go rename to internal/tracer/workflow_execution_update_test.go diff --git a/temporalcli/internal/tracer/workflow_state_worker.go b/internal/tracer/workflow_state_worker.go similarity index 100% rename from temporalcli/internal/tracer/workflow_state_worker.go rename to internal/tracer/workflow_state_worker.go diff --git a/temporalcli/internal/tracer/workflow_tracer.go b/internal/tracer/workflow_tracer.go similarity index 100% rename from temporalcli/internal/tracer/workflow_tracer.go rename to internal/tracer/workflow_tracer.go diff --git a/temporalcli/commandsgen/docs.go b/temporalcli/commandsgen/docs.go deleted file mode 100644 index 2cd912a94..000000000 --- a/temporalcli/commandsgen/docs.go +++ /dev/null @@ -1,192 +0,0 @@ -package commandsgen - -import ( - "bytes" - "fmt" - "regexp" - "sort" - "strings" -) - -func GenerateDocsFiles(commands Commands) (map[string][]byte, error) { - optionSetMap := make(map[string]OptionSets) - for i, optionSet := range commands.OptionSets { - optionSetMap[optionSet.Name] = commands.OptionSets[i] - } - - w := &docWriter{ - fileMap: make(map[string]*bytes.Buffer), - optionSetMap: optionSetMap, - allCommands: commands.CommandList, - } - - // sorted ascending by full name of command (activity complete, batch list, etc) - for _, cmd := range commands.CommandList { - if err := cmd.writeDoc(w); err != nil { - return nil, fmt.Errorf("failed writing docs for command %s: %w", cmd.FullName, err) - } - } - - // Format and return - var finalMap = make(map[string][]byte) - for key, buf := range w.fileMap { - finalMap[key] = buf.Bytes() - } - return finalMap, nil -} - -type docWriter struct { - allCommands []Command - fileMap map[string]*bytes.Buffer - optionSetMap map[string]OptionSets - optionsStack [][]Option -} - -func (c *Command) writeDoc(w *docWriter) error { - w.processOptions(c) - - // If this is a root command, write a new file - depth := c.depth() - if depth == 1 { - w.writeCommand(c) - } else if depth > 1 { - w.writeSubcommand(c) - } - return nil -} - -func (w *docWriter) writeCommand(c *Command) { - fileName := c.fileName() - w.fileMap[fileName] = &bytes.Buffer{} - w.fileMap[fileName].WriteString("---\n") - w.fileMap[fileName].WriteString("id: " + fileName + "\n") - w.fileMap[fileName].WriteString("title: Temporal CLI " + fileName + " command reference\n") - w.fileMap[fileName].WriteString("sidebar_label: " + fileName + "\n") - w.fileMap[fileName].WriteString("description: " + c.Docs.DescriptionHeader + "\n") - w.fileMap[fileName].WriteString("toc_max_heading_level: 4\n") - - w.fileMap[fileName].WriteString("keywords:\n") - for _, keyword := range c.Docs.Keywords { - w.fileMap[fileName].WriteString(" - " + keyword + "\n") - } - w.fileMap[fileName].WriteString("tags:\n") - for _, tag := range c.Docs.Tags { - w.fileMap[fileName].WriteString(" - " + tag + "\n") - } - w.fileMap[fileName].WriteString("---") - w.fileMap[fileName].WriteString("\n\n") - w.fileMap[fileName].WriteString("{/* NOTE: This is an auto-generated file. Any edit to this file will be overwritten.\n") - w.fileMap[fileName].WriteString("This file is generated from https://github.com/temporalio/cli/blob/main/temporalcli/commandsgen/commands.yml */}\n") -} - -func (w *docWriter) writeSubcommand(c *Command) { - fileName := c.fileName() - prefix := strings.Repeat("#", c.depth()) - w.fileMap[fileName].WriteString(prefix + " " + c.leafName() + "\n\n") - w.fileMap[fileName].WriteString(c.Description + "\n\n") - - if w.isLeafCommand(c) { - w.fileMap[fileName].WriteString("Use the following options to change the behavior of this command.\n\n") - - // gather options from command and all options aviailable from parent commands - var options = make([]Option, 0) - var globalOptions = make([]Option, 0) - for i, o := range w.optionsStack { - if i == len(w.optionsStack)-1 { - options = append(options, o...) - } else { - globalOptions = append(globalOptions, o...) - } - } - - // alphabetize options - sort.Slice(options, func(i, j int) bool { - return options[i].Name < options[j].Name - }) - - sort.Slice(globalOptions, func(i, j int) bool { - return globalOptions[i].Name < globalOptions[j].Name - }) - - w.writeOptions("Flags", options, c) - w.writeOptions("Global Flags", globalOptions, c) - - } -} - -func (w *docWriter) writeOptions(prefix string, options []Option, c *Command) { - if len(options) == 0 { - return - } - - fileName := c.fileName() - - w.fileMap[fileName].WriteString(fmt.Sprintf("**%s:**\n\n", prefix)) - - for _, o := range options { - // option name and alias - w.fileMap[fileName].WriteString(fmt.Sprintf("**--%s**", o.Name)) - if len(o.Short) > 0 { - w.fileMap[fileName].WriteString(fmt.Sprintf(", **-%s**", o.Short)) - } - w.fileMap[fileName].WriteString(fmt.Sprintf(" _%s_\n\n", o.Type)) - - // description - w.fileMap[fileName].WriteString(encodeJSONExample(o.Description)) - if o.Required { - w.fileMap[fileName].WriteString(" Required.") - } - if len(o.EnumValues) > 0 { - w.fileMap[fileName].WriteString(fmt.Sprintf(" Accepted values: %s.", strings.Join(o.EnumValues, ", "))) - } - if len(o.Default) > 0 { - w.fileMap[fileName].WriteString(fmt.Sprintf(` (default "%s")`, o.Default)) - } - w.fileMap[fileName].WriteString("\n\n") - - if o.Experimental { - w.fileMap[fileName].WriteString(":::note" + "\n\n") - w.fileMap[fileName].WriteString("Option is experimental." + "\n\n") - w.fileMap[fileName].WriteString(":::" + "\n\n") - } - } -} - -func (w *docWriter) processOptions(c *Command) { - // Pop options from stack if we are moving up a level - if len(w.optionsStack) >= len(strings.Split(c.FullName, " ")) { - w.optionsStack = w.optionsStack[:len(w.optionsStack)-1] - } - var options []Option - options = append(options, c.Options...) - - // Maintain stack of options available from parent commands - for _, set := range c.OptionSets { - optionSet, ok := w.optionSetMap[set] - if !ok { - panic(fmt.Sprintf("invalid option set %v used", set)) - } - optionSetOptions := optionSet.Options - options = append(options, optionSetOptions...) - } - - w.optionsStack = append(w.optionsStack, options) -} - -func (w *docWriter) isLeafCommand(c *Command) bool { - for _, maybeSubCmd := range w.allCommands { - if maybeSubCmd.isSubCommand(c) { - return false - } - } - return true -} - -func encodeJSONExample(v string) string { - // example: 'YourKey={"your": "value"}' - // results in an mdx acorn rendering error - // and wrapping in backticks lets it render - re := regexp.MustCompile(`('[a-zA-Z0-9]*={.*}')`) - v = re.ReplaceAllString(v, "`$1`") - return v -} diff --git a/temporalcli/duration.go b/temporalcli/duration.go deleted file mode 100644 index eef73473d..000000000 --- a/temporalcli/duration.go +++ /dev/null @@ -1,30 +0,0 @@ -package temporalcli - -import ( - "time" - - "go.temporal.io/server/common/primitives/timestamp" -) - -type Duration time.Duration - -func (d Duration) Duration() time.Duration { - return time.Duration(d) -} - -func (d *Duration) String() string { - return d.Duration().String() -} - -func (d *Duration) Set(s string) error { - p, err := timestamp.ParseDuration(s) - if err != nil { - return err - } - *d = Duration(p) - return nil -} - -func (d *Duration) Type() string { - return "duration" -} diff --git a/temporalcli/internal/cmd/gen-commands/main.go b/temporalcli/internal/cmd/gen-commands/main.go deleted file mode 100644 index 817ff2968..000000000 --- a/temporalcli/internal/cmd/gen-commands/main.go +++ /dev/null @@ -1,41 +0,0 @@ -package main - -import ( - "fmt" - "log" - "os" - "path/filepath" - "runtime" - - "github.com/temporalio/cli/temporalcli/commandsgen" -) - -func main() { - if err := run(); err != nil { - log.Fatal(err) - } -} - -func run() error { - // Get commands dir - _, file, _, _ := runtime.Caller(0) - commandsDir := filepath.Join(file, "../../../../") - - // Parse YAML - cmds, err := commandsgen.ParseCommands() - if err != nil { - return fmt.Errorf("failed parsing markdown: %w", err) - } - - // Generate code - b, err := commandsgen.GenerateCommandsCode("temporalcli", cmds) - if err != nil { - return fmt.Errorf("failed generating code: %w", err) - } - - // Write - if err := os.WriteFile(filepath.Join(commandsDir, "commands.gen.go"), b, 0644); err != nil { - return fmt.Errorf("failed writing file: %w", err) - } - return nil -} diff --git a/temporalcli/internal/cmd/gen-docs/main.go b/temporalcli/internal/cmd/gen-docs/main.go deleted file mode 100644 index 94ff20ba4..000000000 --- a/temporalcli/internal/cmd/gen-docs/main.go +++ /dev/null @@ -1,50 +0,0 @@ -package main - -import ( - "fmt" - "log" - "os" - "path/filepath" - "runtime" - - "github.com/temporalio/cli/temporalcli/commandsgen" -) - -func main() { - if err := run(); err != nil { - log.Fatal(err) - } -} - -func run() error { - // Get commands dir - _, file, _, _ := runtime.Caller(0) - docsDir := filepath.Join(file, "../../../../docs/") - - err := os.MkdirAll(docsDir, os.ModePerm) - if err != nil { - log.Fatalf("Error creating directory: %v", err) - } - - // Parse markdown - cmds, err := commandsgen.ParseCommands() - if err != nil { - return fmt.Errorf("failed parsing markdown: %w", err) - } - - // Generate docs - b, err := commandsgen.GenerateDocsFiles(cmds) - if err != nil { - return err - } - - // Write - for filename, content := range b { - filePath := filepath.Join(docsDir, filename+".mdx") - if err := os.WriteFile(filePath, content, 0644); err != nil { - return fmt.Errorf("failed writing file: %w", err) - } - } - - return nil -}