mirror of
https://github.com/temporalio/temporal.git
synced 2026-08-30 18:41:49 -07:00
Merge branch 'main' into ss/versioning-override-version-presence
This commit is contained in:
47
.github/actions/build-binaries/action.yml
vendored
Normal file
47
.github/actions/build-binaries/action.yml
vendored
Normal 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 }}
|
||||
133
.github/actions/build-docker-images/action.yml
vendored
Normal file
133
.github/actions/build-docker-images/action.yml
vendored
Normal 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
|
||||
1
.github/actions/build-docker-images/scripts/.gitignore
vendored
Normal file
1
.github/actions/build-docker-images/scripts/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
docker-build-helper
|
||||
561
.github/actions/build-docker-images/scripts/main.go
vendored
Normal file
561
.github/actions/build-docker-images/scripts/main.go
vendored
Normal 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
108
.github/actions/trivy-scan/action.yml
vendored
Normal 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
|
||||
23
.github/docker/targets/admin-tools.Dockerfile
vendored
23
.github/docker/targets/admin-tools.Dockerfile
vendored
@@ -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
62
.github/workflows/build-and-publish.yml
vendored
Normal 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
|
||||
2
.github/workflows/check-pr-placeholders.yml
vendored
2
.github/workflows/check-pr-placeholders.yml
vendored
@@ -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({
|
||||
|
||||
4
.github/workflows/create-tag.yml
vendored
4
.github/workflows/create-tag.yml
vendored
@@ -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 }}
|
||||
|
||||
105
.github/workflows/docker-build-manual.yml
vendored
Normal file
105
.github/workflows/docker-build-manual.yml
vendored
Normal 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"
|
||||
64
.github/workflows/features-integration.yml
vendored
64
.github/workflows/features-integration.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/flaky-tests-report.yml
vendored
2
.github/workflows/flaky-tests-report.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
owner: ${{ github.repository_owner }}
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
32
.github/workflows/goreleaser.yml
vendored
32
.github/workflows/goreleaser.yml
vendored
@@ -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 }}
|
||||
24
.github/workflows/linters.yml
vendored
24
.github/workflows/linters.yml
vendored
@@ -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
|
||||
|
||||
31
.github/workflows/promote-admin-tools-image.yml
vendored
Normal file
31
.github/workflows/promote-admin-tools-image.yml
vendored
Normal 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 }}
|
||||
211
.github/workflows/promote-docker-image.yml
vendored
Normal file
211
.github/workflows/promote-docker-image.yml
vendored
Normal 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`);
|
||||
31
.github/workflows/promote-server-image.yml
vendored
Normal file
31
.github/workflows/promote-server-image.yml
vendored
Normal 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
25
.github/workflows/release.yml
vendored
Normal 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
|
||||
30
.github/workflows/run-tests.yml
vendored
30
.github/workflows/run-tests.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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}",
|
||||
33
docker/targets/admin-tools.Dockerfile
Normal file
33
docker/targets/admin-tools.Dockerfile
Normal 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"]
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user