Merge branch 'main' into ss/versioning-override-version-presence

This commit is contained in:
Shivam
2025-12-19 10:26:17 -05:00
committed by GitHub
34 changed files with 2130 additions and 215 deletions

View File

@@ -0,0 +1,47 @@
name: Build Binaries
description: Build Temporal binaries using GoReleaser
inputs:
snapshot:
description: "Use snapshot mode (true) or release mode (false). Only applies to release command (build always uses snapshot)."
required: false
default: "true"
single-arch:
description: "Single architecture to build (amd64 or arm64, empty for all). Only used with build command, ignored if release is true."
required: false
default: ""
release:
description: "Use release command (true) or build command (false). When true, single-arch is ignored and snapshot is respected."
required: false
default: "false"
runs:
using: composite
steps:
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
cache: true
- name: Run GoReleaser (release)
if: inputs.release == 'true'
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: v2.13.1
args: release ${{ inputs.snapshot == 'true' && '--snapshot --skip=publish' || '' }} --clean
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Run GoReleaser (build)
if: inputs.release != 'true'
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: v2.13.1
args: build --snapshot ${{ inputs.single-arch != '' && '--single-target' || '' }}
env:
GITHUB_TOKEN: ${{ github.token }}
GOOS: ${{ inputs.single-arch != '' && 'linux' || '' }}
GOARCH: ${{ inputs.single-arch }}

View File

@@ -0,0 +1,133 @@
name: Build Docker Images
description: |
Build Temporal Docker images from binaries.
Prerequisites:
- The build-binaries action must run before this action to produce the binaries.
inputs:
push:
description: "Push images to Docker Hub"
required: false
default: "false"
tag-latest:
description: "Tag images as latest"
required: false
default: "false"
platform:
description: "Single platform to build (e.g., linux/amd64)"
required: false
default: ""
load:
description: "Load image into local Docker daemon (only works with linux/amd64 - the runner architecture)"
required: false
default: "false"
cli-version:
description: "Temporal CLI version to download"
required: false
default: "1.5.1"
alpine-tag:
description: "Alpine base image tag with digest"
required: false
default: "3.23@sha256:c78ded0fee4493809c8ca71d4a6057a46237763d952fae15ea418f6d14137f2d"
dockerhub-username:
description: "Docker Hub username"
required: false
dockerhub-token:
description: "Docker Hub token"
required: false
runs:
using: composite
steps:
- name: Build docker-build-helper
shell: bash
working-directory: ${{ github.workspace }}/.github/actions/build-docker-images/scripts
run: |
go build -o docker-build-helper .
- name: Validate and sanitize branch name for Docker tag
id: sanitize-tag
shell: bash
working-directory: ${{ github.workspace }}
run: |
.github/actions/build-docker-images/scripts/docker-build-helper sanitize-tag
- name: Organize binaries for Docker
id: organize-binaries
shell: bash
working-directory: ${{ github.workspace }}
env:
PLATFORM: ${{ inputs.platform }}
run: |
.github/actions/build-docker-images/scripts/docker-build-helper organize-binaries
- name: Download Temporal CLI
shell: bash
working-directory: ${{ github.workspace }}
env:
AVAILABLE_ARCHS: ${{ steps.organize-binaries.outputs.available-archs }}
CLI_VERSION: ${{ inputs.cli-version }}
run: |
.github/actions/build-docker-images/scripts/docker-build-helper download-cli
- name: Extract server version from binary
id: extract-version
shell: bash
working-directory: ${{ github.workspace }}
run: |
.github/actions/build-docker-images/scripts/docker-build-helper extract-version
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
if: inputs.push == 'true'
uses: docker/login-action@v3
with:
username: ${{ inputs.dockerhub-username }}
password: ${{ inputs.dockerhub-token }}
- name: Build Docker images
if: inputs.push != 'true'
shell: bash
working-directory: ${{ github.workspace }}
env:
IMAGE_REPO: temporaliotest
IMAGE_SHA_TAG: ${{ github.sha }}
IMAGE_BRANCH_TAG: ${{ steps.sanitize-tag.outputs.tag }}
TEMPORAL_SHA: ${{ github.sha }}
TAG_LATEST: ${{ inputs.tag-latest }}
ALPINE_TAG: ${{ inputs.alpine-tag }}
SERVER_VERSION: ${{ steps.extract-version.outputs.server-version }}
run: |
if [ -n "${{ inputs.platform }}" ]; then
docker buildx bake \
--set "*.platform=${{ inputs.platform }}" \
${{ inputs.load == 'true' && '--load' || '' }} \
-f docker/docker-bake.hcl \
server admin-tools
else
docker buildx bake \
${{ inputs.load == 'true' && '--load' || '' }} \
-f docker/docker-bake.hcl \
server admin-tools
fi
- name: Build and push Docker images
if: inputs.push == 'true'
shell: bash
working-directory: ${{ github.workspace }}
env:
IMAGE_REPO: temporaliotest
IMAGE_SHA_TAG: ${{ github.sha }}
IMAGE_BRANCH_TAG: ${{ steps.sanitize-tag.outputs.tag }}
TEMPORAL_SHA: ${{ github.sha }}
TAG_LATEST: ${{ inputs.tag-latest }}
ALPINE_TAG: ${{ inputs.alpine-tag }}
SERVER_VERSION: ${{ steps.extract-version.outputs.server-version }}
run: |
docker buildx bake \
--push \
-f docker/docker-bake.hcl \
server admin-tools

View File

@@ -0,0 +1 @@
docker-build-helper

View File

@@ -0,0 +1,561 @@
package main
import (
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
)
var validArchs = []string{"amd64", "arm64"}
// defaultCliVersion should be updated to the latest cli version
const defaultCliVersion = "1.5.1"
func main() {
if len(os.Args) < 2 {
fmt.Fprintf(os.Stderr, "Usage: %s <command>\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Commands:\n")
fmt.Fprintf(os.Stderr, " sanitize-tag - Sanitize branch name for Docker tag\n")
fmt.Fprintf(os.Stderr, " organize-binaries - Organize binaries for Docker\n")
fmt.Fprintf(os.Stderr, " download-cli - Download Temporal CLI\n")
fmt.Fprintf(os.Stderr, " extract-version - Extract version from temporal-server binary\n")
os.Exit(1)
}
command := os.Args[1]
switch command {
case "sanitize-tag":
if err := sanitizeTag(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
case "organize-binaries":
if err := organizeBinaries(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
case "download-cli":
if err := downloadCLI(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
case "extract-version":
if err := extractVersion(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", command)
os.Exit(1)
}
}
// sanitizeTag sanitizes branch names for Docker tags
func sanitizeTag() error {
// Get GITHUB_REF from environment
ref := os.Getenv("GITHUB_REF")
if ref == "" {
return fmt.Errorf("GITHUB_REF environment variable not set")
}
// Remove refs/heads/ or refs/tags/ prefix
ref = strings.TrimPrefix(ref, "refs/heads/")
ref = strings.TrimPrefix(ref, "refs/tags/")
// Sanitize ref name first
// Replace any non-alphanumeric (except .-_) with dash
reg := regexp.MustCompile(`[^a-zA-Z0-9._-]`)
sanitizedRef := reg.ReplaceAllString(ref, "-")
// Collapse multiple consecutive dashes
multiDashReg := regexp.MustCompile(`-+`)
sanitizedRef = multiDashReg.ReplaceAllString(sanitizedRef, "-")
// Remove leading and trailing dashes
sanitizedRef = strings.Trim(sanitizedRef, "-")
// Prefix with "branch-" for branch builds
safeTag := fmt.Sprintf("branch-%s", sanitizedRef)
// Docker tags must be lowercase
safeTag = strings.ToLower(safeTag)
// Truncate to 128 characters (Docker tag limit)
if len(safeTag) > 128 {
safeTag = safeTag[:128]
}
if safeTag == "" {
return fmt.Errorf("failed to generate valid Docker tag from branch name")
}
fmt.Printf("Original: %s\n", ref)
fmt.Printf("Sanitized: %s\n", safeTag)
// Set output for GitHub Actions
if err := setOutput("tag", safeTag); err != nil {
return fmt.Errorf("failed to set output: %w", err)
}
return nil
}
// organizeBinaries organizes binaries for Docker builds
func organizeBinaries() error {
// Determine target architectures based on PLATFORM environment variable
platform := os.Getenv("PLATFORM")
var archs []string
if platform != "" {
// Parse platform (e.g., "linux/amd64" -> "amd64")
parts := strings.Split(platform, "/")
if len(parts) != 2 {
return fmt.Errorf("invalid platform format: %s (expected format: os/arch)", platform)
}
arch := parts[1]
// Check if arch is in valid list
found := false
for _, validArch := range validArchs {
if arch == validArch {
found = true
break
}
}
if !found {
return fmt.Errorf("architecture %s not in supported list: %v", arch, validArchs)
}
archs = []string{arch}
fmt.Printf("Single architecture build: %s\n", arch)
} else {
// Default to all architectures
archs = []string{"amd64", "arm64"}
fmt.Println("Multi-architecture build: amd64, arm64")
}
// Admin tool binaries (for admin-tools image)
adminToolBinaries := []string{
"temporal-cassandra-tool",
"temporal-sql-tool",
"temporal-elasticsearch-tool",
"tdbg",
}
// Server binaries (for server image)
serverBinaries := []string{
"temporal-server",
}
// All binaries to copy
binaries := append(adminToolBinaries, serverBinaries...)
// Validate architecture and binary names
archReg := regexp.MustCompile(`^[a-z0-9]+$`)
for _, arch := range archs {
if !archReg.MatchString(arch) {
return fmt.Errorf("invalid architecture name: %s", arch)
}
}
binReg := regexp.MustCompile(`^[a-z0-9-]+$`)
for _, binary := range binaries {
if !binReg.MatchString(binary) {
return fmt.Errorf("invalid binary name: %s", binary)
}
}
// Create architecture directories
for _, arch := range archs {
dir := filepath.Join("docker", "build", arch)
if err := validatePath(dir, "docker/build"); err != nil {
return err
}
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %w", dir, err)
}
}
// Map GoReleaser dist structure to build structure
archMap := map[string]string{
"amd64": "amd64_v1",
"arm64": "arm64",
}
// Copy binaries
for _, binary := range binaries {
for _, arch := range archs {
distArch := archMap[arch]
distPath := filepath.Join("dist", fmt.Sprintf("%s_linux_%s", binary, distArch), binary)
buildPath := filepath.Join("docker", "build", arch, binary)
// Validate paths before file operations
if err := validatePath(distPath, "dist"); err != nil {
return fmt.Errorf("invalid dist path: %w", err)
}
if err := validatePath(buildPath, "docker/build"); err != nil {
return fmt.Errorf("invalid build path: %w", err)
}
if _, err := os.Stat(distPath); err == nil {
if err := copyFile(distPath, buildPath); err != nil {
return fmt.Errorf("failed to copy %s to %s: %w", distPath, buildPath, err)
}
if err := os.Chmod(buildPath, 0755); err != nil {
return fmt.Errorf("failed to chmod %s: %w", buildPath, err)
}
fmt.Printf("Copied %s -> %s\n", distPath, buildPath)
} else {
return fmt.Errorf("binary not found: %s for architecture %s (expected at %s)", binary, arch, distPath)
}
}
}
// Copy schema directory for admin-tools
schemaDir := filepath.Join("docker", "build", "temporal", "schema")
if err := validatePath(schemaDir, "docker/build"); err != nil {
return err
}
if err := os.MkdirAll(schemaDir, 0755); err != nil {
return fmt.Errorf("failed to create schema directory: %w", err)
}
// Copy all schema files recursively with path validation
if _, err := os.Stat("schema"); err == nil {
if err := copyRecursive("schema", schemaDir); err != nil {
return fmt.Errorf("failed to copy schema directory: %w", err)
}
fmt.Println("Copied schema directory")
}
// Validate required binaries for Docker images
fmt.Println("\nValidating required binaries for Docker images...")
// Check which architectures have binaries
var availableArchs []string
for _, arch := range archs {
testBinary := filepath.Join("docker", "build", arch, "temporal-server")
if _, err := os.Stat(testBinary); err == nil {
availableArchs = append(availableArchs, arch)
}
}
if len(availableArchs) == 0 {
return fmt.Errorf("❌ No binaries found for any architecture")
}
fmt.Printf("Found binaries for architectures: %s\n", strings.Join(availableArchs, ", "))
// Validate that each available architecture has all required binaries
missingFiles := false
for _, arch := range availableArchs {
for _, binary := range binaries {
binaryPath := filepath.Join("docker", "build", arch, binary)
if _, err := os.Stat(binaryPath); err != nil {
fmt.Fprintf(os.Stderr, "Error: Missing %s\n", binaryPath)
missingFiles = true
}
}
}
// Validate schema directory exists
if _, err := os.Stat(filepath.Join("docker", "build", "temporal", "schema")); err != nil {
fmt.Fprintln(os.Stderr, "Error: Missing docker/build/temporal/schema directory")
missingFiles = true
}
if missingFiles {
return fmt.Errorf("❌ Binary validation failed")
}
fmt.Println("✓ All required binaries present for available architectures")
// Export available architectures for Docker build
if err := setOutput("available-archs", strings.Join(availableArchs, ",")); err != nil {
return fmt.Errorf("failed to set output: %w", err)
}
return nil
}
// downloadCLI downloads the Temporal CLI for available architectures
func downloadCLI() error {
// Get available architectures from environment or input
availableArchsStr := os.Getenv("AVAILABLE_ARCHS")
if availableArchsStr == "" {
return fmt.Errorf("AVAILABLE_ARCHS environment variable not set")
}
availableArchs := strings.Split(availableArchsStr, ",")
// Filter to only valid architectures
var validAvailableArchs []string
for _, arch := range availableArchs {
arch = strings.TrimSpace(arch)
for _, validArch := range validArchs {
if arch == validArch {
validAvailableArchs = append(validAvailableArchs, arch)
break
}
}
}
if len(validAvailableArchs) == 0 {
return fmt.Errorf("no valid architectures found in: %s", availableArchsStr)
}
for _, arch := range validAvailableArchs {
if err := downloadCLIForArch(arch); err != nil {
return fmt.Errorf("failed to download CLI for %s: %w", arch, err)
}
}
return nil
}
func downloadCLIForArch(arch string) error {
cliVersion := os.Getenv("CLI_VERSION")
if cliVersion == "" {
cliVersion = defaultCliVersion
}
tarballName := fmt.Sprintf("temporal_cli_%s_linux_%s.tar.gz", cliVersion, arch)
downloadURL := fmt.Sprintf("https://github.com/temporalio/cli/releases/download/v%s/%s", cliVersion, tarballName)
fmt.Printf("Downloading Temporal CLI v%s for %s from %s\n", cliVersion, arch, downloadURL)
tempDir := filepath.Join(os.TempDir(), fmt.Sprintf("temporal-cli-%s", arch))
tarballPath := filepath.Join(os.TempDir(), tarballName)
// Download tarball
if err := downloadFile(downloadURL, tarballPath); err != nil {
return fmt.Errorf("failed to download: %w", err)
}
defer os.Remove(tarballPath)
// Create temp directory
if err := os.MkdirAll(tempDir, 0755); err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tempDir)
// Extract tarball
cmd := exec.Command("tar", "-xzf", tarballPath, "-C", tempDir)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to extract: %w\nOutput: %s", err, string(output))
}
// Move to build directory
destDir := filepath.Join("docker", "build", arch)
if err := validatePath(destDir, "docker/build"); err != nil {
return fmt.Errorf("invalid build directory path: %w", err)
}
if err := os.MkdirAll(destDir, 0755); err != nil {
return fmt.Errorf("failed to create build directory: %w", err)
}
sourcePath := filepath.Join(tempDir, "temporal")
destPath := filepath.Join(destDir, "temporal")
if err := validatePath(destPath, "docker/build"); err != nil {
return fmt.Errorf("invalid destination path: %w", err)
}
if err := os.Rename(sourcePath, destPath); err != nil {
// If rename fails (e.g., cross-device), try copy and delete
if err := copyFile(sourcePath, destPath); err != nil {
return fmt.Errorf("failed to copy binary: %w", err)
}
os.Remove(sourcePath)
}
if err := os.Chmod(destPath, 0755); err != nil {
return fmt.Errorf("failed to chmod binary: %w", err)
}
fmt.Printf("Installed Temporal CLI to %s\n", destPath)
return nil
}
// extractVersion extracts the version from the temporal-server binary
func extractVersion() error {
// Try to find the temporal-server binary in any available architecture directory
var binaryPath string
for _, arch := range validArchs {
candidatePath := filepath.Join("docker", "build", arch, "temporal-server")
if _, err := os.Stat(candidatePath); err == nil {
binaryPath = candidatePath
break
}
}
if binaryPath == "" {
return fmt.Errorf("temporal-server binary not found in docker/build/{amd64,arm64}/")
}
fmt.Printf("Extracting version from %s\n", binaryPath)
// Run the binary with --version flag
cmd := exec.Command(binaryPath, "--version")
output, err := cmd.Output()
if err != nil {
return fmt.Errorf("failed to run %s --version: %w", binaryPath, err)
}
// Parse the version from output like "temporal version 1.29.0"
outputStr := strings.TrimSpace(string(output))
versionRegex := regexp.MustCompile(`^temporal version (\d+\.\d+\.\d+)`)
matches := versionRegex.FindStringSubmatch(outputStr)
if len(matches) < 2 {
return fmt.Errorf("failed to parse version from output: %s", outputStr)
}
version := matches[1]
fmt.Printf("Extracted version: %s\n", version)
// Set output for GitHub Actions
if err := setOutput("server-version", version); err != nil {
return fmt.Errorf("failed to set output: %w", err)
}
return nil
}
// Helper functions
func setOutput(name, value string) error {
outputFile := os.Getenv("GITHUB_OUTPUT")
if outputFile == "" {
return fmt.Errorf("GITHUB_OUTPUT environment variable not set")
}
f, err := os.OpenFile(outputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
_, err = fmt.Fprintf(f, "%s=%s\n", name, value)
return err
}
func validatePath(path, allowedPrefix string) error {
// Clean and resolve paths
normalized := filepath.Clean(path)
resolved, err := filepath.Abs(normalized)
if err != nil {
return fmt.Errorf("failed to resolve path: %w", err)
}
allowedResolved, err := filepath.Abs(allowedPrefix)
if err != nil {
return fmt.Errorf("failed to resolve allowed prefix: %w", err)
}
// Check for path traversal
if strings.Contains(normalized, "..") {
return fmt.Errorf("path traversal detected in: %s", path)
}
// Ensure path is within allowed directory
if !strings.HasPrefix(resolved, allowedResolved) {
return fmt.Errorf("path outside allowed directory: %s", path)
}
return nil
}
func copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
destFile, err := os.Create(dst)
if err != nil {
return err
}
defer destFile.Close()
_, err = io.Copy(destFile, sourceFile)
return err
}
func copyRecursive(src, dst string) error {
// Validate paths
if err := validatePath(src, "schema"); err != nil {
return err
}
if err := validatePath(dst, "docker/build"); err != nil {
return err
}
srcInfo, err := os.Stat(src)
if err != nil {
return err
}
if srcInfo.IsDir() {
// Create destination directory
if err := os.MkdirAll(dst, 0755); err != nil {
return err
}
// Read directory entries
entries, err := os.ReadDir(src)
if err != nil {
return err
}
// Copy each entry
for _, entry := range entries {
// Validate item name to prevent directory traversal
if strings.Contains(entry.Name(), "..") || strings.Contains(entry.Name(), "/") || strings.Contains(entry.Name(), "\\") {
return fmt.Errorf("invalid file name: %s", entry.Name())
}
srcPath := filepath.Join(src, entry.Name())
dstPath := filepath.Join(dst, entry.Name())
if err := copyRecursive(srcPath, dstPath); err != nil {
return err
}
}
} else {
// Copy file
if err := copyFile(src, dst); err != nil {
return err
}
}
return nil
}
func downloadFile(url, fpath string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bad status: %s", resp.Status)
}
out, err := os.Create(fpath)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
return err
}

108
.github/actions/trivy-scan/action.yml vendored Normal file
View File

@@ -0,0 +1,108 @@
name: "Trivy Security Scan"
description: "Run Trivy vulnerability scanner on Docker images"
inputs:
image-tags:
description: "Image tags (one per line or comma-separated). First tag will be used for scanning."
required: true
image-name:
description: "Image name without tag (e.g., temporaliotest/server)"
required: true
outputs:
scan-result:
description: "Result of the scan (pass or fail)"
value: ${{ steps.evaluate.outputs.result }}
critical-count:
description: "Number of critical vulnerabilities found"
value: ${{ steps.evaluate.outputs.critical-count }}
high-count:
description: "Number of high vulnerabilities found"
value: ${{ steps.evaluate.outputs.high-count }}
runs:
using: "composite"
steps:
- name: Set variables
id: vars
shell: bash
env:
IMAGE_NAME: ${{ inputs.image-name }}
IMAGE_TAGS: ${{ inputs.image-tags }}
run: |
# Get first tag
tag=$(echo "$IMAGE_TAGS" | head -1)
# Construct image reference
image_ref="${IMAGE_NAME}:${tag}"
echo "image-ref=${image_ref}" >> $GITHUB_OUTPUT
# Create safe name for artifacts
name=$(echo "$IMAGE_NAME" | sed 's/.*\///; s/[^a-zA-Z0-9._-]/-/g')
echo "safe-name=${name}" >> $GITHUB_OUTPUT
echo "artifact-name=trivy-${name}-results" >> $GITHUB_OUTPUT
- name: Install Trivy
uses: aquasecurity/setup-trivy@v0.2.4
with:
version: v0.65.0
- name: Scan Container Image
id: scan
shell: bash
env:
IMAGE_REF: ${{ steps.vars.outputs.image-ref }}
SAFE_NAME: ${{ steps.vars.outputs.safe-name }}
run: |
echo "Scanning $IMAGE_REF for CRITICAL,HIGH vulnerabilities..."
# Scan and output table format for logs
trivy image \
--severity CRITICAL,HIGH \
--format table \
--no-progress \
"$IMAGE_REF" | tee trivy-scan-${SAFE_NAME}.txt
echo ""
echo "Generating detailed JSON report..."
# Scan and output JSON format for parsing
trivy image \
--severity CRITICAL,HIGH \
--format json \
--no-progress \
"$IMAGE_REF" > trivy-scan-${SAFE_NAME}.json
- name: Evaluate scan results
id: evaluate
shell: bash
env:
SAFE_NAME: ${{ steps.vars.outputs.safe-name }}
FAIL_ON_VULNS: ${{ inputs.fail-on-vulnerabilities }}
run: |
# Count vulnerabilities by severity
CRITICAL_COUNT=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="CRITICAL")] | length' trivy-scan-${SAFE_NAME}.json)
HIGH_COUNT=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="HIGH")] | length' trivy-scan-${SAFE_NAME}.json)
MEDIUM_COUNT=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="MEDIUM")] | length' trivy-scan-${SAFE_NAME}.json)
LOW_COUNT=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity=="LOW")] | length' trivy-scan-${SAFE_NAME}.json)
echo "critical-count=${CRITICAL_COUNT}" >> $GITHUB_OUTPUT
echo "high-count=${HIGH_COUNT}" >> $GITHUB_OUTPUT
echo ""
echo "=== Vulnerability Summary ==="
echo "Critical: $CRITICAL_COUNT"
echo "High: $HIGH_COUNT"
echo "Medium: $MEDIUM_COUNT"
echo "Low: $LOW_COUNT"
echo "============================"
echo ""
# Set result status without failing
if [ "$CRITICAL_COUNT" -gt 0 ] || [ "$HIGH_COUNT" -gt 0 ]; then
echo "result=fail" >> $GITHUB_OUTPUT
echo "⚠️ Security vulnerabilities found!"
else
echo "result=pass" >> $GITHUB_OUTPUT
echo "✓ No critical or high vulnerabilities found."
fi

View File

@@ -1,23 +0,0 @@
FROM alpine:3.22@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412
ARG TARGETARCH
RUN apk add --no-cache \
ca-certificates \
tzdata && addgroup -g 1000 temporal && \
adduser -u 1000 -G temporal -D temporal
# Copy all admin tool binaries:
# - temporal (CLI)
# - temporal-server
# - temporal-cassandra-tool
# - temporal-sql-tool
# - temporal-elasticsearch-tool
# - tdbg
COPY --chmod=755 ./build/${TARGETARCH}/* /usr/local/bin/
COPY ./build/temporal/schema /etc/temporal/schema
USER temporal
CMD ["sh", "-c", "trap exit INT HUP TERM; sleep infinity"]

62
.github/workflows/build-and-publish.yml vendored Normal file
View File

@@ -0,0 +1,62 @@
name: Build and Publish
on:
push:
branches:
- main
- cloud/*
- feature/*
- release/*
permissions:
contents: read
jobs:
build-and-push-docker:
runs-on: ubuntu-latest
# Only push for main, cloud, release branches (not feature)
if: |
github.ref == 'refs/heads/main' ||
startsWith(github.ref, 'refs/heads/cloud/') ||
startsWith(github.ref, 'refs/heads/release/')
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ github.ref }}
fetch-depth: 0
- name: Build binaries
uses: ./.github/actions/build-binaries
with:
snapshot: true
- name: Build and push Docker images
uses: ./.github/actions/build-docker-images
with:
push: true
tag-latest: ${{ github.ref == 'refs/heads/main' }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
# For feature branches, just build (no push)
build-docker-feature:
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/heads/feature/')
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ github.ref }}
fetch-depth: 0
- name: Build binaries
uses: ./.github/actions/build-binaries
with:
snapshot: true
- name: Build Docker images
uses: ./.github/actions/build-docker-images
with:
push: false
tag-latest: false

View File

@@ -13,7 +13,7 @@ jobs:
steps:
- name: Validate PR description for placeholder lines or empty sections
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const pr = await github.rest.pulls.get({

View File

@@ -44,7 +44,7 @@ jobs:
owner: ${{ github.repository_owner }}
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: true
token: ${{ steps.generate_token.outputs.token }}
@@ -190,7 +190,7 @@ jobs:
owner: ${{ github.repository_owner }}
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
persist-credentials: true
token: ${{ steps.generate_token.outputs.token }}

View File

@@ -0,0 +1,105 @@
name: Manual Docker Build
on:
workflow_dispatch:
inputs:
ref:
description: "Git ref (branch, tag, or SHA) to build from"
required: true
default: "main"
cli-version:
description: "Temporal CLI version to include in images"
required: true
default: "1.5.1"
alpine-tag:
description: "Alpine base image tag with digest (e.g., 3.23@sha256:...)"
required: true
default: "3.23@sha256:c78ded0fee4493809c8ca71d4a6057a46237763d952fae15ea418f6d14137f2d"
push:
description: "Push images to Docker Hub"
required: true
type: boolean
default: false
tag-latest:
description: "Tag images as latest"
required: true
type: boolean
default: false
platform:
description: "Platform to build (leave empty for multi-arch, or specify linux/amd64 or linux/arm64)"
required: false
default: ""
snapshot:
description: "Build in snapshot mode (for non-release builds)"
required: true
type: boolean
default: true
permissions:
contents: read
jobs:
build-docker:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Determine single-arch parameter
id: arch-param
run: |
if [ -n "${{ inputs.platform }}" ]; then
# Extract arch from platform (e.g., linux/amd64 -> amd64)
ARCH=$(echo "${{ inputs.platform }}" | cut -d'/' -f2)
echo "single-arch=${ARCH}" >> "$GITHUB_OUTPUT"
else
echo "single-arch=" >> "$GITHUB_OUTPUT"
fi
- name: Build binaries
uses: ./.github/actions/build-binaries
with:
snapshot: ${{ inputs.snapshot }}
single-arch: ${{ steps.arch-param.outputs.single-arch }}
- name: Build Docker images
if: ${{ !inputs.push }}
uses: ./.github/actions/build-docker-images
with:
push: false
tag-latest: ${{ inputs.tag-latest }}
platform: ${{ inputs.platform }}
cli-version: ${{ inputs.cli-version }}
alpine-tag: ${{ inputs.alpine-tag }}
load: ${{ inputs.platform == 'linux/amd64' || inputs.platform == '' }}
- name: Build and push Docker images
if: ${{ inputs.push }}
uses: ./.github/actions/build-docker-images
with:
push: true
tag-latest: ${{ inputs.tag-latest }}
platform: ${{ inputs.platform }}
cli-version: ${{ inputs.cli-version }}
alpine-tag: ${{ inputs.alpine-tag }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Output image tags
run: |
{
echo "### Docker Images Built"
echo ""
echo "**Git Ref:** ${{ inputs.ref }}"
echo "**CLI Version:** ${{ inputs.cli-version }}"
echo "**Platform:** ${{ inputs.platform || 'linux/amd64,linux/arm64' }}"
echo "**Pushed to Docker Hub:** ${{ inputs.push }}"
echo "**Tagged as latest:** ${{ inputs.tag-latest }}"
echo ""
echo "**Image Tags:**"
echo "- temporaliotest/server:sha-${GITHUB_SHA:0:7}"
echo "- temporaliotest/admin-tools:sha-${GITHUB_SHA:0:7}"
} >> "$GITHUB_STEP_SUMMARY"

View File

@@ -14,13 +14,69 @@ concurrency: # Auto-cancel existing runs in the PR when a new commit is pushed
jobs:
build-docker-image:
runs-on: ubuntu-latest
permissions:
actions: write
contents: read
uses: temporalio/docker-builds/.github/workflows/docker-build-only.yml@main
with:
temporal-server-repo-path: ${{github.event.pull_request.head.repo.full_name}}
temporal-server-repo-ref: ${{github.event.pull_request.head.ref}}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
- name: Build binaries
uses: ./.github/actions/build-binaries
with:
snapshot: true
single-arch: amd64
- name: Build Docker images
uses: ./.github/actions/build-docker-images
with:
push: false
tag-latest: false
platform: linux/amd64
load: true
- name: Get Docker image tag
id: image-tag
uses: actions/github-script@v8
with:
script: |
const ref = context.ref.replace('refs/heads/', '').replace('refs/tags/', '');
const branchName = `branch-${ref}`;
let safeTag = branchName.replace(/[^a-zA-Z0-9._-]/g, '-');
safeTag = safeTag.toLowerCase();
safeTag = safeTag.replace(/^[^a-z0-9]+/, '');
safeTag = safeTag.substring(0, 128);
core.setOutput('tag', safeTag);
- name: Save Docker image as artifact
run: |
docker save temporaliotest/server:${{ steps.image-tag.outputs.tag }} -o /tmp/temporal-server.tar
docker save temporaliotest/admin-tools:${{ steps.image-tag.outputs.tag }} -o /tmp/temporal-admin-tools.tar
echo ${{ steps.image-tag.outputs.tag }} > /tmp/image_tag
- name: Prepare artifact
working-directory: ${{ github.workspace }}
run: |
# Upload-artifact has no good way to flatten paths, so we need to move the compose file
# to avoid some disgustingly long inner path inside the artifact zip.
cp ./develop/docker-compose/docker-compose.yml /tmp/docker-compose.yml
echo -n "${{env.IMAGE_SHA_TAG}}" > /tmp/image_sha_tag
- name: Upload Docker artifact
uses: actions/upload-artifact@v4
with:
name: temporal-server-docker
path: |
/tmp/temporal-server.tar
/tmp/temporal-admin-tools.tar
/tmp/image_tag
/tmp/docker-compose.yml
retention-days: 7
feature-tests-ts:
needs: build-docker-image

View File

@@ -34,7 +34,7 @@ jobs:
owner: ${{ github.repository_owner }}
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0

View File

@@ -1,32 +0,0 @@
name: goreleaser
on:
release:
types:
- released
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
check-latest: true
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
with:
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -7,11 +7,11 @@ jobs:
lint-actions:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
check-latest: true
@@ -26,11 +26,11 @@ jobs:
lint-protos:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
submodules: true
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
check-latest: true
@@ -46,11 +46,11 @@ jobs:
lint-api:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
submodules: true
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
check-latest: true
@@ -67,11 +67,11 @@ jobs:
lint-workflows:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
check-latest: true
@@ -86,11 +86,11 @@ jobs:
fmt-imports:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
check-latest: true
@@ -111,11 +111,11 @@ jobs:
golangci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
check-latest: true

View File

@@ -0,0 +1,31 @@
name: Promote Admin Tools Image
on:
workflow_dispatch:
inputs:
source-tag:
description: "Source tag from temporaliotest registry (e.g. sha-abc123)"
required: true
target-tags:
description: "Target tags for temporalio registry (comma or newline separated, e.g., 1.29.1, latest)"
required: true
override-security-scan:
description: "Override security scan failures (use with caution)"
type: boolean
default: false
required: false
permissions:
contents: read
jobs:
promote:
uses: ./.github/workflows/promote-docker-image.yml
with:
image-name: admin-tools
source-tag: ${{ inputs.source-tag }}
target-tags: ${{ inputs.target-tags }}
override-security-scan: ${{ inputs.override-security-scan }}
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}

View File

@@ -0,0 +1,211 @@
name: Promote Docker Image
on:
workflow_call:
inputs:
image-name:
description: "Image name (e.g., server, admin-tools)"
required: true
type: string
source-tag:
description: "Source tag from temporaliotest registry (e.g. sha-abc123)"
required: true
type: string
target-tags:
description: "Target tags for temporalio registry (comma or newline separated, e.g., 1.29.1, latest)"
required: true
type: string
override-security-scan:
description: "Override security scan failures (use with caution)"
type: boolean
default: false
secrets:
DOCKERHUB_USERNAME:
required: true
DOCKERHUB_TOKEN:
required: true
jobs:
validate-inputs:
runs-on: ubuntu-latest
outputs:
source-tag-safe: ${{ steps.validate.outputs.source-tag }}
target-tags-safe: ${{ steps.validate.outputs.target-tags }}
steps:
- name: Validate input tags
id: validate
uses: actions/github-script@v8
env:
SOURCE_TAG: ${{ inputs.source-tag }}
TARGET_TAGS: ${{ inputs.target-tags }}
with:
script: |
const sourceTag = process.env.SOURCE_TAG;
const targetTagsInput = process.env.TARGET_TAGS;
// Source tag: must be short SHA (sha-XXXXXXX) or full sha256 digest
// Examples: sha-b5b2dfe, sha256:082943409e71ae50d8dd8693593070eac8173f01fb5bfd4970ae59e52176753e
const sourceTagPattern = /^sha-[a-f0-9]{7,}$|^sha256:[a-f0-9]{64}$/;
// Target tag: semantic version pattern (major.minor.patch.build or shorter)
// Examples: 1.29.1, 1.29.1.1, 1.29, latest
const targetTagPattern = /^(\d+\.)*\d+$|^latest$/;
// Validate source tag format
if (!sourceTagPattern.test(sourceTag)) {
core.setFailed('Error: Invalid source tag format. Must be sha-XXXXXXX or sha256:...');
return;
}
// Parse and validate target tags (split by newline or comma)
const targetTags = targetTagsInput
.split(/[\n,]/)
.map(tag => tag.trim())
.filter(tag => tag.length > 0);
if (targetTags.length === 0) {
core.setFailed('Error: At least one target tag must be provided');
return;
}
for (const tag of targetTags) {
if (!targetTagPattern.test(tag)) {
core.setFailed(`Error: Invalid target tag format: "${tag}". Must be semantic version (e.g., 1.29.1, 1.29.1.1) or "latest"`);
return;
}
}
core.setOutput('source-tag', sourceTag);
core.setOutput('target-tags', JSON.stringify(targetTags));
core.info('✓ Tag validation passed');
core.info(` Source: ${sourceTag}`);
core.info(` Target tags: ${targetTags.join(', ')}`);
scan-image:
needs: validate-inputs
runs-on: ubuntu-latest
outputs:
scan-result: ${{ steps.scan.outputs.scan-result }}
critical-count: ${{ steps.scan.outputs.critical-count }}
high-count: ${{ steps.scan.outputs.high-count }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Pull source image
env:
SOURCE_TAG: ${{ needs.validate-inputs.outputs.source-tag-safe }}
IMAGE_NAME: ${{ inputs.image-name }}
run: |
echo "Pulling temporaliotest/${IMAGE_NAME}:${SOURCE_TAG}"
docker pull "temporaliotest/${IMAGE_NAME}:${SOURCE_TAG}"
- name: Scan image with Trivy
id: scan
uses: ./.github/actions/trivy-scan
with:
image-name: temporaliotest/${{ inputs.image-name }}
image-tags: ${{ needs.validate-inputs.outputs.source-tag-safe }}
- name: Upload Trivy scan results
if: always()
uses: actions/upload-artifact@v4
with:
name: trivy-${{ inputs.image-name }}-scan-results
path: trivy-scan-${{ inputs.image-name }}.json
retention-days: 30
check-security-gate:
needs: [scan-image]
runs-on: ubuntu-latest
outputs:
can-promote: ${{ steps.check.outputs.can-promote }}
steps:
- name: Evaluate security scan results
id: check
uses: actions/github-script@v8
env:
OVERRIDE: ${{ inputs.override-security-scan }}
SCAN_RESULT: ${{ needs.scan-image.outputs.scan-result }}
CRITICAL_COUNT: ${{ needs.scan-image.outputs.critical-count }}
HIGH_COUNT: ${{ needs.scan-image.outputs.high-count }}
IMAGE_NAME: ${{ inputs.image-name }}
with:
script: |
const override = process.env.OVERRIDE === 'true';
const scanResult = process.env.SCAN_RESULT;
const criticalCount = process.env.CRITICAL_COUNT;
const highCount = process.env.HIGH_COUNT;
const imageName = process.env.IMAGE_NAME;
core.info('=== Security Scan Results ===');
core.info(`${imageName} image: ${scanResult}`);
core.info(` Critical: ${criticalCount}`);
core.info(` High: ${highCount}`);
core.info('');
core.info(`Override enabled: ${override}`);
core.info('==============================');
if (scanResult === 'fail') {
if (override) {
core.info('');
core.warning('Security vulnerabilities detected but OVERRIDE is enabled!');
core.info('Proceeding with image promotion despite security issues.');
core.setOutput('can-promote', 'true');
} else {
core.info('');
core.error('Security vulnerabilities detected. Promotion BLOCKED.');
core.info("To proceed anyway, re-run with 'override-security-scan' enabled.");
core.setOutput('can-promote', 'false');
core.setFailed('Security vulnerabilities detected');
}
} else {
core.info('');
core.info('✓ Security scan passed. Proceeding with promotion.');
core.setOutput('can-promote', 'true');
}
promote:
needs: [validate-inputs, check-security-gate]
if: needs.check-security-gate.outputs.can-promote == 'true'
runs-on: ubuntu-latest
steps:
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Pull, tag, and push image
uses: actions/github-script@v8
env:
SOURCE_TAG: ${{ needs.validate-inputs.outputs.source-tag-safe }}
TARGET_TAGS: ${{ needs.validate-inputs.outputs.target-tags-safe }}
IMAGE_NAME: ${{ inputs.image-name }}
with:
script: |
const { execSync } = require('child_process');
const sourceTag = process.env.SOURCE_TAG;
const targetTags = JSON.parse(process.env.TARGET_TAGS);
const imageName = process.env.IMAGE_NAME;
core.info(`Promoting ${imageName} image...`);
core.info(` From: temporaliotest/${imageName}:${sourceTag}`);
core.info(` To: ${targetTags.map(t => `temporalio/${imageName}:${t}`).join(', ')}`);
// Pull from test registry
core.info('Pulling source image...');
execSync(`docker pull temporaliotest/${imageName}:${sourceTag}`, { stdio: 'inherit' });
// Tag for each target tag
for (const targetTag of targetTags) {
core.info(`Tagging as temporalio/${imageName}:${targetTag}`);
execSync(`docker tag temporaliotest/${imageName}:${sourceTag} temporalio/${imageName}:${targetTag}`, { stdio: 'inherit' });
}
// Push all tags at once
core.info('Pushing all tags to production registry...');
execSync(`docker push --all-tags temporalio/${imageName}`, { stdio: 'inherit' });
core.info(`✓ ${imageName} image promoted successfully to all tags`);

View File

@@ -0,0 +1,31 @@
name: Promote Server Image
on:
workflow_dispatch:
inputs:
source-tag:
description: "Source tag from temporaliotest registry (e.g. sha-abc123)"
required: true
target-tags:
description: "Target tags for temporalio registry (comma or newline separated, e.g., 1.29.1, latest)"
required: true
override-security-scan:
description: "Override security scan failures (use with caution)"
type: boolean
default: false
required: false
permissions:
contents: read
jobs:
promote:
uses: ./.github/workflows/promote-docker-image.yml
with:
image-name: server
source-tag: ${{ inputs.source-tag }}
target-tags: ${{ inputs.target-tags }}
override-security-scan: ${{ inputs.override-security-scan }}
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}

25
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Release
on:
release:
types:
- released
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ github.ref }}
fetch-depth: 0
- name: Build binaries
uses: ./.github/actions/build-binaries
with:
snapshot: false
release: true

View File

@@ -108,7 +108,7 @@ jobs:
cat "$GITHUB_OUTPUT"
- name: Checkout Code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ env.COMMIT }}
@@ -139,13 +139,13 @@ jobs:
name: Pre-build for cache
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
if: ${{ !inputs.run_single_functional_test && !inputs.run_single_unit_test }}
with:
token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ env.COMMIT }}
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
if: ${{ !inputs.run_single_functional_test && !inputs.run_single_unit_test }}
with:
go-version-file: "go.mod"
@@ -181,7 +181,7 @@ jobs:
needs: pre-build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
if: ${{ !inputs.run_single_functional_test && !inputs.run_single_unit_test }}
with:
token: ${{ secrets.GITHUB_TOKEN }}
@@ -189,7 +189,7 @@ jobs:
# buf-breaking tries to compare HEAD against merge base so we need to be able to find it
fetch-depth: 100
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
if: ${{ !inputs.run_single_functional_test && !inputs.run_single_unit_test }}
with:
go-version-file: "go.mod"
@@ -229,12 +229,12 @@ jobs:
needs: [pre-build, set-up-single-test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ env.COMMIT }}
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
cache: false # do our own caching
@@ -319,7 +319,7 @@ jobs:
needs: [pre-build, set-up-single-test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ env.COMMIT }}
@@ -334,7 +334,7 @@ jobs:
postgresql
down-flags: -v
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
cache: false # do our own caching
@@ -481,7 +481,7 @@ jobs:
with:
key: docker-${{ runner.os }}${{ runner.arch }}-${{ hashFiles(env.DOCKER_COMPOSE_FILE) }}
- uses: actions/checkout@v4
- uses: actions/checkout@v6
if: ${{ inputs.run_single_functional_test != true || (inputs.run_single_functional_test == true && contains(fromJSON(needs.set-up-single-test.outputs.dbs), env.PERSISTENCE_DRIVER)) }}
with:
token: ${{ secrets.GITHUB_TOKEN }}
@@ -495,7 +495,7 @@ jobs:
services: "${{ join(matrix.containers, '\n') }}"
down-flags: -v
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
if: ${{ inputs.run_single_functional_test != true || (inputs.run_single_functional_test == true && contains(fromJSON(needs.set-up-single-test.outputs.dbs), env.PERSISTENCE_DRIVER)) }}
with:
go-version-file: "go.mod"
@@ -645,7 +645,7 @@ jobs:
PERSISTENCE_DRIVER: ${{ matrix.persistence_driver }}
TEST_PARALLEL_FLAGS: ${{ matrix.parallel_flags }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ env.COMMIT }}
@@ -665,7 +665,7 @@ jobs:
services: "${{ join(matrix.containers, '\n') }}"
down-flags: -v
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
cache: false # do our own caching
@@ -806,7 +806,7 @@ jobs:
PERSISTENCE_DRIVER: ${{ matrix.persistence_driver }}
ES_VERSION: ${{ matrix.es_version }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ env.COMMIT }}
@@ -826,7 +826,7 @@ jobs:
services: "${{ join(matrix.containers, '\n') }}"
down-flags: -v
- uses: actions/setup-go@v5
- uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
cache: false # do our own caching

View File

@@ -1,18 +1,25 @@
# GoReleaser v2 configuration
version: 2
before:
hooks:
- go mod download
snapshot:
version_template: "{{ .Version }}-SNAPSHOT-{{ .ShortCommit }}"
archives:
- id: default
builds:
ids:
- temporal-server
- temporal-cassandra-tool
- temporal-sql-tool
- temporal-elasticsearch-tool
- tdbg
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
format_overrides:
- goos: windows
format: zip
formats: [zip]
files:
- ./config/*
@@ -79,11 +86,11 @@ builds:
- arm64
checksum:
name_template: 'checksums.txt'
name_template: "checksums.txt"
algorithm: sha256
changelog:
skip: true
disable: true
announce:
skip: "true"
skip: true

View File

@@ -7,7 +7,10 @@ import (
"crypto/x509/pkix"
"time"
enumspb "go.temporal.io/api/enums/v1"
"go.temporal.io/api/serviceerror"
"go.temporal.io/api/workflowservice/v1"
"go.temporal.io/server/common/api"
"go.temporal.io/server/common/dynamicconfig"
"go.temporal.io/server/common/headers"
"go.temporal.io/server/common/log"
@@ -77,15 +80,16 @@ func PeerCert(tlsInfo *credentials.TLSInfo) *x509.Certificate {
}
type Interceptor struct {
claimMapper ClaimMapper
authorizer Authorizer
metricsHandler metrics.Handler
logger log.Logger
namespaceChecker NamespaceChecker
audienceGetter JWTAudienceMapper
authHeaderName string
authExtraHeaderName string
exposeAuthorizerErrors dynamicconfig.BoolPropertyFn
claimMapper ClaimMapper
authorizer Authorizer
metricsHandler metrics.Handler
logger log.Logger
namespaceChecker NamespaceChecker
audienceGetter JWTAudienceMapper
authHeaderName string
authExtraHeaderName string
exposeAuthorizerErrors dynamicconfig.BoolPropertyFn
enableCrossNamespaceCommands dynamicconfig.BoolPropertyFn
}
// NewInterceptor creates an authorization interceptor.
@@ -99,17 +103,19 @@ func NewInterceptor(
authHeaderName string,
authExtraHeaderName string,
exposeAuthorizerErrors dynamicconfig.BoolPropertyFn,
enableCrossNamespaceCommands dynamicconfig.BoolPropertyFn,
) *Interceptor {
return &Interceptor{
claimMapper: claimMapper,
authorizer: authorizer,
logger: logger,
namespaceChecker: namespaceChecker,
metricsHandler: metricsHandler,
authHeaderName: cmp.Or(authHeaderName, defaultAuthHeaderName),
authExtraHeaderName: cmp.Or(authExtraHeaderName, defaultAuthExtraHeaderName),
audienceGetter: audienceGetter,
exposeAuthorizerErrors: exposeAuthorizerErrors,
claimMapper: claimMapper,
authorizer: authorizer,
logger: logger,
namespaceChecker: namespaceChecker,
metricsHandler: metricsHandler,
authHeaderName: cmp.Or(authHeaderName, defaultAuthHeaderName),
authExtraHeaderName: cmp.Or(authExtraHeaderName, defaultAuthExtraHeaderName),
audienceGetter: audienceGetter,
exposeAuthorizerErrors: exposeAuthorizerErrors,
enableCrossNamespaceCommands: enableCrossNamespaceCommands,
}
}
@@ -154,6 +160,11 @@ func (a *Interceptor) Intercept(
if err := a.Authorize(ctx, claims, ct); err != nil {
return nil, err
}
// Authorize target namespaces in cross-namespace commands
if err := a.authorizeTargetNamespaces(ctx, claims, namespace, req); err != nil {
return nil, err
}
}
return handler(ctx, req)
}
@@ -255,3 +266,75 @@ func (a *Interceptor) getMetricsHandler(nsName string) metrics.Handler {
}
return a.metricsHandler.WithTags(metrics.OperationTag(metrics.AuthorizationScope), nsTag)
}
// authorizeTargetNamespaces authorizes cross-namespace commands in RespondWorkflowTaskCompleted.
// Commands like SignalExternalWorkflow, StartChildWorkflow, and CancelExternalWorkflow can target
// workflows in different namespaces. This method ensures the caller has permission in those target
// namespaces as well.
func (a *Interceptor) authorizeTargetNamespaces(
ctx context.Context,
claims *Claims,
sourceNamespace string,
req interface{},
) error {
// Skip if cross-namespace commands are not enabled
if !a.enableCrossNamespaceCommands() {
return nil
}
// Cross-namespace commands can only be initiated via RespondWorkflowTaskCompletedRequest.
// Here we handle authorization for all such commands: SignalExternalWorkflow,
// StartChildWorkflow, and RequestCancelExternalWorkflow targeting a different namespace.
wftRequest, ok := req.(*workflowservice.RespondWorkflowTaskCompletedRequest)
if !ok {
return nil
}
// Track namespace+API combinations we've already authorized to avoid duplicate checks
authorizedNamespaceAPIs := make(map[string]struct{})
for _, cmd := range wftRequest.GetCommands() {
var targetNamespace string
var apiName string
switch cmd.GetCommandType() {
case enumspb.COMMAND_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION:
if attr := cmd.GetSignalExternalWorkflowExecutionCommandAttributes(); attr != nil {
targetNamespace = attr.GetNamespace()
apiName = "SignalWorkflowExecution"
}
case enumspb.COMMAND_TYPE_START_CHILD_WORKFLOW_EXECUTION:
if attr := cmd.GetStartChildWorkflowExecutionCommandAttributes(); attr != nil {
targetNamespace = attr.GetNamespace()
apiName = "StartWorkflowExecution"
}
case enumspb.COMMAND_TYPE_REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION:
if attr := cmd.GetRequestCancelExternalWorkflowExecutionCommandAttributes(); attr != nil {
targetNamespace = attr.GetNamespace()
apiName = "RequestCancelWorkflowExecution"
}
default:
// Other command types don't target external namespaces
}
// Skip if empty, same as source, or already authorized
if targetNamespace == "" || targetNamespace == sourceNamespace {
continue
}
key := targetNamespace + ":" + apiName
if _, ok := authorizedNamespaceAPIs[key]; ok {
continue
}
// Authorize access to target namespace for this specific API
if err := a.Authorize(ctx, claims, &CallTarget{
APIName: api.WorkflowServicePrefix + apiName,
Namespace: targetNamespace,
Request: req,
}); err != nil {
return err
}
authorizedNamespaceAPIs[key] = struct{}{}
}
return nil
}

View File

@@ -7,8 +7,11 @@ import (
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
commandpb "go.temporal.io/api/command/v1"
enumspb "go.temporal.io/api/enums/v1"
"go.temporal.io/api/serviceerror"
"go.temporal.io/api/workflowservice/v1"
"go.temporal.io/server/common/api"
"go.temporal.io/server/common/dynamicconfig"
"go.temporal.io/server/common/log"
"go.temporal.io/server/common/metrics"
@@ -19,7 +22,9 @@ import (
)
const (
testNamespace string = "test-namespace"
testNamespace string = "test-namespace"
targetNamespace string = "target-namespace"
anotherNamespace string = "another-namespace"
)
var (
@@ -30,6 +35,8 @@ var (
startWorkflowExecutionRequest = &workflowservice.StartWorkflowExecutionRequest{Namespace: testNamespace}
startWorkflowExecutionTarget = &CallTarget{Namespace: testNamespace, Request: startWorkflowExecutionRequest, APIName: "/temporal.api.workflowservice.v1.WorkflowService/StartWorkflowExecution"}
startWorkflowExecutionInfo = &grpc.UnaryServerInfo{FullMethod: "/temporal.api.workflowservice.v1.WorkflowService/StartWorkflowExecution"}
respondWorkflowTaskCompletedInfo = &grpc.UnaryServerInfo{FullMethod: api.WorkflowServicePrefix + "RespondWorkflowTaskCompleted"}
)
type (
@@ -75,7 +82,8 @@ func (s *authorizerInterceptorSuite) SetupTest() {
nil,
"",
"",
dynamicconfig.GetBoolPropertyFn(false),
dynamicconfig.GetBoolPropertyFn(false), // exposeAuthorizerErrors
dynamicconfig.GetBoolPropertyFn(false), // enableCrossNamespaceCommands
)
s.handler = func(ctx context.Context, req interface{}) (interface{}, error) { return true, nil }
}
@@ -149,7 +157,8 @@ func (s *authorizerInterceptorSuite) TestAuthorizationFailedExposed() {
nil,
"",
"",
dynamicconfig.GetBoolPropertyFn(true),
dynamicconfig.GetBoolPropertyFn(true), // exposeAuthorizerErrors
dynamicconfig.GetBoolPropertyFn(false), // enableCrossNamespaceCommands
)
authErr := serviceerror.NewInternal("intentional test failure")
@@ -181,7 +190,8 @@ func (s *authorizerInterceptorSuite) TestNoopClaimMapperWithoutTLS() {
nil,
"",
"",
dynamicconfig.GetBoolPropertyFn(false),
dynamicconfig.GetBoolPropertyFn(false), // exposeAuthorizerErrors
dynamicconfig.GetBoolPropertyFn(false), // enableCrossNamespaceCommands
)
_, err := interceptor.Intercept(ctx, describeNamespaceRequest, describeNamespaceInfo, s.handler)
s.NoError(err)
@@ -197,7 +207,8 @@ func (s *authorizerInterceptorSuite) TestAlternateHeaders() {
nil,
"custom-header",
"custom-extra-header",
dynamicconfig.GetBoolPropertyFn(false),
dynamicconfig.GetBoolPropertyFn(false), // exposeAuthorizerErrors
dynamicconfig.GetBoolPropertyFn(false), // enableCrossNamespaceCommands
)
cases := []struct {
@@ -246,3 +257,306 @@ func (n mockNamespaceChecker) Exists(name namespace.Name) error {
}
return errors.New("doesn't exist")
}
// multiNamespaceChecker is a mock that recognizes multiple namespaces
type multiNamespaceChecker []string
func (m multiNamespaceChecker) Exists(name namespace.Name) error {
for _, ns := range m {
if ns == string(name) {
return nil
}
}
return errors.New("doesn't exist")
}
// Helper to create a cross-namespace command
func makeCrossNamespaceCommand(commandType enumspb.CommandType, targetNs string) *commandpb.Command {
switch commandType {
case enumspb.COMMAND_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION:
return &commandpb.Command{
CommandType: commandType,
Attributes: &commandpb.Command_SignalExternalWorkflowExecutionCommandAttributes{
SignalExternalWorkflowExecutionCommandAttributes: &commandpb.SignalExternalWorkflowExecutionCommandAttributes{
Namespace: targetNs,
},
},
}
case enumspb.COMMAND_TYPE_START_CHILD_WORKFLOW_EXECUTION:
return &commandpb.Command{
CommandType: commandType,
Attributes: &commandpb.Command_StartChildWorkflowExecutionCommandAttributes{
StartChildWorkflowExecutionCommandAttributes: &commandpb.StartChildWorkflowExecutionCommandAttributes{
Namespace: targetNs,
},
},
}
case enumspb.COMMAND_TYPE_REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION:
return &commandpb.Command{
CommandType: commandType,
Attributes: &commandpb.Command_RequestCancelExternalWorkflowExecutionCommandAttributes{
RequestCancelExternalWorkflowExecutionCommandAttributes: &commandpb.RequestCancelExternalWorkflowExecutionCommandAttributes{
Namespace: targetNs,
},
},
}
default:
return nil
}
}
// Helper to create interceptor with cross-namespace commands enabled
func (s *authorizerInterceptorSuite) newCrossNamespaceInterceptor(namespaces ...string) *Interceptor {
return NewInterceptor(
s.mockClaimMapper,
s.mockAuthorizer,
s.mockMetricsHandler,
log.NewNoopLogger(),
multiNamespaceChecker(namespaces),
nil,
"",
"",
dynamicconfig.GetBoolPropertyFn(false), // exposeAuthorizerErrors
dynamicconfig.GetBoolPropertyFn(true), // enableCrossNamespaceCommands
)
}
func (s *authorizerInterceptorSuite) TestCrossNamespaceCommands_Authorized() {
testCases := []struct {
name string
commandType enumspb.CommandType
expectedAPI string
}{
{
name: "SignalExternalWorkflow",
commandType: enumspb.COMMAND_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION,
expectedAPI: "SignalWorkflowExecution",
},
{
name: "StartChildWorkflow",
commandType: enumspb.COMMAND_TYPE_START_CHILD_WORKFLOW_EXECUTION,
expectedAPI: "StartWorkflowExecution",
},
{
name: "CancelExternalWorkflow",
commandType: enumspb.COMMAND_TYPE_REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION,
expectedAPI: "RequestCancelWorkflowExecution",
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
request := &workflowservice.RespondWorkflowTaskCompletedRequest{
Namespace: testNamespace,
Commands: []*commandpb.Command{makeCrossNamespaceCommand(tc.commandType, targetNamespace)},
}
sourceTarget := &CallTarget{
Namespace: testNamespace,
Request: request,
APIName: api.WorkflowServicePrefix + "RespondWorkflowTaskCompleted",
}
crossNsTarget := &CallTarget{
Namespace: targetNamespace,
Request: request,
APIName: api.WorkflowServicePrefix + tc.expectedAPI,
}
interceptor := s.newCrossNamespaceInterceptor(testNamespace, targetNamespace)
s.mockAuthorizer.EXPECT().Authorize(ctx, nil, sourceTarget).
Return(Result{Decision: DecisionAllow}, nil)
s.mockMetricsHandler.EXPECT().WithTags(
metrics.OperationTag(metrics.AuthorizationScope),
metrics.NamespaceTag(targetNamespace),
).Return(s.mockMetricsHandler)
s.mockAuthorizer.EXPECT().Authorize(ctx, nil, crossNsTarget).
Return(Result{Decision: DecisionAllow}, nil)
res, err := interceptor.Intercept(ctx, request, respondWorkflowTaskCompletedInfo, s.handler)
s.True(res.(bool))
s.NoError(err)
})
}
}
func (s *authorizerInterceptorSuite) TestCrossNamespaceCommand_Unauthorized() {
request := &workflowservice.RespondWorkflowTaskCompletedRequest{
Namespace: testNamespace,
Commands: []*commandpb.Command{makeCrossNamespaceCommand(enumspb.COMMAND_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION, targetNamespace)},
}
sourceTarget := &CallTarget{
Namespace: testNamespace,
Request: request,
APIName: api.WorkflowServicePrefix + "RespondWorkflowTaskCompleted",
}
crossNsTarget := &CallTarget{
Namespace: targetNamespace,
Request: request,
APIName: api.WorkflowServicePrefix + "SignalWorkflowExecution",
}
interceptor := s.newCrossNamespaceInterceptor(testNamespace, targetNamespace)
s.mockAuthorizer.EXPECT().Authorize(ctx, nil, sourceTarget).
Return(Result{Decision: DecisionAllow}, nil)
s.mockMetricsHandler.EXPECT().WithTags(
metrics.OperationTag(metrics.AuthorizationScope),
metrics.NamespaceTag(targetNamespace),
).Return(s.mockMetricsHandler)
s.mockAuthorizer.EXPECT().Authorize(ctx, nil, crossNsTarget).
Return(Result{Decision: DecisionDeny}, nil)
s.mockMetricsHandler.EXPECT().Counter(metrics.ServiceErrUnauthorizedCounter.Name()).Return(metrics.NoopCounterMetricFunc)
res, err := interceptor.Intercept(ctx, request, respondWorkflowTaskCompletedInfo, s.handler)
s.Nil(res)
s.Error(err)
}
func (s *authorizerInterceptorSuite) TestNoExtraAuthCheck() {
testCases := []struct {
name string
targetNs string
description string
}{
{
name: "SameNamespace",
targetNs: testNamespace, // Same as source
description: "command targeting same namespace should not trigger extra auth",
},
{
name: "EmptyNamespace",
targetNs: "", // Empty defaults to source
description: "command with empty namespace should not trigger extra auth",
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
request := &workflowservice.RespondWorkflowTaskCompletedRequest{
Namespace: testNamespace,
Commands: []*commandpb.Command{makeCrossNamespaceCommand(enumspb.COMMAND_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION, tc.targetNs)},
}
sourceTarget := &CallTarget{
Namespace: testNamespace,
Request: request,
APIName: api.WorkflowServicePrefix + "RespondWorkflowTaskCompleted",
}
// Only expect authorization for source namespace
s.mockAuthorizer.EXPECT().Authorize(ctx, nil, sourceTarget).
Return(Result{Decision: DecisionAllow}, nil)
res, err := s.interceptor.Intercept(ctx, request, respondWorkflowTaskCompletedInfo, s.handler)
s.True(res.(bool))
s.NoError(err)
})
}
}
func (s *authorizerInterceptorSuite) TestCrossNamespaceCommand_DisabledFeature() {
// When cross-namespace commands are disabled, no extra auth check should happen
request := &workflowservice.RespondWorkflowTaskCompletedRequest{
Namespace: testNamespace,
Commands: []*commandpb.Command{makeCrossNamespaceCommand(enumspb.COMMAND_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION, targetNamespace)},
}
sourceTarget := &CallTarget{
Namespace: testNamespace,
Request: request,
APIName: api.WorkflowServicePrefix + "RespondWorkflowTaskCompleted",
}
// Interceptor with cross-namespace commands DISABLED (uses default s.interceptor which has it disabled)
s.mockAuthorizer.EXPECT().Authorize(ctx, nil, sourceTarget).
Return(Result{Decision: DecisionAllow}, nil)
res, err := s.interceptor.Intercept(ctx, request, respondWorkflowTaskCompletedInfo, s.handler)
s.True(res.(bool))
s.NoError(err)
}
func (s *authorizerInterceptorSuite) TestMultipleCommands_AuthDeduplication() {
// Test that authorization is deduplicated per namespace+API combination
request := &workflowservice.RespondWorkflowTaskCompletedRequest{
Namespace: testNamespace,
Commands: []*commandpb.Command{
makeCrossNamespaceCommand(enumspb.COMMAND_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION, targetNamespace),
makeCrossNamespaceCommand(enumspb.COMMAND_TYPE_START_CHILD_WORKFLOW_EXECUTION, targetNamespace),
makeCrossNamespaceCommand(enumspb.COMMAND_TYPE_REQUEST_CANCEL_EXTERNAL_WORKFLOW_EXECUTION, targetNamespace),
// Duplicate signal to same namespace - should not trigger extra auth
makeCrossNamespaceCommand(enumspb.COMMAND_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION, targetNamespace),
},
}
sourceTarget := &CallTarget{
Namespace: testNamespace,
Request: request,
APIName: api.WorkflowServicePrefix + "RespondWorkflowTaskCompleted",
}
interceptor := s.newCrossNamespaceInterceptor(testNamespace, targetNamespace)
s.mockAuthorizer.EXPECT().Authorize(ctx, nil, sourceTarget).
Return(Result{Decision: DecisionAllow}, nil)
// Expect 3 auth checks (one per unique API type), not 4
s.mockMetricsHandler.EXPECT().WithTags(
metrics.OperationTag(metrics.AuthorizationScope),
metrics.NamespaceTag(targetNamespace),
).Return(s.mockMetricsHandler).Times(3)
s.mockAuthorizer.EXPECT().Authorize(ctx, nil, &CallTarget{
Namespace: targetNamespace, Request: request, APIName: api.WorkflowServicePrefix + "SignalWorkflowExecution",
}).Return(Result{Decision: DecisionAllow}, nil)
s.mockAuthorizer.EXPECT().Authorize(ctx, nil, &CallTarget{
Namespace: targetNamespace, Request: request, APIName: api.WorkflowServicePrefix + "StartWorkflowExecution",
}).Return(Result{Decision: DecisionAllow}, nil)
s.mockAuthorizer.EXPECT().Authorize(ctx, nil, &CallTarget{
Namespace: targetNamespace, Request: request, APIName: api.WorkflowServicePrefix + "RequestCancelWorkflowExecution",
}).Return(Result{Decision: DecisionAllow}, nil)
res, err := interceptor.Intercept(ctx, request, respondWorkflowTaskCompletedInfo, s.handler)
s.True(res.(bool))
s.NoError(err)
}
func (s *authorizerInterceptorSuite) TestMultipleTargetNamespaces() {
// Test commands targeting different namespaces
request := &workflowservice.RespondWorkflowTaskCompletedRequest{
Namespace: testNamespace,
Commands: []*commandpb.Command{
makeCrossNamespaceCommand(enumspb.COMMAND_TYPE_SIGNAL_EXTERNAL_WORKFLOW_EXECUTION, targetNamespace),
makeCrossNamespaceCommand(enumspb.COMMAND_TYPE_START_CHILD_WORKFLOW_EXECUTION, anotherNamespace),
},
}
sourceTarget := &CallTarget{
Namespace: testNamespace,
Request: request,
APIName: api.WorkflowServicePrefix + "RespondWorkflowTaskCompleted",
}
interceptor := s.newCrossNamespaceInterceptor(testNamespace, targetNamespace, anotherNamespace)
s.mockAuthorizer.EXPECT().Authorize(ctx, nil, sourceTarget).
Return(Result{Decision: DecisionAllow}, nil)
s.mockMetricsHandler.EXPECT().WithTags(
metrics.OperationTag(metrics.AuthorizationScope),
metrics.NamespaceTag(targetNamespace),
).Return(s.mockMetricsHandler)
s.mockAuthorizer.EXPECT().Authorize(ctx, nil, &CallTarget{
Namespace: targetNamespace, Request: request, APIName: api.WorkflowServicePrefix + "SignalWorkflowExecution",
}).Return(Result{Decision: DecisionAllow}, nil)
s.mockMetricsHandler.EXPECT().WithTags(
metrics.OperationTag(metrics.AuthorizationScope),
metrics.NamespaceTag(anotherNamespace),
).Return(s.mockMetricsHandler)
s.mockAuthorizer.EXPECT().Authorize(ctx, nil, &CallTarget{
Namespace: anotherNamespace, Request: request, APIName: api.WorkflowServicePrefix + "StartWorkflowExecution",
}).Return(Result{Decision: DecisionAllow}, nil)
res, err := interceptor.Intercept(ctx, request, respondWorkflowTaskCompletedInfo, s.handler)
s.True(res.(bool))
s.NoError(err)
}

View File

@@ -1,18 +1,22 @@
variable "SERVER_VERSION" {
default = "1.29.1"
default = "unknown"
}
variable "CLI_VERSION" {
default = "1.5.0"
default = "unknown"
}
variable "IMAGE_REPO" {
default = "temporaliotest"
}
variable "IMAGE_SHA_TAG" {}
variable "IMAGE_SHA_TAG" {
default = ""
}
variable "IMAGE_BRANCH_TAG" {}
variable "IMAGE_BRANCH_TAG" {
default = ""
}
variable "SAFE_IMAGE_BRANCH_TAG" {
default = join("-", [for c in regexall("[a-z0-9]+", lower(IMAGE_BRANCH_TAG)) : c])
@@ -26,12 +30,19 @@ variable "TAG_LATEST" {
default = false
}
# Legacy targets (legacy-admin-tools, legacy-server) are for building images with server versions
# older than v1.27.0 (3 minor versions behind v1.30.0). Once support for pre-1.27.0 versions is
# no longer needed, these legacy targets can be removed and only the standard targets should be used.
# IMPORTANT: When updating ALPINE_TAG, also update the default value in:
# - docker/targets/admin-tools.Dockerfile
# - docker/targets/server.Dockerfile
variable "ALPINE_TAG" {
default = "3.23@sha256:c78ded0fee4493809c8ca71d4a6057a46237763d952fae15ea418f6d14137f2d"
}
target "admin-tools" {
context = "docker"
dockerfile = "targets/admin-tools.Dockerfile"
args = {
ALPINE_TAG = "${ALPINE_TAG}"
}
tags = compact([
"${IMAGE_REPO}/admin-tools:${IMAGE_SHA_TAG}",
"${IMAGE_REPO}/admin-tools:${SAFE_IMAGE_BRANCH_TAG}",
@@ -44,7 +55,6 @@ target "admin-tools" {
"org.opencontainers.image.url" = "https://github.com/temporalio/temporal"
"org.opencontainers.image.source" = "https://github.com/temporalio/temporal"
"org.opencontainers.image.licenses" = "MIT"
"org.opencontainers.image.version" = "${SERVER_VERSION}"
"org.opencontainers.image.revision" = "${TEMPORAL_SHA}"
"org.opencontainers.image.created" = timestamp()
"com.temporal.server.version" = "${SERVER_VERSION}"
@@ -53,7 +63,11 @@ target "admin-tools" {
}
target "server" {
context = "docker"
dockerfile = "targets/server.Dockerfile"
args = {
ALPINE_TAG = "${ALPINE_TAG}"
}
tags = compact([
"${IMAGE_REPO}/server:${IMAGE_SHA_TAG}",
"${IMAGE_REPO}/server:${SAFE_IMAGE_BRANCH_TAG}",

View File

@@ -0,0 +1,33 @@
# IMPORTANT: When updating ALPINE_TAG, also update the default value in:
# - docker/docker-bake.hcl (variable "ALPINE_TAG")
# - docker/targets/server.Dockerfile (ARG ALPINE_TAG)
ARG ALPINE_TAG=3.23@sha256:c78ded0fee4493809c8ca71d4a6057a46237763d952fae15ea418f6d14137f2d
FROM alpine:${ALPINE_TAG}
ARG TARGETARCH
RUN apk add --no-cache \
ca-certificates \
tzdata && addgroup -g 1000 temporal && \
adduser -u 1000 -G temporal -D temporal
# Copy all admin tool binaries:
# - temporal (CLI)
# - temporal-cassandra-tool
# - temporal-sql-tool
# - temporal-elasticsearch-tool
# - tdbg
COPY --chmod=755 \
./build/${TARGETARCH}/temporal \
./build/${TARGETARCH}/temporal-cassandra-tool \
./build/${TARGETARCH}/temporal-sql-tool \
./build/${TARGETARCH}/temporal-elasticsearch-tool \
./build/${TARGETARCH}/tdbg \
/usr/local/bin/
COPY ./build/temporal/schema /etc/temporal/schema
USER temporal
CMD ["sh", "-c", "trap exit INT HUP TERM; sleep infinity"]

View File

@@ -1,4 +1,9 @@
FROM alpine:3.22@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412
# IMPORTANT: When updating ALPINE_TAG, also update the default value in:
# - docker/docker-bake.hcl (variable "ALPINE_TAG")
# - docker/targets/admin-tools.Dockerfile (ARG ALPINE_TAG)
ARG ALPINE_TAG=3.23@sha256:c78ded0fee4493809c8ca71d4a6057a46237763d952fae15ea418f6d14137f2d
FROM alpine:${ALPINE_TAG}
ARG TARGETARCH

View File

@@ -163,6 +163,7 @@ func AuthorizationInterceptorProvider(
authorizer authorization.Authorizer,
claimMapper authorization.ClaimMapper,
audienceGetter authorization.JWTAudienceMapper,
dc *dynamicconfig.Collection,
) *authorization.Interceptor {
return authorization.NewInterceptor(
claimMapper,
@@ -174,6 +175,7 @@ func AuthorizationInterceptorProvider(
cfg.Global.Authorization.AuthHeaderName,
cfg.Global.Authorization.AuthExtraHeaderName,
serviceConfig.ExposeAuthorizerErrors,
dynamicconfig.EnableCrossNamespaceCommands.Get(dc),
)
}

View File

@@ -117,7 +117,18 @@ func newOperationContext(options contextOptions) *operationContext {
)
checker := mockNamespaceChecker(oc.namespace.Name())
oc.auth = authorization.NewInterceptor(nil, mockAuthorizer{}, oc.metricsHandler, oc.logger, checker, nil, "", "", dynamicconfig.GetBoolPropertyFn(false))
oc.auth = authorization.NewInterceptor(
nil,
mockAuthorizer{},
oc.metricsHandler,
oc.logger,
checker,
nil,
"",
"",
dynamicconfig.GetBoolPropertyFn(false), // exposeAuthorizerErrors
dynamicconfig.GetBoolPropertyFn(false), // enableCrossNamespaceCommands
)
oc.namespaceConcurrencyLimitInterceptor = interceptor.NewConcurrentRequestLimitInterceptor(
nil,
nil,

View File

@@ -2722,7 +2722,7 @@ func (wh *WorkflowHandler) ShutdownWorker(ctx context.Context, request *workflow
}
// route heartbeat to the matching service
if request.WorkerHeartbeat != nil {
if request.WorkerHeartbeat != nil && wh.config.WorkerHeartbeatsEnabled(request.GetNamespace()) {
heartbeats := []*workerpb.WorkerHeartbeat{request.WorkerHeartbeat}
_, err = wh.matchingClient.RecordWorkerHeartbeat(ctx, &matchingservice.RecordWorkerHeartbeatRequest{
NamespaceId: namespaceId.String(),
@@ -5139,7 +5139,7 @@ func (wh *WorkflowHandler) PollNexusTaskQueue(ctx context.Context, request *work
}
// route heartbeat to the matching service
if len(request.WorkerHeartbeat) > 0 {
if len(request.WorkerHeartbeat) > 0 && wh.config.WorkerHeartbeatsEnabled(request.GetNamespace()) {
workerHeartbeat := request.WorkerHeartbeat
request.WorkerHeartbeat = nil // Clear the field to avoid sending it to matching service.
@@ -6245,7 +6245,7 @@ func (wh *WorkflowHandler) RecordWorkerHeartbeat(
ctx context.Context, request *workflowservice.RecordWorkerHeartbeatRequest,
) (*workflowservice.RecordWorkerHeartbeatResponse, error) {
if !wh.config.WorkerHeartbeatsEnabled(request.GetNamespace()) {
return nil, serviceerror.NewUnimplemented("method RecordWorkerHeartbeat not supported")
return &workflowservice.RecordWorkerHeartbeatResponse{}, nil
}
namespaceName := namespace.Name(request.GetNamespace())
namespaceID, err := wh.namespaceRegistry.GetNamespaceID(namespaceName)

View File

@@ -266,6 +266,7 @@ func (s *TaskQueueSuite) configureRateLimitAndLaunchWorkflows(
// testing anything because the test will succeed even if all the activities complete immediately as if there were no rate limit.
// TODO(matching team): Possibly rewrite test if this issue persists.
func (s *TaskQueueSuite) TestTaskQueueAPIRateLimitOverridesWorkerLimit() {
s.T().Skip("skip until we make it less flaky")
const (
apiRPS = 5.0
taskCount = 25
@@ -498,6 +499,7 @@ func (s *TaskQueueSuite) TestTaskQueueRateLimit_UpdateFromWorkerConfigAndAPI() {
}
func (s *TaskQueueSuite) TestWholeQueueLimit_TighterThanPerKeyDefault_IsEnforced() {
s.T().Skip("skip until we make it less flaky")
const (
wholeQueueRPS = 10.0 // tighter
perKeyRPS = 50.0 // looser than whole queue, should not bind
@@ -545,6 +547,7 @@ func (s *TaskQueueSuite) TestWholeQueueLimit_TighterThanPerKeyDefault_IsEnforced
}
func (s *TaskQueueSuite) TestPerKeyRateLimit_Default_IsEnforcedAcrossThreeKeys() {
s.T().Skip("skip until we make it less flaky")
const (
perKeyRPS = 5.0
wholeQueueRPS = 1000.0 // tighter
@@ -598,6 +601,7 @@ func (s *TaskQueueSuite) TestPerKeyRateLimit_Default_IsEnforcedAcrossThreeKeys()
}
func (s *TaskQueueSuite) TestPerKeyRateLimit_WeightOverride_IsEnforcedAcrossThreeKeys() {
s.T().Skip("skip until we make it less flaky")
const (
perKeyRPS = 5.0 // base per-key limit
wholeQueueRPS = 1000.0 // keep high so only per-key gates

View File

@@ -288,21 +288,21 @@ func (s *Versioning3Suite) testWorkflowWithPinnedOverride(sticky bool) {
runID := s.startWorkflow(tv, tv.VersioningOverridePinned(s.useV32))
s.WaitForChannel(ctx, wftCompleted)
s.verifyWorkflowVersioning(tv, vbUnpinned, tv.Deployment(), tv.VersioningOverridePinned(s.useV32), nil)
s.verifyWorkflowVersioning(s.Assertions, tv, vbUnpinned, tv.Deployment(), tv.VersioningOverridePinned(s.useV32), nil)
s.verifyVersioningSAs(tv, vbPinned, tv)
if sticky {
s.verifyWorkflowStickyQueue(tv.WithRunID(runID))
}
s.WaitForChannel(ctx, actCompleted)
s.verifyWorkflowVersioning(tv, vbUnpinned, tv.Deployment(), tv.VersioningOverridePinned(s.useV32), nil)
s.verifyWorkflowVersioning(s.Assertions, tv, vbUnpinned, tv.Deployment(), tv.VersioningOverridePinned(s.useV32), nil)
s.pollWftAndHandle(tv, sticky, nil,
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
s.NotNil(task)
return respondCompleteWorkflow(tv, vbUnpinned), nil
})
s.verifyWorkflowVersioning(tv, vbUnpinned, tv.Deployment(), tv.VersioningOverridePinned(s.useV32), nil)
s.verifyWorkflowVersioning(s.Assertions, tv, vbUnpinned, tv.Deployment(), tv.VersioningOverridePinned(s.useV32), nil)
}
func (s *Versioning3Suite) TestQueryWithPinnedOverride_NoSticky() {
@@ -375,7 +375,7 @@ func (s *Versioning3Suite) testPinnedQuery_DrainedVersion(pollersPresent bool, r
s.startWorkflow(tv, tv.VersioningOverridePinned(s.useV32))
s.WaitForChannel(ctx, wftCompleted)
s.verifyWorkflowVersioning(tv, vbPinned, tv.Deployment(), tv.VersioningOverridePinned(s.useV32), nil)
s.verifyWorkflowVersioning(s.Assertions, tv, vbPinned, tv.Deployment(), tv.VersioningOverridePinned(s.useV32), nil)
// create version v2 and make it current which shall make v1 go from current -> draining/drained
idlePollerDone = make(chan struct{})
@@ -471,7 +471,7 @@ func (s *Versioning3Suite) testQueryWithPinnedOverride(sticky bool) {
runID := s.startWorkflow(tv, tv.VersioningOverridePinned(s.useV32))
s.WaitForChannel(ctx, wftCompleted)
s.verifyWorkflowVersioning(tv, vbUnpinned, tv.Deployment(), tv.VersioningOverridePinned(s.useV32), nil)
s.verifyWorkflowVersioning(s.Assertions, tv, vbUnpinned, tv.Deployment(), tv.VersioningOverridePinned(s.useV32), nil)
if sticky {
s.verifyWorkflowStickyQueue(tv.WithRunID(runID))
}
@@ -509,7 +509,7 @@ func (s *Versioning3Suite) testUnpinnedQuery(sticky bool) {
s.pollWftAndHandle(tv, false, wftCompleted,
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
s.NotNil(task)
s.verifyWorkflowVersioning(tv, vbUnspecified, nil, nil, tv.DeploymentVersionTransition())
s.verifyWorkflowVersioning(s.Assertions, tv, vbUnspecified, nil, nil, tv.DeploymentVersionTransition())
return respondEmptyWft(tv, sticky, vbUnpinned), nil
})
@@ -519,7 +519,7 @@ func (s *Versioning3Suite) testUnpinnedQuery(sticky bool) {
runID := s.startWorkflow(tv, nil)
s.WaitForChannel(ctx, wftCompleted)
s.verifyWorkflowVersioning(tv, vbUnpinned, tv.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv, vbUnpinned, tv.Deployment(), nil, nil)
if sticky {
s.verifyWorkflowStickyQueue(tv.WithRunID(runID))
}
@@ -589,7 +589,7 @@ func (s *Versioning3Suite) testPinnedWorkflowWithLateActivityPoller() {
s.startWorkflow(tv, override)
s.WaitForChannel(ctx, wftCompleted)
s.verifyWorkflowVersioning(tv, vbUnpinned, tv.Deployment(), override, nil)
s.verifyWorkflowVersioning(s.Assertions, tv, vbUnpinned, tv.Deployment(), override, nil)
// Wait long enough to make sure the activity is backlogged.
s.validateBacklogCount(tv, tqTypeAct, 1)
@@ -600,7 +600,7 @@ func (s *Versioning3Suite) testPinnedWorkflowWithLateActivityPoller() {
s.NotNil(task)
return respondActivity(), nil
})
s.verifyWorkflowVersioning(tv, vbUnpinned, tv.Deployment(), override, nil)
s.verifyWorkflowVersioning(s.Assertions, tv, vbUnpinned, tv.Deployment(), override, nil)
s.validateBacklogCount(tv, tqTypeAct, 0)
s.pollWftAndHandle(tv, false, nil,
@@ -608,7 +608,7 @@ func (s *Versioning3Suite) testPinnedWorkflowWithLateActivityPoller() {
s.NotNil(task)
return respondCompleteWorkflow(tv, vbUnpinned), nil
})
s.verifyWorkflowVersioning(tv, vbUnpinned, tv.Deployment(), override, nil)
s.verifyWorkflowVersioning(s.Assertions, tv, vbUnpinned, tv.Deployment(), override, nil)
}
func (s *Versioning3Suite) TestUnpinnedWorkflow_Sticky() {
@@ -641,7 +641,7 @@ func (s *Versioning3Suite) testUnpinnedWorkflow(sticky bool) {
s.pollWftAndHandle(tv, false, wftCompleted,
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
s.NotNil(task)
s.verifyWorkflowVersioning(tv, vbUnspecified, nil, nil, tv.DeploymentVersionTransition())
s.verifyWorkflowVersioning(s.Assertions, tv, vbUnspecified, nil, nil, tv.DeploymentVersionTransition())
return respondWftWithActivities(tv, tv, sticky, vbUnpinned, "5"), nil
})
@@ -657,21 +657,21 @@ func (s *Versioning3Suite) testUnpinnedWorkflow(sticky bool) {
runID := s.startWorkflow(tv, nil)
s.WaitForChannel(ctx, wftCompleted)
s.verifyWorkflowVersioning(tv, vbUnpinned, tv.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv, vbUnpinned, tv.Deployment(), nil, nil)
s.verifyVersioningSAs(tv, vbUnpinned, tv)
if sticky {
s.verifyWorkflowStickyQueue(tv.WithRunID(runID))
}
s.WaitForChannel(ctx, actCompleted)
s.verifyWorkflowVersioning(tv, vbUnpinned, tv.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv, vbUnpinned, tv.Deployment(), nil, nil)
s.pollWftAndHandle(tv, sticky, nil,
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
s.NotNil(task)
return respondCompleteWorkflow(tv, vbUnpinned), nil
})
s.verifyWorkflowVersioning(tv, vbUnpinned, tv.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv, vbUnpinned, tv.Deployment(), nil, nil)
}
// drainWorkflowTaskAfterSetCurrent is a helper that sets the current deployment version,
@@ -684,7 +684,7 @@ func (s *Versioning3Suite) drainWorkflowTaskAfterSetCurrent(
s.pollWftAndHandle(tv, false, wftCompleted,
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
s.NotNil(task)
s.verifyWorkflowVersioning(tv, vbUnspecified, nil, nil, tv.DeploymentVersionTransition())
s.verifyWorkflowVersioning(s.Assertions, tv, vbUnspecified, nil, nil, tv.DeploymentVersionTransition())
return respondEmptyWft(tv, false, vbUnpinned), nil
})
s.waitForDeploymentDataPropagation(tv, versionStatusInactive, false, tqTypeWf)
@@ -739,7 +739,7 @@ func (s *Versioning3Suite) TestUnpinnedWorkflow_SuccessfulUpdate_TransitionsToNe
// VersioningInfo should not have changed before the update has been processed by the poller.
// Deployment version transition should also be nil since this is a speculative task.
s.verifyWorkflowVersioning(tv1, vbUnpinned, tv1.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv1, vbUnpinned, tv1.Deployment(), nil, nil)
return &workflowservice.RespondWorkflowTaskCompletedRequest{
Commands: s.UpdateAcceptCompleteCommands(tv2),
@@ -781,7 +781,7 @@ func (s *Versioning3Suite) TestUnpinnedWorkflow_SuccessfulUpdate_TransitionsToNe
// Since the poller accepted the update, the Worker Deployment Version that completed the last workflow task
// of this workflow execution should have changed to the new version. However, the version transition should
// still be nil.
s.verifyWorkflowVersioning(tv2, vbUnpinned, tv2.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv2, vbUnpinned, tv2.Deployment(), nil, nil)
}
@@ -825,7 +825,7 @@ func (s *Versioning3Suite) TestUnpinnedWorkflow_FailedUpdate_DoesNotTransitionTo
// VersioningInfo should not have changed before the update has been processed by the poller.
// Deployment version transition should also be nil since this is a speculative task.
s.verifyWorkflowVersioning(tv1, vbUnpinned, tv1.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv1, vbUnpinned, tv1.Deployment(), nil, nil)
updRequestMsg := task.Messages[0]
updRequest := protoutils.UnmarshalAny[*updatepb.Request](s.T(), updRequestMsg.GetBody())
@@ -859,7 +859,7 @@ func (s *Versioning3Suite) TestUnpinnedWorkflow_FailedUpdate_DoesNotTransitionTo
// Since the poller rejected the update, the Worker Deployment Version that completed the last workflow task
// of this workflow execution should not have changed.
s.verifyWorkflowVersioning(tv1, vbUnpinned, tv1.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv1, vbUnpinned, tv1.Deployment(), nil, nil)
}
func (s *Versioning3Suite) sendUpdateNoError(tv *testvars.TestVars) <-chan *workflowservice.UpdateWorkflowExecutionResponse {
@@ -1247,10 +1247,10 @@ func (s *Versioning3Suite) testTransitionFromWft(sticky bool, toUnversioned bool
s.pollWftAndHandle(tv1, false, nil,
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
s.NotNil(task)
s.verifyWorkflowVersioning(tv1, vbUnspecified, nil, nil, tv1.DeploymentVersionTransition())
s.verifyWorkflowVersioning(s.Assertions, tv1, vbUnspecified, nil, nil, tv1.DeploymentVersionTransition())
return respondWftWithActivities(tv1, tv1, sticky, vbUnpinned, "5"), nil
})
s.verifyWorkflowVersioning(tv1, vbUnpinned, tv1.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv1, vbUnpinned, tv1.Deployment(), nil, nil)
if sticky {
s.verifyWorkflowStickyQueue(tv1.WithRunID(runID))
}
@@ -1260,7 +1260,7 @@ func (s *Versioning3Suite) testTransitionFromWft(sticky bool, toUnversioned bool
s.NotNil(task)
return respondActivity(), nil
})
s.verifyWorkflowVersioning(tv1, vbUnpinned, tv1.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv1, vbUnpinned, tv1.Deployment(), nil, nil)
if toUnversioned {
// unset A as current
@@ -1277,10 +1277,10 @@ func (s *Versioning3Suite) testTransitionFromWft(sticky bool, toUnversioned bool
s.unversionedPollWftAndHandle(tv1, false, nil,
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
s.NotNil(task)
s.verifyWorkflowVersioning(tv1, vbUnpinned, tv1.Deployment(), nil, &workflowpb.DeploymentVersionTransition{Version: "__unversioned__"})
s.verifyWorkflowVersioning(s.Assertions, tv1, vbUnpinned, tv1.Deployment(), nil, &workflowpb.DeploymentVersionTransition{Version: "__unversioned__"})
return respondCompleteWorkflowUnversioned(tv1), nil
})
s.verifyWorkflowVersioning(tv1, vbUnspecified, nil, nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv1, vbUnspecified, nil, nil, nil)
} else {
// Set B as the current deployment
@@ -1301,10 +1301,10 @@ func (s *Versioning3Suite) testTransitionFromWft(sticky bool, toUnversioned bool
s.pollWftAndHandle(tv2, false, nil,
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
s.NotNil(task)
s.verifyWorkflowVersioning(tv1, vbUnpinned, tv1.Deployment(), nil, tv2.DeploymentVersionTransition())
s.verifyWorkflowVersioning(s.Assertions, tv1, vbUnpinned, tv1.Deployment(), nil, tv2.DeploymentVersionTransition())
return respondCompleteWorkflow(tv2, vbUnpinned), nil
})
s.verifyWorkflowVersioning(tv2, vbUnpinned, tv2.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv2, vbUnpinned, tv2.Deployment(), nil, nil)
}
}
@@ -1372,7 +1372,7 @@ func (s *Versioning3Suite) testDoubleTransition(unversionedSrc bool, signal bool
s.NotNil(task)
return respondWftWithActivities(tv1, tv1, false, sourceVB, "5"), nil
})
s.verifyWorkflowVersioning(tv1, sourceVB, sourceV, nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv1, sourceVB, sourceV, nil, nil)
if signal {
// Send a signal so a wf task is scheduled before we poll the activity
@@ -1443,16 +1443,16 @@ func (s *Versioning3Suite) testDoubleTransition(unversionedSrc bool, signal bool
s.doPollWftAndHandle(tv1, !unversionedSrc, false, nil,
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
s.NotNil(task)
s.verifyWorkflowVersioning(tv1, sourceVB, sourceV, nil, sourceTransition)
s.verifyWorkflowVersioning(s.Assertions, tv1, sourceVB, sourceV, nil, sourceTransition)
return respondEmptyWft(tv1, false, sourceVB), nil
})
s.verifyWorkflowVersioning(tv1, sourceVB, sourceV, nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv1, sourceVB, sourceV, nil, nil)
// Activity should be unblocked now to sourceV poller
s.doPollActivityAndHandle(tv1, !unversionedSrc, nil,
func(task *workflowservice.PollActivityTaskQueueResponse) (*workflowservice.RespondActivityTaskCompletedRequest, error) {
s.NotNil(task)
s.verifyWorkflowVersioning(tv1, sourceVB, sourceV, nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv1, sourceVB, sourceV, nil, nil)
return respondActivity(), nil
})
@@ -1474,10 +1474,10 @@ func (s *Versioning3Suite) testDoubleTransition(unversionedSrc bool, signal bool
s.pollWftAndHandle(tv2, false, nil,
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
s.NotNil(task)
s.verifyWorkflowVersioning(tv2, sourceVB, sourceV, nil, tv2.DeploymentVersionTransition())
s.verifyWorkflowVersioning(s.Assertions, tv2, sourceVB, sourceV, nil, tv2.DeploymentVersionTransition())
return respondCompleteWorkflow(tv2, vbUnpinned), nil
})
s.verifyWorkflowVersioning(tv2, vbUnpinned, tv2.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv2, vbUnpinned, tv2.Deployment(), nil, nil)
}
func (s *Versioning3Suite) TestNexusTask_StaysOnCurrentDeployment() {
@@ -1582,12 +1582,12 @@ func (s *Versioning3Suite) TestEagerActivity() {
poller, resp := s.pollWftAndHandle(tv, false, nil,
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
s.NotNil(task)
s.verifyWorkflowVersioning(tv, vbUnspecified, nil, nil, tv.DeploymentVersionTransition())
s.verifyWorkflowVersioning(s.Assertions, tv, vbUnspecified, nil, nil, tv.DeploymentVersionTransition())
resp := respondWftWithActivities(tv, tv, true, vbUnpinned, "5")
resp.Commands[0].GetScheduleActivityTaskCommandAttributes().RequestEagerExecution = true
return resp, nil
})
s.verifyWorkflowVersioning(tv, vbUnpinned, tv.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv, vbUnpinned, tv.Deployment(), nil, nil)
s.NotEmpty(resp.GetActivityTasks())
@@ -1597,14 +1597,14 @@ func (s *Versioning3Suite) TestEagerActivity() {
return respondActivity(), nil
})
s.NoError(err)
s.verifyWorkflowVersioning(tv, vbUnpinned, tv.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv, vbUnpinned, tv.Deployment(), nil, nil)
s.pollWftAndHandle(tv, false, nil,
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
s.NotNil(task)
return respondCompleteWorkflow(tv, vbUnpinned), nil
})
s.verifyWorkflowVersioning(tv, vbUnpinned, tv.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv, vbUnpinned, tv.Deployment(), nil, nil)
}
func (s *Versioning3Suite) TestTransitionFromActivity_Sticky() {
@@ -1654,10 +1654,10 @@ func (s *Versioning3Suite) testTransitionFromActivity(sticky bool) {
s.pollWftAndHandle(tv1, false, nil,
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
s.NotNil(task)
s.verifyWorkflowVersioning(tv1, vbUnspecified, nil, nil, tv1.DeploymentVersionTransition())
s.verifyWorkflowVersioning(s.Assertions, tv1, vbUnspecified, nil, nil, tv1.DeploymentVersionTransition())
return respondWftWithActivities(tv1, tv1, sticky, vbUnpinned, "5", "6", "7", "8"), nil
})
s.verifyWorkflowVersioning(tv1, vbUnpinned, tv1.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv1, vbUnpinned, tv1.Deployment(), nil, nil)
if sticky {
s.verifyWorkflowStickyQueue(tv1.WithRunID(runID))
}
@@ -1697,7 +1697,7 @@ func (s *Versioning3Suite) testTransitionFromActivity(sticky bool) {
})
s.WaitForChannel(ctx, act2Started)
s.verifyWorkflowVersioning(tv1, vbUnpinned, tv1.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv1, vbUnpinned, tv1.Deployment(), nil, nil)
// 2. Set d2 as the current deployment
if s.useNewDeploymentData {
@@ -1743,7 +1743,7 @@ func (s *Versioning3Suite) testTransitionFromActivity(sticky bool) {
s.pollWftAndHandle(tv2, false, nil,
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
s.NotNil(task)
s.verifyWorkflowVersioning(tv1, vbUnpinned, tv1.Deployment(), nil, tv2.DeploymentVersionTransition())
s.verifyWorkflowVersioning(s.Assertions, tv1, vbUnpinned, tv1.Deployment(), nil, tv2.DeploymentVersionTransition())
close(transitionStarted)
s.Logger.Info("Transition wft started")
// 8. Complete the transition after act1 completes and act2's first attempt fails.
@@ -1753,7 +1753,7 @@ func (s *Versioning3Suite) testTransitionFromActivity(sticky bool) {
s.Logger.Info("Transition wft completed")
return respondEmptyWft(tv2, sticky, vbUnpinned), nil
})
s.verifyWorkflowVersioning(tv2, vbUnpinned, tv2.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv2, vbUnpinned, tv2.Deployment(), nil, nil)
if sticky {
s.verifyWorkflowStickyQueue(tv2)
}
@@ -1766,7 +1766,7 @@ func (s *Versioning3Suite) testTransitionFromActivity(sticky bool) {
s.Logger.Info("Final wft completed")
return respondCompleteWorkflow(tv2, vbUnpinned), nil
})
s.verifyWorkflowVersioning(tv2, vbUnpinned, tv2.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv2, vbUnpinned, tv2.Deployment(), nil, nil)
}
func (s *Versioning3Suite) TestIndependentVersionedActivity_Pinned() {
@@ -1827,11 +1827,11 @@ func (s *Versioning3Suite) testIndependentActivity(behavior enumspb.VersioningBe
s.pollWftAndHandle(tvWf, false, nil,
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
s.NotNil(task)
s.verifyWorkflowVersioning(tvWf, vbUnspecified, nil, nil, tvWf.DeploymentVersionTransition())
s.verifyWorkflowVersioning(s.Assertions, tvWf, vbUnspecified, nil, nil, tvWf.DeploymentVersionTransition())
s.Logger.Info("First wf task completed")
return respondWftWithActivities(tvWf, tvAct, false, behavior, "5"), nil
})
s.verifyWorkflowVersioning(tvWf, behavior, tvWf.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tvWf, behavior, tvWf.Deployment(), nil, nil)
if unversionedActivity {
s.unversionedPollActivityAndHandle(tvAct, nil,
@@ -1848,14 +1848,14 @@ func (s *Versioning3Suite) testIndependentActivity(behavior enumspb.VersioningBe
return respondActivity(), nil
})
}
s.verifyWorkflowVersioning(tvWf, behavior, tvWf.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tvWf, behavior, tvWf.Deployment(), nil, nil)
s.pollWftAndHandle(tvWf, false, nil,
func(task *workflowservice.PollWorkflowTaskQueueResponse) (*workflowservice.RespondWorkflowTaskCompletedRequest, error) {
s.NotNil(task)
return respondCompleteWorkflow(tvWf, behavior), nil
})
s.verifyWorkflowVersioning(tvWf, behavior, tvWf.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tvWf, behavior, tvWf.Deployment(), nil, nil)
}
func (s *Versioning3Suite) TestChildWorkflowInheritance_PinnedParent() {
@@ -1902,7 +1902,7 @@ func (s *Versioning3Suite) testChildWorkflowInheritance_ExpectInherit(crossTq bo
currentChanged := make(chan struct{}, 1)
childv1 := func(ctx workflow.Context) (string, error) {
s.verifyWorkflowVersioning(tv1Child, vbPinned, tv1Child.Deployment(), override, nil)
s.verifyWorkflowVersioning(s.Assertions, tv1Child, vbPinned, tv1Child.Deployment(), override, nil)
return "v1", nil
}
wf1 := func(ctx workflow.Context) (string, error) {
@@ -1918,7 +1918,7 @@ func (s *Versioning3Suite) testChildWorkflowInheritance_ExpectInherit(crossTq bo
var val1 string
s.NoError(fut1.Get(ctx, &val1))
s.verifyWorkflowVersioning(tv1, parentRegistrationBehavior, tv1.Deployment(), override, nil)
s.verifyWorkflowVersioning(s.Assertions, tv1, parentRegistrationBehavior, tv1.Deployment(), override, nil)
return val1, nil
}
@@ -2056,7 +2056,7 @@ func (s *Versioning3Suite) testChildWorkflowInheritance_ExpectNoInherit(crossTq
var val1 string
s.NoError(fut1.Get(ctx, &val1))
s.verifyWorkflowVersioning(tv1, parentBehavior, tv1.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv1, parentBehavior, tv1.Deployment(), nil, nil)
return val1, nil
}
@@ -2147,11 +2147,11 @@ func (s *Versioning3Suite) testChildWorkflowInheritance_ExpectNoInherit(crossTq
s.Equal("v2", out)
if parentBehavior == vbPinned {
s.verifyWorkflowVersioning(tv1, parentBehavior, tv1.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv1, parentBehavior, tv1.Deployment(), nil, nil)
} else {
s.verifyWorkflowVersioning(tv1, parentBehavior, tv2.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv1, parentBehavior, tv2.Deployment(), nil, nil)
}
s.verifyWorkflowVersioning(tv2Child, vbPinned, tv2Child.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv2Child, vbPinned, tv2Child.Deployment(), nil, nil)
}
func (s *Versioning3Suite) TestPinnedCaN_SameTQ() {
@@ -2193,13 +2193,13 @@ func (s *Versioning3Suite) testCan(crossTq bool, behavior enumspb.VersioningBeha
if crossTq {
newCtx = workflow.WithWorkflowTaskQueue(newCtx, canxTq)
}
s.verifyWorkflowVersioning(tv1, vbUnspecified, nil, nil, tv1.DeploymentVersionTransition())
s.verifyWorkflowVersioning(s.Assertions, tv1, vbUnspecified, nil, nil, tv1.DeploymentVersionTransition())
wfStarted <- struct{}{}
// wait for current version to change.
<-currentChanged
return "", workflow.NewContinueAsNewError(newCtx, "wf", attempt+1)
case 1:
s.verifyWorkflowVersioning(tv1, vbPinned, tv1.Deployment(), nil, nil)
s.verifyWorkflowVersioning(s.Assertions, tv1, vbPinned, tv1.Deployment(), nil, nil)
return "v1", nil
}
s.FailNow("workflow should not get to this point")
@@ -2209,9 +2209,9 @@ func (s *Versioning3Suite) testCan(crossTq bool, behavior enumspb.VersioningBeha
wf2 := func(ctx workflow.Context, attempt int) (string, error) {
if behavior == vbUnpinned && s.deploymentWorkflowVersion >= workerdeployment.AsyncSetCurrentAndRamping {
// Unpinned CaN should inherit parent deployment version and behaviour
s.verifyWorkflowVersioning(tv2, vbUnpinned, tv1.Deployment(), nil, tv2.DeploymentVersionTransition())
s.verifyWorkflowVersioning(s.Assertions, tv2, vbUnpinned, tv1.Deployment(), nil, tv2.DeploymentVersionTransition())
} else {
s.verifyWorkflowVersioning(tv2, vbUnspecified, nil, nil, tv2.DeploymentVersionTransition())
s.verifyWorkflowVersioning(s.Assertions, tv2, vbUnspecified, nil, nil, tv2.DeploymentVersionTransition())
}
return "v2", nil
}
@@ -2994,6 +2994,7 @@ func (s *Versioning3Suite) forgetTaskQueueDeploymentVersion(
}
func (s *Versioning3Suite) verifyWorkflowVersioning(
a *require.Assertions,
tv *testvars.TestVars,
behavior enumspb.VersioningBehavior,
deployment *deploymentpb.Deployment,
@@ -3008,23 +3009,23 @@ func (s *Versioning3Suite) verifyWorkflowVersioning(
},
},
)
s.NoError(err)
a.NoError(err)
versioningInfo := dwf.WorkflowExecutionInfo.GetVersioningInfo()
s.Equal(behavior.String(), versioningInfo.GetBehavior().String())
a.Equal(behavior.String(), versioningInfo.GetBehavior().String())
var v *deploymentspb.WorkerDeploymentVersion
if versioningInfo.GetVersion() != "" { //nolint:staticcheck // SA1019: worker versioning v0.31
//nolint:staticcheck // SA1019: worker versioning v0.31
v, err = worker_versioning.WorkerDeploymentVersionFromStringV31(versioningInfo.GetVersion())
s.NoError(err)
s.NotNil(versioningInfo.GetDeploymentVersion()) // make sure we are always populating this whenever Version string is populated
a.NoError(err)
a.NotNil(versioningInfo.GetDeploymentVersion()) // make sure we are always populating this whenever Version string is populated
}
if dv := versioningInfo.GetDeploymentVersion(); dv != nil {
v = worker_versioning.DeploymentVersionFromDeployment(worker_versioning.DeploymentFromExternalDeploymentVersion(dv))
}
actualDeployment := worker_versioning.DeploymentFromDeploymentVersion(v)
if !deployment.Equal(actualDeployment) {
s.Fail(fmt.Sprintf("deployment version mismatch. expected: {%s}, actual: {%s}",
a.Fail(fmt.Sprintf("deployment version mismatch. expected: {%s}, actual: {%s}",
deployment,
actualDeployment,
))
@@ -3032,30 +3033,30 @@ func (s *Versioning3Suite) verifyWorkflowVersioning(
if s.useV32 {
// v0.32 override
s.Equal(override.GetAutoUpgrade(), versioningInfo.GetVersioningOverride().GetAutoUpgrade())
s.Equal(override.GetPinned().GetVersion().GetBuildId(), versioningInfo.GetVersioningOverride().GetPinned().GetVersion().GetBuildId())
s.Equal(override.GetPinned().GetVersion().GetDeploymentName(), versioningInfo.GetVersioningOverride().GetPinned().GetVersion().GetDeploymentName())
s.Equal(override.GetPinned().GetBehavior(), versioningInfo.GetVersioningOverride().GetPinned().GetBehavior())
a.Equal(override.GetAutoUpgrade(), versioningInfo.GetVersioningOverride().GetAutoUpgrade())
a.Equal(override.GetPinned().GetVersion().GetBuildId(), versioningInfo.GetVersioningOverride().GetPinned().GetVersion().GetBuildId())
a.Equal(override.GetPinned().GetVersion().GetDeploymentName(), versioningInfo.GetVersioningOverride().GetPinned().GetVersion().GetDeploymentName())
a.Equal(override.GetPinned().GetBehavior(), versioningInfo.GetVersioningOverride().GetPinned().GetBehavior())
if worker_versioning.OverrideIsPinned(override) {
s.Equal(override.GetPinned().GetVersion().GetDeploymentName(), dwf.WorkflowExecutionInfo.GetWorkerDeploymentName())
a.Equal(override.GetPinned().GetVersion().GetDeploymentName(), dwf.WorkflowExecutionInfo.GetWorkerDeploymentName())
}
} else {
// v0.31 override
s.Equal(override.GetBehavior().String(), versioningInfo.GetVersioningOverride().GetBehavior().String()) //nolint:staticcheck // SA1019: worker versioning v0.31
a.Equal(override.GetBehavior().String(), versioningInfo.GetVersioningOverride().GetBehavior().String()) //nolint:staticcheck // SA1019: worker versioning v0.31
if actualOverrideDeployment := versioningInfo.GetVersioningOverride().GetPinnedVersion(); override.GetPinnedVersion() != actualOverrideDeployment { //nolint:staticcheck // SA1019: worker versioning v0.31
s.Fail(fmt.Sprintf("pinned override mismatch. expected: {%s}, actual: {%s}",
a.Fail(fmt.Sprintf("pinned override mismatch. expected: {%s}, actual: {%s}",
override.GetPinnedVersion(), //nolint:staticcheck // SA1019: worker versioning v0.31
actualOverrideDeployment,
))
}
if worker_versioning.OverrideIsPinned(override) {
d, _ := worker_versioning.WorkerDeploymentVersionFromStringV31(override.GetPinnedVersion()) //nolint:staticcheck // SA1019: worker versioning v0.31
s.Equal(d.GetDeploymentName(), dwf.WorkflowExecutionInfo.GetWorkerDeploymentName())
a.Equal(d.GetDeploymentName(), dwf.WorkflowExecutionInfo.GetWorkerDeploymentName())
}
}
if !versioningInfo.GetVersionTransition().Equal(transition) {
s.Fail(fmt.Sprintf("version transition mismatch. expected: {%s}, actual: {%s}",
a.Fail(fmt.Sprintf("version transition mismatch. expected: {%s}, actual: {%s}",
transition,
versioningInfo.GetVersionTransition(),
))
@@ -3710,7 +3711,8 @@ func (s *Versioning3Suite) TestAutoUpgradeWorkflows_NoBouncingBetweenVersions()
// Verify that the workflow is running on v1
s.EventuallyWithT(func(t *assert.CollectT) {
s.verifyWorkflowVersioning(tv1, vbUnpinned, tv1.Deployment(), nil, nil)
a := require.New(t)
s.verifyWorkflowVersioning(a, tv1, vbUnpinned, tv1.Deployment(), nil, nil)
}, 10*time.Second, 100*time.Millisecond)
// Start v0 workers to ensure they never receive a task
@@ -3780,7 +3782,8 @@ func (s *Versioning3Suite) TestWorkflowTQLags_DependentActivityStartsTransition(
// Verify that the workflow is running on v1.
s.EventuallyWithT(func(t *assert.CollectT) {
s.verifyWorkflowVersioning(tv0, vbUnpinned, tv0.Deployment(), nil, nil)
a := require.New(t)
s.verifyWorkflowVersioning(a, tv0, vbUnpinned, tv0.Deployment(), nil, nil)
}, 10*time.Second, 100*time.Millisecond)
// Update the userData for the activity TQ by setting the current version to v1.
@@ -3818,7 +3821,8 @@ func (s *Versioning3Suite) TestWorkflowTQLags_DependentActivityStartsTransition(
// Verify that the workflow is running on v1.
s.EventuallyWithT(func(t *assert.CollectT) {
s.verifyWorkflowVersioning(tv0, vbUnpinned, tv1.Deployment(), nil, nil)
a := require.New(t)
s.verifyWorkflowVersioning(a, tv0, vbUnpinned, tv1.Deployment(), nil, nil)
}, 10*time.Second, 100*time.Millisecond)
}
@@ -3876,7 +3880,8 @@ func (s *Versioning3Suite) TestActivityTQLags_DependentActivityCompletesOnTheNew
// Verify that the workflow is running on v0.
s.EventuallyWithT(func(t *assert.CollectT) {
s.verifyWorkflowVersioning(tv0, vbUnpinned, tv0.Deployment(), nil, nil)
a := require.New(t)
s.verifyWorkflowVersioning(a, tv0, vbUnpinned, tv0.Deployment(), nil, nil)
}, 10*time.Second, 100*time.Millisecond)
// Update the userData for the workflow TQ *only* by setting the current version to v1
@@ -3918,7 +3923,8 @@ func (s *Versioning3Suite) TestActivityTQLags_DependentActivityCompletesOnTheNew
// Verify that the workflow is still running on v1.
s.EventuallyWithT(func(t *assert.CollectT) {
s.verifyWorkflowVersioning(tv1, vbUnpinned, tv1.Deployment(), nil, nil)
a := require.New(t)
s.verifyWorkflowVersioning(a, tv1, vbUnpinned, tv1.Deployment(), nil, nil)
}, 10*time.Second, 100*time.Millisecond)
}

View File

@@ -24,15 +24,21 @@ COMMANDS:
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--endpoint value hostname or ip address of elasticsearch server (default: "http://127.0.0.1:9200") [$ES_SERVER]
--user value username for elasticsearch or aws_access_key_id if using static aws credentials [$ES_USER]
--password value password for elasticsearch or aws_secret_access_key if using static aws credentials [$ES_PWD]
--aws-credentials value AWS credentials provider (supported ['static', 'environment', 'aws-sdk-default']) [$AWS_CREDENTIALS]
--aws-session-token value AWS sessiontoken for use with 'static' AWS credentials provider [$AWS_SESSION_TOKEN]
--index value name of the visibility index [$ES_VISIBILITY_INDEX]
--quiet don't log errors to stderr (default: false)
--help, -h show help
--version, -v print the version
--endpoint value hostname or ip address of elasticsearch server (default: "http://127.0.0.1:9200") [$ES_SERVER]
--user value username for elasticsearch or aws_access_key_id if using static aws credentials [$ES_USER]
--password value password for elasticsearch or aws_secret_access_key if using static aws credentials [$ES_PWD]
--aws-credentials value AWS credentials provider (supported ['static', 'environment', 'aws-sdk-default']) [$AWS_CREDENTIALS]
--aws-session-token value AWS sessiontoken for use with 'static' AWS credentials provider [$AWS_SESSION_TOKEN]
--tls enable TLS for elasticsearch connection [$ES_TLS]
--tls-cert-file value path to TLS certificate file (tls must be enabled) [$ES_TLS_CERT_FILE]
--tls-key-file value path to TLS key file (tls must be enabled) [$ES_TLS_KEY_FILE]
--tls-ca-file value path to TLS CA certificate file (tls must be enabled) [$ES_TLS_CA_FILE]
--tls-server-name value TLS server name for host name verification (tls must be enabled) [$ES_TLS_SERVER_NAME]
--tls-disable-host-verification disable TLS host name verification (tls must be enabled) [$ES_TLS_DISABLE_HOST_VERIFICATION]
--index value name of the visibility index [$ES_VISIBILITY_INDEX]
--quiet don't log errors to stderr (default: false)
--help, -h show help
--version, -v print the version
```
## For localhost development
@@ -150,6 +156,74 @@ temporal-elasticsearch-tool --aws static setup-schema
temporal-elasticsearch-tool --aws static create-index
```
### TLS Configuration
The tool supports TLS for secure connections to Elasticsearch.
#### Basic TLS with CA certificate
```bash
export ES_SERVER=https://elasticsearch.example.com:9200
export ES_TLS=true
export ES_TLS_CA_FILE=/path/to/ca.crt
export ES_USER=elastic
export ES_PWD=password
temporal-elasticsearch-tool setup-schema
temporal-elasticsearch-tool create-index
```
#### TLS with client certificate authentication
```bash
export ES_SERVER=https://elasticsearch.example.com:9200
export ES_TLS=true
export ES_TLS_CA_FILE=/path/to/ca.crt
export ES_TLS_CERT_FILE=/path/to/client.crt
export ES_TLS_KEY_FILE=/path/to/client.key
export ES_USER=elastic
export ES_PWD=password
temporal-elasticsearch-tool setup-schema
temporal-elasticsearch-tool create-index
```
#### TLS with custom server name
```bash
export ES_SERVER=https://elasticsearch.example.com:9200
export ES_TLS=true
export ES_TLS_CA_FILE=/path/to/ca.crt
export ES_TLS_SERVER_NAME=elasticsearch.internal
export ES_USER=elastic
export ES_PWD=password
temporal-elasticsearch-tool setup-schema
```
#### TLS with disabled host verification (not recommended for production)
```bash
export ES_SERVER=https://elasticsearch.example.com:9200
export ES_TLS=true
export ES_TLS_DISABLE_HOST_VERIFICATION=true
export ES_USER=elastic
export ES_PWD=password
temporal-elasticsearch-tool setup-schema
```
#### Using command line flags
All TLS options can also be specified as command line flags:
```bash
temporal-elasticsearch-tool \
--endpoint https://elasticsearch.example.com:9200 \
--tls \
--tls-ca-file /path/to/ca.crt \
--tls-cert-file /path/to/client.crt \
--tls-key-file /path/to/client.key \
--tls-server-name elasticsearch.internal \
--user elastic \
--password password \
setup-schema
```
### Additional Commands
#### Update Schema

View File

@@ -7,6 +7,7 @@ import (
"net/url"
"github.com/urfave/cli"
"go.temporal.io/server/common/auth"
"go.temporal.io/server/common/log"
"go.temporal.io/server/common/log/tag"
esclient "go.temporal.io/server/common/persistence/visibility/store/elasticsearch/client"
@@ -120,6 +121,17 @@ func parseElasticConfig(cli *cli.Context) (*esclient.Config, error) {
}
}
if cli.GlobalBool(commonschema.CLIFlagEnableTLS) {
cfg.TLS = &auth.TLS{
Enabled: true,
CertFile: cli.GlobalString(commonschema.CLIFlagTLSCertFile),
KeyFile: cli.GlobalString(commonschema.CLIFlagTLSKeyFile),
CaFile: cli.GlobalString(commonschema.CLIFlagTLSCaFile),
ServerName: cli.GlobalString(commonschema.CLIFlagTLSHostName),
EnableHostVerification: !cli.GlobalBool(commonschema.CLIFlagTLSDisableHostVerification),
}
}
return cfg, nil
}

View File

@@ -82,6 +82,40 @@ func BuildCLIOptions() *cli.App {
Name: commonschema.CLIOptQuiet,
Usage: "don't log errors to stderr",
},
cli.BoolFlag{
Name: commonschema.CLIFlagEnableTLS,
Usage: "enable TLS for elasticsearch connection",
EnvVar: "ES_TLS",
},
cli.StringFlag{
Name: commonschema.CLIFlagTLSCertFile,
Value: "",
Usage: "path to TLS certificate file (tls must be enabled)",
EnvVar: "ES_TLS_CERT_FILE",
},
cli.StringFlag{
Name: commonschema.CLIFlagTLSKeyFile,
Value: "",
Usage: "path to TLS key file (tls must be enabled)",
EnvVar: "ES_TLS_KEY_FILE",
},
cli.StringFlag{
Name: commonschema.CLIFlagTLSCaFile,
Value: "",
Usage: "path to TLS CA certificate file (tls must be enabled)",
EnvVar: "ES_TLS_CA_FILE",
},
cli.BoolFlag{
Name: commonschema.CLIFlagTLSDisableHostVerification,
Usage: "disable TLS host name verification (tls must be enabled)",
EnvVar: "ES_TLS_DISABLE_HOST_VERIFICATION",
},
cli.StringFlag{
Name: commonschema.CLIFlagTLSHostName,
Value: "",
Usage: "TLS server name for host name verification (tls must be enabled)",
EnvVar: "ES_TLS_SERVER_NAME",
},
}
app.Commands = []cli.Command{