diff --git a/.agents/skills/utils/check-schema-diff/SKILL.md b/.agents/skills/utils/check-schema-diff/SKILL.md new file mode 100644 index 000000000000..5e44a355f4f5 --- /dev/null +++ b/.agents/skills/utils/check-schema-diff/SKILL.md @@ -0,0 +1,42 @@ +--- +name: check-schema-diff +description: "Run comprehensive checks (breaking changes, missing tests, and missing documentation) on Magic Modules schema changes between the current branch and a base branch (usually main) using the local diff-processor." +--- + +# `check-schema-diff` + +> **Note to AI Agents:** You MUST read the YAML frontmatter above first. Only read the rest of this file if the `description` matches your required task. +> This skill allows you to run local breaking changes, missing tests, and missing documentation checks on Magic Modules schema changes. + +## Prerequisites + +- You must be operating in the `magic-modules` root directory. +- You must have `go` installed and configured. + +## Execution Steps + +### 1. Make the script executable (if not already) +Ensure the script is executable: +```bash +chmod +x .agents/skills/utils/check-schema-diff/scripts/check_schema_diff.sh +``` + +### 2. Run the Schema Diff Checker +Run the script to compare your current local changes (committed or uncommitted) against the base branch (defaults to the merge base with `origin/main` or `main`): +```bash +./.agents/skills/utils/check-schema-diff/scripts/check_schema_diff.sh +``` + +To compare against a specific branch or commit, pass it as an argument: +```bash +./.agents/skills/utils/check-schema-diff/scripts/check_schema_diff.sh +``` + +### 3. Analyze the Output +The tool runs three checks across both GA and Beta provider versions: +- **Breaking Changes:** Detects backwards-incompatible schema changes. +- **Missing Tests:** Identifies new or changed fields that are not covered by any acceptance tests. +- **Missing Documentation:** Identifies new fields that are not documented in the resource/datasource markdown files. + +### 4. Verification & Handoff +If any issues (breaking changes, missing tests, or missing docs) are detected, present them clearly to the user. Discuss potential mitigations and fixes before proceeding. diff --git a/.agents/skills/utils/check-schema-diff/scripts/check_schema_diff.sh b/.agents/skills/utils/check-schema-diff/scripts/check_schema_diff.sh new file mode 100755 index 000000000000..588b6ad1e63e --- /dev/null +++ b/.agents/skills/utils/check-schema-diff/scripts/check_schema_diff.sh @@ -0,0 +1,347 @@ +#!/usr/bin/env bash +# +# check_schema_diff.sh +# +# Automates local schema difference checks (breaking changes, missing tests, +# and missing documentation) for Magic Modules by comparing the current +# working tree against a base branch (e.g., main). +# +# Usage: +# ./.agents/skills/utils/check-schema-diff/scripts/check_schema_diff.sh [base_ref] +# + +set -e + +# Define color codes for pretty printing +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}=== Magic Modules Schema Diff Checker ===${NC}" + +# 1. Validate environment +if [ ! -d "mmv1" ] || [ ! -d "tools/diff-processor" ]; then + echo -e "${RED}Error: This script must be run from the root of the magic-modules repository.${NC}" + exit 1 +fi + +# 2. Determine base ref +BASE_REF="$1" +if [ -z "$BASE_REF" ]; then + # Try to find the merge base with origin/main, fallback to main, fallback to origin/master + BASE_REF=$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main 2>/dev/null || echo "main") +fi + +echo -e "${BLUE}Comparing current changes against base ref: ${YELLOW}${BASE_REF}${NC}" + +# 3. Set up paths +REPO_ROOT=$(pwd) +SCRATCH_DIR="${REPO_ROOT}/scratch/schema-diff-check" +PROVIDER_CACHE="${REPO_ROOT}/scratch/provider-cache" +DIFF_PROCESSOR_DIR="${REPO_ROOT}/tools/diff-processor" + +mkdir -p "$SCRATCH_DIR" + +# 4. Set up cleanup trap to ensure we never leave git worktrees behind +cleanup() { + echo -e "${BLUE}Cleaning up temporary files and worktrees...${NC}" + if [ -d "$SCRATCH_DIR/mm-base-worktree" ]; then + git worktree remove --force "$SCRATCH_DIR/mm-base-worktree" 2>/dev/null || true + fi +} +trap cleanup EXIT + +# Helper function to check if the docs JSON output has missing docs +has_missing_docs() { + local json="$1" + if command -v jq &> /dev/null; then + # Returns 0 (true) if there are missing docs, 1 (false) otherwise + echo "$json" | jq -e '(.Resource | length > 0) or (.DataSource | length > 0)' >/dev/null 2>&1 + return $? + else + # Simple Python fallback to check if either list is non-empty + python3 -c " +import sys, json +data = json.loads(sys.argv[1]) +resources = data.get('Resource') or [] +datasources = data.get('DataSource') or [] +if len(resources) > 0 or len(datasources) > 0: + sys.exit(0) +sys.exit(1) +" "$json" 2>/dev/null + return $? + fi +} + +# 5. Build current mmv1 binary in the current context (once for all checks) +echo -e "${BLUE}Building current mmv1 binary...${NC}" +if [ -f "MODULE.bazel" ] && command -v bazel &> /dev/null; then + bazel build //mmv1 + MM_BINARY="$(pwd)/bazel-bin/mmv1/mmv1_/mmv1" +else + ( + cd mmv1 + go build -o ../bin/mmv1 . + ) + MM_BINARY="$(pwd)/bin/mmv1" +fi + +# 6. Create temporary worktree for base commit (once for all checks) +echo -e "${BLUE}Creating temporary worktree for base commit ${YELLOW}${BASE_REF}${BLUE}...${NC}" +WORKTREE_DIR="$SCRATCH_DIR/mm-base-worktree" +rm -rf "$WORKTREE_DIR" +git worktree add --detach "$WORKTREE_DIR" "$BASE_REF" + + + +run_version_checks() { + local VERSION="$1" + local VERSION_UPPER=$(echo "$VERSION" | tr '[:lower:]' '[:upper:]') + local TARGET_DIFF_PROC="$SCRATCH_DIR/diff-proc-${VERSION}" + local PROVIDER_REPO + local PROVIDER_CACHE + local REAL_PACKAGE_NAME + local REAL_FOLDER_NAME + + if [ "$VERSION" = "ga" ]; then + PROVIDER_REPO="https://github.com/hashicorp/terraform-provider-google.git" + PROVIDER_CACHE="${REPO_ROOT}/scratch/provider-cache-ga" + REAL_PACKAGE_NAME="github.com/hashicorp/terraform-provider-google" + REAL_FOLDER_NAME="google" + else + PROVIDER_REPO="https://github.com/hashicorp/terraform-provider-google-beta.git" + if [ -d "${REPO_ROOT}/scratch/provider-cache" ] && [ ! -d "${REPO_ROOT}/scratch/provider-cache-beta" ]; then + PROVIDER_CACHE="${REPO_ROOT}/scratch/provider-cache" + else + PROVIDER_CACHE="${REPO_ROOT}/scratch/provider-cache-beta" + fi + REAL_PACKAGE_NAME="github.com/hashicorp/terraform-provider-google-beta" + REAL_FOLDER_NAME="google-beta" + fi + + # Prepare isolated diff-processor directory + rm -rf "$TARGET_DIFF_PROC" + mkdir -p "$TARGET_DIFF_PROC" + (cd "$DIFF_PROCESSOR_DIR" && tar cf - --exclude=old --exclude=new --exclude=bin .) | (cd "$TARGET_DIFF_PROC" && tar xf -) + + # Ensure we have a cached downstream provider clone + if [ ! -d "$PROVIDER_CACHE" ]; then + echo -e "${BLUE}[${VERSION_UPPER}] Cloning downstream provider into cache...${NC}" + git clone --depth 500 "$PROVIDER_REPO" "$PROVIDER_CACHE" >/dev/null 2>&1 + else + echo -e "${BLUE}[${VERSION_UPPER}] Updating cached downstream provider...${NC}" + ( + cd "$PROVIDER_CACHE" + git fetch --depth 500 origin main >/dev/null 2>&1 + git reset --hard origin/main >/dev/null 2>&1 + ) + fi + + # Find downstream commit matching base ref + echo -e "${BLUE}[${VERSION_UPPER}] Finding downstream commit matching base ref ${YELLOW}${BASE_REF}${BLUE}...${NC}" + local MATCHING_COMMIT="" + for MM_SHA in $(git log --format="%H" -n 100 "$BASE_REF"); do + MATCHING_COMMIT=$(cd "$PROVIDER_CACHE" && git log --grep="\[upstream:${MM_SHA}\]" --format="%H" -n 1) + if [ -n "$MATCHING_COMMIT" ]; then + echo -e "${GREEN}[${VERSION_UPPER}] Found matching provider commit: ${YELLOW}${MATCHING_COMMIT}${GREEN} (for MM commit ${MM_SHA:0:8})${NC}" + break + fi + done + + if [ -z "$MATCHING_COMMIT" ]; then + echo -e "${YELLOW}[${VERSION_UPPER}] Warning: No matching provider commit found in recent ancestors of ${BASE_REF}; using provider HEAD.${NC}" + MATCHING_COMMIT=$(cd "$PROVIDER_CACHE" && git rev-parse HEAD) + fi + + # Prepare old and new directories in diff-processor + echo -e "${BLUE}[${VERSION_UPPER}] Preparing diff-processor environment...${NC}" + cp -R "$PROVIDER_CACHE" "$TARGET_DIFF_PROC/old" + cp -R "$PROVIDER_CACHE" "$TARGET_DIFF_PROC/new" + (cd "$TARGET_DIFF_PROC/old" && git checkout "$MATCHING_COMMIT" >/dev/null 2>&1) + (cd "$TARGET_DIFF_PROC/new" && git checkout "$MATCHING_COMMIT" >/dev/null 2>&1) + + # Generate base and current provider code in parallel + echo -e "${BLUE}[${VERSION_UPPER}] Generating base and current provider code...${NC}" + ( + cd mmv1 + $MM_BINARY --output "$TARGET_DIFF_PROC/old" --version "$VERSION" --base "${WORKTREE_DIR}/mmv1" + ) & + local OLD_GEN_PID=$! + + ( + cd mmv1 + $MM_BINARY --output "$TARGET_DIFF_PROC/new" --version "$VERSION" + ) & + local NEW_GEN_PID=$! + + wait $OLD_GEN_PID + wait $NEW_GEN_PID + + # Perform package substitutions for side-by-side compilation + echo -e "${BLUE}[${VERSION_UPPER}] Preparing provider code for compilation...${NC}" + ( + cd "$TARGET_DIFF_PROC" + + # Old package substitution + ( + cd old/ + if [ -d "$REAL_FOLDER_NAME" ] && [ "$REAL_FOLDER_NAME" != "google" ]; then + mv "$REAL_FOLDER_NAME" google + fi + local fake_package_name=google/provider/old + + if [ "$(uname)" = "Darwin" ]; then + find . -type f -name "*.go" -exec sed -i "" "s~${REAL_PACKAGE_NAME}/${REAL_FOLDER_NAME}~${fake_package_name}/google~g" {} + + sed -i "" "s|${REAL_PACKAGE_NAME}|${fake_package_name}|g" go.mod + else + find . -type f -name "*.go" -exec sed -i "s~${REAL_PACKAGE_NAME}/${REAL_FOLDER_NAME}~${fake_package_name}/google~g" {} + + sed -i "s|${REAL_PACKAGE_NAME}|${fake_package_name}|g" go.mod + fi + ) & + local OLD_SUB_PID=$! + + # New package substitution + ( + cd new/ + if [ -d "$REAL_FOLDER_NAME" ] && [ "$REAL_FOLDER_NAME" != "google" ]; then + mv "$REAL_FOLDER_NAME" google + fi + local fake_package_name=google/provider/new + + if [ "$(uname)" = "Darwin" ]; then + find . -type f -name "*.go" -exec sed -i "" "s~${REAL_PACKAGE_NAME}/${REAL_FOLDER_NAME}~${fake_package_name}/google~g" {} + + sed -i "" "s|${REAL_PACKAGE_NAME}|${fake_package_name}|g" go.mod + else + find . -type f -name "*.go" -exec sed -i "s~${REAL_PACKAGE_NAME}/${REAL_FOLDER_NAME}~${fake_package_name}/google~g" {} + + sed -i "s|${REAL_PACKAGE_NAME}|${fake_package_name}|g" go.mod + fi + ) & + local NEW_SUB_PID=$! + + wait $OLD_SUB_PID + wait $NEW_SUB_PID + + # Tidy and build diff-processor + echo -e "${BLUE}[${VERSION_UPPER}] Compiling diff-processor tool...${NC}" + go mod tidy >/dev/null 2>&1 + mkdir -p bin/ + go build -o ./bin/diff-processor . + ) + + local HAS_ERR=0 + + # Run breaking changes check + echo -e "\n${BLUE}=== Running Breaking Changes Check (${VERSION_UPPER}) ===${NC}" + set +e + local BREAKING_OUTPUT=$("$TARGET_DIFF_PROC/bin/diff-processor" breaking-changes 2>&1) + local BREAKING_EXIT_CODE=$? + set -e + + if [ $BREAKING_EXIT_CODE -ne 0 ]; then + echo -e "${RED}[${VERSION_UPPER}] Error: diff-processor breaking-changes failed:${NC}" + echo "$BREAKING_OUTPUT" + return $BREAKING_EXIT_CODE + fi + + if [ -n "$BREAKING_OUTPUT" ] && [ "$BREAKING_OUTPUT" != "[]" ] && [ "$BREAKING_OUTPUT" != "null" ]; then + echo -e "${RED}[${VERSION_UPPER}] Breaking changes detected!${NC}" + if command -v jq &> /dev/null; then + echo "$BREAKING_OUTPUT" | jq . + else + echo "$BREAKING_OUTPUT" | python3 -m json.tool + fi + HAS_ERR=1 + else + echo -e "${GREEN}[${VERSION_UPPER}] No breaking changes detected!${NC}" + fi + + # Run missing tests check + echo -e "\n${BLUE}=== Running Missing Tests Check (${VERSION_UPPER}) ===${NC}" + set +e + local TESTS_OUTPUT=$("$TARGET_DIFF_PROC/bin/diff-processor" detect-missing-tests "$TARGET_DIFF_PROC/new/google/services" 2>&1) + local TESTS_EXIT_CODE=$? + set -e + + if [ $TESTS_EXIT_CODE -ne 0 ]; then + echo -e "${RED}[${VERSION_UPPER}] Error: diff-processor detect-missing-tests failed:${NC}" + echo "$TESTS_OUTPUT" + return $TESTS_EXIT_CODE + fi + + if [ -n "$TESTS_OUTPUT" ] && [ "$TESTS_OUTPUT" != "{}" ] && [ "$TESTS_OUTPUT" != "null" ]; then + echo -e "${YELLOW}[${VERSION_UPPER}] Missing tests detected!${NC}" + if command -v jq &> /dev/null; then + echo "$TESTS_OUTPUT" | jq . + else + echo "$TESTS_OUTPUT" | python3 -m json.tool + fi + HAS_ERR=1 + else + echo -e "${GREEN}[${VERSION_UPPER}] No missing tests detected!${NC}" + fi + + # Run missing documentation check + echo -e "\n${BLUE}=== Running Missing Documentation Check (${VERSION_UPPER}) ===${NC}" + set +e + local DOCS_OUTPUT=$("$TARGET_DIFF_PROC/bin/diff-processor" detect-missing-docs "$TARGET_DIFF_PROC/new" 2>&1) + local DOCS_EXIT_CODE=$? + set -e + + if [ $DOCS_EXIT_CODE -ne 0 ]; then + echo -e "${RED}[${VERSION_UPPER}] Error: diff-processor detect-missing-docs failed:${NC}" + echo "$DOCS_OUTPUT" + return $DOCS_EXIT_CODE + fi + + if has_missing_docs "$DOCS_OUTPUT"; then + echo -e "${YELLOW}[${VERSION_UPPER}] Missing documentation detected!${NC}" + if command -v jq &> /dev/null; then + echo "$DOCS_OUTPUT" | jq . + else + echo "$DOCS_OUTPUT" | python3 -m json.tool + fi + HAS_ERR=1 + else + echo -e "${GREEN}[${VERSION_UPPER}] No missing documentation detected!${NC}" + fi + + return $HAS_ERR +} + +echo -e "${BLUE}Running GA and Beta schema diff checks in parallel...${NC}" + +run_version_checks ga > "$SCRATCH_DIR/check-ga.log" 2>&1 & +GA_PID=$! + +run_version_checks beta > "$SCRATCH_DIR/check-beta.log" 2>&1 & +BETA_PID=$! + +set +e +wait $GA_PID +GA_STATUS=$? + +wait $BETA_PID +BETA_STATUS=$? +set -e + +echo -e "\n${BLUE}=====================================================${NC}" +echo -e "${BLUE}=== GA Provider Schema Diff Results ===${NC}" +echo -e "${BLUE}=====================================================${NC}" +cat "$SCRATCH_DIR/check-ga.log" + +echo -e "\n${BLUE}=====================================================${NC}" +echo -e "${BLUE}=== Beta Provider Schema Diff Results ===${NC}" +echo -e "${BLUE}=====================================================${NC}" +cat "$SCRATCH_DIR/check-beta.log" + +# 7. Final status report +echo -e "\n${BLUE}===========================================${NC}" +if [ $GA_STATUS -ne 0 ] || [ $BETA_STATUS -ne 0 ]; then + echo -e "${RED}=== Schema change checks failed! ===${NC}" + exit 1 +else + echo -e "${GREEN}=== All schema change checks passed successfully! ===${NC}" + exit 0 +fi diff --git a/.ci/magician/cmd/sandbox_sanity_test.go b/.ci/magician/cmd/sandbox_sanity_test.go index c6327a665567..b06bc73b5357 100644 --- a/.ci/magician/cmd/sandbox_sanity_test.go +++ b/.ci/magician/cmd/sandbox_sanity_test.go @@ -17,6 +17,7 @@ package cmd import ( "os" + "path/filepath" "strings" "testing" ) @@ -28,6 +29,7 @@ func TestSandboxInitialization(t *testing.T) { t.Fatalf("Sandbox directory %s does not exist", sb.Dir) } + sb.AllowPassthrough("git") output, err := sb.Runner.Run("git", []string{"status"}, nil) if err != nil { t.Fatalf("Sandbox is not a valid git repository: %v\nOutput: %s", err, output) @@ -37,3 +39,39 @@ func TestSandboxInitialization(t *testing.T) { t.Errorf("Expected 'On branch main' in git status output, got: %s", output) } } + +func TestSandboxInterceptor_AllowedCommand(t *testing.T) { + sb := newSandbox(t) + sb.RequireAllowlist() + _, err := sb.Runner.Run("touch", []string{"test_file.txt"}, nil) + if err != nil { + t.Fatalf("Expected allowlisted command to succeed, got: %v", err) + } +} + +func TestSandboxInterceptor_MockOverridesAllowed(t *testing.T) { + sb := newSandbox(t) + sb.RequireAllowlist() + sb.Runner.WriteFile("echo", "#!/bin/bash\necho 'intercepted echo'") + os.Chmod(filepath.Join(sb.Dir, "echo"), 0755) + + output, err := sb.Runner.Run("echo", []string{"hello"}, nil) + if err != nil { + t.Fatalf("Expected mocked command to succeed, got: %v", err) + } + if !strings.Contains(output, "intercepted echo") { + t.Errorf("Expected sandbox mock to override allowlist, got: %s", output) + } +} + +func TestSandboxInterceptor_UnhandledCommandPanics(t *testing.T) { + sb := newSandbox(t) + sb.RequireAllowlist() + _, err := sb.Runner.Run("curl", []string{"http://example.com"}, nil) + if err == nil { + t.Fatalf("Expected unhandled command to return error, but it succeeded") + } + if !strings.Contains(err.Error(), "not in the passthrough list") { + t.Errorf("Expected strict interceptor error, got: %v", err) + } +} diff --git a/.ci/magician/cmd/sandbox_test.go b/.ci/magician/cmd/sandbox_test.go index 444f100bef98..1e456357e645 100644 --- a/.ci/magician/cmd/sandbox_test.go +++ b/.ci/magician/cmd/sandbox_test.go @@ -16,6 +16,10 @@ package cmd import ( + "fmt" + "os" + "path/filepath" + "strings" "testing" "magician/exec" @@ -26,29 +30,93 @@ type sandbox struct { Runner ExecRunner } +// Routes some commands to fake sandbox scripts, bypassing $PATH. +type interceptingRunner struct { + *exec.Runner + sbDir string + allowedPassthrough map[string]bool + strictMode bool +} + +func (s *interceptingRunner) Run(name string, args []string, env map[string]string) (string, error) { + if !s.strictMode { + if !strings.Contains(name, string(filepath.Separator)) { + sandboxBin := filepath.Join(s.sbDir, name) + if _, err := os.Stat(sandboxBin); err == nil { + name = sandboxBin + } + } + return s.Runner.Run(name, args, env) + } + + baseName := filepath.Base(name) + + sandboxBin := filepath.Join(s.sbDir, baseName) + if _, err := os.Stat(sandboxBin); err == nil { + return s.Runner.Run(sandboxBin, args, env) + } + + if s.allowedPassthrough[baseName] { + return s.Runner.Run(name, args, env) + } + + return "", fmt.Errorf("command %q is not in the passthrough list and no sandbox mock was found. Please mock it or add it to the passthrough list", baseName) +} + func newSandbox(t *testing.T) *sandbox { dir := t.TempDir() - runner, err := exec.NewRunner() + realRunner, err := exec.NewRunner() if err != nil { t.Fatalf("Failed to create runner: %v", err) } - if err := runner.PushDir(dir); err != nil { + if err := realRunner.PushDir(dir); err != nil { t.Fatalf("Failed to push dir: %v", err) } - runner.MustRun("git", []string{"init", "-b", "main"}, nil) + realRunner.MustRun("git", []string{"init", "-b", "main"}, nil) + + realRunner.MustRun("touch", []string{"README.md"}, nil) + realRunner.MustRun("git", []string{"add", "."}, nil) + realRunner.MustRun("git", []string{"config", "user.email", "test@example.com"}, nil) + realRunner.MustRun("git", []string{"config", "user.name", "Test Sandbox"}, nil) + realRunner.MustRun("git", []string{"config", "commit.gpgsign", "false"}, nil) + realRunner.MustRun("git", []string{"commit", "-m", "Initial sandbox commit"}, nil) - runner.MustRun("touch", []string{"README.md"}, nil) - runner.MustRun("git", []string{"add", "."}, nil) - runner.MustRun("git", []string{"config", "user.email", "test@example.com"}, nil) - runner.MustRun("git", []string{"config", "user.name", "Test Sandbox"}, nil) - runner.MustRun("git", []string{"config", "commit.gpgsign", "false"}, nil) - runner.MustRun("git", []string{"commit", "-m", "Initial sandbox commit"}, nil) + runner := &interceptingRunner{ + Runner: realRunner, + sbDir: dir, + allowedPassthrough: map[string]bool{ + "chmod": true, + "rm": true, + "mkdir": true, + "touch": true, + "cp": true, + "mv": true, + "ls": true, + "echo": true, + "cat": true, + "grep": true, + }, + } return &sandbox{ Dir: dir, Runner: runner, } } + +func (s *sandbox) AllowPassthrough(commands ...string) { + if runner, ok := s.Runner.(*interceptingRunner); ok { + for _, cmd := range commands { + runner.allowedPassthrough[cmd] = true + } + } +} + +func (s *sandbox) RequireAllowlist() { + if runner, ok := s.Runner.(*interceptingRunner); ok { + runner.strictMode = true + } +} diff --git a/.ci/magician/cmd/test_terraform_vcr.go b/.ci/magician/cmd/test_terraform_vcr.go index b5d15f7d0aff..202127877f43 100644 --- a/.ci/magician/cmd/test_terraform_vcr.go +++ b/.ci/magician/cmd/test_terraform_vcr.go @@ -168,7 +168,7 @@ The following environment variables are required: return fmt.Errorf("error creating VCR tester: %w", err) } - return execTestTerraformVCR(args[0], args[1], args[2], args[3], args[4], baseBranch, gh, rnr, ctlr, vt) + return execTestTerraformVCR(args[0], args[1], args[2], args[3], args[4], baseBranch, "/workspace", gh, rnr, ctlr, vt) }, } @@ -180,7 +180,7 @@ func listTTVRequiredEnvironmentVariables() string { return result } -func execTestTerraformVCR(prNumber, mmCommitSha, buildID, projectID, buildStep, baseBranch string, gh GithubClient, rnr ExecRunner, ctlr *source.Controller, vt *vcr.Tester) error { +func execTestTerraformVCR(prNumber, mmCommitSha, buildID, projectID, buildStep, baseBranch, workspace string, gh GithubClient, rnr ExecRunner, ctlr *source.Controller, vt *vcr.Tester) error { newBranch := "auto-pr-" + prNumber oldBranch := newBranch + "-old" @@ -251,13 +251,13 @@ func execTestTerraformVCR(prNumber, mmCommitSha, buildID, projectID, buildStep, return fmt.Errorf("error uploading replaying logs: %w", err) } - if hasPanics, err := handlePanics(prNumber, buildID, buildStepUrl, mmCommitSha, replayingResult, vcr.Replaying, gh, rnr); err != nil { + if hasPanics, err := handlePanics(prNumber, buildID, buildStepUrl, mmCommitSha, workspace, replayingResult, vcr.Replaying, gh, rnr); err != nil { return fmt.Errorf("error handling panics: %w", err) } else if hasPanics { return nil } - if hasBuildFailures, err := handleBuildFailures(prNumber, buildID, buildStepUrl, mmCommitSha, replayingResult, vcr.Replaying, gh, rnr); err != nil { + if hasBuildFailures, err := handleBuildFailures(prNumber, buildID, buildStepUrl, mmCommitSha, workspace, replayingResult, vcr.Replaying, gh, rnr); err != nil { return fmt.Errorf("error handling build failures: %w", err) } else if hasBuildFailures { return nil @@ -293,7 +293,7 @@ func execTestTerraformVCR(prNumber, mmCommitSha, buildID, projectID, buildStep, comment = fmt.Sprintf("%s\n\n%s VCR tests complete for %s!", comment, mentionStr, mmCommitSha) } } - if err := appendVCRResultToDiffComment(prNumber, comment, gh, rnr); err != nil { + if err := appendVCRResultToDiffComment(prNumber, comment, workspace, gh, rnr); err != nil { return fmt.Errorf("error appending comment: %w", err) } if len(replayingResult.FailedTests) > 0 { @@ -324,13 +324,13 @@ func execTestTerraformVCR(prNumber, mmCommitSha, buildID, projectID, buildStep, return fmt.Errorf("error uploading recording logs: %w", err) } - if hasPanics, err := handlePanics(prNumber, buildID, buildStepUrl, mmCommitSha, recordingResult, vcr.Recording, gh, rnr); err != nil { + if hasPanics, err := handlePanics(prNumber, buildID, buildStepUrl, mmCommitSha, workspace, recordingResult, vcr.Recording, gh, rnr); err != nil { return fmt.Errorf("error handling panics: %w", err) } else if hasPanics { return nil } - if hasBuildFailures, err := handleBuildFailures(prNumber, buildID, buildStepUrl, mmCommitSha, recordingResult, vcr.Recording, gh, rnr); err != nil { + if hasBuildFailures, err := handleBuildFailures(prNumber, buildID, buildStepUrl, mmCommitSha, workspace, recordingResult, vcr.Recording, gh, rnr); err != nil { return fmt.Errorf("error handling build failures: %w", err) } else if hasBuildFailures { return nil @@ -400,7 +400,7 @@ func execTestTerraformVCR(prNumber, mmCommitSha, buildID, projectID, buildStep, if mentionStr != "" { recordReplayComment = fmt.Sprintf("%s\n\n%s VCR tests complete for %s!", recordReplayComment, mentionStr, mmCommitSha) } - if err := appendVCRResultToDiffComment(prNumber, recordReplayComment, gh, rnr); err != nil { + if err := appendVCRResultToDiffComment(prNumber, recordReplayComment, workspace, gh, rnr); err != nil { return fmt.Errorf("error appending comment: %w", err) } } @@ -553,7 +553,7 @@ func runReplaying(runFullVCR bool, version provider.Version, services map[string return result, testDirs, replayingErr } -func handlePanics(prNumber, buildID, buildStepUrl, mmCommitSha string, result vcr.Result, mode vcr.Mode, gh GithubClient, rnr ExecRunner) (bool, error) { +func handlePanics(prNumber, buildID, buildStepUrl, mmCommitSha, workspace string, result vcr.Result, mode vcr.Mode, gh GithubClient, rnr ExecRunner) (bool, error) { if len(result.Panics) > 0 { comment := "> [!CAUTION]\n" comment += "> **Panic occurred during VCR tests**\n>\n" @@ -575,7 +575,7 @@ func handlePanics(prNumber, buildID, buildStepUrl, mmCommitSha string, result vc } comment = header + comment - if err := appendVCRResultToDiffComment(prNumber, comment, gh, rnr); err != nil { + if err := appendVCRResultToDiffComment(prNumber, comment, workspace, gh, rnr); err != nil { return true, fmt.Errorf("error appending comment: %v", err) } if err := gh.PostBuildStatus(prNumber, "VCR-test", "failure", buildStepUrl, mmCommitSha); err != nil { @@ -586,7 +586,7 @@ func handlePanics(prNumber, buildID, buildStepUrl, mmCommitSha string, result vc return false, nil } -func handleBuildFailures(prNumber, buildID, buildStepUrl, mmCommitSha string, result vcr.Result, mode vcr.Mode, gh GithubClient, rnr ExecRunner) (bool, error) { +func handleBuildFailures(prNumber, buildID, buildStepUrl, mmCommitSha, workspace string, result vcr.Result, mode vcr.Mode, gh GithubClient, rnr ExecRunner) (bool, error) { if len(result.BuildFailures) > 0 { comment := "> [!CAUTION]\n" comment += "> **Build Failure during VCR tests**\n>\n" @@ -611,7 +611,7 @@ func handleBuildFailures(prNumber, buildID, buildStepUrl, mmCommitSha string, re } comment = header + comment - if err := appendVCRResultToDiffComment(prNumber, comment, gh, rnr); err != nil { + if err := appendVCRResultToDiffComment(prNumber, comment, workspace, gh, rnr); err != nil { return true, fmt.Errorf("error appending comment: %v", err) } if err := gh.PostBuildStatus(prNumber, "VCR-test", "failure", buildStepUrl, mmCommitSha); err != nil { @@ -654,11 +654,15 @@ func getMentions(prNumber string, gh GithubClient) string { // appendVCRResultToDiffComment appends content to the existing diff report comment // identified by the ID in /workspace/diff_comment_id.txt. // If the file is missing or the comment cannot be fetched, it falls back to posting a new comment. -func appendVCRResultToDiffComment(prNumber string, content string, gh GithubClient, rnr ExecRunner) error { +func appendVCRResultToDiffComment(prNumber string, content string, workspace string, gh GithubClient, rnr ExecRunner) error { var diffComment *github.PullRequestComment + if workspace == "" { + workspace = "/workspace" + } + // Try to find by ID from file - if idStr, err := rnr.ReadFile("/workspace/diff_comment_id.txt"); err == nil { + if idStr, err := rnr.ReadFile(filepath.Join(workspace, "diff_comment_id.txt")); err == nil { if id, err := strconv.Atoi(strings.TrimSpace(idStr)); err == nil { if comment, err := gh.GetPullRequestComment(id); err == nil { diffComment = &comment diff --git a/.ci/magician/cmd/test_terraform_vcr_test.go b/.ci/magician/cmd/test_terraform_vcr_test.go index d7e6cf902840..3256a71697fd 100644 --- a/.ci/magician/cmd/test_terraform_vcr_test.go +++ b/.ci/magician/cmd/test_terraform_vcr_test.go @@ -663,8 +663,9 @@ func TestHandleBuildFailures(t *testing.T) { BuildFailures: []string{"package1", "package2"}, } - rnr := &mockRunner{} - handled, err := handleBuildFailures("123", "build-456", "http://target", "sha789", result, vcr.Replaying, gh, rnr) + sb := newSandbox(t) + sb.RequireAllowlist() + handled, err := handleBuildFailures("123", "build-456", "http://target", "sha789", sb.Dir, result, vcr.Replaying, gh, sb.Runner) assert.NoError(t, err) assert.True(t, handled) @@ -690,8 +691,9 @@ func TestHandleBuildFailures_Recording(t *testing.T) { BuildFailures: []string{"package1", "package2"}, } - rnr := &mockRunner{} - handled, err := handleBuildFailures("123", "build-456", "http://target", "sha789", result, vcr.Recording, gh, rnr) + sb := newSandbox(t) + sb.RequireAllowlist() + handled, err := handleBuildFailures("123", "build-456", "http://target", "sha789", sb.Dir, result, vcr.Recording, gh, sb.Runner) assert.NoError(t, err) assert.True(t, handled) @@ -709,8 +711,9 @@ func TestHandleBuildFailures_NoFailures(t *testing.T) { } result := vcr.Result{} - rnr := &mockRunner{} - handled, err := handleBuildFailures("123", "build-456", "http://target", "sha789", result, vcr.Replaying, gh, rnr) + sb := newSandbox(t) + sb.RequireAllowlist() + handled, err := handleBuildFailures("123", "build-456", "http://target", "sha789", sb.Dir, result, vcr.Replaying, gh, sb.Runner) assert.NoError(t, err) assert.False(t, handled) @@ -736,8 +739,9 @@ func TestAppendVCRResultToDiffComment_NotExists(t *testing.T) { }, } - rnr := &mockRunner{} - err := appendVCRResultToDiffComment("123", "VCR Results", gh, rnr) + sb := newSandbox(t) + sb.RequireAllowlist() + err := appendVCRResultToDiffComment("123", "VCR Results", sb.Dir, gh, sb.Runner) assert.NoError(t, err) assert.Len(t, gh.calledMethods["PostComment"], 1) @@ -761,13 +765,12 @@ func TestAppendVCRResultToDiffComment_UseFileID(t *testing.T) { }, }, } - rnr := &mockRunner{ - fileContents: map[string]string{ - "/workspace/diff_comment_id.txt": "456", - }, - } + sb := newSandbox(t) + sb.RequireAllowlist() + + sb.Runner.WriteFile("diff_comment_id.txt", "456") - err := appendVCRResultToDiffComment("123", "VCR Results", gh, rnr) + err := appendVCRResultToDiffComment("123", "VCR Results", sb.Dir, gh, sb.Runner) assert.NoError(t, err) assert.Len(t, gh.calledMethods["UpdateComment"], 1) diff --git a/.ci/magician/cmd/wait_for_commit_test.go b/.ci/magician/cmd/wait_for_commit_test.go index 054b5c841ad3..800f80b93717 100644 --- a/.ci/magician/cmd/wait_for_commit_test.go +++ b/.ci/magician/cmd/wait_for_commit_test.go @@ -87,6 +87,8 @@ func TestExecWaitForCommit(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() sb := newSandbox(t) + sb.RequireAllowlist() + sb.AllowPassthrough("git") originDir := t.TempDir() sb.Runner.MustRun("git", []string{"init", "--bare", "-b", "main", originDir}, nil) sb.Runner.MustRun("git", []string{"remote", "add", "origin", originDir}, nil) diff --git a/.ci/magician/github/membership_data.go b/.ci/magician/github/membership_data.go index ad298184e6c4..7020f731b692 100644 --- a/.ci/magician/github/membership_data.go +++ b/.ci/magician/github/membership_data.go @@ -83,6 +83,10 @@ var ( startDate: newDate(2026, 4, 19), endDate: newDate(2026, 4, 26), }, + { + startDate: newDate(2026, 7, 10), + endDate: newDate(2026, 7, 17), + }, }, }, "malhotrasagar2212": { @@ -91,12 +95,8 @@ var ( "melinath": { vacations: []Vacation{ { - startDate: newDate(2026, 3, 31), - endDate: newDate(2026, 4, 5), - }, - { - startDate: newDate(2026, 4, 10), - endDate: newDate(2026, 4, 12), + startDate: newDate(2026, 6, 26), + endDate: newDate(2026, 7, 6), }, }, }, @@ -122,6 +122,10 @@ var ( startDate: newDate(2026, 06, 01), endDate: newDate(2026, 06, 12), }, + { + startDate: newDate(2026, 06, 29), + endDate: newDate(2026, 07, 06), + }, }, }, "roaks3": { @@ -130,13 +134,25 @@ var ( startDate: newDate(2026, 5, 7), endDate: newDate(2026, 5, 11), }, + { + startDate: newDate(2026, 7, 5), + endDate: newDate(2026, 8, 17), + }, }, }, "ScottSuarez": { vacations: []Vacation{ { startDate: newDate(2026, 4, 4), - endDate: newDate(2026, 7, 13), + endDate: newDate(2026, 7, 5), + }, + }, + }, + "shuyama1": { + vacations: []Vacation{ + { + startDate: newDate(2026, 07, 28), + endDate: newDate(2026, 12, 31), }, }, }, @@ -168,7 +184,6 @@ var ( "tavasyag": {}, "hao-nan-li": {}, "NickElliot": {}, - "shuyama1": {}, "trodge": {}, "zli82016": {}, "vr-ibm": {}, diff --git a/.ci/magician/go.mod b/.ci/magician/go.mod index 508be6d432a2..0a44acc810e6 100644 --- a/.ci/magician/go.mod +++ b/.ci/magician/go.mod @@ -66,12 +66,12 @@ require ( go.opentelemetry.io/otel/sdk v1.40.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.41.0 // indirect - golang.org/x/crypto v0.46.0 // indirect - golang.org/x/net v0.48.0 // indirect + golang.org/x/crypto v0.51.0 // indirect + golang.org/x/net v0.55.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.8.0 // indirect google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect diff --git a/.ci/magician/go.sum b/.ci/magician/go.sum index 6940dd6bdac0..71f70ab7a77d 100644 --- a/.ci/magician/go.sum +++ b/.ci/magician/go.sum @@ -170,8 +170,8 @@ go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230810033253-352e893a4cad h1:g0bG7Z4uG+OgH2QDODnjp6ggkk1bJDsINcuWmJN1iJU= golang.org/x/exp v0.0.0-20230810033253-352e893a4cad/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= @@ -184,26 +184,26 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/.github/workflows/unit-test-mmv1.yml b/.github/workflows/unit-test-mmv1.yml index ddcda7b69649..0621df306c20 100644 --- a/.github/workflows/unit-test-mmv1.yml +++ b/.github/workflows/unit-test-mmv1.yml @@ -90,7 +90,7 @@ jobs: cd repo if [ "${{ github.event_name }}" == "pull_request" ]; then # For PRs, get only changed files - yamlfiles=$(git diff --name-only origin/${GITHUB_BASE_REF} -- mmv1/products) + yamlfiles=$(git diff --name-only --diff-filter=d origin/${GITHUB_BASE_REF} -- mmv1/products) if [ ! -z "$yamlfiles" ]; then echo "yamlfiles=${yamlfiles//$'\n'/ }" >> $GITHUB_OUTPUT fi diff --git a/.gitignore b/.gitignore index 15f0bf57c472..5bdfea630dab 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ *# *.swp *.bak +/scratch/ # Python *.pyc diff --git a/docs/content/breaking-changes/make-a-breaking-change.md b/docs/content/breaking-changes/make-a-breaking-change.md index f948ab961f24..9e96ca4cd1d8 100644 --- a/docs/content/breaking-changes/make-a-breaking-change.md +++ b/docs/content/breaking-changes/make-a-breaking-change.md @@ -1,6 +1,6 @@ --- -majorVersion: "7.0.0" -upgradeGuide: "version_7_upgrade.html.markdown" +majorVersion: "8.0.0" +upgradeGuide: "version_8_upgrade.html.markdown" title: "Make a breaking change" summary: "Guidance on making a breaking changes" weight: 20 @@ -65,7 +65,7 @@ The general process for contributing a breaking change to the 1. Make the `main` branch forwards-compatible with the major release 2. Add deprecations and warnings to the `main` branch of `magic-modules` -3. Add upgrade guide entries to the `FEATURE-BRANCH-major-release-7.0.0` branch of `magic-modules` +3. Add upgrade guide entries to the `FEATURE-BRANCH-major-release-{{% param "majorVersion" %}}` branch of `magic-modules` 4. Make the breaking change on `FEATURE-BRANCH-major-release-{{% param "majorVersion" %}}` These are covered in more detail in the following sections. The upgrade guide diff --git a/docs/content/code-review/create-pr.md b/docs/content/code-review/create-pr.md index 698b21df9968..07a59f2de4f6 100644 --- a/docs/content/code-review/create-pr.md +++ b/docs/content/code-review/create-pr.md @@ -18,10 +18,11 @@ aliases: ## Code review -1. A reviewer will automatically be assigned to your PR. +1. (For Google-driven changes) Assign a service team engineer to review your change. Get their approval before proceeding with review by maintainers. 1. Creating a new pull request or pushing a new commit automatically triggers our CI pipelines and workflows. After CI starts, downstream diff generation takes about 10 minutes; [VCR tests]({{< ref "/test/test" >}}) can take up to 2 hours. If you are a community contributor, some tests will only run after approval from a reviewer. - While convenient, relying on CI to test iterative changes to PRs often adds extreme latency to reviews if there are errors in test configurations or at runtime. We **strongly** recommend you [test your changes locally before pushing]({{< ref "/test/run-tests" >}}) even after the initial change. - VCR tests will first attempt to play back recorded HTTP requests (REPLAYING mode). If any tests fail, they will run in RECORDING mode to generate a new cassette; then, the same tests will run again in REPLAYING mode to detect any nondeterministic behavior in the test (which can cause flaky tests.) +1. A maintainer will automatically be assigned to your PR for final review. 1. If your assigned reviewer does not respond to changes on a pull request within two US business days, ping them on the pull request. > [!NOTE] diff --git a/docs/content/code-review/release-notes.md b/docs/content/code-review/release-notes.md index 13e26272f2d5..eb469b5fa5f0 100644 --- a/docs/content/code-review/release-notes.md +++ b/docs/content/code-review/release-notes.md @@ -1,6 +1,6 @@ --- title: "Write release notes" -weight: 20 +weight: 30 aliases: - /contribute/release-notes --- diff --git a/docs/content/code-review/review-pr.md b/docs/content/code-review/review-pr.md index 3a2ed0a086cf..19686de2a683 100644 --- a/docs/content/code-review/review-pr.md +++ b/docs/content/code-review/review-pr.md @@ -1,6 +1,6 @@ --- title: "Review a pull request" -weight: 30 +weight: 20 aliases: - /contribute/review-pr --- @@ -9,12 +9,8 @@ aliases: This page provides guidelines for reviewing a Magic Modules pull request (PR). -## Special cases - -The following types of PRs may require additional scrutiny and/or multiple reviewers. - -- DCL to MMv1 migrations -- Adding multi-actor fields (fields whose values can be altered as a side effect of changes made to a different resource) +Google-driven changes must have a service team engineer review changes prior +to review by repository maintainers. For community members, this is not required. ## Review @@ -58,3 +54,12 @@ The following types of PRs may require additional scrutiny and/or multiple revie 1. resource-level and field-level documentation are generated correctly for MMv1-based resource 1. documentation is added manually for handwritten resources. 1. Check if release notes capture all changes in the PR, and are correctly formatted following the guidance in [write release notes]({{< ref "release-notes" >}}) before merging the PR. + +If any of these steps are impossible, clearly explain that in a comment on the PR or in a summary before handing off to a maintainer for final review. + +## Special cases + +The following types of PRs may require additional scrutiny and/or multiple reviewers. + +- DCL to MMv1 migrations +- Adding multi-actor fields (fields whose values can be altered as a side effect of changes made to a different resource) \ No newline at end of file diff --git a/docs/content/contribution-process.md b/docs/content/contribution-process.md index 81a75a8a32fc..159a51539336 100644 --- a/docs/content/contribution-process.md +++ b/docs/content/contribution-process.md @@ -33,7 +33,8 @@ This page explains how you can contribute code and documentation to the `magic-m 1. [Generate the providers]({{< ref "/develop/generate-providers" >}}) that include your change. 1. [Run provider tests locally]({{< ref "/test/run-tests" >}}) that are relevant to the change you made 1. [Create a pull request (PR)]({{< ref "/code-review/create-pr" >}}) -1. Make changes in response to [code review]({{< ref "/code-review/create-pr#code-review" >}}) +1. (For Google-driven changes) Do a [local review]({{< ref "/code-review/review-pr" >}}) with a service team engineer +1. Make changes in response to maintainers' [code review]({{< ref "/code-review/create-pr#code-review" >}}) ## After your change is merged diff --git a/docs/content/test/test.md b/docs/content/test/test.md index 7c7dd12c69e6..9f95022869a0 100644 --- a/docs/content/test/test.md +++ b/docs/content/test/test.md @@ -258,64 +258,6 @@ An update test ensures that updatable fields can be changed without recreating t {{< /tab >}} {{% /tabs %}} -## Legacy: Examples-based Framework - -> [!WARNING] -> The `examples` block is legacy and has been replaced by `samples`. This footprint is still supported for approximately 2 months to allow for transition. New tests should use the `samples` framework. - -Tests within the legacy `examples` generator are configured directly on the resource YAML. **Note that this legacy framework only supports generating create tests;** multi-step update tests must be configured via the new `samples` framework or handwritten manual testing procedures. - -### Steps - -1. Add an entry to your `RESOURCE_NAME.yaml` file's `examples`. The fields listed here are the most commonly-used. For a comprehensive reference, see [MMv1 resource reference: `examples` ↗]({{}}). - ```yaml - examples: - # name must correspond to a configuration file that you'll create in the next step. - # The name should include the product name, resource name, and a basic description - # of the test. This will be used to generate the test name and the documentation - # header. - - name: "PRODUCT_RESOURCE_basic" - # primary_resource_id will be used for the Terraform resource id in the configuration file. - primary_resource_id: "example" - # vars contains key/value pairs of variables to inject into the configuration file. - # These can be referenced in the configuration file as a key inside `{{$.Vars}}`. - # All resource IDs (even for resources not under test) should be declared - # with variables that contain a `-` or `_`; this will ensure that, in tests, - # the resources are created with a `tf-test` prefix to allow automatic cleanup - # of dangling resources and a random suffix to avoid name collisions. - vars: - network_name: "example-network" - # test_vars_overrides contains key/value pairs of literal overrides for - # variables used in tests. This can be used to call functions to - # generate or determine a variable's value – for example, bootstrapping - # a shared network for your product to avoid test failures due to limits - # on the default network. - test_vars_overrides: - network_name: 'servicenetworking.BootstrapSharedServiceNetworkingConnection(t, "PRODUCT-RESOURCE-network-config")' - # Set min_version: beta if the resource is not beta-only and any beta-only fields are being tested. - min_version: beta - ``` - -2. Create a `.tf.tmpl` file in [`mmv1/templates/terraform/examples/`](https://github.com/GoogleCloudPlatform/magic-modules/tree/main/mmv1/templates/terraform/examples). The name of the file should match the name of the example created in the previous step. For example, `PRODUCT_RESOURCE_basic.tf.tmpl`. -3. In that file, write the Terraform configuration for your test. This should include all of the required dependencies. For example, `google_compute_subnetwork` has a dependency on `google_compute_network`: - ```tf - resource "google_compute_subnetwork" "{{$.PrimaryResourceId}}" { - name = "{{index $.Vars "subnetwork_name"}}" - ip_cidr_range = "10.1.0.0/16" - region = "us-central1" - network = google_compute_network.network.name - } - - resource "google_compute_network" "network" { - name = "{{index $.Vars "network_name"}}" - auto_create_subnetworks = false - } - ``` -4. If the resource or the example is beta-only: - - Add `provider = google-beta` to every resource in the file. - -For instructions on how to migrate existing `examples` over to `samples`, see the [Test Template Migration Guide]({{< ref "/reference/update-test-changes" >}}). - ## Bootstrap API resources {#bootstrapping} Most acceptance tests run in a the default org and default test project, which means that they can conflict for quota, resource namespaces, and control over shared resources. You can work around these limitations with "bootstrapped" resources. diff --git a/mmv1/api/resource.go b/mmv1/api/resource.go index f8f191eb9c89..2bf5800e48b7 100644 --- a/mmv1/api/resource.go +++ b/mmv1/api/resource.go @@ -1337,7 +1337,7 @@ func (r Resource) PackageName() string { // general defined timeouts, or default Timeouts func (r Resource) GetTimeouts() *Timeouts { timeoutsFiltered := r.Timeouts - if timeoutsFiltered == nil { + if timeoutsFiltered == nil || timeoutsFiltered.IsZero() { if async := r.GetAsync(); async != nil && async.Operation != nil { timeoutsFiltered = async.Operation.Timeouts } @@ -1841,6 +1841,9 @@ func (r Resource) SamplePrimaryResourceId() string { return (r.ProductMetadata.VersionObjOrClosest(r.TargetVersionName).CompareTo(r.ProductMetadata.VersionObjOrClosest(s.MinVersion)) < 0) }) } + if len(samples) == 0 { + return "" + } return samples[0].PrimaryResourceId } diff --git a/mmv1/api/resource_test.go b/mmv1/api/resource_test.go index cb558ad2e3b5..26e35132e687 100644 --- a/mmv1/api/resource_test.go +++ b/mmv1/api/resource_test.go @@ -990,3 +990,128 @@ func TestIdentityPropertiesFlattenObject(t *testing.T) { t.Errorf("expected IdentityProperties to include flattened identifier \"datasetId\", got %v", got) } } + +func TestSamplePrimaryResourceId(t *testing.T) { + t.Parallel() + + p := &api.Product{ + Name: "test", + Versions: []*product.Version{ + { + Name: "ga", + BaseUrl: "ga_url", + }, + { + Name: "beta", + BaseUrl: "beta_url", + }, + }, + } + + cases := []struct { + description string + resource api.Resource + expected string + }{ + { + description: "empty samples returns empty string", + resource: api.Resource{ + Samples: []*resource.Sample{}, + ProductMetadata: p, + TargetVersionName: "ga", + }, + expected: "", + }, + { + description: "samples with higher min_version returns empty string", + resource: api.Resource{ + Samples: []*resource.Sample{ + { + PrimaryResourceId: "beta-res", + MinVersion: "beta", + }, + }, + ProductMetadata: p, + TargetVersionName: "ga", + }, + expected: "", + }, + { + description: "valid sample returns primary resource id", + resource: api.Resource{ + Samples: []*resource.Sample{ + { + PrimaryResourceId: "ga-res", + }, + }, + ProductMetadata: p, + TargetVersionName: "ga", + }, + expected: "ga-res", + }, + { + description: "only the first sample should be used", + resource: api.Resource{ + Samples: []*resource.Sample{ + { + PrimaryResourceId: "first-res", + MinVersion: "ga", + }, + { + PrimaryResourceId: "second-res", + MinVersion: "ga", + }, + }, + ProductMetadata: p, + TargetVersionName: "ga", + }, + expected: "first-res", + }, + { + description: "excludetest should be honored using first non-excluded sample", + resource: api.Resource{ + Samples: []*resource.Sample{ + { + PrimaryResourceId: "excluded-res", + ExcludeTest: true, + }, + { + PrimaryResourceId: "included-res", + ExcludeTest: false, + }, + }, + ProductMetadata: p, + TargetVersionName: "ga", + }, + expected: "included-res", + }, + { + description: "fallback to first matching excluded sample when all are excluded", + resource: api.Resource{ + Samples: []*resource.Sample{ + { + PrimaryResourceId: "excluded-first", + ExcludeTest: true, + }, + { + PrimaryResourceId: "excluded-second", + ExcludeTest: true, + }, + }, + ProductMetadata: p, + TargetVersionName: "ga", + }, + expected: "excluded-first", + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.description, func(t *testing.T) { + t.Parallel() + if got := tc.resource.SamplePrimaryResourceId(); got != tc.expected { + t.Errorf("SamplePrimaryResourceId() = %q, want %q", got, tc.expected) + } + }) + } +} diff --git a/mmv1/main.go b/mmv1/main.go index b65c7a4374af..d5904b33fdad 100644 --- a/mmv1/main.go +++ b/mmv1/main.go @@ -103,8 +103,9 @@ func GenerateProducts(product, resource, providerName, version, outputPath, base productsToGenerate = append(productsToGenerate, p.PackagePath) } } else { - var productToGenerate = fmt.Sprintf("products/%s", product) - productsToGenerate = []string{productToGenerate} + for _, prod := range strings.Split(product, ",") { + productsToGenerate = append(productsToGenerate, fmt.Sprintf("products/%s", strings.TrimSpace(prod))) + } } for _, productApi := range loadedProducts { @@ -122,8 +123,8 @@ func GenerateProducts(product, resource, providerName, version, outputPath, base }) // In order to only copy/compile files once per provider this must be called outside - // of the products loop. Create an MMv1 provider with an arbitrary product (the first loaded). - providerToGenerate := newProvider(providerName, version, productsForVersion[0], startTime, wrappedFS) + // of the products loop. Create an MMv1 provider with a nil product to trigger shared file behavior. + providerToGenerate := newProvider(providerName, version, nil, startTime, wrappedFS) providerToGenerate.CopyCommonFiles(outputPath, generateCode, generateDocs) if generateCode { @@ -148,6 +149,11 @@ func GenerateProduct(version, providerName string, productApi *api.Product, outp log.Printf("%s: Generating files", productApi.PackagePath) providerToGenerate := newProvider(providerName, version, productApi, startTime, fsys) providerToGenerate.Generate(outputPath, resourceToGenerate, generateCode, generateDocs) + + providerToGenerate.CopyCommonFiles(outputPath, generateCode, generateDocs) + if generateCode { + providerToGenerate.CompileCommonFiles(outputPath, []*api.Product{productApi}, "") + } } func newProvider(providerName, version string, productApi *api.Product, startTime time.Time, fsys fs.FS) provider.Provider { diff --git a/mmv1/products/agentregistry/Binding.yaml b/mmv1/products/agentregistry/Binding.yaml index 61ad43356a0a..5c6b587787d1 100644 --- a/mmv1/products/agentregistry/Binding.yaml +++ b/mmv1/products/agentregistry/Binding.yaml @@ -58,6 +58,11 @@ parameters: required: true url_param_only: true properties: + - name: name + type: String + description: | + The resource name of the Binding. + output: true - name: displayName type: String description: | diff --git a/mmv1/products/agentregistry/Service.yaml b/mmv1/products/agentregistry/Service.yaml index 019082232dc8..091f1764f5bb 100644 --- a/mmv1/products/agentregistry/Service.yaml +++ b/mmv1/products/agentregistry/Service.yaml @@ -61,6 +61,11 @@ parameters: required: true url_param_only: true properties: + - name: name + type: String + description: | + The resource name of the Service. + output: true - name: displayName type: String description: | diff --git a/mmv1/products/appengine/FirewallRule.yaml b/mmv1/products/appengine/FirewallRule.yaml index 8c945d43865a..6317d4e5a68d 100644 --- a/mmv1/products/appengine/FirewallRule.yaml +++ b/mmv1/products/appengine/FirewallRule.yaml @@ -45,6 +45,7 @@ custom_code: samples: - name: app_engine_firewall_rule_basic primary_resource_id: rule + external_providers: [time] steps: - name: app_engine_firewall_rule_basic resource_id_vars: diff --git a/mmv1/products/appengine/StandardAppVersion.yaml b/mmv1/products/appengine/StandardAppVersion.yaml index 93c60a6f7f2e..e8ab443f16f1 100644 --- a/mmv1/products/appengine/StandardAppVersion.yaml +++ b/mmv1/products/appengine/StandardAppVersion.yaml @@ -74,6 +74,17 @@ samples: org_id: ORG_ID ignore_read_extra: - delete_service_on_destroy + - name: app_engine_standard_app_version_bundled_services + primary_resource_id: gae-std-app-ver-bundled + steps: + - name: app_engine_standard_app_version_bundled_services + resource_id_vars: + project_id: tf-test-project + service_id: bundled-service + bucket_name: tf-test-gae-bkt-bundled + sa_email: gae-sa + ignore_read_extra: + - delete_service_on_destroy virtual_fields: - name: noop_on_destroy description: | @@ -125,6 +136,10 @@ properties: type: Boolean description: | Allows App Engine second generation runtimes to access the legacy bundled services. + Cannot specify both `app_engine_apis` and 'app_engine_bundled_services` together. + immutable: true + conflicts: + - app_engine_bundled_services - name: runtimeApiVersion type: String description: | @@ -443,3 +458,32 @@ properties: **Note:** When managing the number of instances at runtime through the App Engine Admin API or the (now deprecated) Python 2 Modules API set_num_instances() you must use `lifecycle.ignore_changes = ["manual_scaling"[0].instances]` to prevent drift detection. required: true + - name: 'appEngineBundledServices' + type: Array + # Developer Note: This field is write-only because the App Engine GET Admin API + # does not return it in the response (we use ignore_read: true to prevent state drift to null). + # It is marked is_set: true to represent the list as a set in Terraform so configuration + # ordering differences do not trigger spurious plans. + description: | + A list of legacy bundled services to enable for this version on an App Engine second-generation runtime. + Cannot specify both `app_engine_apis` and `app_engine_bundled_services` together. + is_set: true + immutable: true + ignore_read: true + item_type: + type: Enum + enum_values: + - 'BUNDLED_SERVICE_TYPE_APP_IDENTITY_SERVICE' + - 'BUNDLED_SERVICE_TYPE_BLOBSTORE' + - 'BUNDLED_SERVICE_TYPE_CAPABILITY_SERVICE' + - 'BUNDLED_SERVICE_TYPE_DATASTORE_V3' + - 'BUNDLED_SERVICE_TYPE_IMAGES' + - 'BUNDLED_SERVICE_TYPE_MAIL' + - 'BUNDLED_SERVICE_TYPE_MEMCACHE' + - 'BUNDLED_SERVICE_TYPE_MODULES' + - 'BUNDLED_SERVICE_TYPE_SEARCH' + - 'BUNDLED_SERVICE_TYPE_TASKQUEUES' + - 'BUNDLED_SERVICE_TYPE_URLFETCH' + - 'BUNDLED_SERVICE_TYPE_USERS' + conflicts: + - app_engine_apis diff --git a/mmv1/products/biglakeiceberg/IcebergCatalog.yaml b/mmv1/products/biglakeiceberg/IcebergCatalog.yaml index 24a08d3688e8..1749e708461a 100644 --- a/mmv1/products/biglakeiceberg/IcebergCatalog.yaml +++ b/mmv1/products/biglakeiceberg/IcebergCatalog.yaml @@ -69,6 +69,24 @@ samples: test_env_vars: GOOGLE_BILLING_PROJECT: PROJECT USER_PROJECT_OVERRIDE: "true" + - name: biglake_iceberg_catalog_federated_unity + primary_resource_id: my_iceberg_catalog + steps: + - name: biglake_iceberg_catalog_federated_unity + resource_id_vars: + name: my_iceberg_catalog + test_env_vars: + GOOGLE_BILLING_PROJECT: PROJECT + USER_PROJECT_OVERRIDE: "true" + - name: biglake_iceberg_catalog_federated_glue + primary_resource_id: my_iceberg_catalog + steps: + - name: biglake_iceberg_catalog_federated_glue + resource_id_vars: + name: my_iceberg_catalog + test_env_vars: + GOOGLE_BILLING_PROJECT: PROJECT + USER_PROJECT_OVERRIDE: "true" parameters: - name: name type: String @@ -105,16 +123,26 @@ properties: type: String description: Output only. The service account used for credential vending. It might be empty if credential vending was never enabled for the catalog. output: true + - name: biglake_service_account_id + api_name: biglake-service-account-id + type: String + description: Output only. The unique ID of the service account used for credential vending. Used for federation scenarios. + output: true - name: catalog_type api_name: catalog-type type: Enum - description: The catalog type of the IcebergCatalog. + description: | + The catalog type of the IcebergCatalog. + * `CATALOG_TYPE_GCS_BUCKET`: Google Cloud Storage bucket catalog type. + * `CATALOG_TYPE_BIGLAKE`: BigLake catalog type. + * `CATALOG_TYPE_FEDERATED`: Federated catalog type, for integrating with external Iceberg REST Catalogs such as Databricks Unity Catalog or AWS Glue. required: true immutable: true output: false enum_values: - CATALOG_TYPE_GCS_BUCKET - CATALOG_TYPE_BIGLAKE + - CATALOG_TYPE_FEDERATED - name: default_location api_name: default-location type: String @@ -141,6 +169,9 @@ properties: type: String description: Output only. The last modification time of the IcebergCatalog. output: true + - name: description + type: String + description: A user-provided description of the catalog. Maximum 1024 UTF-8 characters. - name: replicas type: Array item_type: @@ -178,3 +209,129 @@ properties: A list of GCS locations (e.g., `gs://my-other-bucket/...`) that are permitted for use by resources within this catalog. Each entry can be either a GCS bucket or a path within it. + - name: federated_catalog_options + api_name: federated-catalog-options + type: NestedObject + description: | + Options for a CATALOG_TYPE_FEDERATED catalog. Required when catalog_type + is CATALOG_TYPE_FEDERATED. + properties: + - name: secret_name + api_name: secret-name + type: String + description: | + The secret resource name in Secret Manager, in the format + `projects/{projectId}/locations/{location}/secrets/{secret_id}`. + Used to store credentials for authenticating with the remote catalog. + - name: service_directory_name + api_name: service-directory-name + type: String + description: | + The Service Directory service name for private network connectivity + through Cross-Cloud Interconnect. + - name: refresh_options + api_name: refresh-options + type: NestedObject + description: Configuration for metadata synchronization from the remote catalog. + properties: + - name: refresh_schedule + api_name: refresh-schedule + type: NestedObject + description: Schedule for periodic metadata refresh. + properties: + - name: refresh_interval + api_name: refresh-interval + type: String + description: | + The interval between metadata refreshes, expressed as a duration + string (e.g., `300s`). + The value must be at least 300s or 0s to disable refresh. + - name: refresh_scope + api_name: refresh-scope + type: NestedObject + description: Scope of metadata to synchronize from the remote catalog. + properties: + - name: namespace_filters + api_name: namespace-filters + type: Array + item_type: + type: String + description: | + A list of namespace filters to limit which namespaces are + synchronized from the remote catalog. + - name: refresh_status + api_name: refresh-status + type: NestedObject + description: Output only. The status of the most recent metadata refresh. + output: true + properties: + - name: start_time + api_name: start-time + type: String + description: Output only. The start time of the most recent refresh. + output: true + - name: end_time + api_name: end-time + type: String + description: Output only. The end time of the most recent refresh. + output: true + - name: status + type: NestedObject + description: Output only. The error result of the last failed refresh, if any. + output: true + properties: + - name: code + type: Integer + description: Output only. The status code, which should be an enum value of google.rpc.Code. + output: true + - name: message + type: String + description: Output only. A developer-facing error message in English. + output: true + - name: unity_catalog_info + api_name: unity-catalog-info + type: NestedObject + description: | + Configuration for a Databricks Unity Catalog remote catalog. Exactly + one of unity_catalog_info or glue_catalog_info must be specified. + properties: + - name: instance_name + api_name: instance-name + type: String + description: The Databricks workspace instance name. + required: true + immutable: true + - name: catalog_name + api_name: catalog-name + type: String + description: The name of the catalog within the Unity Catalog instance. + required: true + immutable: true + - name: service_principal_application_id + api_name: service-principal-application-id + type: String + description: The application ID of the Databricks service principal for OIDC authentication. + - name: glue_catalog_info + api_name: glue-catalog-info + type: NestedObject + description: | + Configuration for an AWS Glue remote catalog. Exactly one of + unity_catalog_info or glue_catalog_info must be specified. + properties: + - name: warehouse + api_name: warehouse + type: String + description: The AWS Glue warehouse identifier (account ID or S3 table bucket). + required: true + immutable: true + - name: aws_region + api_name: aws-region + type: String + description: The AWS region where the Glue catalog is located. + required: true + immutable: true + - name: aws_role_arn + api_name: aws-role-arn + type: String + description: The ARN of the AWS IAM role to assume for accessing the Glue catalog. + required: true diff --git a/mmv1/products/bigqueryanalyticshub/QueryTemplate.yaml b/mmv1/products/bigqueryanalyticshub/QueryTemplate.yaml new file mode 100644 index 000000000000..3959477dea5e --- /dev/null +++ b/mmv1/products/bigqueryanalyticshub/QueryTemplate.yaml @@ -0,0 +1,158 @@ +# Copyright 2025 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +name: "QueryTemplate" +min_version: beta +deletion_policy_default: "DELETE_IF_DRAFTED" +deletion_policy_custom_docs: true +description: | + Represents a BigQuery Query Template within a Data Exchange. + This resource defines a reusable SQL routine (e.g., a TVF) that can be + shared or executed via the Data Exchange. +references: + guides: + "Use query templates": "https://docs.cloud.google.com/bigquery/docs/query-templates" + api: "https://docs.cloud.google.com/bigquery/docs/reference/analytics-hub/rest/v1/projects.locations.dataExchanges.queryTemplates" +self_link: "projects/{{project}}/locations/{{location}}/dataExchanges/{{data_exchange_id}}/queryTemplates/{{query_template_id}}" +id_format: "projects/{{project}}/locations/{{location}}/dataExchanges/{{data_exchange_id}}/queryTemplates/{{query_template_id}}" +create_url: "projects/{{project}}/locations/{{location}}/dataExchanges/{{data_exchange_id}}/queryTemplates?queryTemplateId={{query_template_id}}" +base_url: "projects/{{project}}/locations/{{location}}/dataExchanges/{{data_exchange_id}}/queryTemplates" +update_verb: "PATCH" +update_mask: true +import_format: + - "projects/{{project}}/locations/{{location}}/dataExchanges/{{data_exchange_id}}/queryTemplates/{{query_template_id}}" +timeouts: + insert_minutes: 20 + update_minutes: 20 + delete_minutes: 20 +samples: + - name: "bigquery_analyticshub_querytemplate_basic" + primary_resource_id: "querytemplate" + region_override: "us" + steps: + - name: "bigquery_analyticshub_querytemplate_basic" + resource_id_vars: + data_exchange_id: "my_data_exchange" + query_template_id: "my_query_template" + vars: + desc: "example of query template" + table_name: "t1" + submit: "false" + ignore_read_extra: + - "submit" + - "deletion_policy" + - name: "bigquery_analyticshub_querytemplate_basic" + resource_id_vars: + data_exchange_id: "my_data_exchange" + query_template_id: "my_query_template" + vars: + desc: "updated example of query template" + table_name: "t1updated" + submit: "true" + ignore_read_extra: + - "submit" + - "deletion_policy" + +custom_code: + constants: "templates/terraform/constants/bigquery_analytics_hub_query_template.go.tmpl" + post_update: "templates/terraform/post_update/bigquery_analytics_hub_query_template.go.tmpl" + post_create: "templates/terraform/post_create/bigquery_analytics_hub_query_template.go.tmpl" + custom_delete: "templates/terraform/custom_delete/bigquery_analytics_hub_query_template.go.tmpl" +custom_diff: + - 'resourceBigqueryAnalyticsHubQueryTemplateCustomDiff' +virtual_fields: + - name: "submit" + type: Boolean + description: "If set to `true`, the QueryTemplate will be submitted for approval and cannot be updated afterwards. This is a one-time action." + - name: "deletion_policy" + type: String + exclude: true + description: | + Sets the policy for deleting the QueryTemplate. Defaults to `DELETE_IF_DRAFTED`. + * `ABANDON`: Untracks the resource from Terraform state but leaves it intact in BigQuery. + * `DELETE`: Deletes the QueryTemplate from BigQuery. + * `DELETE_IF_DRAFTED`: Deletes the QueryTemplate only if it is in a `DRAFTED` state; otherwise, it abandons it. + * `PREVENT`: Prevents deletion of the QueryTemplate. +properties: + - name: "name" + type: String + description: "The resource name of the QueryTemplate. e.g. `projects/myproject/locations/us/dataExchanges/123/queryTemplates/456`" + output: true + - name: "location" + type: String + description: "The name of the location this data exchange query template." + required: true + url_param_only: true + immutable: true + - name: "data_exchange_id" + type: String + description: "The ID of the data exchange. Must contain only Unicode letters, numbers (0-9), underscores (_). Should not use characters that require URL-escaping, or characters outside of ASCII, spaces." + required: true + url_param_only: true + immutable: true + - name: "query_template_id" + type: String + description: "Unique QueryTemplate ID." + required: true + url_param_only: true + immutable: true + - name: "displayName" + type: String + description: | + Human-readable display name of the QueryTemplate. The display name must + contain only Unicode letters, numbers (0-9), underscores (_), dashes (-), + spaces ( ), ampersands (&) and can't start or end with spaces. Default + value is an empty string. + required: true + immutable: true + - name: "description" + type: String + description: | + Short description of the QueryTemplate. The description must not contain + Unicode non-characters and C0 and C1 control codes except tabs, + new lines, carriage returns, and page breaks. + Default value is an empty string. Max length: 2000 bytes. + - name: "primaryContact" + type: String + description: | + Email or URL of the primary point of contact of the QueryTemplate. + immutable: true + - name: "documentation" + type: String + description: "Documentation describing the QueryTemplate." + immutable: true + - name: "state" + type: String + description: "The QueryTemplate lifecycle state." + output: true + - name: "routine" + type: NestedObject + description: "The routine associated with the QueryTemplate." + properties: + - name: "routineType" + type: Enum + description: "Type of routine (e.g., TABLE_VALUED_FUNCTION)." + enum_values: + - "TABLE_VALUED_FUNCTION" + - name: "definitionBody" + type: String + description: "SQL query logic." + - name: "createTime" + type: String + description: "Timestamp when the QueryTemplate was created." + output: true + - name: "updateTime" + type: String + description: "Timestamp when the QueryTemplate was last modified." + output: true diff --git a/mmv1/products/ces/AppVersion.yaml b/mmv1/products/ces/AppVersion.yaml index 2d26f694e3ae..54e223af2998 100644 --- a/mmv1/products/ces/AppVersion.yaml +++ b/mmv1/products/ces/AppVersion.yaml @@ -2507,7 +2507,7 @@ properties: changes. - name: executionType type: String - description: |2- + description: |- Possible values: SYNCHRONOUS ASYNCHRONOUS diff --git a/mmv1/products/ces/Toolset.yaml b/mmv1/products/ces/Toolset.yaml index 93aa5f2fc75b..8c2b8f9602d2 100644 --- a/mmv1/products/ces/Toolset.yaml +++ b/mmv1/products/ces/Toolset.yaml @@ -211,7 +211,7 @@ properties: changes. - name: executionType type: String - description: |2- + description: |- Possible values: SYNCHRONOUS ASYNCHRONOUS diff --git a/mmv1/products/chronicle/DataExport.yaml b/mmv1/products/chronicle/DataExport.yaml new file mode 100644 index 000000000000..92fd822a8af4 --- /dev/null +++ b/mmv1/products/chronicle/DataExport.yaml @@ -0,0 +1,161 @@ +# Copyright 2026 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +name: DataExport +description: | + DataExport resource represents a request to export data from Chronicle to a GCS bucket. +references: + guides: + 'Data Export Overview': 'https://cloud.google.com/chronicle/docs/secops/data-export-overview' + api: 'https://cloud.google.com/chronicle/docs/reference/rest/v1beta/projects.locations.instances.dataExports' +base_url: projects/{{project}}/locations/{{location}}/instances/{{instance}}/dataExports +self_link: projects/{{project}}/locations/{{location}}/instances/{{instance}}/dataExports/{{data_export_id}} +create_url: projects/{{project}}/locations/{{location}}/instances/{{instance}}/dataExports +id_format: projects/{{project}}/locations/{{location}}/instances/{{instance}}/dataExports/{{data_export_id}} +import_format: + - projects/{{project}}/locations/{{location}}/instances/{{instance}}/dataExports/{{data_export_id}} +immutable: true +exclude_delete: true +autogen_status: RGF0YUV4cG9ydA== + +samples: + - name: 'chronicle_data_export_basic' + primary_resource_id: 'example' + bootstrap_iam: + - member: "serviceAccount:service-{project_number}@gcp-sa-chronicle.iam.gserviceaccount.com" + role: "roles/storage.admin" + steps: + - name: 'chronicle_data_export_basic' + vars: + location: 'us' + start_time: '2025-01-01T00:00:00Z' + end_time: '2025-01-01T12:00:00Z' + test_env_vars: + chronicle_id: 'CHRONICLE_ID' + project_name: 'PROJECT_NAME' + - name: 'chronicle_data_export_full' + primary_resource_id: 'example' + bootstrap_iam: + - member: "serviceAccount:service-{project_number}@gcp-sa-chronicle.iam.gserviceaccount.com" + role: "roles/storage.admin" + steps: + - name: 'chronicle_data_export_full' + vars: + location: 'us' + start_time: '2025-01-01T00:00:00Z' + end_time: '2025-01-01T12:00:00Z' + test_env_vars: + chronicle_id: 'CHRONICLE_ID' + project_name: 'PROJECT_NAME' + +parameters: + - name: location + type: String + description: The location of the resource. + url_param_only: true + required: true + immutable: true + - name: instance + type: String + description: The unique identifier for the Chronicle instance. + url_param_only: true + required: true + immutable: true +properties: + - name: name + type: String + description: The resource name of the data export. + output: true + - name: dataExportId + type: String + description: The unique identifier for the data export. + output: true + custom_flatten: 'templates/terraform/custom_flatten/id_from_name.tmpl' + - name: startTime + type: String + required: true + description: Start, inclusive time from the range. + - name: endTime + type: String + required: true + description: Last, exclusive time from the range. + - name: gcsBucket + type: String + required: true + description: Link to the destination Cloud Storage bucket. + - name: includeLogTypes + type: Array + default_from_api: true + item_type: + type: String + diff_suppress_func: 'tpgresource.ProjectNumberDiffSuppress' + description: The specific log types to include in the Data Export request. + - name: estimatedVolume + type: Integer + description: The estimated export volume in bytes. + output: true + - name: exportedVolume + type: Integer + description: Actual volume of data exported. + output: true + - name: createTime + type: String + description: Timestamp indicating when the DataExport resource was created. + output: true + - name: updateTime + type: String + description: Timestamp indicating the last time the DataExport resource was updated. + output: true + - name: namespaces + type: Array + item_type: + type: String + description: The namespaces used to filter the export. + - name: ingestionLabels + type: Array + description: The ingestion labels used to filter the export. + item_type: + type: NestedObject + properties: + - name: key + type: String + required: true + description: The key. + - name: value + type: String + required: true + description: The value. + - name: dataExportStatus + type: NestedObject + description: Status of the current export. + output: true + properties: + - name: stage + type: String + description: The stage/status of a given data export request. + output: true + - name: dataRbacFiltered + type: Boolean + description: Indicates whether the data export is filtered by RBAC. + output: true + - name: error + type: String + description: The error message if the stage is FINISHED_FAILURE. + output: true + - name: exportedGlobPatterns + type: Array + item_type: + type: String + description: List of exported glob patterns. + output: true diff --git a/mmv1/products/chronicle/Environment.yaml b/mmv1/products/chronicle/Environment.yaml index e6c5b30ad544..2488afa9409a 100644 --- a/mmv1/products/chronicle/Environment.yaml +++ b/mmv1/products/chronicle/Environment.yaml @@ -16,11 +16,10 @@ name: Environment description: > An environment is logical container for different networks or customers that are managed by the SOC or MSSP. This is useful for SOCs who provide services to several different networks, customers or business units within the organization. The Platform comes with a predefined environment named Default Environment. -min_version: 'beta' references: guides: 'Google SecOps Guides': 'https://cloud.google.com/chronicle/docs/secops/secops-overview' - api: 'https://docs.cloud.google.com/chronicle/docs/reference/rest/v1beta/projects.locations.instances.environments' + api: 'https://docs.cloud.google.com/chronicle/docs/reference/rest/v1/projects.locations.instances.environments' base_url: projects/{{project}}/locations/{{location}}/instances/{{instance}}/environments self_link: projects/{{project}}/locations/{{location}}/instances/{{instance}}/environments/{{environment_id}} create_url: projects/{{project}}/locations/{{location}}/instances/{{instance}}/environments @@ -35,7 +34,6 @@ autogen_status: RW52aXJvbm1lbnQ= samples: - name: chronicle_environment_update primary_resource_id: sample - min_version: beta steps: - name: "chronicle_environment_basic" test_env_vars: diff --git a/mmv1/products/chronicle/EnvironmentGroup.yaml b/mmv1/products/chronicle/EnvironmentGroup.yaml new file mode 100644 index 000000000000..09df15b286b8 --- /dev/null +++ b/mmv1/products/chronicle/EnvironmentGroup.yaml @@ -0,0 +1,87 @@ +# Copyright 2026 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +name: EnvironmentGroup +description: > + Environment groups let you organize multiple environments into logical categories, making it easier to manage large organizations or multiple customers as a Managed Security Service Provider (MSSP). + +min_version: 'beta' +references: + guides: + 'Google SecOps Guides': 'https://cloud.google.com/chronicle/docs/secops/secops-overview' + api: 'https://docs.cloud.google.com/chronicle/docs/reference/rest/v1beta/projects.locations.instances.environmentGroups' +base_url: projects/{{project}}/locations/{{location}}/instances/{{instance}}/environmentGroups +self_link: projects/{{project}}/locations/{{location}}/instances/{{instance}}/environmentGroups/{{environment_group_id}} +create_url: projects/{{project}}/locations/{{location}}/instances/{{instance}}/environmentGroups +id_format: projects/{{project}}/locations/{{location}}/instances/{{instance}}/environmentGroups/{{environment_group_id}} +update_mask: true +update_verb: PATCH +import_format: + - projects/{{project}}/locations/{{location}}/instances/{{instance}}/environmentGroups/{{environment_group_id}} +autogen_status: RW52aXJvbm1lbnRHcm91cA== +samples: + - name: chronicle_environmentgroup_update + primary_resource_id: sample + min_version: beta + steps: + - name: "chronicle_environmentgroup_basic" + test_env_vars: + chronicle_id: 'CHRONICLE_ID' + - name: "chronicle_environmentgroup_full" + test_env_vars: + chronicle_id: 'CHRONICLE_ID' +parameters: + - name: location + type: String + required: true + description: Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. + immutable: true + url_param_only: true + - name: instance + type: String + required: true + description: Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. + immutable: true + url_param_only: true + - name: environmentGroupId + type: String + output: true + custom_flatten: 'templates/terraform/custom_flatten/id_from_name.tmpl' + description: |- + Id of the environment group record. +properties: + - name: description + type: String + required: true + description: |- + The EnvironmentGroup description. + This value is optional. This value should be up to + 250 characters, and valid characters are /a-z-/. + - name: displayName + type: String + required: true + description: The group name. + - name: environmentsIds + type: Array + required: true + description: The environment IDs for the group. + item_type: + type: String + - name: name + type: String + description: |- + Identifier. The resource name of the environment group. + Format: + projects/{project}/locations/{location}/instances/{instance}/environmentGroups/{environment_group} + output: true diff --git a/mmv1/products/chronicle/FindingsRefinementDeployment.yaml b/mmv1/products/chronicle/FindingsRefinementDeployment.yaml index ad43f76c08ab..d0e94b400043 100644 --- a/mmv1/products/chronicle/FindingsRefinementDeployment.yaml +++ b/mmv1/products/chronicle/FindingsRefinementDeployment.yaml @@ -17,8 +17,7 @@ description: The FindingsRefinementDeployment resource represents the deployment references: guides: 'Google SecOps Guides': 'https://cloud.google.com/chronicle/docs/secops/secops-overview' - api: 'https://docs.cloud.google.com/chronicle/docs/reference/rest/v1beta/FindingsRefinementDeployment' -min_version: 'beta' + api: 'https://docs.cloud.google.com/chronicle/docs/reference/rest/v1/FindingsRefinementDeployment' base_url: projects/{{project}}/locations/{{location}}/instances/{{instance}}/findingsRefinements/{{findings_refinement}}/deployment self_link: projects/{{project}}/locations/{{location}}/instances/{{instance}}/findingsRefinements/{{findings_refinement}}/deployment create_url: projects/{{project}}/locations/{{location}}/instances/{{instance}}/findingsRefinements/{{findings_refinement}}/deployment?updateMask=* @@ -34,7 +33,6 @@ autogen_status: RmluZGluZ3NSZWZpbmVtZW50RGVwbG95bWVudA== samples: - name: 'chronicle_findings_refinement_deployment_update' primary_resource_id: 'example' - min_version: 'beta' steps: - name: 'chronicle_findings_refinement_deployment_full' test_env_vars: diff --git a/mmv1/products/chronicle/ReferenceList.yaml b/mmv1/products/chronicle/ReferenceList.yaml index 17ac73894260..c913672b41aa 100644 --- a/mmv1/products/chronicle/ReferenceList.yaml +++ b/mmv1/products/chronicle/ReferenceList.yaml @@ -127,7 +127,7 @@ properties: type: String - name: syntaxType type: String - description: |2- + description: |- Possible values: REFERENCE_LIST_SYNTAX_TYPE_PLAIN_TEXT_STRING REFERENCE_LIST_SYNTAX_TYPE_REGEX diff --git a/mmv1/products/chronicle/Rule.yaml b/mmv1/products/chronicle/Rule.yaml index 81148cc27b4a..7bdffef47179 100644 --- a/mmv1/products/chronicle/Rule.yaml +++ b/mmv1/products/chronicle/Rule.yaml @@ -58,6 +58,7 @@ samples: chronicle_id: 'CHRONICLE_ID' exclude_import_test: true custom_code: + constants: 'templates/terraform/constants/chronicle_rule.go.tmpl' pre_delete: 'templates/terraform/pre_delete/chronicle_rule.go.tmpl' virtual_fields: - name: 'deletion_policy' @@ -110,6 +111,7 @@ properties: Rule Id is the ID of the Rule. - name: text type: String + diff_suppress_func: 'chronicleRuleTextDiffSuppress' description: |- The YARA-L content of the rule. Populated in FULL view. @@ -174,7 +176,7 @@ properties: - name: type type: String output: true - description: |2- + description: |- Possible values: RULE_TYPE_UNSPECIFIED SINGLE_EVENT diff --git a/mmv1/products/chronicle/RuleDeployment.yaml b/mmv1/products/chronicle/RuleDeployment.yaml index 4005f0c55cf2..5f06f84364a6 100644 --- a/mmv1/products/chronicle/RuleDeployment.yaml +++ b/mmv1/products/chronicle/RuleDeployment.yaml @@ -107,7 +107,7 @@ properties: - name: runFrequency type: String diff_suppress_func: 'chronicleRuleDeploymentNilRunFrequencyDiffSuppressFunc' - description: |2- + description: |- The run frequency of the rule deployment. Possible values: LIVE @@ -115,7 +115,7 @@ properties: DAILY - name: executionState type: String - description: |2- + description: |- The execution state of the rule deployment. Possible values: DEFAULT diff --git a/mmv1/products/chronicle/SoarDomain.yaml b/mmv1/products/chronicle/SoarDomain.yaml new file mode 100644 index 000000000000..c32d9627aa2f --- /dev/null +++ b/mmv1/products/chronicle/SoarDomain.yaml @@ -0,0 +1,78 @@ +# Copyright 2026 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +name: SoarDomain +description: > + A SOAR domain designate a domain as internal, ensuring that entities associated with it are treated as organizational assets rather than external threats during ingestion. + +min_version: 'beta' +references: + guides: + 'Google SecOps Guides': 'https://cloud.google.com/chronicle/docs/secops/secops-overview' + api: 'https://docs.cloud.google.com/chronicle/docs/reference/rest/v1beta/projects.locations.instances.soarDomains' +base_url: projects/{{project}}/locations/{{location}}/instances/{{instance}}/soarDomains +self_link: projects/{{project}}/locations/{{location}}/instances/{{instance}}/soarDomains/{{soar_domain_id}} +create_url: projects/{{project}}/locations/{{location}}/instances/{{instance}}/soarDomains +id_format: projects/{{project}}/locations/{{location}}/instances/{{instance}}/soarDomains/{{soar_domain_id}} +update_mask: true +update_verb: PATCH +import_format: + - projects/{{project}}/locations/{{location}}/instances/{{instance}}/soarDomains/{{soar_domain_id}} +autogen_status: U29hckRvbWFpbg== +samples: + - name: chronicle_soardomain_update + primary_resource_id: example + min_version: beta + steps: + - name: "chronicle_soardomain_basic" + test_env_vars: + chronicle_id: 'CHRONICLE_ID' + - name: "chronicle_soardomain_full" + test_env_vars: + chronicle_id: 'CHRONICLE_ID' +parameters: + - name: location + type: String + required: true + description: Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. + immutable: true + url_param_only: true + - name: instance + type: String + required: true + description: Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. + immutable: true + url_param_only: true + - name: soarDomainId + type: String + output: true + custom_flatten: 'templates/terraform/custom_flatten/id_from_name.tmpl' + description: |- + Id of the domain record. +properties: + - name: displayName + type: String + required: true + description: SoarDomain display name, limited to 4096 characters. + - name: environmentsJson + type: String + required: true + description: SoarDomain associated logical environments (json). + - name: name + type: String + description: |- + Identifier. The unique name(ID) of the SoarDomain. + Format: + projects/{project}/locations/{location}/instances/{instance}/soarDomains/{soar_domain_id} + output: true diff --git a/mmv1/products/cloudscheduler/Job.yaml b/mmv1/products/cloudscheduler/Job.yaml index 7f3d9fdf29e5..e6d81a151394 100644 --- a/mmv1/products/cloudscheduler/Job.yaml +++ b/mmv1/products/cloudscheduler/Job.yaml @@ -29,6 +29,8 @@ timeouts: insert_minutes: 20 update_minutes: 20 delete_minutes: 20 +error_retry_predicates: + - transport_tpg.Is409SyncMutateCannotBeQueuedError custom_code: constants: 'templates/terraform/constants/scheduler.tmpl' encoder: 'templates/terraform/encoders/cloud_scheduler.go.tmpl' diff --git a/mmv1/products/compute/InstantSnapshot.yaml b/mmv1/products/compute/InstantSnapshot.yaml index 999699ad82bf..1196caf2e523 100644 --- a/mmv1/products/compute/InstantSnapshot.yaml +++ b/mmv1/products/compute/InstantSnapshot.yaml @@ -32,6 +32,7 @@ docs: base_url: 'projects/{{project}}/zones/{{zone}}/instantSnapshots' has_self_link: true immutable: true +generate_list_resource: true timeouts: insert_minutes: 20 update_minutes: 20 diff --git a/mmv1/products/compute/OrganizationSecurityPolicyRule.yaml b/mmv1/products/compute/OrganizationSecurityPolicyRule.yaml index 0da489f77967..1dd50c091c6e 100644 --- a/mmv1/products/compute/OrganizationSecurityPolicyRule.yaml +++ b/mmv1/products/compute/OrganizationSecurityPolicyRule.yaml @@ -338,6 +338,7 @@ properties: type: String - name: 'preview' type: Boolean + send_empty_value: true description: | If set to true, the specified action is not enforced. - name: 'redirectOptions' diff --git a/mmv1/products/compute/RegionInstantSnapshot.yaml b/mmv1/products/compute/RegionInstantSnapshot.yaml index 0d11b78c83b9..c4fc2d6a4a03 100644 --- a/mmv1/products/compute/RegionInstantSnapshot.yaml +++ b/mmv1/products/compute/RegionInstantSnapshot.yaml @@ -32,6 +32,7 @@ docs: base_url: 'projects/{{project}}/regions/{{region}}/instantSnapshots' has_self_link: true immutable: true +generate_list_resource: true timeouts: insert_minutes: 20 update_minutes: 20 diff --git a/mmv1/products/compute/RegionNetworkFirewallPolicyWithRules.yaml b/mmv1/products/compute/RegionNetworkFirewallPolicyWithRules.yaml index 7c11f810405c..bc1c7beb97ad 100644 --- a/mmv1/products/compute/RegionNetworkFirewallPolicyWithRules.yaml +++ b/mmv1/products/compute/RegionNetworkFirewallPolicyWithRules.yaml @@ -372,6 +372,29 @@ properties: the firewall policy rule is not enforced and traffic behaves as if it did not exist. If this is unspecified, the firewall policy rule will be enabled. + - name: 'targetType' + type: Enum + description: | + Target types of the firewall policy rule. + Default value is INSTANCES. + When target_type is INTERNAL_MANAGED_LB, target_forwarding_rules must be set + default_from_api: true + enum_values: + - 'INSTANCES' + - 'INTERNAL_MANAGED_LB' + - name: 'targetForwardingRules' + type: Array + description: | + A list of forwarding rules to which this rule applies. + This field allows you to control which load balancers get this rule. + For example, the following are valid values: + - https://www.googleapis.com/compute/v1/projects/project/global/forwardingRules/forwardingRule + - https://www.googleapis.com/compute/v1/projects/project/regions/region/forwardingRules/forwardingRule + - projects/project/global/forwardingRules/forwardingRule + - projects/project/regions/region/forwardingRules/forwardingRule + item_type: + type: String + diff_suppress_func: 'tpgresource.CompareSelfLinkRelativePaths' - name: 'predefinedRules' type: Array description: A list of firewall policy pre-defined rules. diff --git a/mmv1/products/compute/RegionTargetHttpProxy.yaml b/mmv1/products/compute/RegionTargetHttpProxy.yaml index b42ca9e36f5e..642005cd6a0b 100644 --- a/mmv1/products/compute/RegionTargetHttpProxy.yaml +++ b/mmv1/products/compute/RegionTargetHttpProxy.yaml @@ -27,6 +27,7 @@ has_self_link: true immutable: true datasource_experimental: generate: true +generate_list_resource: true timeouts: insert_minutes: 20 update_minutes: 20 diff --git a/mmv1/products/compute/RegionTargetTcpProxy.yaml b/mmv1/products/compute/RegionTargetTcpProxy.yaml index 3fa907b91ec7..0eaba50c9aa7 100644 --- a/mmv1/products/compute/RegionTargetTcpProxy.yaml +++ b/mmv1/products/compute/RegionTargetTcpProxy.yaml @@ -25,6 +25,7 @@ references: base_url: projects/{{project}}/regions/{{region}}/targetTcpProxies immutable: true has_self_link: true +generate_list_resource: true id_format: projects/{{project}}/regions/{{region}}/targetTcpProxies/{{name}} sweeper: url_substitutions: @@ -39,6 +40,8 @@ samples: primary_resource_id: default steps: - name: region_target_tcp_proxy_basic + vars: + region: us-central1 resource_id_vars: health_check_name: health-check region_backend_service_name: backend-service diff --git a/mmv1/products/compute/RegionUrlMap.yaml b/mmv1/products/compute/RegionUrlMap.yaml index 2116ed3f3c4e..d00a9bacb482 100644 --- a/mmv1/products/compute/RegionUrlMap.yaml +++ b/mmv1/products/compute/RegionUrlMap.yaml @@ -21,6 +21,7 @@ description: | docs: base_url: 'projects/{{project}}/regions/{{region}}/urlMaps' has_self_link: true +generate_list_resource: true timeouts: insert_minutes: 20 update_minutes: 20 diff --git a/mmv1/products/compute/RolloutPlan.yaml b/mmv1/products/compute/RolloutPlan.yaml index 5737e7ea65fc..79b3b9a7622e 100644 --- a/mmv1/products/compute/RolloutPlan.yaml +++ b/mmv1/products/compute/RolloutPlan.yaml @@ -24,6 +24,7 @@ references: api: 'https://cloud.google.com/compute/docs/reference/rest/beta/rolloutPlans' base_url: 'projects/{{project}}/global/rolloutPlans' has_self_link: true +generate_list_resource: true immutable: true create_url: 'projects/{{project}}/global/rolloutPlans' create_verb: 'POST' diff --git a/mmv1/products/compute/ServiceAttachment.yaml b/mmv1/products/compute/ServiceAttachment.yaml index 6672d31599d8..372c7135f21d 100644 --- a/mmv1/products/compute/ServiceAttachment.yaml +++ b/mmv1/products/compute/ServiceAttachment.yaml @@ -262,6 +262,7 @@ properties: address data in TCP connections that traverse proxies on their way to destination servers. required: true + send_empty_value: true - name: 'domainNames' type: Array description: | diff --git a/mmv1/products/compute/TargetGrpcProxy.yaml b/mmv1/products/compute/TargetGrpcProxy.yaml index c9774e01c784..34ab40a94367 100644 --- a/mmv1/products/compute/TargetGrpcProxy.yaml +++ b/mmv1/products/compute/TargetGrpcProxy.yaml @@ -27,6 +27,7 @@ docs: base_url: 'projects/{{project}}/global/targetGrpcProxies' has_self_link: true update_verb: 'PATCH' +generate_list_resource: true timeouts: insert_minutes: 20 update_minutes: 20 diff --git a/mmv1/products/compute/TargetHttpProxy.yaml b/mmv1/products/compute/TargetHttpProxy.yaml index 9f3afb92dace..2f8728a2c9ba 100644 --- a/mmv1/products/compute/TargetHttpProxy.yaml +++ b/mmv1/products/compute/TargetHttpProxy.yaml @@ -27,6 +27,7 @@ has_self_link: true immutable: true datasource_experimental: generate: true +generate_list_resource: true timeouts: insert_minutes: 20 update_minutes: 20 diff --git a/mmv1/products/compute/TargetSslProxy.yaml b/mmv1/products/compute/TargetSslProxy.yaml index 5872df473d47..62c3c0509a6d 100644 --- a/mmv1/products/compute/TargetSslProxy.yaml +++ b/mmv1/products/compute/TargetSslProxy.yaml @@ -26,6 +26,7 @@ docs: base_url: 'projects/{{project}}/global/targetSslProxies' has_self_link: true immutable: true +generate_list_resource: true timeouts: insert_minutes: 20 update_minutes: 20 diff --git a/mmv1/products/compute/TargetTcpProxy.yaml b/mmv1/products/compute/TargetTcpProxy.yaml index 34a315638fb3..0105842a72ad 100644 --- a/mmv1/products/compute/TargetTcpProxy.yaml +++ b/mmv1/products/compute/TargetTcpProxy.yaml @@ -24,6 +24,7 @@ references: base_url: projects/{{project}}/global/targetTcpProxies immutable: true has_self_link: true +generate_list_resource: true collection_url_key: items kind: compute#targetTcpProxy id_format: projects/{{project}}/global/targetTcpProxies/{{name}} diff --git a/mmv1/products/dataplex/Datascan.yaml b/mmv1/products/dataplex/Datascan.yaml index 5af682bdbf14..62fa847182d2 100644 --- a/mmv1/products/dataplex/Datascan.yaml +++ b/mmv1/products/dataplex/Datascan.yaml @@ -900,4 +900,8 @@ properties: - data_profile_spec - data_discovery_spec - data_documentation_spec - properties: [] + properties: + - name: catalogPublishingEnabled + type: Boolean + description: | + If set, the latest DataScan job result will be published to Knowledge Catalog. diff --git a/mmv1/products/dlp/InspectTemplate.yaml b/mmv1/products/dlp/InspectTemplate.yaml index 8830c701352c..668a76b479b6 100644 --- a/mmv1/products/dlp/InspectTemplate.yaml +++ b/mmv1/products/dlp/InspectTemplate.yaml @@ -139,6 +139,42 @@ properties: - POSSIBLE - LIKELY - VERY_LIKELY + - name: minLikelihoodPerInfoType + type: Array + description: | + Minimum likelihood per infotype. For each infotype, a user can specify a minimum likelihood. + The system only returns a finding if its likelihood is above this threshold. If this field + is not set, the system uses the InspectConfig min_likelihood. + item_type: + type: NestedObject + properties: + - name: infoType + type: NestedObject + description: | + Type of information the likeliness threshold applies to. Only one likelihood per info_type should be provided. + If InfoTypeLikelihood does not have an info_type, the configuration fails. + properties: + - name: name + type: String + description: | + Name of the information type. Either a name of your choosing when creating a CustomInfoType, or one of the names listed + at https://cloud.google.com/dlp/docs/infotypes-reference when specifying a built-in type. + required: true + - name: version + type: String + description: | + Version name for this InfoType. + - name: minLikelihood + type: Enum + description: | + Only returns findings equal or above this threshold. See https://cloud.google.com/dlp/docs/likelihood for more info. + required: true + enum_values: + - VERY_UNLIKELY + - UNLIKELY + - POSSIBLE + - LIKELY + - VERY_LIKELY - name: limits type: NestedObject description: Configuration to control the number of findings returned. diff --git a/mmv1/products/dns/ManagedZone.yaml b/mmv1/products/dns/ManagedZone.yaml index 31c0b398a48f..eaf4ff763a90 100644 --- a/mmv1/products/dns/ManagedZone.yaml +++ b/mmv1/products/dns/ManagedZone.yaml @@ -55,7 +55,7 @@ samples: example_zone_googlecloudexample: example-zone-googlecloudexample dns_name: googlecloudexample.net. test_vars_overrides: - dns_name: '"m-z.gcp.tfacc.hashicorptest.com."' + dns_name: '"m-z-" + randomSuffix + ".gcp.tfacc.hashicorptest.com."' ignore_read_extra: - force_destroy - name: dns_record_set_basic diff --git a/mmv1/products/eventarc/Trigger.yaml b/mmv1/products/eventarc/Trigger.yaml index 5b8761ecbd8f..309984330a50 100644 --- a/mmv1/products/eventarc/Trigger.yaml +++ b/mmv1/products/eventarc/Trigger.yaml @@ -275,6 +275,7 @@ properties: type: KeyValuePairs description: Output only. The reason(s) why a trigger is in FAILED state. output: true + custom_flatten: templates/terraform/custom_flatten/eventarc_trigger_conditions_flatten.go.tmpl - name: eventDataContentType type: String description: Optional. EventDataContentType specifies the type of payload in MIME format that is expected from the CloudEvent data field. This is set to `application/json` if the value is not defined. diff --git a/mmv1/products/filestore/Instance.yaml b/mmv1/products/filestore/Instance.yaml index 0490eaaa89de..2a3902406196 100644 --- a/mmv1/products/filestore/Instance.yaml +++ b/mmv1/products/filestore/Instance.yaml @@ -28,9 +28,9 @@ create_url: projects/{{project}}/locations/{{location}}/instances?instanceId={{n update_mask: true update_verb: PATCH timeouts: - insert_minutes: 20 - update_minutes: 20 - delete_minutes: 20 + insert_minutes: 14400 + update_minutes: 120 + delete_minutes: 120 sweeper: url_substitutions: - region: us-central1-b diff --git a/mmv1/products/firebaseapphosting/Build.yaml b/mmv1/products/firebaseapphosting/Build.yaml index 9034fe546c1e..17c18f9cde24 100644 --- a/mmv1/products/firebaseapphosting/Build.yaml +++ b/mmv1/products/firebaseapphosting/Build.yaml @@ -33,6 +33,8 @@ async: operation: timeouts: insert_minutes: 20 + update_minutes: 20 + delete_minutes: 20 base_url: '{{op_id}}' actions: - create diff --git a/mmv1/products/firebaseapphosting/DefaultDomain.yaml b/mmv1/products/firebaseapphosting/DefaultDomain.yaml index 197cee9c9541..5065dc4864a8 100644 --- a/mmv1/products/firebaseapphosting/DefaultDomain.yaml +++ b/mmv1/products/firebaseapphosting/DefaultDomain.yaml @@ -32,6 +32,7 @@ async: timeouts: insert_minutes: 20 update_minutes: 20 + delete_minutes: 20 base_url: '{{op_id}}' actions: - create diff --git a/mmv1/products/firebaseapphosting/Domain.yaml b/mmv1/products/firebaseapphosting/Domain.yaml index f24292d5866d..69e691ce5c5f 100644 --- a/mmv1/products/firebaseapphosting/Domain.yaml +++ b/mmv1/products/firebaseapphosting/Domain.yaml @@ -28,6 +28,7 @@ async: type: PollAsync operation: timeouts: + insert_minutes: 20 update_minutes: 20 delete_minutes: 20 base_url: '{{op_id}}' diff --git a/mmv1/products/firestore/Field.yaml b/mmv1/products/firestore/Field.yaml index ddd7d402195d..92caaaaededd 100644 --- a/mmv1/products/firestore/Field.yaml +++ b/mmv1/products/firestore/Field.yaml @@ -53,6 +53,8 @@ error_retry_predicates: - transport_tpg.FirestoreField409RetryUnderlyingDataChanged autogen_async: true custom_code: + custom_create: templates/terraform/custom_create/firestore_field.go.tmpl + custom_update: templates/terraform/custom_update/firestore_field.go.tmpl encoder: templates/terraform/encoders/firestore_field.go.tmpl custom_delete: templates/terraform/custom_delete/firestore_field_delete.go.tmpl custom_import: templates/terraform/custom_import/firestore_field.go.tmpl @@ -135,6 +137,25 @@ samples: project_id: PROJECT_NAME test_vars_overrides: delete_protection_state: '"DELETE_PROTECTION_DISABLED"' + - name: firestore_field_skip_wait + primary_resource_id: skip_wait + steps: + - name: firestore_field_skip_wait + ignore_read_extra: + - skip_wait + resource_id_vars: + database_id: database-id + delete_protection_state: DELETE_PROTECTION_ENABLED + test_env_vars: + project_id: PROJECT_NAME + test_vars_overrides: + delete_protection_state: '"DELETE_PROTECTION_DISABLED"' +virtual_fields: + - name: skip_wait + type: Boolean + default_value: false + description: Whether to skip waiting for the field operation to complete. + ignore_read: true parameters: properties: - name: database diff --git a/mmv1/products/hypercomputecluster/Cluster.yaml b/mmv1/products/hypercomputecluster/Cluster.yaml index d19f60bf432b..e8373b0e65ca 100644 --- a/mmv1/products/hypercomputecluster/Cluster.yaml +++ b/mmv1/products/hypercomputecluster/Cluster.yaml @@ -21,6 +21,10 @@ update_mask: true update_verb: PATCH import_format: - projects/{{project}}/locations/{{location}}/clusters/{{cluster_id}} +timeouts: + insert_minutes: 60 + update_minutes: 60 + delete_minutes: 60 async: operation: timeouts: diff --git a/mmv1/products/iap/AgentRegistryAgent.yaml b/mmv1/products/iap/AgentRegistryAgent.yaml new file mode 100644 index 000000000000..2d0ab4ff2507 --- /dev/null +++ b/mmv1/products/iap/AgentRegistryAgent.yaml @@ -0,0 +1,55 @@ +# Copyright 2026 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +name: AgentRegistryAgent +description: | + Only used to generate IAM resources +# This resource is only used to generate IAM resources. They do not correspond to real +# GCP resources, and should not be used to generate anything other than IAM support. +exclude_resource: true +id_format: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/agents/{{agent_id}} +base_url: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/agents/{{agent_id}} +self_link: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/agents/{{agent_id}} +import_format: + - projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/agents/{{agent_id}} + - '{{agent_id}}' +timeouts: + insert_minutes: 20 + update_minutes: 20 + delete_minutes: 20 +iam_policy: + method_name_separator: ':' + parent_resource_type: data.google_agent_registry_agent + fetch_iam_policy_verb: POST + allowed_iam_role: roles/iap.egressor + parent_resource_attribute: agent_id + iam_conditions_request_type: REQUEST_BODY +exclude_tgc: true +examples: + - name: agent_registry_agent_basic + primary_resource_id: default + external_providers: [time] + vars: + agent_id: agent-id + test_env_vars: + org_id: ORG_ID +parameters: + - name: location + type: String + required: true + description: The location of the resource. +properties: + - name: agent_id + type: String + required: true + description: The id of the agent. diff --git a/mmv1/products/iap/AgentRegistryEndpoint.yaml b/mmv1/products/iap/AgentRegistryEndpoint.yaml new file mode 100644 index 000000000000..d14e7094b8fe --- /dev/null +++ b/mmv1/products/iap/AgentRegistryEndpoint.yaml @@ -0,0 +1,56 @@ +# Copyright 2026 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +name: AgentRegistryEndpoint +description: | + Only used to generate IAM resources +# This resource is only used to generate IAM resources. They do not correspond to real +# GCP resources, and should not be used to generate anything other than IAM support. +exclude_resource: true +docs: +base_url: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/endpoints/{{endpoint_id}} +self_link: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/endpoints/{{endpoint_id}} +iam_policy: + method_name_separator: ':' + parent_resource_type: data.google_agent_registry_endpoint + fetch_iam_policy_verb: POST + allowed_iam_role: roles/iap.egressor + parent_resource_attribute: endpoint_id + iam_conditions_request_type: REQUEST_BODY +id_format: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/endpoints/{{endpoint_id}} +import_format: + - projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/endpoints/{{endpoint_id}} + - '{{endpoint_id}}' +timeouts: + insert_minutes: 20 + update_minutes: 20 + delete_minutes: 20 +exclude_tgc: true +examples: + - name: agent_registry_endpoint_basic + primary_resource_id: default + external_providers: [time] + vars: + endpoint_id: endpoint-id + test_env_vars: + org_id: ORG_ID +parameters: + - name: location + type: String + required: true + description: The location of the resource. +properties: + - name: endpoint_id + type: String + required: true + description: The id of the Endpoint. diff --git a/mmv1/products/iap/AgentRegistryMcpServer.yaml b/mmv1/products/iap/AgentRegistryMcpServer.yaml new file mode 100644 index 000000000000..abdeaf441b6a --- /dev/null +++ b/mmv1/products/iap/AgentRegistryMcpServer.yaml @@ -0,0 +1,56 @@ +# Copyright 2026 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +name: AgentRegistryMcpServer +description: | + Only used to generate IAM resources +# This resource is only used to generate IAM resources. They do not correspond to real +# GCP resources, and should not be used to generate anything other than IAM support. +exclude_resource: true +docs: +id_format: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/mcpServers/{{mcp_server_id}} +base_url: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/mcpServers/{{mcp_server_id}} +self_link: projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/mcpServers/{{mcp_server_id}} +import_format: + - projects/{{project}}/locations/{{location}}/iap_web/agentRegistry/mcpServers/{{mcp_server_id}} + - '{{mcp_server_id}}' +timeouts: + insert_minutes: 20 + update_minutes: 20 + delete_minutes: 20 +iam_policy: + method_name_separator: ':' + parent_resource_type: data.google_agent_registry_mcp_server + fetch_iam_policy_verb: POST + allowed_iam_role: roles/iap.egressor + parent_resource_attribute: mcp_server_id + iam_conditions_request_type: REQUEST_BODY +exclude_tgc: true +examples: + - name: agent_registry_mcp_server_basic + primary_resource_id: default + external_providers: [time] + vars: + mcp_server_id: mcp-server-id + test_env_vars: + org_id: ORG_ID +parameters: + - name: location + type: String + required: true + description: The location of the resource. +properties: + - name: mcp_server_id + type: String + required: true + description: The id of the MCP Server. diff --git a/mmv1/products/networkconnectivity/CustomHardwareInstance.yaml b/mmv1/products/networkconnectivity/CustomHardwareInstance.yaml new file mode 100755 index 000000000000..649b3439b1b5 --- /dev/null +++ b/mmv1/products/networkconnectivity/CustomHardwareInstance.yaml @@ -0,0 +1,85 @@ +# Copyright 2026 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +name: CustomHardwareInstance +description: Description +base_url: projects/{{project}}/locations/{{location}}/customHardwareInstances +self_link: projects/{{project}}/locations/{{location}}/customHardwareInstances/{{custom_hardware_instance_id}} +create_url: projects/{{project}}/locations/{{location}}/customHardwareInstances?customHardwareInstanceId={{custom_hardware_instance_id}} +update_mask: true +update_verb: PATCH +import_format: + - projects/{{project}}/locations/{{location}}/customHardwareInstances/{{custom_hardware_instance_id}} +samples: + - name: network_connectivity_custom_hardware_instance_basic + primary_resource_id: instance + min_version: beta + steps: + - name: network_connectivity_custom_hardware_instance_basic + resource_id_vars: + resource_name: ch-test-instance + vars: + env_label: "test" + - name: network_connectivity_custom_hardware_instance_basic + resource_id_vars: + resource_name: ch-test-instance + vars: + env_label: "prod" +async: + operation: + base_url: '{{op_id}}' + actions: + - create + - update + - delete + result: + resource_inside_response: true +autogen_status: Q3VzdG9tSGFyZHdhcmVJbnN0YW5jZQ== +autogen_async: true +parameters: + - name: location + type: String + required: true + description: Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. + immutable: true + url_param_only: true + - name: customHardwareInstanceId + type: String + required: true + description: |- + Unique id of the CustomHardwareInstance. + This is restricted to letters, numbers and with the first + character being a letter, the last a letter or a number. Matching the regex + `^[a-zA-Z0-9-]*[a-zA-Z0-9]$`. + immutable: true + url_param_only: true +properties: + - name: createTime + type: String + description: Time when the CustomHardwareInstance was created. + output: true + - name: labels + type: KeyValueLabels + description: User-defined labels. + - name: name + type: String + description: |- + Identifier. The name of a CustomHardwareInstance. + This is populated by the service. + output: true + - name: updateTime + type: String + description: Time when the CustomHardwareInstance was updated. + output: true +min_version: beta diff --git a/mmv1/products/observability/FolderSettings.yaml b/mmv1/products/observability/FolderSettings.yaml index ef861a149d4a..0720606544b5 100644 --- a/mmv1/products/observability/FolderSettings.yaml +++ b/mmv1/products/observability/FolderSettings.yaml @@ -29,6 +29,7 @@ async: timeouts: insert_minutes: 10 update_minutes: 10 + delete_minutes: 20 base_url: '{{op_id}}' result: resource_inside_response: true diff --git a/mmv1/products/observability/OrganizationSettings.yaml b/mmv1/products/observability/OrganizationSettings.yaml index 2390db7fd317..61749d7e1fe4 100644 --- a/mmv1/products/observability/OrganizationSettings.yaml +++ b/mmv1/products/observability/OrganizationSettings.yaml @@ -29,6 +29,7 @@ async: timeouts: insert_minutes: 10 update_minutes: 10 + delete_minutes: 20 base_url: '{{op_id}}' result: resource_inside_response: true diff --git a/mmv1/products/observability/ProjectSettings.yaml b/mmv1/products/observability/ProjectSettings.yaml index 7371c3c0d7c8..f57a8f4cbd64 100644 --- a/mmv1/products/observability/ProjectSettings.yaml +++ b/mmv1/products/observability/ProjectSettings.yaml @@ -29,6 +29,7 @@ async: timeouts: insert_minutes: 10 update_minutes: 10 + delete_minutes: 20 base_url: '{{op_id}}' result: resource_inside_response: true diff --git a/mmv1/products/oracledatabase/CloudVmCluster.yaml b/mmv1/products/oracledatabase/CloudVmCluster.yaml index c90dafe6d2c1..c2f28f00b215 100644 --- a/mmv1/products/oracledatabase/CloudVmCluster.yaml +++ b/mmv1/products/oracledatabase/CloudVmCluster.yaml @@ -113,6 +113,24 @@ samples: # See: https://github.com/hashicorp/terraform-provider-google/issues/20599 cloud_vm_cluster_id: fmt.Sprintf("ofake-tf-test-vmcluster-full-%s", acctest.RandString(t, 10)) cloud_exadata_infrastructure_id: fmt.Sprintf("ofake-tf-test-exadata-for-vmcluster-full-%s", acctest.RandString(t, 10)) + - name: oracledatabase_cloud_vmcluster_exascale + primary_resource_id: my_vmcluster + steps: + - name: oracledatabase_cloud_vmcluster_exascale + resource_id_vars: + project: my-project + cloud_vm_cluster_id: my-instance + cloud_exadata_infrastructure_id: my-exadata + exascale_db_storage_vault_id: my-vault + deletion_protection: "true" + ignore_read_extra: + - deletion_protection + test_vars_overrides: + deletion_protection: "false" + project: '"oci-terraform-testing-prod"' + cloud_vm_cluster_id: fmt.Sprintf("ofake-tf-test-vmcluster-exascale-%s", acctest.RandString(t, 10)) + cloud_exadata_infrastructure_id: fmt.Sprintf("ofake-tf-test-exadata-for-vmcluster-exascale-%s", acctest.RandString(t, 10)) + exascale_db_storage_vault_id: fmt.Sprintf("ofake-tf-test-vault-for-vmcluster-exascale-%s", acctest.RandString(t, 10)) virtual_fields: - name: deletion_protection type: Boolean @@ -300,6 +318,15 @@ properties: type: String description: 'OCI Cluster name. ' default_from_api: true + - name: storageManagementType + type: String + description: |- + The storage management type of the VM Cluster. + Possible values: + STORAGE_MANAGEMENT_TYPE_UNSPECIFIED + ASM + EXASCALE + output: true - name: labels type: KeyValueLabels description: 'Labels or tags associated with the VM Cluster. ' @@ -364,3 +391,11 @@ properties: PARTIALLY_CONNECTED DISCONNECTED UNKNOWN + - name: exascaleDbStorageVault + type: String + description: |- + The name of ExascaleDbStorageVault associated with the VM Cluster. + Format: + projects/{project}/locations/{location}/exascaleDbStorageVaults/{exascale_db_storage_vault} + immutable: true + diff_suppress_func: tpgresource.CompareSelfLinkOrResourceName diff --git a/mmv1/products/oracledatabase/ExadbVmCluster.yaml b/mmv1/products/oracledatabase/ExadbVmCluster.yaml index 56957f3e353e..89fa5b52da2d 100644 --- a/mmv1/products/oracledatabase/ExadbVmCluster.yaml +++ b/mmv1/products/oracledatabase/ExadbVmCluster.yaml @@ -22,6 +22,10 @@ update_verb: PATCH id_format: projects/{{project}}/locations/{{location}}/exadbVmClusters/{{exadb_vm_cluster_id}} import_format: - projects/{{project}}/locations/{{location}}/exadbVmClusters/{{exadb_vm_cluster_id}} +timeouts: + insert_minutes: 120 + update_minutes: 60 + delete_minutes: 60 async: type: OpAsync operation: diff --git a/mmv1/products/oracledatabase/ExascaleDbStorageVault.yaml b/mmv1/products/oracledatabase/ExascaleDbStorageVault.yaml index c3c064e18bd4..b619e20e83e9 100644 --- a/mmv1/products/oracledatabase/ExascaleDbStorageVault.yaml +++ b/mmv1/products/oracledatabase/ExascaleDbStorageVault.yaml @@ -21,6 +21,10 @@ create_url: projects/{{project}}/locations/{{location}}/exascaleDbStorageVaults? id_format: projects/{{project}}/locations/{{location}}/exascaleDbStorageVaults/{{exascale_db_storage_vault_id}} import_format: - projects/{{project}}/locations/{{location}}/exascaleDbStorageVaults/{{exascale_db_storage_vault_id}} +timeouts: + insert_minutes: 120 + update_minutes: 60 + delete_minutes: 60 async: type: OpAsync operation: @@ -77,6 +81,22 @@ samples: # As a result these resources are not sweepable # See: https://github.com/hashicorp/terraform-provider-google/issues/20599 exascale_db_storage_vault_id: fmt.Sprintf("ofake-tf-test-storage-vault-full-%s", acctest.RandString(t, 10)) + - name: oracledatabase_exascale_db_storage_vault_dedicated_exadata_infrastructure + primary_resource_id: my_storage_vault + steps: + - name: oracledatabase_exascale_db_storage_vault_dedicated_exadata_infrastructure + resource_id_vars: + project: my-project + cloud_exadata_infrastructure_id: my-infra + exascale_db_storage_vault_id: my-instance + deletion_protection: true + ignore_read_extra: + - deletion_protection + test_vars_overrides: + deletion_protection: "false" + project: '"oci-terraform-testing-prod"' + cloud_exadata_infrastructure_id: 'fmt.Sprintf("ofake-tf-configured-exadata-%s", acctest.RandString(t, 10))' + exascale_db_storage_vault_id: 'fmt.Sprintf("ofake-tf-test-vault-on-exadata-%s", acctest.RandString(t, 10))' virtual_fields: - name: deletion_protection type: Boolean @@ -134,6 +154,15 @@ properties: Format: projects/{project}/locations/{location}/exascaleDbStorageVaults/{exascale_db_storage_vault} output: true + - name: exadataInfrastructure + type: ResourceRef + description: | + The Exadata Infrastructure resource on which ExascaleDbStorageVault resource is created. + In the format: projects/{project}/locations/{region}/cloudExadataInfrastructures/{cloud_extradata_infrastructure} + resource: CloudExadataInfrastructure + imports: name + immutable: true + diff_suppress_func: tpgresource.CompareSelfLinkOrResourceName - name: properties type: NestedObject description: |- diff --git a/mmv1/products/oracledatabase/GoldengateConnection.yaml b/mmv1/products/oracledatabase/GoldengateConnection.yaml index 646d98293a94..6b4e953fe340 100755 --- a/mmv1/products/oracledatabase/GoldengateConnection.yaml +++ b/mmv1/products/oracledatabase/GoldengateConnection.yaml @@ -21,6 +21,10 @@ create_url: projects/{{project}}/locations/{{location}}/goldengateConnections?go id_format: projects/{{project}}/locations/{{location}}/goldengateConnections/{{goldengate_connection_id}} import_format: - projects/{{project}}/locations/{{location}}/goldengateConnections/{{goldengate_connection_id}} +timeouts: + insert_minutes: 30 + update_minutes: 30 + delete_minutes: 30 async: type: OpAsync operation: diff --git a/mmv1/products/oracledatabase/GoldengateConnectionAssignment.yaml b/mmv1/products/oracledatabase/GoldengateConnectionAssignment.yaml index 77786bfcfab6..48043f7b2103 100755 --- a/mmv1/products/oracledatabase/GoldengateConnectionAssignment.yaml +++ b/mmv1/products/oracledatabase/GoldengateConnectionAssignment.yaml @@ -27,6 +27,10 @@ timeouts: delete_minutes: 60 async: operation: + timeouts: + insert_minutes: 60 + update_minutes: 60 + delete_minutes: 60 base_url: '{{op_id}}' actions: - create diff --git a/mmv1/products/oracledatabase/GoldengateDeployment.yaml b/mmv1/products/oracledatabase/GoldengateDeployment.yaml index a44922876454..59416f625cac 100755 --- a/mmv1/products/oracledatabase/GoldengateDeployment.yaml +++ b/mmv1/products/oracledatabase/GoldengateDeployment.yaml @@ -28,6 +28,10 @@ timeouts: async: type: OpAsync operation: + timeouts: + insert_minutes: 60 + update_minutes: 60 + delete_minutes: 60 base_url: '{{op_id}}' actions: - create diff --git a/mmv1/products/oracledatabase/OdbNetwork.yaml b/mmv1/products/oracledatabase/OdbNetwork.yaml index 17faaccb7ce1..ce4ea77807aa 100644 --- a/mmv1/products/oracledatabase/OdbNetwork.yaml +++ b/mmv1/products/oracledatabase/OdbNetwork.yaml @@ -24,6 +24,10 @@ create_url: projects/{{project}}/locations/{{location}}/odbNetworks?odbNetworkId id_format: projects/{{project}}/locations/{{location}}/odbNetworks/{{odb_network_id}} import_format: - projects/{{project}}/locations/{{location}}/odbNetworks/{{odb_network_id}} +timeouts: + insert_minutes: 20 + update_minutes: 20 + delete_minutes: 20 async: type: OpAsync operation: diff --git a/mmv1/products/oracledatabase/OdbSubnet.yaml b/mmv1/products/oracledatabase/OdbSubnet.yaml index 2dc25a7c3cc7..217afd2a8d1a 100644 --- a/mmv1/products/oracledatabase/OdbSubnet.yaml +++ b/mmv1/products/oracledatabase/OdbSubnet.yaml @@ -24,6 +24,10 @@ create_url: projects/{{project}}/locations/{{location}}/odbNetworks/{{odbnetwork id_format: projects/{{project}}/locations/{{location}}/odbNetworks/{{odbnetwork}}/odbSubnets/{{odb_subnet_id}} import_format: - projects/{{project}}/locations/{{location}}/odbNetworks/{{odbnetwork}}/odbSubnets/{{odb_subnet_id}} +timeouts: + insert_minutes: 90 + update_minutes: 90 + delete_minutes: 90 async: type: OpAsync operation: diff --git a/mmv1/products/privilegedaccessmanager/Entitlement.yaml b/mmv1/products/privilegedaccessmanager/Entitlement.yaml index 754bfe83005d..8f07efa2dbc5 100644 --- a/mmv1/products/privilegedaccessmanager/Entitlement.yaml +++ b/mmv1/products/privilegedaccessmanager/Entitlement.yaml @@ -127,6 +127,10 @@ properties: The approvals needed before access will be granted to a requester. No approvals will be needed if this field is null. Different types of approval workflows that can be used to gate privileged access granting. immutable: true + # This flattener is entirely handwritten and must be updated with any new field or subfield. + # Solves a permadiff when the API returns `{"approvalWorkflow": {"manualApprovals": {}}}` + # for entitlements created without an approval workflow. + custom_flatten: templates/terraform/custom_flatten/privileged_access_manager_entitlement_approval_workflow.go.tmpl properties: - name: manualApprovals type: NestedObject diff --git a/mmv1/products/storage/AnywhereCache.yaml b/mmv1/products/storage/AnywhereCache.yaml index 0f1f6bc20429..c1fe3b1003b4 100644 --- a/mmv1/products/storage/AnywhereCache.yaml +++ b/mmv1/products/storage/AnywhereCache.yaml @@ -43,6 +43,7 @@ async: resource_inside_response: true autogen_async: false custom_code: + constants: templates/terraform/constants/storage_anywhere_cache.go.tmpl custom_create: templates/terraform/custom_create/storage_anywhere_cache.go.tmpl custom_update: templates/terraform/custom_update/storage_anywhere_cache.go.tmpl samples: @@ -71,11 +72,15 @@ properties: description: The zone in which the cache instance needs to be created. For example, `us-central1-a.` - name: admissionPolicy type: Enum + default_value: admit-on-first-miss + deprecation_message: '`admit-on-second-miss` is deprecated and will be removed in a future major release. The backend will ignore this attribute and treat it as `admit-on-first-miss`.' + description: | + The cache admission policy dictates whether a block should be inserted upon a cache miss. + Note: "admit-on-second-miss" is deprecated and will fallback to "admit-on-first-miss". enum_values: - admit-on-first-miss - admit-on-second-miss - default_value: admit-on-first-miss - description: The cache admission policy dictates whether a block should be inserted upon a cache miss. + diff_suppress_func: StorageAnywhereCacheAdmissionPolicyDiffSuppress - name: ttl type: String default_value: 86400s diff --git a/mmv1/products/vectorsearch/Index.yaml b/mmv1/products/vectorsearch/Index.yaml new file mode 100644 index 000000000000..b4b1ae4af2f6 --- /dev/null +++ b/mmv1/products/vectorsearch/Index.yaml @@ -0,0 +1,188 @@ +# Copyright 2026 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +name: Index +description: |- + An Index defines an approximate nearest-neighbor search structure over a + field of a Vector Search Collection. +base_url: projects/{{project}}/locations/{{location}}/collections/{{collection_id}}/indexes +self_link: projects/{{project}}/locations/{{location}}/collections/{{collection_id}}/indexes/{{index_id}} +create_url: projects/{{project}}/locations/{{location}}/collections/{{collection_id}}/indexes?indexId={{index_id}} +update_mask: true +update_verb: PATCH +import_format: + - projects/{{project}}/locations/{{location}}/collections/{{collection_id}}/indexes/{{index_id}} +timeouts: + insert_minutes: 90 + update_minutes: 60 + delete_minutes: 60 +async: + operation: + timeouts: + insert_minutes: 90 + update_minutes: 60 + delete_minutes: 60 + base_url: '{{op_id}}' + actions: + - create + - update + - delete + result: + resource_inside_response: true +autogen_status: SW5kZXg= +autogen_async: true +samples: + - name: vectorsearch_index_basic + primary_resource_id: example-index + steps: + - name: vectorsearch_index_basic + resource_id_vars: + index_id: example-index + collection_id: example-collection + - name: vectorsearch_index_dedicated + primary_resource_id: example-dedicated-index + steps: + - name: vectorsearch_index_dedicated + resource_id_vars: + index_id: example-dedicated-index + collection_id: example-dedicated-collection +parameters: + - name: location + type: String + required: true + description: Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. + immutable: true + url_param_only: true + - name: collectionId + type: String + required: true + description: |- + The ID of the parent Collection. + immutable: true + url_param_only: true + - name: indexId + type: String + required: true + description: |- + ID of the Index to create. + The id must be 1-63 characters long, and comply with + [RFC1035](https://www.ietf.org/rfc/rfc1035.txt). + Specifically, it must be 1-63 characters long and match the regular + expression `[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?`. + immutable: true + url_param_only: true +properties: + - name: name + type: String + description: Identifier. name of resource + output: true + - name: createTime + type: String + description: '[Output only] Create time stamp' + output: true + - name: updateTime + type: String + description: '[Output only] Update time stamp' + output: true + - name: displayName + type: String + description: User-specified display name of the index + - name: description + type: String + description: User-specified description of the index + - name: labels + type: KeyValueLabels + description: Labels as key value pairs. + - name: distanceMetric + type: Enum + description: |- + Distance metric used for indexing. If not specified, will default to + `DOT_PRODUCT`. + immutable: true + default_from_api: true + enum_values: + - 'DOT_PRODUCT' + - 'COSINE_DISTANCE' + - name: indexField + type: String + required: true + immutable: true + description: The collection schema field to index. + - name: filterFields + type: Array + immutable: true + description: The fields to push into the index to enable fast ANN inline filtering. + item_type: + type: String + - name: storeFields + type: Array + immutable: true + description: The fields to push into the index to enable inline data retrieval. + item_type: + type: String + - name: dedicatedInfrastructure + type: NestedObject + default_from_api: true + description: |- + Dedicated infrastructure for the index. This field belongs to the + `infra_type` oneof; if omitted, the server populates it with the + default `PERFORMANCE_OPTIMIZED` mode and an autoscaling spec of + `min_replica_count=2`, `max_replica_count=2`. + properties: + - name: mode + type: Enum + immutable: true + default_from_api: true + description: |- + Mode of the dedicated infrastructure. Defaults to `PERFORMANCE_OPTIMIZED`. + enum_values: + - 'MODE_UNSPECIFIED' + - 'STORAGE_OPTIMIZED' + - 'PERFORMANCE_OPTIMIZED' + - name: autoscalingSpec + type: NestedObject + default_from_api: true + description: Autoscaling specification. + properties: + - name: minReplicaCount + type: Integer + default_from_api: true + description: |- + The minimum number of replicas. If not set or set to `0`, defaults + to `2`. Must be >= `1` and <= `1000`. + - name: maxReplicaCount + type: Integer + default_from_api: true + description: |- + The maximum number of replicas. Must be >= `min_replica_count` + and <= `1000`. If not set or set to `0`, defaults to the greater + of `min_replica_count` and `2` (or `5` for the v1beta version). + - name: denseScann + type: NestedObject + immutable: true + default_from_api: true + description: |- + Dense ScaNN index configuration. This field belongs to the + `index_type` oneof; if omitted, the server populates it with default + ScaNN settings. + properties: + - name: featureNormType + type: Enum + immutable: true + default_from_api: true + description: Feature norm type for the ScaNN index. + enum_values: + - 'FEATURE_NORM_TYPE_UNSPECIFIED' + - 'NONE' + - 'UNIT_L2_NORM' diff --git a/mmv1/products/vertexai/OnlineEvaluator.yaml b/mmv1/products/vertexai/OnlineEvaluator.yaml deleted file mode 100644 index 671d2655c6ee..000000000000 --- a/mmv1/products/vertexai/OnlineEvaluator.yaml +++ /dev/null @@ -1,200 +0,0 @@ ---- -# Copyright 2026 Google Inc. -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -name: OnlineEvaluator -description: Description -base_url: projects/{{project}}/locations/{{region}}/onlineEvaluators -min_version: beta -self_link: projects/{{project}}/locations/{{region}}/onlineEvaluators/{{name}} -create_url: projects/{{project}}/locations/{{region}}/onlineEvaluators -update_mask: true -update_verb: PATCH -timeouts: - insert_minutes: 20 - update_minutes: 20 - delete_minutes: 60 -async: - type: OpAsync - operation: - base_url: '{{op_id}}' - actions: - - create - - update - - delete - result: - resource_inside_response: true -autogen_status: T25saW5lRXZhbHVhdG9y -samples: - - name: vertex_ai_online_evaluator_basic - primary_resource_id: evaluator - steps: - - name: vertex_ai_online_evaluator_basic - resource_id_vars: - evaluator_name: my-evaluator - engine_name: my-engine -parameters: - - name: region - type: String - required: true - description: Resource ID segment making up resource `name`. It identifies the resource within its parent collection as described in https://google.aip.dev/122. - immutable: true - url_param_only: true -properties: - - name: agentResource - type: String - required: true - description: The name of the agent that the OnlineEvaluator evaluates periodically. This value is used to filter the traces with a matching cloud.resource_id and link the evaluation results with relevant dashboards/UIs. This field is immutable. Once set, it cannot be changed. - immutable: true - - name: cloudObservability - type: NestedObject - description: Data source for the OnlineEvaluator, based on Google Cloud Observability stack (Cloud Trace & Cloud Logging). - properties: - - name: logView - type: String - description: Optional log view that will be used to query logs. If empty, the `_Default` view will be used. - immutable: true - - name: openTelemetry - type: NestedObject - description: Configuration for data source following OpenTelemetry. - immutable: true - ignore_read: true - properties: - - name: semconvVersion - type: String - required: true - description: Defines which version OTel Semantic Convention the data follows. Can be "1.39.0" or newer. - immutable: true - - name: traceScope - type: NestedObject - description: If chosen, the online evaluator will evaluate single traces matching specified `filter`. - immutable: true - properties: - - name: filter - type: Array - description: A list of predicates to filter traces. Multiple predicates are combined using AND. The maximum number of predicates is 10. - immutable: true - item_type: - type: NestedObject - properties: - - name: duration - type: NestedObject - description: Defines a predicate for filtering based on a numeric value. - immutable: true - properties: - - name: comparisonOperator - type: String - required: true - description: 'The comparison operator to apply. Possible values: LESS LESS_OR_EQUAL EQUAL NOT_EQUAL GREATER_OR_EQUAL GREATER' - immutable: true - - name: value - type: Double - required: true - description: The value to compare against. - immutable: true - - name: totalTokenUsage - type: NestedObject - description: Defines a predicate for filtering based on a numeric value. - immutable: true - properties: - - name: comparisonOperator - type: String - required: true - description: 'The comparison operator to apply. Possible values: LESS LESS_OR_EQUAL EQUAL NOT_EQUAL GREATER_OR_EQUAL GREATER' - immutable: true - - name: value - type: Double - required: true - description: The value to compare against. - immutable: true - - name: traceView - type: String - description: 'Optional trace view that will be used to query traces. If empty, the `_Default` view will be used. NOTE: This field is not supported yet and will be ignored if set.' - immutable: true - required: true - immutable: true - - name: config - type: NestedObject - required: true - description: Configuration for sampling behavior of the OnlineEvaluator. The OnlineEvaluator runs at a fixed interval of 10 minutes. - properties: - - name: maxEvaluatedSamplesPerRun - type: String - description: The maximum number of evaluations to perform per run. If set to 0, the number is unbounded. - - name: randomSampling - type: NestedObject - description: Configuration for random sampling. - properties: - - name: percentage - type: Integer - required: true - description: The percentage of traces to sample for evaluation. Must be an integer between `1` and `100`. - - name: createTime - type: String - description: Timestamp when the OnlineEvaluator was created. - output: true - - name: displayName - type: String - description: Human-readable name for the `OnlineEvaluator`. The name doesn't have to be unique. The name can consist of any UTF-8 characters. The maximum length is `63` characters. If the display name exceeds max characters, an `INVALID_ARGUMENT` error is returned. - - name: metricSources - type: Array - required: true - immutable: true - description: A list of metric sources to be used for evaluating samples. At least one MetricSource must be provided. Right now, only predefined metrics and registered metrics are supported. Every registered metric must have `display_name` (or `title`) and `score_range` defined. Otherwise, the evaluations will fail. The maximum number of `metric_sources` is 25. - item_type: - type: NestedObject - properties: - # TODO: Support structured metric config instead of JSON string. - - name: metric - type: String - description: | - Inline metric config. Provide this field as a JSON-formatted string. - Suggested predefined metricSpecName values: - - `final_response_quality_v1` - - `tool_use_quality_v1` - - `hallucination_v1` - - `safety_v1` - - `multi_turn_task_success_v1` - - `multi_turn_tool_use_quality_v1` - - `multi_turn_trajectory_quality_v1` - immutable: true - custom_expand: templates/terraform/custom_expand/json_value.tmpl - custom_flatten: templates/terraform/custom_flatten/json_schema.tmpl - - name: metricResourceName - type: String - description: Resource name for registered metric. - immutable: true - - name: name - type: String - description: 'Identifier. The resource name of the OnlineEvaluator. Format: projects/{project}/locations/{region}/onlineEvaluators/{id}.' - output: true - custom_flatten: templates/terraform/custom_flatten/name_from_self_link.tmpl - - name: state - type: String - description: 'The state of the OnlineEvaluator. Possible values: ACTIVE SUSPENDED FAILED WARNING' - output: true - - name: stateDetails - type: Array - description: Contains additional information about the state of the OnlineEvaluator. This is used to provide more details in the event of a failure. - output: true - item_type: - type: NestedObject - output: true - properties: - - name: message - type: String - description: Human-readable message describing the state of the OnlineEvaluator. - output: true - - name: updateTime - type: String - description: Timestamp when the OnlineEvaluator was last updated. - output: true diff --git a/mmv1/products/vertexai/ReasoningEngine.yaml b/mmv1/products/vertexai/ReasoningEngine.yaml index ab33a657360a..5d8389861418 100644 --- a/mmv1/products/vertexai/ReasoningEngine.yaml +++ b/mmv1/products/vertexai/ReasoningEngine.yaml @@ -251,6 +251,252 @@ properties: description: | Optional. Declarations for object class methods in OpenAPI specification format. + + **Note**: When deploying via Terraform, this field must be populated manually. + Otherwise, client SDKs (like `agent_engines.get()`) will not be able to discover the methods, and calls to the engine (or A2A integrations) will fail. + + Depending on the template/framework used (`agent_framework`), the required class methods and their parameters differ: + + **Warning**: The configuration snippets below are illustrative, may not be exhaustive, and could stop working over time. For the most up-to-date method lists and schemas, please consult the respective SDK source code: + * For Google ADK: See [ADK Python SDK cli_deploy.py](https://github.com/google/adk-python/blob/68a780306e3bdd648a882ef34c0abf8e5148353e/src/google/adk/cli/cli_deploy.py#L109). + * For Langchain: See [Vertex AI Python SDK langchain.py](https://github.com/googleapis/python-aiplatform/blob/c8a38a085931b01f4d6071f0ab7a64cb42851829/agentplatform/agent_engines/templates/langchain.py#L642-L717). + + ### 1. Langchain Template + * `query` (api_mode = "sync" or empty) + * `stream_query` (api_mode = "stream") + + Example for Langchain: + ```hcl + class_methods = jsonencode([ + { + name = "query" + api_mode = "sync" + description = "Queries the reasoning engine" + parameters = { + type = "object" + required = ["input"] + properties = { + input = { + type = "string" + description = "The input prompt" + } + } + } + }, + { + name = "stream_query" + api_mode = "stream" + description = "Streams queries from the reasoning engine" + parameters = { + type = "object" + required = ["input"] + properties = { + input = { + type = "string" + description = "The input prompt" + } + } + } + } + ]) + ``` + + ### 2. Google ADK Template (Standard - No A2A) + For standard Google ADK (Agent Development Kit) deployments, you must define the following 11 methods: + + Example for Standard ADK: + ```hcl + class_methods = jsonencode([ + { + name = "get_session" + api_mode = "" + description = "Retrieve session by ID" + parameters = { + type = "object" + required = ["user_id", "session_id"] + properties = { + user_id = { type = "string" } + session_id = { type = "string" } + } + } + }, + { + name = "async_get_session" + api_mode = "async" + description = "Retrieve session asynchronously by ID" + parameters = { + type = "object" + required = ["user_id", "session_id"] + properties = { + user_id = { type = "string" } + session_id = { type = "string" } + } + } + }, + { + name = "list_sessions" + api_mode = "" + description = "List all sessions for a user" + parameters = { + type = "object" + required = ["user_id"] + properties = { + user_id = { type = "string" } + } + } + }, + { + name = "async_list_sessions" + api_mode = "async" + description = "List all sessions for a user asynchronously" + parameters = { + type = "object" + required = ["user_id"] + properties = { + user_id = { type = "string" } + } + } + }, + { + name = "create_session" + api_mode = "" + description = "Create a new session" + parameters = { + type = "object" + required = ["user_id"] + properties = { + user_id = { type = "string" } + session_id = { type = "string" } + state = { type = "object" } + } + } + }, + { + name = "async_create_session" + api_mode = "async" + description = "Create a new session asynchronously" + parameters = { + type = "object" + required = ["user_id"] + properties = { + user_id = { type = "string" } + session_id = { type = "string" } + state = { type = "object" } + } + } + }, + { + name = "delete_session" + api_mode = "" + description = "Delete session by ID" + parameters = { + type = "object" + required = ["user_id", "session_id"] + properties = { + user_id = { type = "string" } + session_id = { type = "string" } + } + } + }, + { + name = "async_delete_session" + api_mode = "async" + description = "Delete session asynchronously by ID" + parameters = { + type = "object" + required = ["user_id", "session_id"] + properties = { + user_id = { type = "string" } + session_id = { type = "string" } + } + } + }, + { + name = "stream_query" + api_mode = "stream" + description = "Stream queries from the agent" + parameters = { + type = "object" + required = ["message", "user_id"] + properties = { + message = { description = "Message string or object" } + user_id = { type = "string" } + session_id = { type = "string" } + run_config = { type = "object" } + } + } + }, + { + name = "async_stream_query" + api_mode = "async_stream" + description = "Stream queries asynchronously from the agent" + parameters = { + type = "object" + required = ["message", "user_id"] + properties = { + message = { description = "Message string or object" } + user_id = { type = "string" } + session_id = { type = "string" } + session_events = { type = "array", items = { type = "object" } } + run_config = { type = "object" } + } + } + }, + { + name = "streaming_agent_run_with_events" + api_mode = "async_stream" + description = "Stream agent run with events asynchronously" + parameters = { + type = "object" + required = ["request_json"] + properties = { + request_json = { type = "string" } + } + } + } + ]) + ``` + + ### 3. Google ADK Template (A2A-Enabled) + If the agent integrates with the Gemini Enterprise Agent Registry (A2A), you must inject the `a2a_agent_card` JSON metadata as a string **specifically inside the `async_create_session` method definition**: + + Example for A2A-Enabled ADK: + ```hcl + locals { + # Construct the A2A endpoint URL + a2a_url = "https://us-central1-aiplatform.googleapis.com/v1/projects/my-project/locations/us-central1/reasoningEngines/my-agent/a2a" + + agent_card = { + name = "my-agent" + description = "A2A Agent" + version = "1.0.0" + preferred_transport = "HTTP_JSON" + supported_interfaces = [{ url = local.a2a_url, protocol_binding = "HTTP_JSON" }] + capabilities = { streaming = true } + } + } + + # In class_methods, append "a2a_agent_card" key ONLY to the "async_create_session" method: + class_methods = jsonencode([ + # ... other 10 standard methods (same as Standard ADK) ... + { + name = "async_create_session" + api_mode = "async" + description = "Create a new session asynchronously" + parameters = { + type = "object" + required = ["user_id"] + properties = { + user_id = { type = "string" } + session_id = { type = "string" } + state = { type = "object" } + } + } + # Inject the serialized Agent Card here + a2a_agent_card = jsonencode(local.agent_card) + } + ]) + ``` state_func: 'func(v interface{}) string { s, _ := structure.NormalizeJsonString(v); return s }' custom_flatten: 'templates/terraform/custom_flatten/json_schema.tmpl' custom_expand: 'templates/terraform/custom_expand/json_value.tmpl' @@ -629,3 +875,97 @@ properties: type: Boolean description: |- If true, no memory revisions will be created for any requests to the Memory Bank. + - name: 'customizationConfigs' + type: Array + description: |- + Optional. Customization configs for how Agent Engine sub-resources manage context at different scope levels. + item_type: + type: NestedObject + properties: + - name: 'scopeKeys' + type: Array + description: |- + Optional. List of scope keys that this customization config applies to. + item_type: + type: String + - name: 'memoryTopics' + type: Array + description: |- + Optional. List of topics that the memory should be associated with. + item_type: + type: NestedObject + properties: + - name: 'managedMemoryTopic' + type: NestedObject + # Note: exactly_one_of within array elements is not enforced client-side by MM; relies on backend API server validation. + exactly_one_of: + - managedMemoryTopic + - customMemoryTopic + description: |- + Optional. Managed memory topic. + properties: + - name: 'managedTopicEnum' + type: String + description: |- + Managed topic enum (e.g. USER_PREFERENCES, EXPLICIT_INSTRUCTIONS). + - name: 'customMemoryTopic' + type: NestedObject + description: |- + Optional. Custom memory topic. + properties: + - name: 'label' + type: String + description: |- + Label of custom memory topic. + - name: 'description' + type: String + description: |- + Description of custom memory topic. + - name: 'consolidationConfig' + type: NestedObject + description: |- + Optional. Configuration for how many memory revisions Memory Bank considers when consolidating each memory candidate. + properties: + - name: 'revisionsPerCandidateCount' + type: Integer + description: |- + Number of revisions to consider per candidate count. + - name: 'enableThirdPersonMemories' + type: Boolean + description: |- + Optional. Generate memories in the third person if set to true. + - name: 'structuredMemoryConfigs' + type: Array + description: |- + Optional. Structured memory configurations for Agent Engine sub-resources. + item_type: + type: NestedObject + properties: + - name: 'scopeKeys' + type: Array + description: |- + Optional. List of scope keys that this structured memory config applies to. + item_type: + type: String + - name: 'schemaConfigs' + type: Array + description: |- + Optional. List of schema configs that this structured memory config applies to. + item_type: + type: NestedObject + properties: + - name: 'id' + type: String + description: |- + Required. Unique ID identifying the memory schema. + required: true + - name: 'memorySchema' + type: String + api_name: 'schema' + description: |- + Optional. The memory schema defined as an OpenAPI Schema Object JSON string. + state_func: 'func(v interface{}) string { s, _ := structure.NormalizeJsonString(v); return s }' + custom_flatten: 'templates/terraform/custom_flatten/json_schema.tmpl' + custom_expand: 'templates/terraform/custom_expand/json_value.tmpl' + validation: + function: 'validation.StringIsJSON' diff --git a/mmv1/products/vertexai/Schedule.yaml b/mmv1/products/vertexai/Schedule.yaml index cd219bd951b5..adf542905084 100644 --- a/mmv1/products/vertexai/Schedule.yaml +++ b/mmv1/products/vertexai/Schedule.yaml @@ -27,7 +27,7 @@ async: - delete result: resource_inside_response: true -autogen_status: U2NoZWR1bGU= +autogen_status: U2NoZWR1bGVBdXRvZ2VuVjJBZ2VudA== autogen_async: false samples: - name: vertex_ai_schedule diff --git a/mmv1/products/vertexai/SemanticGovernancePolicyEngine.yaml b/mmv1/products/vertexai/SemanticGovernancePolicyEngine.yaml new file mode 100644 index 000000000000..acd822881b89 --- /dev/null +++ b/mmv1/products/vertexai/SemanticGovernancePolicyEngine.yaml @@ -0,0 +1,129 @@ +# Copyright 2026 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +name: 'SemanticGovernancePolicyEngine' +description: | + A SemanticGovernancePolicyEngine (SGPE) is the managed, runtime evaluation + infrastructure for Semantic Governance Policies (SGP): the natural-language + constraints that govern an AI agent's tool calls. It is a project-level, + regional singleton, so each project has at most one engine per region. + + Provisioning the engine sets up managed Private Service Connect (PSC) + networking in your VPC and a policy decision point that the Agent Gateway + consults at runtime to allow or deny an agent's proposed tool calls. The + Semantic Governance Policies themselves, and the Agent Gateway integration + that routes agent traffic through the engine, are configured separately and + are not managed by this resource. + + Reading an uninitialized or deprovisioned engine returns the singleton + with state INACTIVE rather than reporting it as absent. +references: + guides: + 'Semantic governance overview': 'https://docs.cloud.google.com/gemini-enterprise-agent-platform/govern/policies/semantic-governance-overview' +docs: +min_version: 'beta' +id_format: 'projects/{{project}}/locations/{{region}}/semanticGovernancePolicyEngine' +base_url: 'projects/{{project}}/locations/{{region}}/semanticGovernancePolicyEngine' +self_link: 'projects/{{project}}/locations/{{region}}/semanticGovernancePolicyEngine' +create_url: 'projects/{{project}}/locations/{{region}}/semanticGovernancePolicyEngine' +# Singleton resource: AIP-156 prescribes upsert via PATCH with no parent +# Create RPC. +create_verb: 'PATCH' +update_url: 'projects/{{project}}/locations/{{region}}/semanticGovernancePolicyEngine' +update_verb: 'PATCH' +# No standard Delete; tear-down is the :deprovision custom method (POST, +# empty body), polled via the async block below. +delete_url: 'projects/{{project}}/locations/{{region}}/semanticGovernancePolicyEngine:deprovision' +delete_verb: 'POST' +import_format: + - 'projects/{{project}}/locations/{{region}}/semanticGovernancePolicyEngine' +timeouts: + insert_minutes: 60 + update_minutes: 60 + delete_minutes: 60 +async: + actions: ['create', 'update', 'delete'] + type: 'OpAsync' + operation: + base_url: '{{op_id}}' + result: + resource_inside_response: true +custom_code: + constants: 'templates/terraform/constants/semantic_governance_policy_engine.go.tmpl' + post_read: 'templates/terraform/post_read/vertex_ai_semantic_governance_policy_engine.go.tmpl' +samples: + - name: 'vertex_ai_semantic_governance_policy_engine_basic' + primary_resource_id: 'sgpe' + # The generator's standard CheckDestroy expects GET to return an error + # (404) after delete, but SGPE is an AIP-156 singleton whose GET returns + # 200 with state INACTIVE after deprovision -- so the example-derived + # test would fail at CheckDestroy even after a successful deprovision. + # Acceptance coverage is instead provided by the handwritten lifecycle + # test (resource_vertex_ai_semantic_governance_policy_engine_test.go), + # which CI still runs because it is a new TestAcc function in the diff. + exclude_test: true + steps: + - name: 'vertex_ai_semantic_governance_policy_engine_basic' +parameters: + - name: 'region' + type: String + description: | + The region of the SemanticGovernancePolicyEngine, e.g. 'us-central1'. + url_param_only: true + immutable: true + default_from_api: true +properties: + - name: 'name' + type: String + description: | + The resource name of the SemanticGovernancePolicyEngine, in the form + 'projects/{project}/locations/{region}/semanticGovernancePolicyEngine'. + output: true + - name: 'createTime' + type: String + description: | + The time the SemanticGovernancePolicyEngine was created, in RFC3339 + UTC "Zulu" format. + output: true + - name: 'updateTime' + type: String + description: | + The time the SemanticGovernancePolicyEngine was last updated, in + RFC3339 UTC "Zulu" format. + output: true + - name: 'pscServiceAttachment' + type: String + description: | + The Private Service Connect service attachment URI for the SGPE's + managed endpoint. + output: true + - name: 'ipAddress' + type: String + description: | + The IP address allocated for the SGPE's managed PSC endpoint. + output: true + - name: 'pscForwardingRule' + type: String + description: | + The Private Service Connect forwarding rule URI for the SGPE's + managed endpoint. + output: true + - name: 'state' + type: String + description: | + The current state of the SemanticGovernancePolicyEngine. One of: + STATE_UNSPECIFIED, PROVISIONING, ACTIVE, FAILED, DEPROVISIONING, + INACTIVE. `FAILED` indicates provisioning did not succeed; recover by + destroying the resource (deprovision) or re-applying (re-provision). + output: true diff --git a/mmv1/provider/terraform.go b/mmv1/provider/terraform.go index 08ae23f035bd..01877b911d82 100644 --- a/mmv1/provider/terraform.go +++ b/mmv1/provider/terraform.go @@ -59,14 +59,16 @@ func NewTerraform(product *api.Product, versionName string, startTime time.Time, IAMResourceCount: 0, Product: product, TargetVersionName: versionName, - Version: *product.VersionObjOrClosest(versionName), StartTime: startTime, templateFS: templateFS, } - t.Product.ImportPath = ImportPathFromVersion(versionName) - for _, r := range t.Product.Objects { - r.ImportPath = t.Product.ImportPath + if product != nil { + t.Version = *product.VersionObjOrClosest(versionName) + t.Product.ImportPath = ImportPathFromVersion(versionName) + for _, r := range t.Product.Objects { + r.ImportPath = t.Product.ImportPath + } } return t @@ -458,6 +460,20 @@ func (t Terraform) getCommonCopyFiles(versionName string, generateCode, generate // key is the target file and value is the source file commonCopyFiles := make(map[string]string, 0) + // Case 0: If we're generating a specific product, only copy files for that product. + if t.Product != nil { + if !generateCode { + return commonCopyFiles + } + googleDir := "google" + if versionName != "ga" { + googleDir = fmt.Sprintf("google-%s", versionName) + } + files := t.getCopyFilesInFolder("third_party/terraform/services/"+t.Product.ApiName, googleDir) + maps.Copy(commonCopyFiles, files) + return commonCopyFiles + } + // Case 1: When copy all of files except .tmpl in a folder to the root directory of downstream repository, // save the folder name to foldersCopiedToRootDir foldersCopiedToRootDir := []string{"third_party/terraform/META.d", "third_party/terraform/version"} @@ -493,7 +509,6 @@ func (t Terraform) getCommonCopyFiles(versionName string, generateCode, generate "third_party/terraform/fwvalidators", "third_party/terraform/provider", "third_party/terraform/registry", - "third_party/terraform/services", "third_party/terraform/sweeper", "third_party/terraform/test-fixtures", "third_party/terraform/tpgdclresource", @@ -596,7 +611,9 @@ func (t Terraform) CopyFileList(outputFolder string, files map[string]string, ge // Compiles files that are shared at the provider level func (t Terraform) CompileCommonFiles(outputFolder string, products []*api.Product, overridePath string) { log.Printf("Generating common files for %s", ProviderName(t)) - t.generateResourcesForVersion(products) + if t.Product == nil { + t.generateResourcesForVersion(products) + } files := t.getCommonCompileFiles(t.TargetVersionName) templateData := NewTemplateData(outputFolder, t.TargetVersionName, t.templateFS) t.CompileFileList(outputFolder, files, *templateData, products) @@ -608,6 +625,14 @@ func (t Terraform) getCommonCompileFiles(versionName string) map[string]string { // key is the target file and the value is the source file commonCompileFiles := make(map[string]string, 0) + if t.Product != nil { + googleDir := "google" + if versionName != "ga" { + googleDir = fmt.Sprintf("google-%s", versionName) + } + return t.getCompileFilesInFolder("third_party/terraform/services/"+t.Product.ApiName, googleDir) + } + // Case 1: When compile all of files except .tmpl in a folder to the root directory of downstream repository, // save the folder name to foldersCopiedToRootDir foldersCompiledToRootDir := []string{"third_party/terraform/scripts"} @@ -628,7 +653,6 @@ func (t Terraform) getCommonCompileFiles(versionName string) map[string]string { "third_party/terraform/fwresource", "third_party/terraform/fwtransport", "third_party/terraform/provider", - "third_party/terraform/services", "third_party/terraform/sweeper", "third_party/terraform/test-fixtures", "third_party/terraform/tpgdclresource", diff --git a/mmv1/provider/terraform_tgc.go b/mmv1/provider/terraform_tgc.go index 8d08f416c882..ec25e4837429 100644 --- a/mmv1/provider/terraform_tgc.go +++ b/mmv1/provider/terraform_tgc.go @@ -58,8 +58,10 @@ func NewTerraformGoogleConversion(product *api.Product, versionName string, star templateFS: templateFS, } - for _, r := range t.Product.Objects { - r.ImportPath = ImportPathFromVersion(versionName) + if product != nil { + for _, r := range t.Product.Objects { + r.ImportPath = ImportPathFromVersion(versionName) + } } return t @@ -185,23 +187,7 @@ func (tgc *TerraformGoogleConversion) generateCaiIamResources(products []*api.Pr func (tgc TerraformGoogleConversion) CompileCommonFiles(outputFolder string, products []*api.Product, overridePath string) { log.Printf("Compiling common files for tgc.") - tgc.generateCaiIamResources(products) - tgc.NonDefinedTests = retrieveFullManifestOfNonDefinedTests(tgc.templateFS) - - files := retrieveFullListOfTestFiles(tgc.templateFS) - for _, file := range files { - tgc.Tests = append(tgc.Tests, strings.Split(file, ".")[0]) - } - tgc.Tests = slices.Compact(tgc.Tests) - - testSource := make(map[string]string) - for target, source := range retrieveTestSourceCodeWithLocation(tgc.templateFS, ".tmpl") { - target := strings.Replace(target, "go.tmpl", "go", 1) - testSource[target] = source - } - templateData := NewTemplateData(outputFolder, tgc.TargetVersionName, tgc.templateFS) - tgc.CompileFileList(outputFolder, testSource, *templateData, products) resourceConverters := map[string]string{ "converters/google/resources/resource_converters.go": "third_party/tgc/resource_converters.go.tmpl", @@ -220,7 +206,40 @@ func (tgc TerraformGoogleConversion) CompileCommonFiles(outputFolder string, pro "converters/google/resources/services/spanner/iam_spanner_database.go": "third_party/terraform/services/spanner/iam_spanner_database.go.tmpl", "converters/google/resources/services/spanner/iam_spanner_instance.go": "third_party/terraform/services/spanner/iam_spanner_instance.go.tmpl", } - tgc.CompileFileList(outputFolder, resourceConverters, *templateData, products) + + filteredFiles := make(map[string]string) + if tgc.Product != nil { + for target, source := range resourceConverters { + if strings.Contains(target, "/services/"+tgc.Product.ApiName+"/") { + filteredFiles[target] = source + } + } + tgc.CompileFileList(outputFolder, filteredFiles, *templateData, products) + } else { + // Shared compilation + tgc.generateCaiIamResources(products) + tgc.NonDefinedTests = retrieveFullManifestOfNonDefinedTests(tgc.templateFS) + + files := retrieveFullListOfTestFiles(tgc.templateFS) + for _, file := range files { + tgc.Tests = append(tgc.Tests, strings.Split(file, ".")[0]) + } + tgc.Tests = slices.Compact(tgc.Tests) + + testSource := make(map[string]string) + for target, source := range retrieveTestSourceCodeWithLocation(tgc.templateFS, ".tmpl") { + target := strings.Replace(target, "go.tmpl", "go", 1) + testSource[target] = source + } + tgc.CompileFileList(outputFolder, testSource, *templateData, products) + + for target, source := range resourceConverters { + if !strings.Contains(target, "/services/") { + filteredFiles[target] = source + } + } + tgc.CompileFileList(outputFolder, filteredFiles, *templateData, products) + } } func (tgc TerraformGoogleConversion) CompileFileList(outputFolder string, files map[string]string, fileTemplate TemplateData, products []*api.Product) { @@ -359,9 +378,6 @@ func (tgc TerraformGoogleConversion) CopyCommonFiles(outputFolder string, genera return } - tgc.CopyFileList(outputFolder, retrieveFullListOfTestTilesWithLocation(tgc.templateFS)) - tgc.CopyFileList(outputFolder, retrieveTestSourceCodeWithLocation(tgc.templateFS, ".go")) - resourceConverters := map[string]string{ "../caiasset/asset.go": "third_party/tgc/caiasset/asset.go", "converters/google/resources/cai/constants.go": "third_party/tgc/cai/constants.go", @@ -456,7 +472,25 @@ func (tgc TerraformGoogleConversion) CopyCommonFiles(outputFolder string, genera "tfplan2cai.go": "third_party/tgc/tfplan2cai.go", "tfplan_to_cai_test.go": "third_party/tgc/tfplan_to_cai_test.go", } - tgc.CopyFileList(outputFolder, resourceConverters) + + filteredFiles := make(map[string]string) + if tgc.Product != nil { + for target, source := range resourceConverters { + if strings.Contains(target, "/services/"+tgc.Product.ApiName+"/") { + filteredFiles[target] = source + } + } + } else { + // Shared files + tgc.CopyFileList(outputFolder, retrieveFullListOfTestTilesWithLocation(tgc.templateFS)) + tgc.CopyFileList(outputFolder, retrieveTestSourceCodeWithLocation(tgc.templateFS, ".go")) + for target, source := range resourceConverters { + if !strings.Contains(target, "/services/") { + filteredFiles[target] = source + } + } + } + tgc.CopyFileList(outputFolder, filteredFiles) } func (tgc TerraformGoogleConversion) CopyFileList(outputFolder string, files map[string]string) { diff --git a/mmv1/provider/terraform_tgc_cai2hcl.go b/mmv1/provider/terraform_tgc_cai2hcl.go index a31220f0de2a..7912466ff7e2 100644 --- a/mmv1/provider/terraform_tgc_cai2hcl.go +++ b/mmv1/provider/terraform_tgc_cai2hcl.go @@ -14,10 +14,13 @@ package provider import ( + "errors" "fmt" "io/fs" "log" "os" + "path/filepath" + "strings" "time" "github.com/GoogleCloudPlatform/magic-modules/mmv1/api" @@ -43,8 +46,10 @@ func NewCaiToTerraformConversion(product *api.Product, versionName string, start templateFS: templateFS, } - for _, r := range t.Product.Objects { - r.ImportPath = ImportPathFromVersion(versionName) + if product != nil { + for _, r := range t.Product.Objects { + r.ImportPath = ImportPathFromVersion(versionName) + } } return t @@ -66,7 +71,19 @@ func (cai2hcl CaiToTerraformConversion) CopyCommonFiles(outputFolder string, gen log.Println(fmt.Errorf("error creating output directory %v: %v", outputFolder, err)) } - if err := copy.Copy("third_party/cai2hcl", outputFolder); err != nil { - log.Println(fmt.Errorf("error copying directory %v: %v", outputFolder, err)) + if cai2hcl.Product != nil { + srcDir := filepath.Join("third_party/cai2hcl/services", cai2hcl.Product.ApiName) + dstDir := filepath.Join(outputFolder, "services", cai2hcl.Product.ApiName) + if err := copy.Copy(srcDir, dstDir); err != nil && !errors.Is(err, os.ErrNotExist) { + log.Println(fmt.Errorf("error copying service directory %v: %v", srcDir, err)) + } + } else { + if err := copy.Copy("third_party/cai2hcl", outputFolder, copy.Options{ + Skip: func(srcinfo os.FileInfo, src, dest string) (bool, error) { + return strings.Contains(src, "/services/"), nil + }, + }); err != nil { + log.Println(fmt.Errorf("error copying directory %v: %v", outputFolder, err)) + } } } diff --git a/mmv1/provider/terraform_tgc_next.go b/mmv1/provider/terraform_tgc_next.go index d4effa4d6e9f..c3181dcec152 100644 --- a/mmv1/provider/terraform_tgc_next.go +++ b/mmv1/provider/terraform_tgc_next.go @@ -75,9 +75,11 @@ func NewTerraformGoogleConversionNext(product *api.Product, versionName string, templateFS: templateFS, } - t.Product.ImportPath = ImportPathFromVersion(versionName) - for _, r := range t.Product.Objects { - r.ImportPath = ImportPathFromVersion(versionName) + if product != nil { + t.Product.ImportPath = ImportPathFromVersion(versionName) + for _, r := range t.Product.Objects { + r.ImportPath = ImportPathFromVersion(versionName) + } } return t @@ -180,8 +182,6 @@ func (tgc *TerraformGoogleConversionNext) GenerateProduct(outputFolder string) { } func (tgc TerraformGoogleConversionNext) CompileCommonFiles(outputFolder string, products []*api.Product, overridePath string) { - tgc.generateResourcesForVersion(products) - resourceConverters := map[string]string{ // common "pkg/transport/config.go": "third_party/terraform/transport/config.go.tmpl", @@ -206,7 +206,25 @@ func (tgc TerraformGoogleConversionNext) CompileCommonFiles(outputFolder string, } templateData := NewTemplateData(outputFolder, tgc.TargetVersionName, tgc.templateFS) - tgc.CompileFileList(outputFolder, resourceConverters, *templateData, products) + filteredFiles := make(map[string]string) + + if tgc.Product != nil { + for target, source := range resourceConverters { + if strings.Contains(target, "/services/"+tgc.Product.ApiName+"/") { + filteredFiles[target] = source + } + } + tgc.CompileFileList(outputFolder, filteredFiles, *templateData, products) + } else { + // Shared compilation + tgc.generateResourcesForVersion(products) + for target, source := range resourceConverters { + if !strings.Contains(target, "/services/") { + filteredFiles[target] = source + } + } + tgc.CompileFileList(outputFolder, filteredFiles, *templateData, products) + } } func (tgc TerraformGoogleConversionNext) CompileFileList(outputFolder string, files map[string]string, fileTemplate TemplateData, products []*api.Product) { @@ -249,10 +267,6 @@ func (tgc TerraformGoogleConversionNext) CopyCommonFiles(outputFolder string, ge log.Println(fmt.Errorf("error creating output directory %v: %v", outputFolder, err)) } - if err := copy.Copy("third_party/tgc_next", outputFolder); err != nil { - log.Println(fmt.Errorf("error copying directory %v: %v", outputFolder, err)) - } - resourceConverters := map[string]string{ // common "pkg/envvar/envvar_utils.go": "third_party/terraform/envvar/envvar_utils.go", @@ -284,7 +298,40 @@ func (tgc TerraformGoogleConversionNext) CopyCommonFiles(outputFolder string, ge "pkg/services/resourcemanagerv3/client.go": "third_party/terraform/services/resourcemanagerv3/client.go", "pkg/services/storage/client.go": "third_party/terraform/services/storage/client.go", } - tgc.CopyFileList(outputFolder, resourceConverters) + + filteredFiles := make(map[string]string) + + if tgc.Product != nil { + // Service-specific copying + for target, source := range resourceConverters { + if strings.Contains(target, "/services/"+tgc.Product.ApiName+"/") { + filteredFiles[target] = source + } + } + tgc.CopyFileList(outputFolder, filteredFiles) + + srcDir := filepath.Join("third_party/tgc_next/pkg/services", tgc.Product.ApiName) + dstDir := filepath.Join(outputFolder, "pkg/services", tgc.Product.ApiName) + if err := copy.Copy(srcDir, dstDir); err != nil && !errors.Is(err, os.ErrNotExist) { + log.Println(fmt.Errorf("error copying service directory %v: %v", srcDir, err)) + } + } else { + // Shared copying + if err := copy.Copy("third_party/tgc_next", outputFolder, copy.Options{ + Skip: func(srcinfo os.FileInfo, src, dest string) (bool, error) { + return strings.Contains(src, "pkg/services/"), nil + }, + }); err != nil { + log.Println(fmt.Errorf("error copying directory %v: %v", outputFolder, err)) + } + + for target, source := range resourceConverters { + if !strings.Contains(target, "/services/") { + filteredFiles[target] = source + } + } + tgc.CopyFileList(outputFolder, filteredFiles) + } } func (tgc TerraformGoogleConversionNext) CopyTfToCaiCommonFiles(outputFolder string) { diff --git a/mmv1/templates/terraform/constants/bigquery_analytics_hub_query_template.go.tmpl b/mmv1/templates/terraform/constants/bigquery_analytics_hub_query_template.go.tmpl new file mode 100644 index 000000000000..a41e7699a71e --- /dev/null +++ b/mmv1/templates/terraform/constants/bigquery_analytics_hub_query_template.go.tmpl @@ -0,0 +1,13 @@ +func resourceBigqueryAnalyticsHubQueryTemplateCustomDiffFunc(diff tpgresource.TerraformResourceDiff) error { + if diff.HasChange("submit") { + old, new := diff.GetChange("submit") + if !new.(bool) && old.(bool) { + return fmt.Errorf("Cannot change `submit` from true to false as unsubmitting is not supported.") + } + } + return nil +} + +func resourceBigqueryAnalyticsHubQueryTemplateCustomDiff(_ context.Context, diff *schema.ResourceDiff, meta interface{}) error { + return resourceBigqueryAnalyticsHubQueryTemplateCustomDiffFunc(diff) +} \ No newline at end of file diff --git a/mmv1/templates/terraform/constants/chronicle_rule.go.tmpl b/mmv1/templates/terraform/constants/chronicle_rule.go.tmpl new file mode 100644 index 000000000000..b3bd2c27cc4e --- /dev/null +++ b/mmv1/templates/terraform/constants/chronicle_rule.go.tmpl @@ -0,0 +1,8 @@ +// chronicleRuleTextDiffSuppress treats two YARA-L rule bodies as equal when +// they differ only by trailing newline characters. The Chronicle Rules API +// silently appends a trailing "\n" to every stored rule body, so a rule +// submitted as `}` is read back as `}\n`, which would otherwise show as a +// permanent diff on every plan (see hashicorp/terraform-provider-google#27881). +func chronicleRuleTextDiffSuppress(_, old, new string, _ *schema.ResourceData) bool { + return strings.TrimRight(old, "\n") == strings.TrimRight(new, "\n") +} diff --git a/mmv1/templates/terraform/constants/semantic_governance_policy_engine.go.tmpl b/mmv1/templates/terraform/constants/semantic_governance_policy_engine.go.tmpl new file mode 100644 index 000000000000..31c0632547a2 --- /dev/null +++ b/mmv1/templates/terraform/constants/semantic_governance_policy_engine.go.tmpl @@ -0,0 +1,37 @@ +// Helper functions for the SemanticGovernancePolicyEngine (SGPE) resource, +// referenced from the resource YAML's custom_code.constants key. + +// sgpePostReadDispatch runs after the standard Read has fetched the engine +// into `res`, but before the schema is flattened from it (per resource.go.tmpl +// ordering). The API returns 200 + INACTIVE (not 404) for a deprovisioned or +// uninitialized singleton, so this translates INACTIVE into "gone" by clearing +// the ID; every other state is left in state. A missing or non-string `state` +// is a backend contract break and returns an error. +// +// It reads `res["state"]` directly because the schema flatten has not yet +// populated d.Get("state") at this point, and accepts the minimal +// sgpeResourceID interface rather than *schema.ResourceData so unit tests can +// substitute a fake. +type sgpeResourceID interface { + Id() string + SetId(string) +} + +func sgpePostReadDispatch(d sgpeResourceID, res map[string]interface{}) error { + state, ok := res["state"].(string) + if !ok { + return fmt.Errorf("SGPE response is missing or has malformed state field; backend regression.") + } + // Only INACTIVE clears the ID (treat as deleted; next apply re-creates). + // Every other state — including FAILED and any unrecognized state a newer + // backend may add — is intentionally left in state. FAILED is a real + // terminal state (per the SGPE API a FAILED engine may be re-provisioned + // or deprovisioned), so we surface it via the Computed `state` attribute + // and let the user choose to destroy (deprovision) or re-apply + // (re-provision). + if state == "INACTIVE" { + log.Printf("[WARN] SemanticGovernancePolicyEngine %q is INACTIVE; treating as deleted, next apply will re-create", d.Id()) + d.SetId("") + } + return nil +} diff --git a/mmv1/templates/terraform/constants/storage_anywhere_cache.go.tmpl b/mmv1/templates/terraform/constants/storage_anywhere_cache.go.tmpl new file mode 100644 index 000000000000..a1907c17b391 --- /dev/null +++ b/mmv1/templates/terraform/constants/storage_anywhere_cache.go.tmpl @@ -0,0 +1,21 @@ +{{/* + The license inside this block applies to this file + Copyright 2026 Google Inc. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ -}} +// Suppresses diffs for Anywhere Cache admission policy where "admit-on-second-miss" +// is deprecated and falls back to "admit-on-first-miss". +func StorageAnywhereCacheAdmissionPolicyDiffSuppress(_, old, new string, _ *schema.ResourceData) bool { + if old == new { + return true + } + return (old == "admit-on-first-miss" && new == "admit-on-second-miss") || + (old == "admit-on-second-miss" && new == "admit-on-first-miss") +} diff --git a/mmv1/templates/terraform/custom_create/firestore_field.go.tmpl b/mmv1/templates/terraform/custom_create/firestore_field.go.tmpl new file mode 100644 index 000000000000..1bae4e1cb699 --- /dev/null +++ b/mmv1/templates/terraform/custom_create/firestore_field.go.tmpl @@ -0,0 +1,120 @@ +userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) +if err != nil { + return err +} + +obj := make(map[string]interface{}) +indexConfigProp, err := expandFirestoreFieldIndexConfig(d.Get("index_config"), d, config) +if err != nil { + return err +} else if v, ok := d.GetOkExists("index_config"); ok || !reflect.DeepEqual(v, indexConfigProp) { + obj["indexConfig"] = indexConfigProp +} +ttlConfigProp, err := expandFirestoreFieldTtlConfig(d.Get("ttl_config"), d, config) +if err != nil { + return err +} else if v, ok := d.GetOkExists("ttl_config"); ok || !reflect.DeepEqual(v, ttlConfigProp) { + obj["ttlConfig"] = ttlConfigProp +} + +obj, err = resourceFirestoreFieldEncoder(d, meta, obj) +if err != nil { + return err +} + +url, err := tpgresource.ReplaceVars(d, config, transport_tpg.BaseUrl(Product, config)+"projects/{{"{{"}}project{{"}}"}}/databases/{{"{{"}}database{{"}}"}}/collectionGroups/{{"{{"}}collection{{"}}"}}/fields/{{"{{"}}field{{"}}"}}") +if err != nil { + return err +} + +log.Printf("[DEBUG] Creating new Field: %#v", obj) +billingProject := "" + +project, err := tpgresource.GetProject(d, config) +if err != nil { + return fmt.Errorf("Error fetching project for Field: %s", err) +} +billingProject = project + +// err == nil indicates that the billing_project value was found +if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp +} + +headers := make(http.Header) +res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "PATCH", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: d.Timeout(schema.TimeoutCreate), + Headers: headers, + ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{transport_tpg.FirestoreField409RetryUnderlyingDataChanged}, +}) +if err != nil { + return fmt.Errorf("Error creating Field: %s", err) +} + +// Store the ID now +id, err := tpgresource.ReplaceVars(d, config, "{{"{{"}}name{{"}}"}}") +if err != nil { + return fmt.Errorf("Error constructing id: %s", err) +} +d.SetId(id) + +if d.Get("skip_wait").(bool) { + // If skip_wait, the LRO will not be complete so the response will not be populated. + // Extract the field name from the metadata field. + metadata, ok := res["metadata"].(map[string]interface{}) + if !ok { + return fmt.Errorf("Error constructing id from result.") + } + field, ok := metadata["field"].(string) + if !ok { + return fmt.Errorf("Error constructing id from result.") + } + if err := d.Set("name", flattenFirestoreFieldName(field, d, config)); err != nil { + return err + } +} else { + // Use the resource in the operation response to populate + // identity fields and d.Id() before read + var opRes map[string]interface{} + err = FirestoreOperationWaitTimeWithResponse( + config, res, &opRes, project, "Creating Field", userAgent, + d.Timeout(schema.TimeoutCreate)) + if err != nil { + // The resource didn't actually create + d.SetId("") + + return fmt.Errorf("Error waiting to create Field: %s", err) + } + + if err := d.Set("name", flattenFirestoreFieldName(opRes["name"], d, config)); err != nil { + return err + } +} + +// This may have caused the ID to update - update it if so. +id, err = tpgresource.ReplaceVars(d, config, "{{"{{"}}name{{"}}"}}") +if err != nil { + return fmt.Errorf("Error constructing id: %s", err) +} +d.SetId(id) + +log.Printf("[DEBUG] Finished creating Field %q: %#v", d.Id(), res) + +identity, err := d.Identity() +if err == nil && identity != nil { + if nameValue, ok := d.GetOk("name"); ok && nameValue.(string) != "" { + if err = identity.Set("name", nameValue.(string)); err != nil { + return fmt.Errorf("Error setting name: %s", err) + } + } +} else { + log.Printf("[DEBUG] (Create) identity not set: %s", err) +} + +return resourceFirestoreFieldRead(d, meta) diff --git a/mmv1/templates/terraform/custom_delete/bigquery_analytics_hub_query_template.go.tmpl b/mmv1/templates/terraform/custom_delete/bigquery_analytics_hub_query_template.go.tmpl new file mode 100644 index 000000000000..b85d090923d3 --- /dev/null +++ b/mmv1/templates/terraform/custom_delete/bigquery_analytics_hub_query_template.go.tmpl @@ -0,0 +1,37 @@ +deletionPolicy := d.Get("deletion_policy").(string) + +url, err := tpgresource.ReplaceVars(d, config, "{{"{{"}}BigqueryAnalyticsHubBasePath{{"}}"}}projects/{{"{{"}}project{{"}}"}}/locations/{{"{{"}}location{{"}}"}}/dataExchanges/{{"{{"}}data_exchange_id{{"}}"}}/queryTemplates/{{"{{"}}query_template_id{{"}}"}}") +if err != nil { + return err +} + +billingProject := "" +if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp +} + +var obj map[string]interface{} +log.Printf("[DEBUG] Deleting QueryTemplate %q", d.Id()) + +// Send the Delete Request +res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "DELETE", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: d.Timeout(schema.TimeoutDelete), +}) + +if err != nil { + if deletionPolicy == "DELETE_IF_DRAFTED" && transport_tpg.IsGoogleApiErrorWithCode(err, 400) && strings.Contains(err.Error(), "must be in DRAFTED state") { + log.Printf("[WARN] QueryTemplate %q is not in DRAFTED state. Abandoning resource due to deletion_policy 'DELETE_IF_DRAFTED'.", d.Id()) + d.SetId("") + return nil + } + return transport_tpg.HandleNotFoundError(err, d, "QueryTemplate") +} + +log.Printf("[DEBUG] Finished deleting QueryTemplate %q: %#v", d.Id(), res) +return nil \ No newline at end of file diff --git a/mmv1/templates/terraform/custom_flatten/eventarc_trigger_conditions_flatten.go.tmpl b/mmv1/templates/terraform/custom_flatten/eventarc_trigger_conditions_flatten.go.tmpl new file mode 100644 index 000000000000..764116702353 --- /dev/null +++ b/mmv1/templates/terraform/custom_flatten/eventarc_trigger_conditions_flatten.go.tmpl @@ -0,0 +1,46 @@ +{{/* + The license inside this block applies to this file + Copyright 2026 Google Inc. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ -}} +func flatten{{$.GetPrefix}}{{$.TitlelizeProperty}}(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return nil + } + + original, ok := v.(map[string]interface{}) + if !ok { + return v + } + + flatConditions := make(map[string]string) + for k, rawVal := range original { + if rawVal == nil { + continue + } + + if nestedMap, ok := rawVal.(map[string]interface{}); ok { + if message, exists := nestedMap["message"]; exists { + if msgStr, isStr := message.(string); isStr { + flatConditions[k] = msgStr + continue + } + } + } + + if valStr, ok := rawVal.(string); ok { + flatConditions[k] = valStr + } else { + flatConditions[k] = fmt.Sprintf("%v", rawVal) + } + } + + return flatConditions +} diff --git a/mmv1/templates/terraform/custom_flatten/privileged_access_manager_entitlement_approval_workflow.go.tmpl b/mmv1/templates/terraform/custom_flatten/privileged_access_manager_entitlement_approval_workflow.go.tmpl new file mode 100644 index 000000000000..84455013a548 --- /dev/null +++ b/mmv1/templates/terraform/custom_flatten/privileged_access_manager_entitlement_approval_workflow.go.tmpl @@ -0,0 +1,150 @@ +{{/* + The license inside this block applies to this file + Copyright 2026 Google Inc. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ -}} +func flattenPrivilegedAccessManagerEntitlementApprovalWorkflow(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["manual_approvals"] = + flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovals(original["manualApprovals"], d, config) + + // begin handwritten code (all other parts of this file are forked from generated code) + // solve for the following diff when no approval workflow is configured: + // + // - approval_workflow { + // - manual_approvals = null + // } + // + // the PAM API returns the field as: + // + // "approvalWorkflow": { + // "manualApprovals": {} + // }, + // + // for entitlements that were created without an approval workflow (for example via + // gcloud or the Cloud console). the outer `len(original) == 0` guard above does not + // fire because the `manualApprovals` key is present, and the inner flattener returns + // nil for the empty `manualApprovals` object. without this guard the resource ends + // up with a phantom `approval_workflow = [{manual_approvals = null}]` block in state, + // which cannot be reconciled with any user configuration (`manual_approvals` is + // Required, `manual_approvals.steps` has MinItems: 1, and `approval_workflow` is + // ForceNew), producing a permanent destroy+recreate diff. + // + // if any new sibling field is added under approvalWorkflow, this block must be updated + // to also check that the new field is nil before short-circuiting. + if transformed["manual_approvals"] == nil { + return nil + } + // end handwritten code + + return []interface{}{transformed} +} +func flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovals(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["require_approver_justification"] = + flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsRequireApproverJustification(original["requireApproverJustification"], d, config) + transformed["steps"] = + flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsSteps(original["steps"], d, config) + return []interface{}{transformed} +} +func flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsRequireApproverJustification(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsSteps(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return v + } + l := v.([]interface{}) + transformed := make([]interface{}, 0, len(l)) + for _, raw := range l { + original := raw.(map[string]interface{}) + if len(original) < 1 { + // Do not include empty json objects coming back from the api + continue + } + transformed = append(transformed, map[string]interface{}{ + "approvers": flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsStepsApprovers(original["approvers"], d, config), + "approvals_needed": flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsStepsApprovalsNeeded(original["approvalsNeeded"], d, config), + "approver_email_recipients": flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsStepsApproverEmailRecipients(original["approverEmailRecipients"], d, config), +{{- if ne $.ResourceMetadata.TargetVersionName "ga" }} + "id": flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsStepsId(original["id"], d, config), +{{- end }} + }) + } + return transformed +} +func flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsStepsApprovers(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return v + } + l := v.([]interface{}) + transformed := make([]interface{}, 0, len(l)) + for _, raw := range l { + original := raw.(map[string]interface{}) + if len(original) < 1 { + // Do not include empty json objects coming back from the api + continue + } + transformed = append(transformed, map[string]interface{}{ + "principals": flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsStepsApproversPrincipals(original["principals"], d, config), + }) + } + return transformed +} +func flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsStepsApproversPrincipals(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return v + } + return schema.NewSet(schema.HashString, v.([]interface{})) +} + +func flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsStepsApprovalsNeeded(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := tpgresource.StringToFixed64(strVal); err == nil { + return intVal + } + } + + // number values are represented as float64 + if floatVal, ok := v.(float64); ok { + intVal := int(floatVal) + return intVal + } + + return v // let terraform core handle it otherwise +} + +func flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsStepsApproverEmailRecipients(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return v + } + return schema.NewSet(schema.HashString, v.([]interface{})) +} +{{ if ne $.ResourceMetadata.TargetVersionName "ga" }} +func flattenPrivilegedAccessManagerEntitlementApprovalWorkflowManualApprovalsStepsId(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} +{{- end }} diff --git a/mmv1/templates/terraform/custom_update/biglake_iceberg_catalog_update.go.tmpl b/mmv1/templates/terraform/custom_update/biglake_iceberg_catalog_update.go.tmpl index 7423b49f88e0..51bd1d6942df 100644 --- a/mmv1/templates/terraform/custom_update/biglake_iceberg_catalog_update.go.tmpl +++ b/mmv1/templates/terraform/custom_update/biglake_iceberg_catalog_update.go.tmpl @@ -32,6 +32,20 @@ if err != nil { obj["restricted-locations-config"] = restrictedLocationsConfigProp } +descriptionProp, err := expandBiglakeIcebergIcebergCatalogDescription(d.Get("description"), d, config) +if err != nil { + return err +} else if v, ok := d.GetOkExists("description"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, descriptionProp)) { + obj["description"] = descriptionProp +} + +federatedCatalogOptionsProp, err := expandBiglakeIcebergIcebergCatalogFederatedCatalogOptions(d.Get("federated_catalog_options"), d, config) +if err != nil { + return err +} else if v, ok := d.GetOkExists("federated_catalog_options"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, federatedCatalogOptionsProp)) { + obj["federated-catalog-options"] = federatedCatalogOptionsProp +} + url, err := tpgresource.ReplaceVars(d, config, "{{"{{"}}BiglakeIcebergBasePath{{"}}"}}iceberg/v1/restcatalog/extensions/projects/{{"{{"}}project{{"}}"}}/catalogs/{{"{{"}}name{{"}}"}}") if err != nil { return err @@ -56,6 +70,28 @@ if d.HasChange("restricted_locations_config") { updateMask = append(updateMask, "restricted_locations_config") } +if d.HasChange("description") { + updateMask = append(updateMask, "description") +} + +if d.HasChange("federated_catalog_options") { + if d.HasChange("federated_catalog_options.0.secret_name") { + updateMask = append(updateMask, "federated_catalog_options.secret_name") + } + if d.HasChange("federated_catalog_options.0.service_directory_name") { + updateMask = append(updateMask, "federated_catalog_options.service_directory_name") + } + if d.HasChange("federated_catalog_options.0.refresh_options") { + updateMask = append(updateMask, "federated_catalog_options.refresh_options") + } + if d.HasChange("federated_catalog_options.0.glue_catalog_info.0.aws_role_arn") { + updateMask = append(updateMask, "federated_catalog_options.glue_catalog_info.aws_role_arn") + } + if d.HasChange("federated_catalog_options.0.unity_catalog_info.0.service_principal_application_id") { + updateMask = append(updateMask, "federated_catalog_options.unity_catalog_info.service_principal_application_id") + } +} + // updateMask is a URL parameter but not present in the schema, so ReplaceVars // won't set it url, err = transport_tpg.AddQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")}) diff --git a/mmv1/templates/terraform/custom_update/firestore_field.go.tmpl b/mmv1/templates/terraform/custom_update/firestore_field.go.tmpl new file mode 100644 index 000000000000..e3a37a82a114 --- /dev/null +++ b/mmv1/templates/terraform/custom_update/firestore_field.go.tmpl @@ -0,0 +1,102 @@ +userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) +if err != nil { + return err +} +identity, err := d.Identity() +if err == nil && identity != nil { + if nameValue, ok := d.GetOk("name"); ok && nameValue.(string) != "" { + if err = identity.Set("name", nameValue.(string)); err != nil { + return fmt.Errorf("Error setting name: %s", err) + } + } +} else { + log.Printf("[DEBUG] (Update) identity not set: %s", err) +} + +billingProject := "" + +project, err := tpgresource.GetProject(d, config) +if err != nil { + return fmt.Errorf("Error fetching project for Field: %s", err) +} +billingProject = project + +obj := make(map[string]interface{}) +indexConfigProp, err := expandFirestoreFieldIndexConfig(d.Get("index_config"), d, config) +if err != nil { + return err +} else if v, ok := d.GetOkExists("index_config"); ok || !reflect.DeepEqual(v, indexConfigProp) { + obj["indexConfig"] = indexConfigProp +} +ttlConfigProp, err := expandFirestoreFieldTtlConfig(d.Get("ttl_config"), d, config) +if err != nil { + return err +} else if v, ok := d.GetOkExists("ttl_config"); ok || !reflect.DeepEqual(v, ttlConfigProp) { + obj["ttlConfig"] = ttlConfigProp +} + +obj, err = resourceFirestoreFieldEncoder(d, meta, obj) +if err != nil { + return err +} + +url, err := tpgresource.ReplaceVars(d, config, transport_tpg.BaseUrl(Product, config)+"{{"{{"}}name{{"}}"}}") +if err != nil { + return err +} + +log.Printf("[DEBUG] Updating Field %q: %#v", d.Id(), obj) +headers := make(http.Header) +updateMask := []string{} + +if d.HasChange("index_config") { + updateMask = append(updateMask, "indexConfig") +} + +if d.HasChange("ttl_config") { + updateMask = append(updateMask, "ttlConfig") +} +// updateMask is a URL parameter but not present in the schema, so ReplaceVars +// won't set it +url, err = transport_tpg.AddQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")}) +if err != nil { + return err +} + +// err == nil indicates that the billing_project value was found +if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp +} + +// if updateMask is empty we are not updating anything so skip the post +if len(updateMask) > 0 { + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "PATCH", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: d.Timeout(schema.TimeoutUpdate), + Headers: headers, + ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{transport_tpg.FirestoreField409RetryUnderlyingDataChanged}, + }) + + if err != nil { + return fmt.Errorf("Error updating Field %q: %s", d.Id(), err) + } else { + log.Printf("[DEBUG] Finished updating Field %q: %#v", d.Id(), res) + } + + if !d.Get("skip_wait").(bool) { + err = FirestoreOperationWaitTime( + config, res, project, "Updating Field", userAgent, + d.Timeout(schema.TimeoutUpdate)) + + if err != nil { + return err + } + } +} + +return resourceFirestoreFieldRead(d, meta) diff --git a/mmv1/templates/terraform/examples/agent_registry_agent_basic.tf.tmpl b/mmv1/templates/terraform/examples/agent_registry_agent_basic.tf.tmpl new file mode 100644 index 000000000000..c4a811e78987 --- /dev/null +++ b/mmv1/templates/terraform/examples/agent_registry_agent_basic.tf.tmpl @@ -0,0 +1,43 @@ +resource "google_project" "project" { + project_id = "tf-test%{random_suffix}" + name = "tf-test%{random_suffix}" + org_id = "%{org_id}" + deletion_policy = "DELETE" +} + +# Needed for CI tests for permissions to propagate +resource "time_sleep" "wait_for_project_propagation" { + create_duration = "60s" + + depends_on = [google_project.project] +} + +resource "google_project_service" "iap_service" { + project = google_project.project.project_id + service = "iap.googleapis.com" + + depends_on = [time_sleep.wait_for_project_propagation] +} + +resource "google_project_service" "agentregistry_service" { + project = google_project.project.project_id + service = "agentregistry.googleapis.com" + + # Chain service resource calls to avoid cache misses during CI test replays + depends_on = [google_project_service.iap_service] +} + +# Needed for CI tests for resource to propagate +resource "time_sleep" "wait_for_agent_creation" { + create_duration = "60s" + + depends_on = [google_project_service.agentregistry_service] +} + +data "google_agent_registry_agent" "default" { + project = google_project.project.project_id + location = "global" + filter = "displayName:Workspace Agent" + + depends_on = [time_sleep.wait_for_agent_creation] +} \ No newline at end of file diff --git a/mmv1/templates/terraform/examples/agent_registry_endpoint_basic.tf.tmpl b/mmv1/templates/terraform/examples/agent_registry_endpoint_basic.tf.tmpl new file mode 100644 index 000000000000..9cbc15218a6c --- /dev/null +++ b/mmv1/templates/terraform/examples/agent_registry_endpoint_basic.tf.tmpl @@ -0,0 +1,55 @@ +resource "google_project" "project" { + project_id = "tf-test%{random_suffix}" + name = "tf-test%{random_suffix}" + org_id = "%{org_id}" + deletion_policy = "DELETE" +} + +# Needed for CI tests for permissions to propagate +resource "time_sleep" "wait_for_project_propagation" { + create_duration = "60s" + + depends_on = [google_project.project] +} + +resource "google_project_service" "iap_service" { + project = google_project.project.project_id + service = "iap.googleapis.com" + + depends_on = [time_sleep.wait_for_project_propagation] +} + +resource "google_project_service" "agentregistry_service" { + project = google_project.project.project_id + service = "agentregistry.googleapis.com" + + # Chain service resource calls to avoid cache misses during CI test replays + depends_on = [google_project_service.iap_service] +} + +resource "google_agent_registry_service" "endpoint_service" { + project = google_project.project.project_id + location = "us-central1" + service_id = "tf-test-endpoint" + description = "Test endpoint service" + display_name = "Test endpoint service" + + interfaces { + url = "https://www.google.com" + protocol_binding = "GRPC" + } + + endpoint_spec { + type = "NO_SPEC" + } + + depends_on = [google_project_service.agentregistry_service] +} + +data "google_agent_registry_endpoint" "default" { + project = google_project.project.project_id + location = "us-central1" + filter = "displayName:Test endpoint service" + + depends_on = [google_agent_registry_service.endpoint_service] +} \ No newline at end of file diff --git a/mmv1/templates/terraform/examples/agent_registry_mcp_server_basic.tf.tmpl b/mmv1/templates/terraform/examples/agent_registry_mcp_server_basic.tf.tmpl new file mode 100644 index 000000000000..fc30cdf7112f --- /dev/null +++ b/mmv1/templates/terraform/examples/agent_registry_mcp_server_basic.tf.tmpl @@ -0,0 +1,56 @@ +resource "google_project" "project" { + project_id = "tf-test%{random_suffix}" + name = "tf-test%{random_suffix}" + org_id = "%{org_id}" + deletion_policy = "DELETE" +} + +# Needed for CI tests for permissions to propagate +resource "time_sleep" "wait_for_project_propagation" { + create_duration = "60s" + + depends_on = [google_project.project] +} + +resource "google_project_service" "iap_service" { + project = google_project.project.project_id + service = "iap.googleapis.com" + + depends_on = [time_sleep.wait_for_project_propagation] +} + +resource "google_project_service" "agentregistry_service" { + project = google_project.project.project_id + service = "agentregistry.googleapis.com" + + # Chain service resource calls to avoid cache misses during CI test replays + depends_on = [google_project_service.iap_service] +} + +resource "google_agent_registry_service" "mcp_service" { + project = google_project.project.project_id + location = "us-central1" + service_id = "tf-test-mcp" + description = "Test MCP service" + display_name = "Test MCP service" + + interfaces { + url = "https://www.google.com" + protocol_binding = "JSONRPC" + } + + mcp_server_spec { + type = "TOOL_SPEC" + content = "{\"tools\":[]}" + } + + depends_on = [google_project_service.agentregistry_service] +} + +data "google_agent_registry_mcp_server" "default" { + project = google_project.project.project_id + location = "us-central1" + filter = "displayName:Test MCP service" + + depends_on = [google_agent_registry_service.mcp_service] +} \ No newline at end of file diff --git a/mmv1/templates/terraform/iam/example_config_body/iap_agent_registry_endpoint.tf.tmpl b/mmv1/templates/terraform/iam/example_config_body/iap_agent_registry_endpoint.tf.tmpl new file mode 100644 index 000000000000..61b6606c107a --- /dev/null +++ b/mmv1/templates/terraform/iam/example_config_body/iap_agent_registry_endpoint.tf.tmpl @@ -0,0 +1,3 @@ + project = "%{project}" + location = "us-central1" + endpoint_id = basename(google_agent_registry_service.default.registry_resource) diff --git a/mmv1/templates/terraform/post_create/bigquery_analytics_hub_query_template.go.tmpl b/mmv1/templates/terraform/post_create/bigquery_analytics_hub_query_template.go.tmpl new file mode 100644 index 000000000000..d22139ba046e --- /dev/null +++ b/mmv1/templates/terraform/post_create/bigquery_analytics_hub_query_template.go.tmpl @@ -0,0 +1,28 @@ +// After successful creation, check and perform submit if requested +if d.Get("submit").(bool) { + log.Printf("[DEBUG] Attempting to submit BigQuery Analytics Hub QueryTemplate %q (ID: %s)", d.Get("name").(string), d.Id()) + + submitUrl, err := tpgresource.ReplaceVars(d, config, "{{"{{"}}BigqueryAnalyticsHubBasePath{{"}}"}}projects/{{"{{"}}project{{"}}"}}/locations/{{"{{"}}location{{"}}"}}/dataExchanges/{{"{{"}}data_exchange_id{{"}}"}}/queryTemplates/{{"{{"}}query_template_id{{"}}"}}:submit") + if err != nil { + return fmt.Errorf("Error constructing submit URL for QueryTemplate %q: %s", d.Id(), err) + } + + submitBodyMap := map[string]interface{}{} + submitHeaders := make(http.Header) + submitHeaders.Set("Content-Type", "application/json") + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "POST", + Project: billingProject, + RawURL: submitUrl, + UserAgent: userAgent, + Headers: submitHeaders, + Body: submitBodyMap, + }) + + if err != nil { + return fmt.Errorf("Error submitting QueryTemplate %q: %s", d.Id(), err) + } + log.Printf("[DEBUG] Successfully submitted QueryTemplate %q", d.Id()) +} \ No newline at end of file diff --git a/mmv1/templates/terraform/post_read/vertex_ai_semantic_governance_policy_engine.go.tmpl b/mmv1/templates/terraform/post_read/vertex_ai_semantic_governance_policy_engine.go.tmpl new file mode 100644 index 000000000000..1385565c7b4c --- /dev/null +++ b/mmv1/templates/terraform/post_read/vertex_ai_semantic_governance_policy_engine.go.tmpl @@ -0,0 +1,6 @@ +// SemanticGovernancePolicyEngine post-Read state dispatch. Logic lives in +// sgpePostReadDispatch (constants template) so it can be unit-tested directly +// via the sgpeResourceID interface (see the resource's internal test). +if err := sgpePostReadDispatch(d, res); err != nil { + return err +} diff --git a/mmv1/templates/terraform/post_update/bigquery_analytics_hub_query_template.go.tmpl b/mmv1/templates/terraform/post_update/bigquery_analytics_hub_query_template.go.tmpl new file mode 100644 index 000000000000..a26e9715ba5b --- /dev/null +++ b/mmv1/templates/terraform/post_update/bigquery_analytics_hub_query_template.go.tmpl @@ -0,0 +1,29 @@ +// After successful update, check and perform submit if requested +if d.Get("submit").(bool) { + // If the update succeeded, we can assume the state is DRAFTED + log.Printf("[DEBUG] Attempting to submit BigQuery Analytics Hub QueryTemplate %q (ID: %s)", d.Get("name").(string), d.Id()) + + submitUrl, err := tpgresource.ReplaceVars(d, config, "{{"{{"}}BigqueryAnalyticsHubBasePath{{"}}"}}projects/{{"{{"}}project{{"}}"}}/locations/{{"{{"}}location{{"}}"}}/dataExchanges/{{"{{"}}data_exchange_id{{"}}"}}/queryTemplates/{{"{{"}}query_template_id{{"}}"}}:submit") + if err != nil { + return fmt.Errorf("Error constructing submit URL for QueryTemplate %q: %s", d.Id(), err) + } + + submitBodyMap := map[string]interface{}{} + submitHeaders := make(http.Header) + submitHeaders.Set("Content-Type", "application/json") + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "POST", + Project: billingProject, + RawURL: submitUrl, + UserAgent: userAgent, + Headers: submitHeaders, + Body: submitBodyMap, + }) + + if err != nil { + return fmt.Errorf("Error submitting QueryTemplate %q: %s", d.Id(), err) + } + log.Printf("[DEBUG] Successfully submitted QueryTemplate %q", d.Id()) +} \ No newline at end of file diff --git a/mmv1/templates/terraform/pre_update/bigtable_app_profile.go.tmpl b/mmv1/templates/terraform/pre_update/bigtable_app_profile.go.tmpl index 307208274cb2..6f325ef59628 100644 --- a/mmv1/templates/terraform/pre_update/bigtable_app_profile.go.tmpl +++ b/mmv1/templates/terraform/pre_update/bigtable_app_profile.go.tmpl @@ -11,7 +11,7 @@ limitations under the License. */}} -if d.HasChange("multi_cluster_routing_cluster_ids") && !tpgresource.StringInSlice(updateMask, "multiClusterRoutingUseAny") { +if (d.HasChange("multi_cluster_routing_cluster_ids") || d.HasChange("row_affinity")) && !tpgresource.StringInSlice(updateMask, "multiClusterRoutingUseAny") { updateMask = append(updateMask, "multiClusterRoutingUseAny") } diff --git a/mmv1/templates/terraform/pre_update/datastream_connection_profile.go.tmpl b/mmv1/templates/terraform/pre_update/datastream_connection_profile.go.tmpl index e9d50eb50034..62f0723ae18e 100644 --- a/mmv1/templates/terraform/pre_update/datastream_connection_profile.go.tmpl +++ b/mmv1/templates/terraform/pre_update/datastream_connection_profile.go.tmpl @@ -24,6 +24,9 @@ if d.HasChange("mongodb_profile.0.replica_set") { if d.HasChange("mongodb_profile.0.host_addresses") { updateMask = append(updateMask, "mongodbProfile.hostAddresses") } +if d.HasChange("mongodb_profile.0.additional_options") { + updateMask = append(updateMask, "mongodbProfile.additionalOptions") +} // Override the previous setting of updateMask to include state. // updateMask is a URL parameter but not present in the schema, so ReplaceVars diff --git a/mmv1/templates/terraform/resource.go.tmpl b/mmv1/templates/terraform/resource.go.tmpl index 1221acd08e0f..d38c9681e78c 100644 --- a/mmv1/templates/terraform/resource.go.tmpl +++ b/mmv1/templates/terraform/resource.go.tmpl @@ -119,11 +119,11 @@ func Resource{{ $.ResourceName -}}() *schema.Resource { {{- end}} Timeouts: &schema.ResourceTimeout { - Create: schema.DefaultTimeout({{ $.Timeouts.InsertMinutes -}} * time.Minute), + Create: schema.DefaultTimeout({{ $.GetTimeouts.InsertMinutes -}} * time.Minute), {{- if or (or $.Updatable $.RootLabels) $.VirtualFields }} - Update: schema.DefaultTimeout({{ $.Timeouts.UpdateMinutes -}} * time.Minute), + Update: schema.DefaultTimeout({{ $.GetTimeouts.UpdateMinutes -}} * time.Minute), {{- end}} - Delete: schema.DefaultTimeout({{ $.Timeouts.DeleteMinutes -}} * time.Minute), + Delete: schema.DefaultTimeout({{ $.GetTimeouts.DeleteMinutes -}} * time.Minute), }, {{ if $.SchemaVersion }} SchemaVersion: {{ $.SchemaVersion -}}, diff --git a/mmv1/templates/terraform/resource.html.markdown.tmpl b/mmv1/templates/terraform/resource.html.markdown.tmpl index 318f4cdc2ab0..4a02370957c5 100644 --- a/mmv1/templates/terraform/resource.html.markdown.tmpl +++ b/mmv1/templates/terraform/resource.html.markdown.tmpl @@ -161,11 +161,11 @@ In addition to the arguments listed above, the following computed attributes are This resource provides the following [Timeouts](https://developer.hashicorp.com/terraform/plugin/sdkv2/resources/retries-and-customizable-timeouts) configuration options: -- `create` - Default is {{$.Timeouts.InsertMinutes}} minutes. +- `create` - Default is {{$.GetTimeouts.InsertMinutes}} minutes. {{- if or $.Updatable $.RootLabels }} -- `update` - Default is {{$.Timeouts.UpdateMinutes}} minutes. +- `update` - Default is {{$.GetTimeouts.UpdateMinutes}} minutes. {{- end }} -- `delete` - Default is {{$.Timeouts.DeleteMinutes}} minutes. +- `delete` - Default is {{$.GetTimeouts.DeleteMinutes}} minutes. {{ if $.ProductMetadata.Version.RepEnabled }} ## Regional Endpoint Policies diff --git a/mmv1/templates/terraform/resource_fw.go.tmpl b/mmv1/templates/terraform/resource_fw.go.tmpl index e24c43f4d5f0..9841feccbf0c 100644 --- a/mmv1/templates/terraform/resource_fw.go.tmpl +++ b/mmv1/templates/terraform/resource_fw.go.tmpl @@ -270,7 +270,7 @@ func (r *{{$.ResourceName}}FWResource) Create(ctx context.Context, req resource. obj["{{ $prop.ApiName -}}"] = {{ $prop.ApiName -}}Prop {{- end }} - createTimeout, diags := data.Timeouts.Create(ctx, {{ $.Timeouts.InsertMinutes }}*time.Minute) + createTimeout, diags := data.Timeouts.Create(ctx, {{ $.GetTimeouts.InsertMinutes }}*time.Minute) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return @@ -487,7 +487,7 @@ func (r *{{$.ResourceName}}FWResource) Update(ctx context.Context, req resource. } {{- end }} - updateTimeout, diags := plan.Timeouts.Update(ctx, {{ $.Timeouts.UpdateMinutes }}*time.Minute) + updateTimeout, diags := plan.Timeouts.Update(ctx, {{ $.GetTimeouts.UpdateMinutes }}*time.Minute) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return @@ -629,7 +629,7 @@ func (r *{{$.ResourceName}}FWResource) Delete(ctx context.Context, req resource. obj := make(map[string]interface{}) - deleteTimeout, diags := data.Timeouts.Delete(ctx, {{ $.Timeouts.DeleteMinutes }}*time.Minute) + deleteTimeout, diags := data.Timeouts.Delete(ctx, {{ $.GetTimeouts.DeleteMinutes }}*time.Minute) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return diff --git a/mmv1/templates/terraform/samples/base_configs/query_test_file.go.tmpl b/mmv1/templates/terraform/samples/base_configs/query_test_file.go.tmpl index 0ce1857eca9f..e5534f0d83ee 100644 --- a/mmv1/templates/terraform/samples/base_configs/query_test_file.go.tmpl +++ b/mmv1/templates/terraform/samples/base_configs/query_test_file.go.tmpl @@ -63,6 +63,7 @@ func TestAcc{{ $.ResourceName }}ListQuery_generated(t *testing.T) { {{- end }} {{- range $scope := $.ListScopeProperties }} {{- $n := underscore $scope.Name }} + {{- if not (index $step.TestContextVars $n) }} {{- if or (eq $n "region") (eq $n "location") }} "{{$n}}": envvar.GetTestRegionFromEnv(), {{- else if eq $n "zone" }} @@ -72,6 +73,7 @@ func TestAcc{{ $.ResourceName }}ListQuery_generated(t *testing.T) { "project": envvar.GetTestProjectFromEnv(), {{- end }} {{- end }} + {{- end }} {{- end }} "random_suffix": randomSuffix, } diff --git a/mmv1/templates/terraform/samples/services/appengine/app_engine_firewall_rule_basic.tf.tmpl b/mmv1/templates/terraform/samples/services/appengine/app_engine_firewall_rule_basic.tf.tmpl index fda913e535f2..79c3e9815581 100644 --- a/mmv1/templates/terraform/samples/services/appengine/app_engine_firewall_rule_basic.tf.tmpl +++ b/mmv1/templates/terraform/samples/services/appengine/app_engine_firewall_rule_basic.tf.tmpl @@ -11,9 +11,18 @@ resource "google_app_engine_application" "app" { location_id = "us-central" } +# Wait for 5 minutes seconds to ensure IAM permission propagation completes +resource "time_sleep" "wait_300_seconds" { + depends_on = [google_app_engine_application.app] + create_duration = "300s" +} + + resource "google_app_engine_firewall_rule" "rule" { project = google_app_engine_application.app.project priority = 1000 action = "ALLOW" source_range = "*" + + depends_on = [time_sleep.wait_300_seconds] } diff --git a/mmv1/templates/terraform/samples/services/appengine/app_engine_standard_app_version_bundled_services.tf.tmpl b/mmv1/templates/terraform/samples/services/appengine/app_engine_standard_app_version_bundled_services.tf.tmpl new file mode 100644 index 000000000000..3af2dc69e1e9 --- /dev/null +++ b/mmv1/templates/terraform/samples/services/appengine/app_engine_standard_app_version_bundled_services.tf.tmpl @@ -0,0 +1,65 @@ +resource "google_service_account" "service_account" { + account_id = "{{index $.ResourceIdVars "sa_email"}}" + display_name = "Test Service Account for GAE" +} + +resource "google_project_iam_member" "gae_api" { + project = google_service_account.service_account.project + role = "roles/compute.networkUser" + member = "serviceAccount:${google_service_account.service_account.email}" +} + +resource "google_project_iam_member" "storage_viewer" { + project = google_service_account.service_account.project + role = "roles/storage.objectViewer" + member = "serviceAccount:${google_service_account.service_account.email}" +} + +resource "google_storage_bucket" "bucket" { + name = "{{index $.ResourceIdVars "bucket_name"}}" + location = "US" +} + +resource "google_storage_bucket_object" "requirements" { + name = "requirements.txt" + bucket = google_storage_bucket.bucket.name + source = "./test-fixtures/hello-world-flask/requirements.txt" +} + +resource "google_storage_bucket_object" "main" { + name = "main.py" + bucket = google_storage_bucket.bucket.name + source = "./test-fixtures/hello-world-flask/main.py" +} + +resource "google_app_engine_standard_app_version" "{{$.PrimaryResourceId}}" { + version_id = "v1" + service = "{{index $.ResourceIdVars "service_id"}}" + runtime = "python310" + + deployment { + files { + name = "main.py" + source_url = "https://storage.googleapis.com/${google_storage_bucket.bucket.name}/${google_storage_bucket_object.main.name}" + } + files { + name = "requirements.txt" + source_url = "https://storage.googleapis.com/${google_storage_bucket.bucket.name}/${google_storage_bucket_object.requirements.name}" + } + } + + entrypoint { + shell = "gunicorn -b :$PORT main:app" + } + +# Testing the app_engine_bundled_services field + app_engine_bundled_services = ["BUNDLED_SERVICE_TYPE_MAIL", "BUNDLED_SERVICE_TYPE_DATASTORE_V3"] + + delete_service_on_destroy = true + service_account = google_service_account.service_account.email + + depends_on = [ + google_project_iam_member.gae_api, + google_project_iam_member.storage_viewer, + ] +} diff --git a/mmv1/templates/terraform/samples/services/biglakeiceberg/biglake_iceberg_catalog_federated_glue.tf.tmpl b/mmv1/templates/terraform/samples/services/biglakeiceberg/biglake_iceberg_catalog_federated_glue.tf.tmpl new file mode 100644 index 000000000000..ed8d7d0a6f79 --- /dev/null +++ b/mmv1/templates/terraform/samples/services/biglakeiceberg/biglake_iceberg_catalog_federated_glue.tf.tmpl @@ -0,0 +1,18 @@ +resource "google_biglake_iceberg_catalog" "{{$.PrimaryResourceId}}" { + catalog_type = "CATALOG_TYPE_FEDERATED" + name = "{{index $.ResourceIdVars "name"}}" + primary_location = "us-central1" + + federated_catalog_options { + glue_catalog_info { + aws_region = "us-east-1" + aws_role_arn = "arn:aws:iam::111222333444:role/my-glue-role" + warehouse = "111222333444:s3tablescatalog/example" + } + refresh_options { + refresh_schedule { + refresh_interval = "300s" + } + } + } +} diff --git a/mmv1/templates/terraform/samples/services/biglakeiceberg/biglake_iceberg_catalog_federated_unity.tf.tmpl b/mmv1/templates/terraform/samples/services/biglakeiceberg/biglake_iceberg_catalog_federated_unity.tf.tmpl new file mode 100644 index 000000000000..95dbb4ca812b --- /dev/null +++ b/mmv1/templates/terraform/samples/services/biglakeiceberg/biglake_iceberg_catalog_federated_unity.tf.tmpl @@ -0,0 +1,18 @@ +resource "google_biglake_iceberg_catalog" "{{$.PrimaryResourceId}}" { + catalog_type = "CATALOG_TYPE_FEDERATED" + name = "{{index $.ResourceIdVars "name"}}" + primary_location = "us-central1" + + federated_catalog_options { + unity_catalog_info { + catalog_name = "my_catalog" + instance_name = "1.1.gcp.databricks.com" + service_principal_application_id = "b3204274-6556-4d40-ad18-556f91659745" + } + refresh_options { + refresh_schedule { + refresh_interval = "300s" + } + } + } +} diff --git a/mmv1/templates/terraform/samples/services/bigqueryanalyticshub/bigquery_analyticshub_querytemplate_basic.tf.tmpl b/mmv1/templates/terraform/samples/services/bigqueryanalyticshub/bigquery_analyticshub_querytemplate_basic.tf.tmpl new file mode 100644 index 000000000000..110fb2946971 --- /dev/null +++ b/mmv1/templates/terraform/samples/services/bigqueryanalyticshub/bigquery_analyticshub_querytemplate_basic.tf.tmpl @@ -0,0 +1,30 @@ +data "google_client_openid_userinfo" "me" { + provider = google-beta +} + +resource "google_bigquery_analytics_hub_data_exchange" "{{$.PrimaryResourceId}}" { + provider = google-beta + display_name = "My Audience Data Exchange" + data_exchange_id = "{{index $.ResourceIdVars "data_exchange_id"}}" + description = "{{index $.Vars "desc"}}" + location = "us" + sharing_environment_config { + dcr_exchange_config {} + } +} + +resource "google_bigquery_analytics_hub_query_template" "{{$.PrimaryResourceId}}" { + provider = google-beta + location = "us" + data_exchange_id = google_bigquery_analytics_hub_data_exchange.{{$.PrimaryResourceId}}.data_exchange_id + query_template_id = "{{index $.ResourceIdVars "query_template_id"}}" + display_name = "{{index $.ResourceIdVars "query_template_id"}}" + description = "{{index $.Vars "desc"}}" + primary_contact = data.google_client_openid_userinfo.me.email + documentation = "This TVF takes a table t1 as input and returns all columns. Useful for basic data pass-through." + routine { + routine_type="TABLE_VALUED_FUNCTION" + definition_body="{{index $.ResourceIdVars "query_template_id"}}() as (select * from {{index $.Vars "table_name"}})" + } + submit={{index $.Vars "submit"}} +} diff --git a/mmv1/templates/terraform/samples/services/chronicle/chronicle_data_export_basic.tf.tmpl b/mmv1/templates/terraform/samples/services/chronicle/chronicle_data_export_basic.tf.tmpl new file mode 100644 index 000000000000..feab2679588e --- /dev/null +++ b/mmv1/templates/terraform/samples/services/chronicle/chronicle_data_export_basic.tf.tmpl @@ -0,0 +1,18 @@ +data "google_project" "project" { + project_id = "{{index $.TestEnvVars "project_name"}}" +} + +resource "google_storage_bucket" "test_bucket" { + name = "chronicle-test-bucket-{{index $.TestEnvVars "project_name"}}-%{random_suffix}" + project = "{{index $.TestEnvVars "project_name"}}" + location = "{{index $.Vars "location"}}" + force_destroy = true +} + +resource "google_chronicle_data_export" "{{$.PrimaryResourceId}}" { + location = "{{index $.Vars "location"}}" + instance = "{{index $.TestEnvVars "chronicle_id"}}" + gcs_bucket = "projects/${data.google_project.project.number}/buckets/${google_storage_bucket.test_bucket.name}" + start_time = "{{index $.Vars "start_time"}}" + end_time = "{{index $.Vars "end_time"}}" +} diff --git a/mmv1/templates/terraform/samples/services/chronicle/chronicle_data_export_full.tf.tmpl b/mmv1/templates/terraform/samples/services/chronicle/chronicle_data_export_full.tf.tmpl new file mode 100644 index 000000000000..26b89e3aedd5 --- /dev/null +++ b/mmv1/templates/terraform/samples/services/chronicle/chronicle_data_export_full.tf.tmpl @@ -0,0 +1,26 @@ +data "google_project" "project" { + project_id = "{{index $.TestEnvVars "project_name"}}" +} + +resource "google_storage_bucket" "test_bucket" { + name = "chronicle-test-bucket-{{index $.TestEnvVars "project_name"}}-%{random_suffix}" + project = "{{index $.TestEnvVars "project_name"}}" + location = "{{index $.Vars "location"}}" + force_destroy = true +} + +resource "google_chronicle_data_export" "{{$.PrimaryResourceId}}" { + location = "{{index $.Vars "location"}}" + instance = "{{index $.TestEnvVars "chronicle_id"}}" + gcs_bucket = "projects/${data.google_project.project.number}/buckets/${google_storage_bucket.test_bucket.name}" + start_time = "{{index $.Vars "start_time"}}" + end_time = "{{index $.Vars "end_time"}}" + include_log_types = ["projects/{{index $.TestEnvVars "project_name"}}/locations/{{index $.Vars "location"}}/instances/{{index $.TestEnvVars "chronicle_id"}}/logTypes/GCP_CLOUDAUDIT"] + + ingestion_labels { + key = "key1" + value = "val1" + } + + namespaces = ["my-namespace"] +} diff --git a/mmv1/templates/terraform/samples/services/chronicle/chronicle_environment_basic.tf.tmpl b/mmv1/templates/terraform/samples/services/chronicle/chronicle_environment_basic.tf.tmpl index efc4481ad2bc..3644495e6d85 100644 --- a/mmv1/templates/terraform/samples/services/chronicle/chronicle_environment_basic.tf.tmpl +++ b/mmv1/templates/terraform/samples/services/chronicle/chronicle_environment_basic.tf.tmpl @@ -1,5 +1,4 @@ resource "google_chronicle_environment" "{{$.PrimaryResourceId}}" { - provider = google-beta location = "us" instance = "{{index $.TestEnvVars "chronicle_id"}}" diff --git a/mmv1/templates/terraform/samples/services/chronicle/chronicle_environment_full.tf.tmpl b/mmv1/templates/terraform/samples/services/chronicle/chronicle_environment_full.tf.tmpl index 01f21afc73ff..042f81df80b7 100644 --- a/mmv1/templates/terraform/samples/services/chronicle/chronicle_environment_full.tf.tmpl +++ b/mmv1/templates/terraform/samples/services/chronicle/chronicle_environment_full.tf.tmpl @@ -1,5 +1,4 @@ resource "google_chronicle_environment" "{{$.PrimaryResourceId}}" { - provider = google-beta location = "us" instance = "{{index $.TestEnvVars "chronicle_id"}}" diff --git a/mmv1/templates/terraform/samples/services/chronicle/chronicle_environmentgroup_basic.tf.tmpl b/mmv1/templates/terraform/samples/services/chronicle/chronicle_environmentgroup_basic.tf.tmpl new file mode 100644 index 000000000000..6dd8fb7354b6 --- /dev/null +++ b/mmv1/templates/terraform/samples/services/chronicle/chronicle_environmentgroup_basic.tf.tmpl @@ -0,0 +1,11 @@ +resource "google_chronicle_environment_group" "{{$.PrimaryResourceId}}" { + provider = google-beta + location = "us" + instance = "{{index $.TestEnvVars "chronicle_id"}}" + + display_name = "myEnvironmentGroup" + description = "My Environment Description" + environments_ids = [ + "Default Environment" + ] +} diff --git a/mmv1/templates/terraform/samples/services/chronicle/chronicle_environmentgroup_full.tf.tmpl b/mmv1/templates/terraform/samples/services/chronicle/chronicle_environmentgroup_full.tf.tmpl new file mode 100644 index 000000000000..52d15a61d7a7 --- /dev/null +++ b/mmv1/templates/terraform/samples/services/chronicle/chronicle_environmentgroup_full.tf.tmpl @@ -0,0 +1,11 @@ +resource "google_chronicle_environment_group" "{{$.PrimaryResourceId}}" { + provider = google-beta + location = "us" + instance = "{{index $.TestEnvVars "chronicle_id"}}" + + display_name = "myEnvironmentGroup1" + description = "My Environment Description1" + environments_ids = [ + "Default Environment" + ] +} diff --git a/mmv1/templates/terraform/samples/services/chronicle/chronicle_findings_refinement_deployment_full.tf.tmpl b/mmv1/templates/terraform/samples/services/chronicle/chronicle_findings_refinement_deployment_full.tf.tmpl index ab0128e8bec3..4c162090bca8 100644 --- a/mmv1/templates/terraform/samples/services/chronicle/chronicle_findings_refinement_deployment_full.tf.tmpl +++ b/mmv1/templates/terraform/samples/services/chronicle/chronicle_findings_refinement_deployment_full.tf.tmpl @@ -1,5 +1,4 @@ resource "google_chronicle_findings_refinement" "my-findings-refinement" { - provider = google-beta location = "us" instance = "{{index $.TestEnvVars "chronicle_id"}}" display_name = "{{index $.ResourceIdVars "display_name"}}" @@ -13,7 +12,6 @@ resource "google_chronicle_findings_refinement" "my-findings-refinement" { } resource "google_chronicle_findings_refinement_deployment" "{{$.PrimaryResourceId}}" { - provider = google-beta location = "us" instance = "{{index $.TestEnvVars "chronicle_id"}}" findings_refinement = element(split("/", resource.google_chronicle_findings_refinement.my-findings-refinement.name), length(split("/", resource.google_chronicle_findings_refinement.my-findings-refinement.name)) - 1) diff --git a/mmv1/templates/terraform/samples/services/chronicle/chronicle_findings_refinement_deployment_update.tf.tmpl b/mmv1/templates/terraform/samples/services/chronicle/chronicle_findings_refinement_deployment_update.tf.tmpl index ab0128e8bec3..4c162090bca8 100644 --- a/mmv1/templates/terraform/samples/services/chronicle/chronicle_findings_refinement_deployment_update.tf.tmpl +++ b/mmv1/templates/terraform/samples/services/chronicle/chronicle_findings_refinement_deployment_update.tf.tmpl @@ -1,5 +1,4 @@ resource "google_chronicle_findings_refinement" "my-findings-refinement" { - provider = google-beta location = "us" instance = "{{index $.TestEnvVars "chronicle_id"}}" display_name = "{{index $.ResourceIdVars "display_name"}}" @@ -13,7 +12,6 @@ resource "google_chronicle_findings_refinement" "my-findings-refinement" { } resource "google_chronicle_findings_refinement_deployment" "{{$.PrimaryResourceId}}" { - provider = google-beta location = "us" instance = "{{index $.TestEnvVars "chronicle_id"}}" findings_refinement = element(split("/", resource.google_chronicle_findings_refinement.my-findings-refinement.name), length(split("/", resource.google_chronicle_findings_refinement.my-findings-refinement.name)) - 1) diff --git a/mmv1/templates/terraform/samples/services/chronicle/chronicle_soardomain_basic.tf.tmpl b/mmv1/templates/terraform/samples/services/chronicle/chronicle_soardomain_basic.tf.tmpl new file mode 100644 index 000000000000..e2c3afa3190b --- /dev/null +++ b/mmv1/templates/terraform/samples/services/chronicle/chronicle_soardomain_basic.tf.tmpl @@ -0,0 +1,8 @@ +resource "google_chronicle_soar_domain" "{{$.PrimaryResourceId}}" { + provider = google-beta + location = "us" + instance = "{{index $.TestEnvVars "chronicle_id"}}" + + display_name = "test.com" + environments_json = "[\"Default Environment\"]" +} diff --git a/mmv1/templates/terraform/samples/services/chronicle/chronicle_soardomain_full.tf.tmpl b/mmv1/templates/terraform/samples/services/chronicle/chronicle_soardomain_full.tf.tmpl new file mode 100644 index 000000000000..34e07c139aaf --- /dev/null +++ b/mmv1/templates/terraform/samples/services/chronicle/chronicle_soardomain_full.tf.tmpl @@ -0,0 +1,8 @@ +resource "google_chronicle_soar_domain" "{{$.PrimaryResourceId}}" { + provider = google-beta + location = "us" + instance = "{{index $.TestEnvVars "chronicle_id"}}" + + display_name = "test1.com" + environments_json = "[\"*\"]" +} diff --git a/mmv1/templates/terraform/samples/services/compute/compute_region_network_firewall_policy_with_rules_full.tf.tmpl b/mmv1/templates/terraform/samples/services/compute/compute_region_network_firewall_policy_with_rules_full.tf.tmpl index deb1c4d4d3c7..1156196ad002 100644 --- a/mmv1/templates/terraform/samples/services/compute/compute_region_network_firewall_policy_with_rules_full.tf.tmpl +++ b/mmv1/templates/terraform/samples/services/compute/compute_region_network_firewall_policy_with_rules_full.tf.tmpl @@ -1,6 +1,71 @@ data "google_project" "project" { } +resource "google_compute_network" "target_forwarding_rule" { + name = "tf-test-network-%{random_suffix}" + auto_create_subnetworks = false +} + +resource "google_compute_subnetwork" "target_forwarding_rule_proxy_subnetwork" { + name = "tf-test-proxy-subnetwork-%{random_suffix}" + region = "us-west2" + network = google_compute_network.target_forwarding_rule.id + ip_cidr_range = "10.20.0.0/24" + purpose = "REGIONAL_MANAGED_PROXY" + role = "ACTIVE" +} + +resource "google_compute_subnetwork" "target_forwarding_rule_default_subnetwork" { + name = "tf-test-default-subnetwork-%{random_suffix}" + region = "us-west2" + network = google_compute_network.target_forwarding_rule.id + ip_cidr_range = "10.10.0.0/24" +} + +resource "google_compute_region_health_check" "target_forwarding_rule" { + name = "tf-test-health-check-%{random_suffix}" + region = "us-west2" + + http_health_check { + port = 80 + } +} + +resource "google_compute_region_backend_service" "target_forwarding_rule" { + name = "tf-test-backend-service-%{random_suffix}" + region = "us-west2" + protocol = "HTTP" + load_balancing_scheme = "INTERNAL_MANAGED" + health_checks = [google_compute_region_health_check.target_forwarding_rule.id] +} + +resource "google_compute_region_url_map" "target_forwarding_rule" { + name = "tf-test-url-map-%{random_suffix}" + region = "us-west2" + default_service = google_compute_region_backend_service.target_forwarding_rule.id +} + +resource "google_compute_region_target_http_proxy" "target_forwarding_rule" { + name = "tf-test-target-http-proxy-%{random_suffix}" + region = "us-west2" + url_map = google_compute_region_url_map.target_forwarding_rule.id +} + +resource "google_compute_forwarding_rule" "target_forwarding_rule" { + name = "tf-test-forwarding-rule-%{random_suffix}" + region = "us-west2" + network = google_compute_network.target_forwarding_rule.id + subnetwork = google_compute_subnetwork.target_forwarding_rule_default_subnetwork.id + load_balancing_scheme = "INTERNAL_MANAGED" + target = google_compute_region_target_http_proxy.target_forwarding_rule.id + ip_protocol = "TCP" + port_range = "80" + + depends_on = [ + google_compute_subnetwork.target_forwarding_rule_proxy_subnetwork, + ] +} + resource "google_compute_region_network_firewall_policy_with_rules" "{{$.PrimaryResourceId}}" { name = "{{index $.ResourceIdVars "fw_policy"}}" region = "us-west2" @@ -56,6 +121,26 @@ resource "google_compute_region_network_firewall_policy_with_rules" "{{$.Primary } } } + + rule { + description = "internal managed lb rule" + priority = 3000 + action = "allow" + direction = "INGRESS" + + target_type = "INTERNAL_MANAGED_LB" + target_forwarding_rules = [ + google_compute_forwarding_rule.target_forwarding_rule.self_link + ] + + match { + src_ip_ranges = ["10.0.0.0/8"] + + layer4_config { + ip_protocol = "tcp" + } + } + } } resource "google_network_security_address_group" "address_group_1" { diff --git a/mmv1/templates/terraform/samples/services/compute/region_target_tcp_proxy_basic.tf.tmpl b/mmv1/templates/terraform/samples/services/compute/region_target_tcp_proxy_basic.tf.tmpl index 7f5968a976d3..f0fe78be1b70 100644 --- a/mmv1/templates/terraform/samples/services/compute/region_target_tcp_proxy_basic.tf.tmpl +++ b/mmv1/templates/terraform/samples/services/compute/region_target_tcp_proxy_basic.tf.tmpl @@ -1,6 +1,6 @@ resource "google_compute_region_target_tcp_proxy" "{{$.PrimaryResourceId}}" { name = "{{index $.ResourceIdVars "region_target_tcp_proxy_name"}}" - region = "europe-west4" + region = "{{index $.Vars "region"}}" backend_service = google_compute_region_backend_service.default.id } @@ -8,7 +8,7 @@ resource "google_compute_region_backend_service" "default" { name = "{{index $.ResourceIdVars "region_backend_service_name"}}" protocol = "TCP" timeout_sec = 10 - region = "europe-west4" + region = "{{index $.Vars "region"}}" health_checks = [google_compute_region_health_check.default.id] load_balancing_scheme = "INTERNAL_MANAGED" @@ -16,7 +16,7 @@ resource "google_compute_region_backend_service" "default" { resource "google_compute_region_health_check" "default" { name = "{{index $.ResourceIdVars "health_check_name"}}" - region = "europe-west4" + region = "{{index $.Vars "region"}}" timeout_sec = 1 check_interval_sec = 1 tcp_health_check { diff --git a/mmv1/templates/terraform/samples/services/dataplex/dataplex_datascan_documentation.tf.tmpl b/mmv1/templates/terraform/samples/services/dataplex/dataplex_datascan_documentation.tf.tmpl index 71fd70131e24..19f4cff68259 100644 --- a/mmv1/templates/terraform/samples/services/dataplex/dataplex_datascan_documentation.tf.tmpl +++ b/mmv1/templates/terraform/samples/services/dataplex/dataplex_datascan_documentation.tf.tmpl @@ -74,7 +74,9 @@ resource "google_dataplex_datascan" "{{$.PrimaryResourceId}}" { } } - data_documentation_spec {} + data_documentation_spec { + catalog_publishing_enabled = true + } project = "{{index $.TestEnvVars "project_name"}}" } diff --git a/mmv1/templates/terraform/samples/services/firestore/firestore_field_skip_wait.tf.tmpl b/mmv1/templates/terraform/samples/services/firestore/firestore_field_skip_wait.tf.tmpl new file mode 100644 index 000000000000..20f4bced7dd3 --- /dev/null +++ b/mmv1/templates/terraform/samples/services/firestore/firestore_field_skip_wait.tf.tmpl @@ -0,0 +1,28 @@ +resource "google_firestore_database" "database" { + project = "{{index $.TestEnvVars "project_id"}}" + name = "{{index $.ResourceIdVars "database_id"}}" + location_id = "nam5" + type = "FIRESTORE_NATIVE" + + delete_protection_state = "{{index $.ResourceIdVars "delete_protection_state"}}" + deletion_policy = "DELETE" +} + +resource "google_firestore_field" "{{$.PrimaryResourceId}}" { + project = "{{index $.TestEnvVars "project_id"}}" + database = google_firestore_database.database.name + collection = "chatrooms_%{random_suffix}" + field = "skip_wait" + + index_config { + indexes { + order = "ASCENDING" + query_scope = "COLLECTION_GROUP" + } + indexes { + array_config = "CONTAINS" + } + } + + skip_wait = true +} diff --git a/mmv1/templates/terraform/samples/services/iap/iap_existing_project_endpoint.tf.tmpl b/mmv1/templates/terraform/samples/services/iap/iap_existing_project_endpoint.tf.tmpl new file mode 100644 index 000000000000..9d7bcfbc9366 --- /dev/null +++ b/mmv1/templates/terraform/samples/services/iap/iap_existing_project_endpoint.tf.tmpl @@ -0,0 +1,15 @@ +resource "google_agent_registry_service" "default" { + location = "us-central1" + service_id = "tf-test-endpoint-%{random_suffix}" + description = "My endpoint agent registry service for IAM test" + display_name = "My Service" + + interfaces { + url = "https://tf-test-endpoint-%{random_suffix}.example.com" + protocol_binding = "HTTP_JSON" # Wait, what protocol bindings are allowed? + } + + endpoint_spec { + type = "NO_SPEC" + } +} diff --git a/mmv1/templates/terraform/samples/services/networkconnectivity/network_connectivity_custom_hardware_instance_basic.tf.tmpl b/mmv1/templates/terraform/samples/services/networkconnectivity/network_connectivity_custom_hardware_instance_basic.tf.tmpl new file mode 100644 index 000000000000..fc8196a739ad --- /dev/null +++ b/mmv1/templates/terraform/samples/services/networkconnectivity/network_connectivity_custom_hardware_instance_basic.tf.tmpl @@ -0,0 +1,8 @@ +resource "google_network_connectivity_custom_hardware_instance" "instance" { + provider = google-beta + custom_hardware_instance_id = "{{index $.ResourceIdVars "resource_name"}}" + location = "us-south1" + labels = { + env = "{{index $.Vars "env_label"}}" + } +} diff --git a/mmv1/templates/terraform/samples/services/oracledatabase/oracledatabase_cloud_vmcluster_exascale.tf.tmpl b/mmv1/templates/terraform/samples/services/oracledatabase/oracledatabase_cloud_vmcluster_exascale.tf.tmpl new file mode 100644 index 000000000000..da5d5c945fc1 --- /dev/null +++ b/mmv1/templates/terraform/samples/services/oracledatabase/oracledatabase_cloud_vmcluster_exascale.tf.tmpl @@ -0,0 +1,83 @@ +resource "google_oracle_database_cloud_vm_cluster" "{{$.PrimaryResourceId}}" { + cloud_vm_cluster_id = "{{index $.ResourceIdVars "cloud_vm_cluster_id"}}" + display_name = "{{index $.ResourceIdVars "cloud_vm_cluster_id"}} displayname" + location = "us-east4" + project = "{{index $.ResourceIdVars "project"}}" + exadata_infrastructure = google_oracle_database_cloud_exadata_infrastructure.infra.id + network = data.google_compute_network.default.id + cidr = "10.5.0.0/24" + backup_subnet_cidr = "10.6.0.0/24" + + exascale_db_storage_vault = google_oracle_database_exascale_db_storage_vault.vault.name + + properties { + license_type = "LICENSE_INCLUDED" + ssh_public_keys = ["ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCz1X2744t+6vRLmE5u6nHi6/QWh8bQDgHmd+OIxRQIGA/IWUtCs2FnaCNZcqvZkaeyjk5v0lTA/n+9jvO42Ipib53athrfVG8gRt8fzPL66C6ZqHq+6zZophhrCdfJh/0G4x9xJh5gdMprlaCR1P8yAaVvhBQSKGc4SiIkyMNBcHJ5YTtMQMTfxaB4G1sHZ6SDAY9a6Cq/zNjDwfPapWLsiP4mRhE5SSjJX6l6EYbkm0JeLQg+AbJiNEPvrvDp1wtTxzlPJtIivthmLMThFxK7+DkrYFuLvN5AHUdo9KTDLvHtDCvV70r8v0gafsrKkM/OE9Jtzoo0e1N/5K/ZdyFRbAkFT4QSF3nwpbmBWLf2Evg//YyEuxnz4CwPqFST2mucnrCCGCVWp1vnHZ0y30nM35njLOmWdRDFy5l27pKUTwLp02y3UYiiZyP7d3/u5pKiN4vC27VuvzprSdJxWoAvluOiDeRh+/oeQDowxoT/Oop8DzB9uJmjktXw8jyMW2+Rpg+ENQqeNgF1OGlEzypaWiRskEFlkpLb4v/s3ZDYkL1oW0Nv/J8LTjTOTEaYt2Udjoe9x2xWiGnQixhdChWuG+MaoWffzUgx1tsVj/DBXijR5DjkPkrA1GA98zd3q8GKEaAdcDenJjHhNYSd4+rE9pIsnYn7fo5X/tFfcQH1XQ== nobody@google.com"] + cpu_core_count = "4" + gi_version = "23.0.0.0" + hostname_prefix = "hostname1" + + # Required fields for Exascale-based VM Clusters: + memory_size_gb = 60 + db_node_storage_size_gb = 120 + db_server_ocids = [ + data.google_oracle_database_db_servers.db_servers.db_servers.0.properties.0.ocid, + data.google_oracle_database_db_servers.db_servers.db_servers.1.properties.0.ocid + ] + } + + deletion_protection = "{{index $.ResourceIdVars "deletion_protection"}}" + + depends_on = [google_oracle_database_cloud_exadata_infrastructure_exascale_config.exascale_config] +} + +resource "google_oracle_database_cloud_exadata_infrastructure" "infra" { + cloud_exadata_infrastructure_id = "{{index $.ResourceIdVars "cloud_exadata_infrastructure_id"}}" + display_name = "{{index $.ResourceIdVars "cloud_exadata_infrastructure_id"}} displayname" + location = "us-east4" + project = "{{index $.ResourceIdVars "project"}}" + properties { + shape = "Exadata.X9M" + compute_count = "2" + storage_count = "3" + } + + deletion_protection = "{{index $.ResourceIdVars "deletion_protection"}}" +} + +resource "google_oracle_database_cloud_exadata_infrastructure_exascale_config" "exascale_config" { + cloud_exadata_infrastructure = google_oracle_database_cloud_exadata_infrastructure.infra.cloud_exadata_infrastructure_id + location = "us-east4" + project = "{{index $.ResourceIdVars "project"}}" + total_storage_size_gb = 10240 +} + +resource "google_oracle_database_exascale_db_storage_vault" "vault" { + exascale_db_storage_vault_id = "{{index $.ResourceIdVars "exascale_db_storage_vault_id"}}" + display_name = "{{index $.ResourceIdVars "exascale_db_storage_vault_id"}} displayname" + location = "us-east4" + project = "{{index $.ResourceIdVars "project"}}" + + exadata_infrastructure = google_oracle_database_cloud_exadata_infrastructure.infra.name + + depends_on = [google_oracle_database_cloud_exadata_infrastructure_exascale_config.exascale_config] + + properties { + exascale_db_storage_details { + total_size_gbs = 2048 + } + } + + deletion_protection = "{{index $.ResourceIdVars "deletion_protection"}}" +} + +data "google_compute_network" "default" { + name = "new" + project = "{{index $.ResourceIdVars "project"}}" +} + +data "google_oracle_database_db_servers" "db_servers" { + location = "us-east4" + project = "{{index $.ResourceIdVars "project"}}" + cloud_exadata_infrastructure = google_oracle_database_cloud_exadata_infrastructure.infra.cloud_exadata_infrastructure_id +} diff --git a/mmv1/templates/terraform/samples/services/oracledatabase/oracledatabase_exascale_db_storage_vault_dedicated_exadata_infrastructure.tf.tmpl b/mmv1/templates/terraform/samples/services/oracledatabase/oracledatabase_exascale_db_storage_vault_dedicated_exadata_infrastructure.tf.tmpl new file mode 100644 index 000000000000..fb57e69d465b --- /dev/null +++ b/mmv1/templates/terraform/samples/services/oracledatabase/oracledatabase_exascale_db_storage_vault_dedicated_exadata_infrastructure.tf.tmpl @@ -0,0 +1,40 @@ +resource "google_oracle_database_cloud_exadata_infrastructure" "infra" { + cloud_exadata_infrastructure_id = "{{index $.ResourceIdVars "cloud_exadata_infrastructure_id"}}" + display_name = "{{index $.ResourceIdVars "cloud_exadata_infrastructure_id"}} displayname" + location = "us-east4" + project = "{{index $.ResourceIdVars "project"}}" + + properties { + shape = "Exadata.X9M" + compute_count = "2" + storage_count = "3" + } + + deletion_protection = "{{index $.ResourceIdVars "deletion_protection"}}" +} + +resource "google_oracle_database_cloud_exadata_infrastructure_exascale_config" "exascale_config" { + cloud_exadata_infrastructure = google_oracle_database_cloud_exadata_infrastructure.infra.cloud_exadata_infrastructure_id + location = "us-east4" + project = "{{index $.ResourceIdVars "project"}}" + total_storage_size_gb = 10240 +} + +resource "google_oracle_database_exascale_db_storage_vault" "{{$.PrimaryResourceId}}" { + exascale_db_storage_vault_id = "{{index $.ResourceIdVars "exascale_db_storage_vault_id"}}" + display_name = "{{index $.ResourceIdVars "exascale_db_storage_vault_id"}} displayname" + location = "us-east4" + project = "{{index $.ResourceIdVars "project"}}" + + exadata_infrastructure = google_oracle_database_cloud_exadata_infrastructure.infra.name + + depends_on = [google_oracle_database_cloud_exadata_infrastructure_exascale_config.exascale_config] + + properties { + exascale_db_storage_details { + total_size_gbs = 2048 + } + } + + deletion_protection = "{{index $.ResourceIdVars "deletion_protection"}}" +} diff --git a/mmv1/templates/terraform/samples/services/oracledatabase/oracledatabase_goldengate_deployment_full.tf.tmpl b/mmv1/templates/terraform/samples/services/oracledatabase/oracledatabase_goldengate_deployment_full.tf.tmpl index c4854740b16f..bd2ef8ce1f0b 100644 --- a/mmv1/templates/terraform/samples/services/oracledatabase/oracledatabase_goldengate_deployment_full.tf.tmpl +++ b/mmv1/templates/terraform/samples/services/oracledatabase/oracledatabase_goldengate_deployment_full.tf.tmpl @@ -22,7 +22,6 @@ resource "google_oracle_database_goldengate_deployment" "{{$.PrimaryResourceId}} admin_username = "admin" admin_password = "123Abpassword!" deployment = "deployment" - ogg_version = "oggoracle:23.26.2.0.0_260417.1915_14223" } maintenance_window { diff --git a/mmv1/templates/terraform/samples/services/vectorsearch/vectorsearch_index_basic.tf.tmpl b/mmv1/templates/terraform/samples/services/vectorsearch/vectorsearch_index_basic.tf.tmpl new file mode 100644 index 000000000000..59e86c6d6e24 --- /dev/null +++ b/mmv1/templates/terraform/samples/services/vectorsearch/vectorsearch_index_basic.tf.tmpl @@ -0,0 +1,57 @@ +# NOTE: For most workloads we recommend creating the Collection and the Index +# in *separate* Terraform configurations (i.e. create and apply the Collection +# first, ingest data via importDataObjects, and only then create the Index in a +# second configuration). Once an Index exists on a Collection you can no longer +# run importDataObjects for bulk ingestion of data objects on that Collection -- +# you are limited to creating data objects one at a time or in small online +# batches. Defining both resources in the same Terraform file (as shown below) +# is convenient for a quick start, but locks you into the online / batched +# create path for any subsequent data ingestion. +resource "google_vector_search_collection" "parent" { + location = "us-central1" + collection_id = "{{index $.ResourceIdVars "collection_id"}}" + + display_name = "My Awesome Collection" + description = "This collection stores important data." + + data_schema = < 1) if the "project" field being added is null or empty. - if k == "fleet.#" && oldValue == "0" && newValue == "1" { - // When transitioning from 0->1 blocks, d.Get/d.GetOk effectively reads the 'new' config value. - projectVal, projectIsSet := d.GetOk("fleet.0.project") - if !projectIsSet || projectVal.(string) == "" { - log.Printf("[DEBUG] Suppressing diff for 'fleet.#' (0 -> 1) because fleet.0.project is null or empty in config.\n") - return true - } - } - return false + if k == "fleet.#" && oldValue == "0" && newValue == "1" { + // When transitioning from 0->1 blocks, d.Get/d.GetOk effectively reads the 'new' config value. + projectVal, projectIsSet := d.GetOk("fleet.0.project") + if !projectIsSet || projectVal.(string) == "" { + log.Printf("[DEBUG] Suppressing diff for 'fleet.#' (0 -> 1) because fleet.0.project is null or empty in config.\n") + return true + } + } + return false }) suppressDiffForConfidentialNodes = schema.SchemaDiffSuppressFunc(func(k, oldValue, newValue string, d *schema.ResourceData) bool { @@ -1234,6 +1243,7 @@ func ResourceContainerCluster() *schema.Resource { ExactlyOneOf: []string{ "maintenance_policy.0.daily_maintenance_window", "maintenance_policy.0.recurring_window", + "maintenance_policy.0.recurring_maintenance_window", }, MaxItems: 1, Description: `Time window specified for daily maintenance operations. Specify start_time in RFC3339 format "HH:MM”, where HH : [00-23] and MM : [00-59] GMT.`, @@ -1259,6 +1269,7 @@ func ResourceContainerCluster() *schema.Resource { ExactlyOneOf: []string{ "maintenance_policy.0.daily_maintenance_window", "maintenance_policy.0.recurring_window", + "maintenance_policy.0.recurring_maintenance_window", }, Description: `Time window for recurring maintenance operations.`, Elem: &schema.Resource{ @@ -1281,6 +1292,74 @@ func ResourceContainerCluster() *schema.Resource { }, }, }, + "recurring_maintenance_window": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ExactlyOneOf: []string{ + "maintenance_policy.0.daily_maintenance_window", + "maintenance_policy.0.recurring_window", + "maintenance_policy.0.recurring_maintenance_window", + }, + Description: `Time window for recurring maintenance operations.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "delay_until": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "year": { + Type: schema.TypeInt, + Required: true, + }, + "month": { + Type: schema.TypeInt, + Required: true, + }, + "day": { + Type: schema.TypeInt, + Required: true, + }, + }, + }, + }, + "window_duration": { + Required: true, + Type: schema.TypeString, + DiffSuppressFunc: tpgresource.DurationDiffSuppress, + ValidateFunc: validateDuration, + }, + "window_start_time": { + Required: true, + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "hours": { + Type: schema.TypeInt, + Required: true, + }, + "minutes": { + Type: schema.TypeInt, + Required: true, + }, + "seconds": { + Type: schema.TypeInt, + Required: true, + }, + }, + }, + }, + "recurrence": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: rfc5545RecurrenceDiffSuppress, + }, + }, + }, + }, "maintenance_exclusion": { Type: schema.TypeSet, Optional: true, @@ -3941,20 +4020,20 @@ func resourceContainerClusterRead(d *schema.ResourceData, meta interface{}) erro return err } - + if err := tpgresource.DeletionPolicyReadDefault(d, config, "DELETE"); err != nil{ return err } - + return nil } func resourceContainerClusterUpdate(d *schema.ResourceData, meta interface{}) error { - + if tpgresource.DeletionPolicyPreUpdate(d, ResourceContainerCluster) { return ResourceContainerCluster().Read(d, meta) } - + config := meta.(*transport_tpg.Config) userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) if err != nil { @@ -5761,13 +5840,13 @@ func resourceContainerClusterUpdate(d *schema.ResourceData, meta interface{}) er } func resourceContainerClusterDelete(d *schema.ResourceData, meta interface{}) error { - + if ok, err := tpgresource.DeletionPolicyPreDelete(d); err != nil{ return err }else if ok{ return nil } - + if d.Get("deletion_protection").(bool) { return fmt.Errorf("Cannot destroy cluster because deletion_protection is set to true. Set it to false to proceed with cluster deletion.") } @@ -6183,7 +6262,7 @@ func expandAutoIpamConfig(configured interface{}) *container.AutoIpamConfig { if !ok || len(l) == 0 || l[0] == nil { return nil } - + return &container.AutoIpamConfig{ Enabled: l[0].(map[string]interface{})["enabled"].(bool), } @@ -6304,6 +6383,35 @@ func expandMaintenancePolicy(d *schema.ResourceData, meta interface{}) *containe ResourceVersion: resourceVersion, } } + if recurringMaintenanceWindow, ok := maintenancePolicy["recurring_maintenance_window"]; ok && len(recurringMaintenanceWindow.([]interface{})) > 0 { + rmw := recurringMaintenanceWindow.([]interface{})[0].(map[string]interface{}) + duration, _ := time.ParseDuration(rmw["window_duration"].(string)) + + policy := &container.MaintenancePolicy{ + Window: &container.MaintenanceWindow{ + MaintenanceExclusions: exclusions, + RecurringMaintenanceWindow: &container.RecurringMaintenanceWindow{ + WindowStartTime: &container.TimeOfDay{ + Hours: int64(rmw["window_start_time"].([]interface{})[0].(map[string]interface{})["hours"].(int)), + Minutes: int64(rmw["window_start_time"].([]interface{})[0].(map[string]interface{})["minutes"].(int)), + Seconds: int64(rmw["window_start_time"].([]interface{})[0].(map[string]interface{})["seconds"].(int)), + }, + WindowDuration: fmt.Sprintf("%ds", int(duration.Seconds())), + Recurrence: rmw["recurrence"].(string), + }, + }, + ResourceVersion: resourceVersion, + } + + if _, ok := rmw["delay_until"]; ok && len(rmw["delay_until"].([]interface{})) == 1 { + policy.Window.RecurringMaintenanceWindow.DelayUntil = &container.Date{ + Year: int64(rmw["delay_until"].([]interface{})[0].(map[string]interface{})["year"].(int)), + Month: int64(rmw["delay_until"].([]interface{})[0].(map[string]interface{})["month"].(int)), + Day: int64(rmw["delay_until"].([]interface{})[0].(map[string]interface{})["day"].(int)), + } + } + return policy + } return nil } @@ -8158,7 +8266,7 @@ func flattenMaintenancePolicy(mp *container.MaintenancePolicy) []map[string]inte if window.EndTime != "" { exclusion["end_time"] = window.EndTime } - } + } } else { if window.EndTime != "" { exclusion["end_time"] = window.EndTime @@ -8216,6 +8324,37 @@ func flattenMaintenancePolicy(mp *container.MaintenancePolicy) []map[string]inte }, } } + if mp.Window.RecurringMaintenanceWindow != nil { + window := []map[string]interface{}{ + { + "recurring_maintenance_window": []map[string]interface{}{ + { + "window_start_time": []map[string]interface{}{ + { + "hours": mp.Window.RecurringMaintenanceWindow.WindowStartTime.Hours, + "minutes": mp.Window.RecurringMaintenanceWindow.WindowStartTime.Minutes, + "seconds": mp.Window.RecurringMaintenanceWindow.WindowStartTime.Seconds, + }, + }, + "window_duration": mp.Window.RecurringMaintenanceWindow.WindowDuration, + "recurrence": mp.Window.RecurringMaintenanceWindow.Recurrence, + }, + }, + "maintenance_exclusion": exclusions, + }, + } + + if mp.Window.RecurringMaintenanceWindow.DelayUntil != nil { + window[0]["recurring_maintenance_window"].([]map[string]interface{})[0]["delay_until"] = []map[string]interface{}{ + { + "year": mp.Window.RecurringMaintenanceWindow.DelayUntil.Year, + "month": mp.Window.RecurringMaintenanceWindow.DelayUntil.Month, + "day": mp.Window.RecurringMaintenanceWindow.DelayUntil.Day, + }, + } + } + return window + } return nil } @@ -9231,7 +9370,7 @@ func clusterAcceleratorNetworkProfileCustomizeDiff(_ context.Context, diff *sche // A. DETECT USER CONFIG (Raw Config Check for this specific pool) userHasAdditionalConfigs := false - + // Re-find the specific raw block for logic checking if !rawNodePools.IsNull() { it := rawNodePools.ElementIterator() @@ -9260,9 +9399,9 @@ func clusterAcceleratorNetworkProfileCustomizeDiff(_ context.Context, diff *sche shouldClear := false basePath := fmt.Sprintf("node_pool.%d", i) networkConfigPath := basePath + ".network_config.0" - + oldProfile, newProfile := diff.GetChange(networkConfigPath + ".accelerator_network_profile") - + newProfileStr := "" if newProfile != nil { newProfileStr = newProfile.(string) } oldProfileStr := "" @@ -9292,9 +9431,9 @@ func clusterAcceleratorNetworkProfileCustomizeDiff(_ context.Context, diff *sche // C. APPLY FIX TO THE MAP if shouldClear { log.Printf("[DEBUG] Cluster ANP CustomizeDiff: Clearing additional configs for INLINE pool %s", currentName) - + var newConfigMap map[string]interface{} - + if ncList, ok := newNpMap["network_config"].([]interface{}); ok && len(ncList) > 0 { if existingMap, ok := ncList[0].(map[string]interface{}); ok { newConfigMap = make(map[string]interface{}) @@ -9303,13 +9442,13 @@ func clusterAcceleratorNetworkProfileCustomizeDiff(_ context.Context, diff *sche } } } - + if newConfigMap == nil { newConfigMap = make(map[string]interface{}) } newConfigMap["additional_node_network_configs"] = []interface{}{} - + if !anpIsActive { newConfigMap["accelerator_network_profile"] = "" } diff --git a/mmv1/third_party/terraform/services/container/resource_container_cluster_meta.yaml.tmpl b/mmv1/third_party/terraform/services/container/resource_container_cluster_meta.yaml.tmpl index 03b02c6ee135..131567ed1704 100644 --- a/mmv1/third_party/terraform/services/container/resource_container_cluster_meta.yaml.tmpl +++ b/mmv1/third_party/terraform/services/container/resource_container_cluster_meta.yaml.tmpl @@ -230,7 +230,23 @@ fields: api_field: 'maintenancePolicy.window.recurringWindow.recurrence' - field: 'maintenance_policy.recurring_window.start_time' api_field: 'maintenancePolicy.window.recurringWindow.window.startTime' - - api_field: 'maintenancePolicy.disruptionBudget.lastDisruptionTime' + - field: 'maintenance_policy.recurring_maintenance_window.window_start_time.hours' + api_field: 'maintenancePolicy.window.recurringMaintenanceWindow.windowStartTime.hours' + - field: 'maintenance_policy.recurring_maintenance_window.window_start_time.minutes' + api_field: 'maintenancePolicy.window.recurringMaintenanceWindow.windowStartTime.minutes' + - field: 'maintenance_policy.recurring_maintenance_window.window_start_time.seconds' + api_field: 'maintenancePolicy.window.recurringMaintenanceWindow.windowStartTime.seconds' + - field: 'maintenance_policy.recurring_maintenance_window.window_duration' + api_field: 'maintenancePolicy.window.recurringMaintenanceWindow.windowDuration.seconds' + - field: 'maintenance_policy.recurring_maintenance_window.delay_until.year' + api_field: 'maintenancePolicy.window.recurringMaintenanceWindow.delayUntil.year' + - field: 'maintenance_policy.recurring_maintenance_window.delay_until.month' + api_field: 'maintenancePolicy.window.recurringMaintenanceWindow.delayUntil.month' + - field: 'maintenance_policy.recurring_maintenance_window.delay_until.day' + api_field: 'maintenancePolicy.window.recurringMaintenanceWindow.delayUntil.day' + - field: 'maintenance_policy.recurring_maintenance_window.recurrence' + api_field: 'maintenancePolicy.window.recurringMaintenanceWindow.recurrence' + - api_field: 'maintenancePolicy.disruptionBudget.lastDisruptionTime' - api_field: 'maintenancePolicy.disruptionBudget.lastMinorVersionDisruptionTime' - api_field: 'maintenancePolicy.disruptionBudget.MinorVersionDisruptionInterval' - api_field: 'maintenancePolicy.disruptionBudget.PatchVersionDisruptionInterval' @@ -368,6 +384,9 @@ fields: - api_field: 'nodeConfig.labels' - api_field: 'nodeConfig.linuxNodeConfig.accurateTimeConfig.enablePtpKvmTimeSync' - api_field: 'nodeConfig.linuxNodeConfig.cgroupMode' + - api_field: 'nodeConfig.linuxNodeConfig.customNodeInit.initScript.gcpSecretManagerSecretUri' + - api_field: 'nodeConfig.linuxNodeConfig.customNodeInit.initScript.gcsGeneration' + - api_field: 'nodeConfig.linuxNodeConfig.customNodeInit.initScript.gcsUri' - field: 'node_config.linux_node_config.hugepages_config.hugepage_size_1g' api_field: 'nodeConfig.linuxNodeConfig.hugepages.hugepageSize1g' - field: 'node_config.linux_node_config.hugepages_config.hugepage_size_2m' @@ -664,6 +683,12 @@ fields: api_field: 'nodePools.config.linuxNodeConfig.accurateTimeConfig.enablePtpKvmTimeSync' - field: 'node_pool.node_config.linux_node_config.cgroup_mode' api_field: 'nodePools.config.linuxNodeConfig.cgroupMode' + - field: 'node_pool.node_config.linux_node_config.custom_node_init.init_script.gcp_secret_manager_secret_uri' + api_field: 'nodePools.config.linuxNodeConfig.customNodeInit.initScript.gcpSecretManagerSecretUri' + - field: 'node_pool.node_config.linux_node_config.custom_node_init.init_script.gcs_generation' + api_field: 'nodePools.config.linuxNodeConfig.customNodeInit.initScript.gcsGeneration' + - field: 'node_pool.node_config.linux_node_config.custom_node_init.init_script.gcs_uri' + api_field: 'nodePools.config.linuxNodeConfig.customNodeInit.initScript.gcsUri' - field: 'node_pool.node_config.linux_node_config.hugepages_config.hugepage_size_1g' api_field: 'nodePools.config.linuxNodeConfig.hugepages.hugepageSize1g' - field: 'node_pool.node_config.linux_node_config.hugepages_config.hugepage_size_2m' @@ -778,6 +803,12 @@ fields: api_field: 'nodePools.nodeDrainConfig.pdbTimeoutDuration' - field: 'node_pool.node_drain_config.respect_pdb_during_node_pool_deletion' api_field: 'nodePools.nodeDrainConfig.respectPdbDuringNodePoolDeletion' + - field: 'node_pool.maintenance_policy.exclusion_until_end_of_support.enabled' + api_field: 'nodePools.maintenancePolicy.exclusionUntilEndOfSupport.enabled' + - field: 'node_pool.maintenance_policy.exclusion_until_end_of_support.start_time' + api_field: 'nodePools.maintenancePolicy.exclusionUntilEndOfSupport.startTime' + - field: 'node_pool.maintenance_policy.exclusion_until_end_of_support.end_time' + api_field: 'nodePools.maintenancePolicy.exclusionUntilEndOfSupport.endTime' - field: 'node_pool.node_count' provider_only: true - field: 'node_pool.node_locations' diff --git a/mmv1/third_party/terraform/services/container/resource_container_cluster_test.go.tmpl b/mmv1/third_party/terraform/services/container/resource_container_cluster_test.go.tmpl index 78ad68f5c8ab..0543a3eba0da 100644 --- a/mmv1/third_party/terraform/services/container/resource_container_cluster_test.go.tmpl +++ b/mmv1/third_party/terraform/services/container/resource_container_cluster_test.go.tmpl @@ -2538,7 +2538,7 @@ func TestAccContainerCluster_withKubeletConfig(t *testing.T) { "node_config.0.kubelet_config.0.topology_manager.0.policy", "best-effort"), resource.TestCheckResourceAttr( "google_container_cluster.with_kubelet_config", - "node_config.0.kubelet_config.0.topology_manager.0.scope", "pod"), + "node_config.0.kubelet_config.0.topology_manager.0.scope", "pod"), ), }, { @@ -3214,6 +3214,38 @@ func TestAccContainerCluster_withNodeConfigReservationAffinitySpecific(t *testin }) } +func TestAccContainerCluster_withNodeConfigReservationAffinityAnyReservationThenFail(t *testing.T) { + t.Parallel() + + reservationName := fmt.Sprintf("tf-test-reservation-%s", acctest.RandString(t, 10)) + clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerCluster_withNodeConfigReservationAffinityAnyReservationThenFail(reservationName, clusterName, networkName, subnetworkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_container_cluster.with_node_config", + "node_config.0.reservation_affinity.#", "1"), + resource.TestCheckResourceAttr("google_container_cluster.with_node_config", + "node_config.0.reservation_affinity.0.consume_reservation_type", "ANY_RESERVATION_THEN_FAIL"), + ), + }, + { + ResourceName: "google_container_cluster.with_node_config", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + }, + }) +} + func TestAccContainerCluster_withNodeConfigNodeImageConfig(t *testing.T) { t.Parallel() @@ -3819,14 +3851,44 @@ func TestAccContainerCluster_withNodePoolNodeDrainConfig(t *testing.T) { }) } +func TestAccContainerCluster_withNodePoolMaintenancePolicy(t *testing.T) { + t.Parallel() + + cluster := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + np := fmt.Sprintf("tf-test-np-%s", acctest.RandString(t, 10)) + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerCluster_withNodePoolMaintenancyPolicy(cluster, np, networkName, subnetworkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_container_cluster.with_node_pool_maintenance_policy", + "node_pool.0.maintenance_policy.0.exclusion_until_end_of_support.0.enabled", "true"), + ), + }, + { + ResourceName: "google_container_cluster.with_node_pool_maintenance_policy", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"min_master_version", "deletion_protection"}, + }, + }, + }) +} + func TestAccContainerCluster_withClusterDisruptionBudget(t *testing.T) { t.Parallel() - + clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) resourceName := "google_container_cluster.with_cluster_disruption_budget" networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) - + acctest.VcrTest(t, resource.TestCase{ PreCheck: func() { acctest.AccTestPreCheck(t) }, ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), @@ -3845,7 +3907,7 @@ func TestAccContainerCluster_withClusterDisruptionBudget(t *testing.T) { }) } -func TestAccContainerCluster_withMaintenanceWindow(t *testing.T) { +func TestAccContainerCluster_withDailyMaintenanceWindow(t *testing.T) { t.Parallel() clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) @@ -3859,7 +3921,7 @@ func TestAccContainerCluster_withMaintenanceWindow(t *testing.T) { CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), Steps: []resource.TestStep{ { - Config: testAccContainerCluster_withMaintenanceWindow(clusterName, "03:00", networkName, subnetworkName), + Config: testAccContainerCluster_withDailyMaintenanceWindow(clusterName, "03:00", networkName, subnetworkName), }, { ResourceName: resourceName, @@ -3868,10 +3930,12 @@ func TestAccContainerCluster_withMaintenanceWindow(t *testing.T) { ImportStateVerifyIgnore: []string{"deletion_protection"}, }, { - Config: testAccContainerCluster_withMaintenanceWindow(clusterName, "", networkName, subnetworkName), + Config: testAccContainerCluster_withDailyMaintenanceWindow(clusterName, "", networkName, subnetworkName), Check: resource.ComposeTestCheckFunc( resource.TestCheckNoResourceAttr(resourceName, "maintenance_policy.0.daily_maintenance_window.0.start_time"), + resource.TestCheckNoResourceAttr(resourceName, + "maintenance_policy.0.recurring_maintenance_window.0.window_start_time"), ), }, { @@ -3886,7 +3950,7 @@ func TestAccContainerCluster_withMaintenanceWindow(t *testing.T) { }) } -func TestAccContainerCluster_withRecurringMaintenanceWindow(t *testing.T) { +func TestAccContainerCluster_withRecurringTimeWindow(t *testing.T) { t.Parallel() cluster := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) resourceName := "google_container_cluster.with_recurring_maintenance_window" @@ -3899,10 +3963,12 @@ func TestAccContainerCluster_withRecurringMaintenanceWindow(t *testing.T) { CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), Steps: []resource.TestStep{ { - Config: testAccContainerCluster_withRecurringMaintenanceWindow(cluster, "2019-01-01T00:00:00Z", "2019-01-02T00:00:00Z", networkName, subnetworkName), + Config: testAccContainerCluster_withRecurringTimeWindow(cluster, "2019-01-01T00:00:00Z", "2019-01-02T00:00:00Z", networkName, subnetworkName), Check: resource.ComposeTestCheckFunc( resource.TestCheckNoResourceAttr(resourceName, "maintenance_policy.0.daily_maintenance_window.0.start_time"), + resource.TestCheckNoResourceAttr(resourceName, + "maintenance_policy.0.recurring_maintenance_window.0.window_start_time"), ), }, { @@ -3914,12 +3980,14 @@ func TestAccContainerCluster_withRecurringMaintenanceWindow(t *testing.T) { }, { - Config: testAccContainerCluster_withRecurringMaintenanceWindow(cluster, "", "", networkName, subnetworkName), + Config: testAccContainerCluster_withRecurringTimeWindow(cluster, "", "", networkName, subnetworkName), Check: resource.ComposeTestCheckFunc( resource.TestCheckNoResourceAttr(resourceName, "maintenance_policy.0.daily_maintenance_window.0.start_time"), resource.TestCheckNoResourceAttr(resourceName, "maintenance_policy.0.recurring_window.0.start_time"), + resource.TestCheckNoResourceAttr(resourceName, + "maintenance_policy.0.recurring_maintenance_window.0.window_start_time"), ), }, { @@ -3935,6 +4003,56 @@ func TestAccContainerCluster_withRecurringMaintenanceWindow(t *testing.T) { }) } +func TestAccContainerCluster_withRecurringMaintenanceWindow(t *testing.T) { + t.Parallel() + cluster := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + resourceName := "google_container_cluster.with_recurring_maintenance_window" + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerCluster_withRecurringMaintenanceWindow(cluster, 2019, 1, 1, 10, 0, "24h", networkName, subnetworkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckNoResourceAttr(resourceName, + "maintenance_policy.0.daily_maintenance_window.0.start_time"), + resource.TestCheckNoResourceAttr(resourceName, + "maintenance_policy.0.recurring_window.0.start_time"), + ), + }, + { + ResourceName: resourceName, + ImportStateIdPrefix: "us-central1-a/", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: testAccContainerCluster_withRecurringMaintenanceWindow(cluster, 0, 0, 0, 10, 0, "4h", networkName, subnetworkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckNoResourceAttr(resourceName, + "maintenance_policy.0.daily_maintenance_window.0.start_time"), + resource.TestCheckNoResourceAttr(resourceName, + "maintenance_policy.0.recurring_window.0.start_time"), + resource.TestCheckNoResourceAttr(resourceName, + "maintenance_policy.0.recurring_maintenance_window.0.delay_until.0.year"), + ), + }, + { + ResourceName: resourceName, + ImportStateIdPrefix: "us-central1-a/", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + }, + }) +} + func TestAccContainerCluster_withMaintenanceExclusionWindow(t *testing.T) { t.Parallel() cluster := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) @@ -5358,71 +5476,71 @@ func TestAccContainerCluster_withSecretManagerConfig(t *testing.T) { func TestAccContainerCluster_withSecretSyncConfig(t *testing.T) { t.Parallel() - clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) - networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") - subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) - pid := envvar.GetTestProjectFromEnv() - acctest.VcrTest(t, resource.TestCase{ - PreCheck: func() { acctest.AccTestPreCheck(t) }, - ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), - CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), - Steps: []resource.TestStep{ - { - Config: testAccContainerCluster_forSecretSyncConfig(pid, clusterName, networkName, subnetworkName), - }, - { - ResourceName: "google_container_cluster.primary", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"deletion_protection"}, - }, - { - Config: testAccContainerCluster_withSecretSyncConfigEnabled(pid, clusterName, networkName, subnetworkName), - }, - { - ResourceName: "google_container_cluster.primary", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"deletion_protection"}, - }, - { - Config: testAccContainerCluster_withSecretSyncRotationPeriodUpdated(pid, clusterName, networkName, subnetworkName), - }, - { - ResourceName: "google_container_cluster.primary", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"deletion_protection"}, - }, - { - Config: testAccContainerCluster_withSecretSyncConfigRotationDisabled(pid, clusterName, networkName, subnetworkName), - }, - { - ResourceName: "google_container_cluster.primary", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"deletion_protection"}, - }, - { - Config: testAccContainerCluster_withSecretSyncConfigDisabled(pid, clusterName, networkName, subnetworkName), - }, - { - ResourceName: "google_container_cluster.primary", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"deletion_protection"}, - }, - { - Config: testAccContainerCluster_forSecretSyncConfig(pid, clusterName, networkName, subnetworkName), - }, - { - ResourceName: "google_container_cluster.primary", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"deletion_protection"}, - }, - }, - }) + clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + pid := envvar.GetTestProjectFromEnv() + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerCluster_forSecretSyncConfig(pid, clusterName, networkName, subnetworkName), + }, + { + ResourceName: "google_container_cluster.primary", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: testAccContainerCluster_withSecretSyncConfigEnabled(pid, clusterName, networkName, subnetworkName), + }, + { + ResourceName: "google_container_cluster.primary", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: testAccContainerCluster_withSecretSyncRotationPeriodUpdated(pid, clusterName, networkName, subnetworkName), + }, + { + ResourceName: "google_container_cluster.primary", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: testAccContainerCluster_withSecretSyncConfigRotationDisabled(pid, clusterName, networkName, subnetworkName), + }, + { + ResourceName: "google_container_cluster.primary", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: testAccContainerCluster_withSecretSyncConfigDisabled(pid, clusterName, networkName, subnetworkName), + }, + { + ResourceName: "google_container_cluster.primary", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: testAccContainerCluster_forSecretSyncConfig(pid, clusterName, networkName, subnetworkName), + }, + { + ResourceName: "google_container_cluster.primary", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + }, + }) } func TestAccContainerCluster_withLoggingConfig(t *testing.T) { @@ -7031,7 +7149,7 @@ func TestAccContainerCluster_withWorkloadALTSConfigAutopilot(t *testing.T) { t.Parallel() clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) - networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) pid := envvar.GetTestProjectFromEnv() acctest.VcrTest(t, resource.TestCase{ @@ -7654,7 +7772,7 @@ func TestAccContainerCluster_autopilot_withAdditiveVPC(t *testing.T) { domain := "additive.autopilot.example" clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) - networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) acctest.VcrTest(t, resource.TestCase{ @@ -7759,7 +7877,7 @@ func TestAccContainerCluster_autopilot_withAdditiveVPCMutation(t *testing.T) { domain := "additive-mutating.autopilot.example" clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) - networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) acctest.VcrTest(t, resource.TestCase{ @@ -10472,28 +10590,97 @@ resource "google_container_cluster" "with_node_config" { `, reservation, clusterName, networkName, subnetworkName) } -func testAccContainerCluster_withNodeConfigNodeImageConfig(clusterName, networkName, subnetworkName, imageName, imageProject string) string { +func testAccContainerCluster_withNodeConfigReservationAffinityAnyReservationThenFail(reservation, clusterName, networkName, subnetworkName string) string { return fmt.Sprintf(` -resource "google_container_cluster" "with_node_image_config" { +resource "google_project_service" "compute" { + service = "compute.googleapis.com" +} + +resource "google_project_service" "container" { + service = "container.googleapis.com" + depends_on = [google_project_service.compute] +} + + +resource "google_compute_reservation" "gce_reservation" { + name = "%s" + zone = "us-central1-f" + + specific_reservation { + count = 1 + instance_properties { + machine_type = "n1-standard-1" + } + } + + specific_reservation_required = false + depends_on = [google_project_service.compute] +} + +resource "google_container_cluster" "with_node_config" { name = "%s" location = "us-central1-f" initial_node_count = 1 node_config { + machine_type = "n1-standard-1" + disk_size_gb = 15 + disk_type = "pd-ssd" oauth_scopes = [ - "https://www.googleapis.com/auth/logging.write", "https://www.googleapis.com/auth/monitoring", + "https://www.googleapis.com/auth/compute", + "https://www.googleapis.com/auth/devstorage.read_only", + "https://www.googleapis.com/auth/logging.write", ] - node_image_config { - image = "%s" - image_project = "%s" + service_account = "default" + metadata = { + foo = "bar" + disable-legacy-endpoints = "true" } - image_type = "CUSTOM_CONTAINERD" - } - network = "%s" - subnetwork = "%s" - + labels = { + foo = "bar" + } + tags = ["foo", "bar"] + + // Updatable fields + image_type = "COS_CONTAINERD" + + reservation_affinity { + consume_reservation_type = "ANY_RESERVATION_THEN_FAIL" + } + } + network = "%s" + subnetwork = "%s" + + deletion_protection = false + depends_on = [google_project_service.container] +} +`, reservation, clusterName, networkName, subnetworkName) +} + +func testAccContainerCluster_withNodeConfigNodeImageConfig(clusterName, networkName, subnetworkName, imageName, imageProject string) string { + return fmt.Sprintf(` + +resource "google_container_cluster" "with_node_image_config" { + name = "%s" + location = "us-central1-f" + initial_node_count = 1 + + node_config { + oauth_scopes = [ + "https://www.googleapis.com/auth/logging.write", + "https://www.googleapis.com/auth/monitoring", + ] + node_image_config { + image = "%s" + image_project = "%s" + } + image_type = "CUSTOM_CONTAINERD" + } + network = "%s" + subnetwork = "%s" + deletion_protection = false } `, clusterName, imageName, imageProject, networkName, subnetworkName) @@ -11710,6 +11897,33 @@ resource "google_container_cluster" "with_node_pool_node_drain_config" { `, cluster, np, privateName, privateVal, networkName, subnetworkName) } +func testAccContainerCluster_withNodePoolMaintenancyPolicy(cluster, np, networkName, subnetworkName string) string { + return fmt.Sprintf(` +data "google_container_engine_versions" "central1a" { + location = "us-central1-a" +} + +resource "google_container_cluster" "with_node_pool_maintenance_policy" { + name = "%s" + location = "us-central1-a" + min_master_version = data.google_container_engine_versions.central1a.latest_master_version + node_pool { + name = "%s" + initial_node_count = 1 + maintenance_policy { + exclusion_until_end_of_support { + enabled = true + } + } + } + + network = "%s" + subnetwork = "%s" + deletion_protection = false +} +`, cluster, np, networkName, subnetworkName) +} + func testAccContainerCluster_withClusterDisruptionBudget(clusterName, interval, networkName, subnetworkName string) string { return fmt.Sprintf(` resource "google_container_cluster" "with_cluster_disruption_budget" { @@ -11734,7 +11948,7 @@ resource "google_container_cluster" "with_cluster_disruption_budget" { `, clusterName, interval, interval, networkName, subnetworkName) } -func testAccContainerCluster_withMaintenanceWindow(clusterName, startTime, networkName, subnetworkName string) string { +func testAccContainerCluster_withDailyMaintenanceWindow(clusterName, startTime, networkName, subnetworkName string) string { maintenancePolicy := "" if len(startTime) > 0 { maintenancePolicy = fmt.Sprintf(` @@ -11760,7 +11974,7 @@ resource "google_container_cluster" "with_maintenance_window" { `, clusterName, maintenancePolicy, networkName, subnetworkName) } -func testAccContainerCluster_withRecurringMaintenanceWindow(clusterName, startTime, endTime, networkName, subnetworkName string) string { +func testAccContainerCluster_withRecurringTimeWindow(clusterName, startTime, endTime, networkName, subnetworkName string) string { maintenancePolicy := "" if len(startTime) > 0 { maintenancePolicy = fmt.Sprintf(` @@ -11789,6 +12003,52 @@ resource "google_container_cluster" "with_recurring_maintenance_window" { } +func testAccContainerCluster_withRecurringMaintenanceWindow(clusterName string, startYear, startMonth, startDay, startHours, startMinutes int, duration string, networkName, subnetworkName string) string { + maintenancePolicy := "" + delayUntil := "" + if startYear != 0 && startMonth != 0 && startDay != 0 { + delayUntil = fmt.Sprintf(` + delay_until { + year = %d + month = %d + day = %d + } + `, startYear, startMonth, startDay) + } + + maintenancePolicy = fmt.Sprintf(` + maintenance_policy { + recurring_maintenance_window { + %s + + window_duration = "%s" + + window_start_time { + hours = %d + minutes = %d + seconds = 0 + } + recurrence = "FREQ=DAILY" + } + } + `, delayUntil, duration, startHours, startMinutes) + + + return fmt.Sprintf(` + resource "google_container_cluster" "with_recurring_maintenance_window" { + name = "%s" + location = "us-central1-a" + initial_node_count = 1 + %s + network = "%s" + subnetwork = "%s" + + deletion_protection = false + } + `, clusterName, maintenancePolicy, networkName, subnetworkName) + +} + func testAccContainerCluster_withExclusion_RecurringMaintenanceWindow(clusterName string, w1startTime, w1endTime, w2startTime, w2endTime, networkName, subnetworkName string) string { return fmt.Sprintf(` @@ -16200,7 +16460,7 @@ func TestAccContainerCluster_withAutopilotGcpFilestoreCsiDriver(t *testing.T) { randomSuffix := acctest.RandString(t, 10) clusterName := fmt.Sprintf("tf-test-cluster-%s", randomSuffix) - networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) acctest.VcrTest(t, resource.TestCase{ @@ -16274,7 +16534,7 @@ func TestAccContainerCluster_withDnsEndpoint(t *testing.T) { t.Parallel() clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) - networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) acctest.VcrTest(t, resource.TestCase{ @@ -16465,7 +16725,7 @@ func TestAccContainerCluster_withCgroupMode(t *testing.T) { t.Parallel() clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) - networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) acctest.VcrTest(t, resource.TestCase{ @@ -16494,7 +16754,7 @@ func TestAccContainerCluster_withCgroupModeUpdate(t *testing.T) { t.Parallel() clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) - networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) acctest.VcrTest(t, resource.TestCase{ @@ -16631,9 +16891,9 @@ resource "google_container_cluster" "with_enterprise_config" { func TestAccContainerCluster_disableControlPlaneIP(t *testing.T) { t.Parallel() - clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) - networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") - subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + clusterName := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) acctest.VcrTest(t, resource.TestCase{ PreCheck: func() { acctest.AccTestPreCheck(t) }, @@ -18409,7 +18669,7 @@ func testAccContainerCluster_custom_subnet(clusterName string, networkName strin } } - `, clusterName, networkName, sri[0].SubnetName, additionalIpRangesStr, firstSubnet, firstSubnet) + `, clusterName, networkName, sri[0].SubnetName, additionalIpRangesStr, firstSubnet, firstSubnet) } func TestAccContainerCluster_withNodeCreationConfig(t *testing.T) { @@ -18452,6 +18712,103 @@ func TestAccContainerCluster_withNodeCreationConfig(t *testing.T) { }) } +func TestAccContainerCluster_withCustomNodeInitGcs(t *testing.T) { + t.Parallel() + + cluster := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + bucketName := fmt.Sprintf("tf-test-bucket-%s", acctest.RandString(t, 10)) + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerCluster_withCustomNodeInitGcs(cluster, bucketName, networkName, subnetworkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_container_cluster.primary", "node_config.0.linux_node_config.0.custom_node_init.0.init_script.0.gcs_uri", fmt.Sprintf("gs://%s/script.sh", bucketName)), + resource.TestCheckResourceAttrSet("google_container_cluster.primary", "node_config.0.linux_node_config.0.custom_node_init.0.init_script.0.gcs_generation"), + ), + }, + { + ResourceName: "google_container_cluster.primary", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection", "min_master_version"}, + }, + }, + }) +} + +func TestAccContainerCluster_withCustomNodeInitSecret(t *testing.T) { + t.Parallel() + + cluster := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + secretId := fmt.Sprintf("tf-test-secret-%s", acctest.RandString(t, 10)) + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + ExternalProviders: map[string]resource.ExternalProvider{ + "time": {}, + }, + CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerCluster_withCustomNodeInitSecret(cluster, secretId, networkName, subnetworkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("google_container_cluster.primary", "node_config.0.linux_node_config.0.custom_node_init.0.init_script.0.gcp_secret_manager_secret_uri"), + ), + }, + { + ResourceName: "google_container_cluster.primary", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection", "min_master_version"}, + }, + }, + }) +} + +func TestAccContainerCluster_withCustomNodeInitInline(t *testing.T) { + t.Parallel() + + cluster := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + bucketName := fmt.Sprintf("tf-test-bucket-%s", acctest.RandString(t, 10)) + secretId := fmt.Sprintf("tf-test-secret-%s", acctest.RandString(t, 10)) + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + ExternalProviders: map[string]resource.ExternalProvider{ + "time": {}, + }, + CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerCluster_withCustomNodeInitInline(cluster, bucketName, secretId, networkName, subnetworkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_container_cluster.primary", "node_pool.0.node_config.0.linux_node_config.0.custom_node_init.0.init_script.0.gcs_uri", fmt.Sprintf("gs://%s/script.sh", bucketName)), + resource.TestCheckResourceAttrSet("google_container_cluster.primary", "node_pool.0.node_config.0.linux_node_config.0.custom_node_init.0.init_script.0.gcs_generation"), + resource.TestCheckResourceAttrSet("google_container_cluster.primary", "node_pool.1.node_config.0.linux_node_config.0.custom_node_init.0.init_script.0.gcp_secret_manager_secret_uri"), + ), + }, + { + ResourceName: "google_container_cluster.primary", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection", "min_master_version"}, + }, + }, + }) +} + func testAccContainerCluster_withNodeCreationConfig(name, networkName, subnetworkName string, mode string) string { return fmt.Sprintf(` resource "google_container_cluster" "primary" { @@ -18468,6 +18825,211 @@ resource "google_container_cluster" "primary" { `, name, networkName, subnetworkName, mode) } +func testAccContainerCluster_withCustomNodeInitGcs(cluster, bucket, networkName, subnetworkName string) string { + return fmt.Sprintf(` +data "google_container_engine_versions" "central1a" { + location = "us-central1-a" +} + +resource "google_storage_bucket" "bucket" { + name = "%s" + location = "US" + force_destroy = true +} + +resource "google_storage_bucket_object" "script" { + name = "script.sh" + bucket = google_storage_bucket.bucket.name + content = "#!/bin/bash\necho 'Hello World' > /tmp/hello.txt" +} + +resource "google_container_cluster" "primary" { + name = "%s" + location = "us-central1-a" + initial_node_count = 1 + min_master_version = data.google_container_engine_versions.central1a.latest_master_version + deletion_protection = false + network = "%s" + subnetwork = "%s" + + node_config { + image_type = "COS_CONTAINERD" + oauth_scopes = [ + "https://www.googleapis.com/auth/cloud-platform", + ] + linux_node_config { + custom_node_init { + init_script { + gcs_uri = "gs://${google_storage_bucket.bucket.name}/${google_storage_bucket_object.script.name}" + gcs_generation = google_storage_bucket_object.script.generation + } + } + } + } +} +`, bucket, cluster, networkName, subnetworkName) +} + +func testAccContainerCluster_withCustomNodeInitSecret(cluster, secretId, networkName, subnetworkName string) string { + return fmt.Sprintf(` +data "google_container_engine_versions" "central1a" { + location = "us-central1-a" +} + +data "google_project" "project" {} + +resource "google_secret_manager_secret" "secret" { + secret_id = "%s" + replication { + auto {} + } +} + +resource "google_secret_manager_secret_version" "version" { + secret = google_secret_manager_secret.secret.id + secret_data = "#!/bin/bash\necho 'Hello World' > /tmp/hello.txt" +} + +resource "google_secret_manager_secret_iam_member" "secret_accessor" { + secret_id = google_secret_manager_secret.secret.id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${data.google_project.project.number}-compute@developer.gserviceaccount.com" +} + +resource "time_sleep" "wait_60_seconds" { + create_duration = "60s" + depends_on = [ + google_secret_manager_secret_iam_member.secret_accessor + ] +} + +resource "google_container_cluster" "primary" { + name = "%s" + location = "us-central1-a" + initial_node_count = 1 + min_master_version = data.google_container_engine_versions.central1a.latest_master_version + deletion_protection = false + network = "%s" + subnetwork = "%s" + + node_config { + image_type = "COS_CONTAINERD" + oauth_scopes = [ + "https://www.googleapis.com/auth/cloud-platform", + ] + linux_node_config { + custom_node_init { + init_script { + gcp_secret_manager_secret_uri = google_secret_manager_secret_version.version.name + } + } + } + } + + depends_on = [ + time_sleep.wait_60_seconds + ] +} +`, secretId, cluster, networkName, subnetworkName) +} + +func testAccContainerCluster_withCustomNodeInitInline(cluster, bucket, secretId, networkName, subnetworkName string) string { + return fmt.Sprintf(` +data "google_container_engine_versions" "central1a" { + location = "us-central1-a" +} + +data "google_project" "project" {} + +resource "google_storage_bucket" "bucket" { + name = "%s" + location = "US" + force_destroy = true +} + +resource "google_storage_bucket_object" "script" { + name = "script.sh" + bucket = google_storage_bucket.bucket.name + content = "#!/bin/bash\necho 'Hello World' > /tmp/hello.txt" +} + +resource "google_secret_manager_secret" "secret" { + secret_id = "%s" + replication { + auto {} + } +} + +resource "google_secret_manager_secret_version" "version" { + secret = google_secret_manager_secret.secret.id + secret_data = "#!/bin/bash\necho 'Hello World' > /tmp/hello.txt" +} + +resource "google_secret_manager_secret_iam_member" "secret_accessor" { + secret_id = google_secret_manager_secret.secret.id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${data.google_project.project.number}-compute@developer.gserviceaccount.com" +} + +resource "time_sleep" "wait_60_seconds" { + create_duration = "60s" + depends_on = [ + google_secret_manager_secret_iam_member.secret_accessor + ] +} + +resource "google_container_cluster" "primary" { + name = "%s" + location = "us-central1-a" + min_master_version = data.google_container_engine_versions.central1a.latest_master_version + deletion_protection = false + network = "%s" + subnetwork = "%s" + + node_pool { + name = "gcs-pool" + initial_node_count = 1 + node_config { + image_type = "COS_CONTAINERD" + oauth_scopes = [ + "https://www.googleapis.com/auth/cloud-platform", + ] + linux_node_config { + custom_node_init { + init_script { + gcs_uri = "gs://${google_storage_bucket.bucket.name}/${google_storage_bucket_object.script.name}" + gcs_generation = google_storage_bucket_object.script.generation + } + } + } + } + } + + node_pool { + name = "secret-pool" + initial_node_count = 1 + node_config { + image_type = "COS_CONTAINERD" + oauth_scopes = [ + "https://www.googleapis.com/auth/cloud-platform", + ] + linux_node_config { + custom_node_init { + init_script { + gcp_secret_manager_secret_uri = google_secret_manager_secret_version.version.name + } + } + } + } + } + + depends_on = [ + time_sleep.wait_60_seconds + ] +} +`, bucket, secretId, cluster, networkName, subnetworkName) +} + func TestAccContainerCluster_withSlurmOperatorConfig(t *testing.T) { t.Parallel() diff --git a/mmv1/third_party/terraform/services/container/resource_container_node_pool.go.tmpl b/mmv1/third_party/terraform/services/container/resource_container_node_pool.go.tmpl index 8b0c129fc2a9..b432f70529f3 100644 --- a/mmv1/third_party/terraform/services/container/resource_container_node_pool.go.tmpl +++ b/mmv1/third_party/terraform/services/container/resource_container_node_pool.go.tmpl @@ -706,6 +706,40 @@ var schemaNodePool = map[string]*schema.Schema{ Optional: true, Description: `When true, the provider ignores external changes (drift) to the node count by skipping GCE API queries to the Instance Group Managers. This is a performance optimization for large clusters that saves API quota. Setting this to true will result in missing managed_instance_group_urls in the state.`, }, + + "maintenance_policy": { + Type: schema.TypeList, + Optional: true, + Description: `Maintenance policy for this NodePool.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "exclusion_until_end_of_support": { + Type: schema.TypeList, + Optional: true, + Description: `Maintenance exclusion until the end of support.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Description: `Whether to enable the maintenance exclusion until the end of support for this NodePool.`, + }, + "start_time": { + Type: schema.TypeString, + Computed: true, + Description: `Start time of the maintenance exclusion.`, + }, + "end_time": { + Type: schema.TypeString, + Computed: true, + Description: `End time of the maintenance exclusion.`, + }, + }, + }, + }, + }, + }, + }, } type NodePoolInformation struct { @@ -1349,6 +1383,19 @@ func expandNodePool(d *schema.ResourceData, prefix string) (*container.NodePool, } } + if v, ok := d.GetOk(prefix + "maintenance_policy"); ok { + maintenancePolicy := v.([]interface{})[0].(map[string]interface{}) + if v, ok := maintenancePolicy["exclusion_until_end_of_support"]; ok && len(v.([]interface{})) > 0 { + np.MaintenancePolicy = &container.NodePoolMaintenancePolicy{ + ExclusionUntilEndOfSupport: &container.ExclusionUntilEndOfSupport {}, + } + exclusionUntilEndOfSupport := v.([]interface{})[0].(map[string]interface{}) + if v, ok := exclusionUntilEndOfSupport["enabled"]; ok { + np.MaintenancePolicy.ExclusionUntilEndOfSupport.Enabled = v.(bool) + } + } + } + return np, nil } @@ -1423,6 +1470,30 @@ func flattenNodePoolNodeDrainConfig(ndc *container.NodeDrainConfig) []map[string return []map[string]interface{}{nodeDrainConfig} } +func flattenExlusionUntilEndOfSupport(ex *container.ExclusionUntilEndOfSupport) []map[string]interface{} { + if ex == nil { + return nil + } + return []map[string]interface{}{ + { + "enabled": ex.Enabled, + "start_time": ex.StartTime, + "end_time": ex.EndTime, + }, + } +} + +func flattenNodePoolMaintenancePolicy(mp *container.NodePoolMaintenancePolicy) []map[string]interface{} { + if mp == nil { + return nil + } + + maintenancePolicy := make(map[string]interface{}) + + maintenancePolicy["exclusion_until_end_of_support"] = flattenExlusionUntilEndOfSupport(mp.ExclusionUntilEndOfSupport) + return []map[string]interface{}{maintenancePolicy} +} + func flattenNodePool(d *schema.ResourceData, config *transport_tpg.Config, np *container.NodePool, prefix string) (map[string]interface{}, error) { userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) if err != nil { @@ -1551,6 +1622,10 @@ func flattenNodePool(d *schema.ResourceData, config *transport_tpg.Config, np *c nodePool["node_drain_config"] = flattenNodePoolNodeDrainConfig(np.NodeDrainConfig) } + if np.MaintenancePolicy != nil { + nodePool["maintenance_policy"] = flattenNodePoolMaintenancePolicy(np.MaintenancePolicy) + } + return nodePool, nil } diff --git a/mmv1/third_party/terraform/services/container/resource_container_node_pool_meta.yaml.tmpl b/mmv1/third_party/terraform/services/container/resource_container_node_pool_meta.yaml.tmpl index 6276135c40aa..6579594519fa 100644 --- a/mmv1/third_party/terraform/services/container/resource_container_node_pool_meta.yaml.tmpl +++ b/mmv1/third_party/terraform/services/container/resource_container_node_pool_meta.yaml.tmpl @@ -220,6 +220,12 @@ fields: api_field: 'config.linuxNodeConfig.accurateTimeConfig.enablePtpKvmTimeSync' - field: 'node_config.linux_node_config.cgroup_mode' api_field: 'config.linuxNodeConfig.cgroupMode' + - field: 'node_config.linux_node_config.custom_node_init.init_script.gcp_secret_manager_secret_uri' + api_field: 'config.linuxNodeConfig.customNodeInit.initScript.gcpSecretManagerSecretUri' + - field: 'node_config.linux_node_config.custom_node_init.init_script.gcs_generation' + api_field: 'config.linuxNodeConfig.customNodeInit.initScript.gcsGeneration' + - field: 'node_config.linux_node_config.custom_node_init.init_script.gcs_uri' + api_field: 'config.linuxNodeConfig.customNodeInit.initScript.gcsUri' - field: 'node_config.linux_node_config.hugepages_config.hugepage_size_1g' api_field: 'config.linuxNodeConfig.hugepages.hugepageSize1g' - field: 'node_config.linux_node_config.hugepages_config.hugepage_size_2m' @@ -329,6 +335,9 @@ fields: - api_field: 'nodeDrainConfig.graceTerminationDuration' - api_field: 'nodeDrainConfig.pdbTimeoutDuration' - api_field: 'nodeDrainConfig.respectPdbDuringNodePoolDeletion' + - api_field: 'maintenancePolicy.exclusionUntilEndOfSupport.enabled' + - api_field: 'maintenancePolicy.exclusionUntilEndOfSupport.startTime' + - api_field: 'maintenancePolicy.exclusionUntilEndOfSupport.endTime' - field: 'node_count' provider_only: true - field: 'node_locations' diff --git a/mmv1/third_party/terraform/services/container/resource_container_node_pool_test.go.tmpl b/mmv1/third_party/terraform/services/container/resource_container_node_pool_test.go.tmpl index 5c942156fba0..c0a923860c82 100644 --- a/mmv1/third_party/terraform/services/container/resource_container_node_pool_test.go.tmpl +++ b/mmv1/third_party/terraform/services/container/resource_container_node_pool_test.go.tmpl @@ -780,6 +780,38 @@ func TestAccContainerNodePool_withReservationAffinitySpecific(t *testing.T) { }) } +func TestAccContainerNodePool_withReservationAffinityAnyReservationThenFail(t *testing.T) { + t.Parallel() + + cluster := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + reservation := fmt.Sprintf("tf-test-reservation-%s", acctest.RandString(t, 10)) + np := fmt.Sprintf("tf-test-np-%s", acctest.RandString(t, 10)) + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckContainerClusterDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerNodePool_withReservationAffinityAnyReservationThenFail(cluster, reservation, np, networkName, subnetworkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_container_node_pool.with_reservation_affinity", + "node_config.0.reservation_affinity.#", "1"), + resource.TestCheckResourceAttr("google_container_node_pool.with_reservation_affinity", + "node_config.0.reservation_affinity.0.consume_reservation_type", "ANY_RESERVATION_THEN_FAIL"), + ), + }, + { + ResourceName: "google_container_node_pool.with_reservation_affinity", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccContainerNodePool_withWorkloadIdentityConfig(t *testing.T) { t.Parallel() @@ -1726,6 +1758,34 @@ func TestAccContainerNodePool_withNodeDrainConfig(t *testing.T) { }) } +func TestAccContainerNodePool_withMaintenancePolicy(t *testing.T) { + t.Parallel() + + cluster := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + nodePool := fmt.Sprintf("tf-test-nodepool-%s", acctest.RandString(t, 10)) + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckContainerNodePoolDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerNodePool_withMaintenancePolicy(cluster, nodePool, networkName, subnetworkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_container_node_pool.np_with_maintenance_policy", "maintenance_policy.0.exclusion_until_end_of_support.0.enabled", "true"), + ), + }, + { + ResourceName: "google_container_node_pool.np_with_maintenance_policy", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccContainerNodePool_withAccurateTimeConfig(t *testing.T) { t.Parallel() @@ -3972,6 +4032,55 @@ resource "google_container_node_pool" "with_reservation_affinity" { `, cluster, networkName, subnetworkName, reservation, np) } +func testAccContainerNodePool_withReservationAffinityAnyReservationThenFail(cluster, reservation, np, networkName, subnetworkName string) string { + return fmt.Sprintf(` +data "google_container_engine_versions" "central1a" { + location = "us-central1-a" +} + +resource "google_container_cluster" "cluster" { + name = "%s" + location = "us-central1-a" + initial_node_count = 1 + min_master_version = data.google_container_engine_versions.central1a.latest_master_version + deletion_protection = false + network = "%s" + subnetwork = "%s" +} + +resource "google_compute_reservation" "gce_reservation" { + name = "%s" + zone = "us-central1-a" + + specific_reservation { + count = 1 + instance_properties { + machine_type = "n1-standard-1" + } + } + + specific_reservation_required = false +} + +resource "google_container_node_pool" "with_reservation_affinity" { + name = "%s" + location = "us-central1-a" + cluster = google_container_cluster.cluster.name + initial_node_count = 1 + node_config { + machine_type = "n1-standard-1" + oauth_scopes = [ + "https://www.googleapis.com/auth/logging.write", + "https://www.googleapis.com/auth/monitoring", + ] + reservation_affinity { + consume_reservation_type = "ANY_RESERVATION_THEN_FAIL" + } + } +} +`, cluster, networkName, subnetworkName, reservation, np) +} + func testAccContainerNodePool_withWorkloadMetadataConfig(cluster, np, networkName, subnetworkName string) string { return fmt.Sprintf(` @@ -5006,6 +5115,36 @@ resource "google_container_node_pool" "np_with_node_drain_config" { `, cluster, networkName, subnetworkName, np, privateName, privateVal) } +func testAccContainerNodePool_withMaintenancePolicy(cluster, np, networkName, subnetworkName string) string { + return fmt.Sprintf(` +data "google_container_engine_versions" "central1a" { + location = "us-central1-a" +} + +resource "google_container_cluster" "cluster" { + name = "%s" + location = "us-central1-a" + initial_node_count = 1 + min_master_version = data.google_container_engine_versions.central1a.latest_master_version + deletion_protection = false + network = "%s" + subnetwork = "%s" +} + +resource "google_container_node_pool" "np_with_maintenance_policy" { + name = "%s" + location = "us-central1-a" + cluster = google_container_cluster.cluster.name + initial_node_count = 1 + maintenance_policy { + exclusion_until_end_of_support { + enabled = true + } + } +} +`, cluster, networkName, subnetworkName, np) +} + func testAccContainerNodePool_withAccurateTimeConfig(cluster, np, networkName, subnetworkName string, enablePTP bool) string { return fmt.Sprintf(` data "google_container_engine_versions" "central1a" { @@ -7293,3 +7432,187 @@ resource "google_container_node_pool" "np" { } `, cluster, networkName, subnetworkName, np, behavior) } + +func TestAccContainerNodePool_withCustomNodeInit(t *testing.T) { + t.Parallel() + + cluster := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + nodePool := fmt.Sprintf("tf-test-nodepool-%s", acctest.RandString(t, 10)) + bucketName := fmt.Sprintf("tf-test-bucket-%s", acctest.RandString(t, 10)) + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckContainerNodePoolDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerNodePool_withCustomNodeInit(cluster, nodePool, bucketName, networkName, subnetworkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_container_node_pool.np_with_custom_node_init", "node_config.0.linux_node_config.0.custom_node_init.0.init_script.0.gcs_uri", fmt.Sprintf("gs://%s/script.sh", bucketName)), + resource.TestCheckResourceAttrSet("google_container_node_pool.np_with_custom_node_init", "node_config.0.linux_node_config.0.custom_node_init.0.init_script.0.gcs_generation"), + ), + }, + { + ResourceName: "google_container_node_pool.np_with_custom_node_init", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"initial_node_count", "cluster"}, + }, + }, + }) +} + +func testAccContainerNodePool_withCustomNodeInit(cluster, np, bucket, networkName, subnetworkName string) string { + return fmt.Sprintf(` +data "google_container_engine_versions" "central1a" { + location = "us-central1-a" +} + +resource "google_storage_bucket" "bucket" { + name = "%s" + location = "US" + force_destroy = true +} + +resource "google_storage_bucket_object" "script" { + name = "script.sh" + bucket = google_storage_bucket.bucket.name + content = "#!/bin/bash\necho 'Hello World' > /tmp/hello.txt" +} + +resource "google_container_cluster" "cluster" { + name = "%s" + location = "us-central1-a" + initial_node_count = 1 + min_master_version = data.google_container_engine_versions.central1a.latest_master_version + deletion_protection = false + network = "%s" + subnetwork = "%s" +} + +resource "google_container_node_pool" "np_with_custom_node_init" { + name = "%s" + location = "us-central1-a" + cluster = google_container_cluster.cluster.name + initial_node_count = 1 + node_config { + image_type = "COS_CONTAINERD" + oauth_scopes = [ + "https://www.googleapis.com/auth/cloud-platform", + ] + linux_node_config { + custom_node_init { + init_script { + gcs_uri = "gs://${google_storage_bucket.bucket.name}/${google_storage_bucket_object.script.name}" + gcs_generation = google_storage_bucket_object.script.generation + } + } + } + } +} +`, bucket, cluster, networkName, subnetworkName, np) +} + +func TestAccContainerNodePool_withCustomNodeInitSecret(t *testing.T) { + t.Parallel() + + cluster := fmt.Sprintf("tf-test-cluster-%s", acctest.RandString(t, 10)) + nodePool := fmt.Sprintf("tf-test-nodepool-%s", acctest.RandString(t, 10)) + secretId := fmt.Sprintf("tf-test-secret-%s", acctest.RandString(t, 10)) + networkName := tpgcompute.BootstrapSharedTestNetwork(t, "gke-cluster") + subnetworkName := tpgcompute.BootstrapSubnet(t, "gke-cluster", networkName) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + ExternalProviders: map[string]resource.ExternalProvider{ + "time": {}, + }, + CheckDestroy: testAccCheckContainerNodePoolDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccContainerNodePool_withCustomNodeInitSecret(cluster, nodePool, secretId, networkName, subnetworkName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("google_container_node_pool.np_with_custom_node_init_secret", "node_config.0.linux_node_config.0.custom_node_init.0.init_script.0.gcp_secret_manager_secret_uri"), + ), + }, + { + ResourceName: "google_container_node_pool.np_with_custom_node_init_secret", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"initial_node_count", "cluster"}, + }, + }, + }) +} + +func testAccContainerNodePool_withCustomNodeInitSecret(cluster, np, secretId, networkName, subnetworkName string) string { + return fmt.Sprintf(` +data "google_container_engine_versions" "central1a" { + location = "us-central1-a" +} + +data "google_project" "project" {} + +resource "google_secret_manager_secret" "secret" { + secret_id = "%s" + replication { + auto {} + } +} + +resource "google_secret_manager_secret_version" "version" { + secret = google_secret_manager_secret.secret.id + secret_data = "#!/bin/bash\necho 'Hello World' > /tmp/hello.txt" +} + +resource "google_secret_manager_secret_iam_member" "secret_accessor" { + secret_id = google_secret_manager_secret.secret.id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${data.google_project.project.number}-compute@developer.gserviceaccount.com" +} + +resource "google_container_cluster" "cluster" { + name = "%s" + location = "us-central1-a" + initial_node_count = 1 + min_master_version = data.google_container_engine_versions.central1a.latest_master_version + deletion_protection = false + network = "%s" + subnetwork = "%s" +} + +resource "time_sleep" "wait_60_seconds" { + create_duration = "60s" + depends_on = [ + google_secret_manager_secret_iam_member.secret_accessor + ] +} + +resource "google_container_node_pool" "np_with_custom_node_init_secret" { + name = "%s" + location = "us-central1-a" + cluster = google_container_cluster.cluster.name + initial_node_count = 1 + node_config { + image_type = "COS_CONTAINERD" + oauth_scopes = [ + "https://www.googleapis.com/auth/cloud-platform", + ] + linux_node_config { + custom_node_init { + init_script { + gcp_secret_manager_secret_uri = google_secret_manager_secret_version.version.name + } + } + } + } + + depends_on = [ + time_sleep.wait_60_seconds + ] +} +`, secretId, cluster, networkName, subnetworkName, np) +} diff --git a/mmv1/third_party/terraform/services/datacatalog/data_source_google_data_catalog_taxonomy.go b/mmv1/third_party/terraform/services/datacatalog/data_source_google_data_catalog_taxonomy.go new file mode 100644 index 000000000000..17dc2edb308c --- /dev/null +++ b/mmv1/third_party/terraform/services/datacatalog/data_source_google_data_catalog_taxonomy.go @@ -0,0 +1,127 @@ +package datacatalog + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-google/google/registry" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +func DataSourceGoogleDataCatalogTaxonomy() *schema.Resource { + return &schema.Resource{ + Read: dataSourceGoogleDataCatalogTaxonomyRead, + + Schema: map[string]*schema.Schema{ + "display_name": { + Type: schema.TypeString, + Required: true, + }, + "region": { + Type: schema.TypeString, + Required: true, + }, + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "activated_policy_types": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} + +func dataSourceGoogleDataCatalogTaxonomyRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + project, err := tpgresource.GetProject(d, config) + if err != nil { + return err + } + + region := d.Get("region").(string) + displayName := d.Get("display_name").(string) + url := fmt.Sprintf("%sprojects/%s/locations/%s/taxonomies", transport_tpg.BaseUrl(Product, config), project, region) + + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + RawURL: url, + UserAgent: userAgent, + }) + if err != nil { + return err + } + + taxonomiesRaw, ok := res["taxonomies"] + if !ok { + return fmt.Errorf("No taxonomy found with display_name %q in project %s, region %s", displayName, project, region) + } + + taxonomies, ok := taxonomiesRaw.([]interface{}) + if !ok { + return fmt.Errorf("unexpected taxonomies response format") + } + + for _, taxonomyRaw := range taxonomies { + taxonomy, ok := taxonomyRaw.(map[string]interface{}) + if !ok { + continue + } + + if taxonomy["displayName"] == displayName { + d.SetId(taxonomy["name"].(string)) + + if err := d.Set("name", taxonomy["name"]); err != nil { + return fmt.Errorf("Error setting name: %s", err) + } + if err := d.Set("display_name", taxonomy["displayName"]); err != nil { + return fmt.Errorf("Error setting display_name: %s", err) + } + if err := d.Set("description", taxonomy["description"]); err != nil { + return fmt.Errorf("Error setting description: %s", err) + } + if err := d.Set("activated_policy_types", taxonomy["activatedPolicyTypes"]); err != nil { + return fmt.Errorf("Error setting activated_policy_types: %s", err) + } + if err := d.Set("project", project); err != nil { + return fmt.Errorf("Error setting project: %s", err) + } + if err := d.Set("region", region); err != nil { + return fmt.Errorf("Error setting region: %s", err) + } + + return nil + } + } + + return fmt.Errorf("No taxonomy found with display_name %q in project %s, region %s", displayName, project, region) +} + +func init() { + registry.Schema{ + Name: "google_data_catalog_taxonomy", + ProductName: "datacatalog", + Type: registry.SchemaTypeDataSource, + Schema: DataSourceGoogleDataCatalogTaxonomy(), + }.Register() +} diff --git a/mmv1/third_party/terraform/services/datacatalog/data_source_google_data_catalog_taxonomy_test.go b/mmv1/third_party/terraform/services/datacatalog/data_source_google_data_catalog_taxonomy_test.go new file mode 100644 index 000000000000..906ec4423c0d --- /dev/null +++ b/mmv1/third_party/terraform/services/datacatalog/data_source_google_data_catalog_taxonomy_test.go @@ -0,0 +1,51 @@ +package datacatalog_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-google/google/acctest" + _ "github.com/hashicorp/terraform-provider-google/google/services/datacatalog" +) + +func TestAccDataSourceGoogleDataCatalogTaxonomy_basic(t *testing.T) { + t.Parallel() + + randomSuffix := acctest.RandString(t, 10) + + context := map[string]interface{}{ + "random_suffix": randomSuffix, + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccDataSourceGoogleDataCatalogTaxonomy_basic(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair("data.google_data_catalog_taxonomy.test", "name", "google_data_catalog_taxonomy.test", "name"), + resource.TestCheckResourceAttrPair("data.google_data_catalog_taxonomy.test", "description", "google_data_catalog_taxonomy.test", "description"), + resource.TestCheckResourceAttrPair("data.google_data_catalog_taxonomy.test", "activated_policy_types.#", "google_data_catalog_taxonomy.test", "activated_policy_types.#"), + resource.TestCheckResourceAttrPair("data.google_data_catalog_taxonomy.test", "project", "google_data_catalog_taxonomy.test", "project"), + ), + }, + }, + }) +} + +func testAccDataSourceGoogleDataCatalogTaxonomy_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_data_catalog_taxonomy" "test" { + display_name = "tf_test_%{random_suffix}" + description = "A test taxonomy" + activated_policy_types = ["FINE_GRAINED_ACCESS_CONTROL"] + region = "us-central1" +} + +data "google_data_catalog_taxonomy" "test" { + display_name = google_data_catalog_taxonomy.test.display_name + region = google_data_catalog_taxonomy.test.region +} +`, context) +} diff --git a/mmv1/third_party/terraform/services/datalossprevention/resource_data_loss_prevention_inspect_template_test.go b/mmv1/third_party/terraform/services/datalossprevention/resource_data_loss_prevention_inspect_template_test.go index f551007bccca..c86d591671a0 100644 --- a/mmv1/third_party/terraform/services/datalossprevention/resource_data_loss_prevention_inspect_template_test.go +++ b/mmv1/third_party/terraform/services/datalossprevention/resource_data_loss_prevention_inspect_template_test.go @@ -1334,3 +1334,116 @@ resource "google_data_loss_prevention_inspect_template" "basic" { } `, context) } + +func TestAccDataLossPreventionInspectTemplate_dlpInspectTemplate_minLikelihoodPerInfoType(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "project": envvar.GetTestProjectFromEnv(), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckDataLossPreventionInspectTemplateDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccDataLossPreventionInspectTemplate_dlpInspectTemplate_withMinLikelihoodPerInfoType(context), + }, + { + ResourceName: "google_data_loss_prevention_inspect_template.basic", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccDataLossPreventionInspectTemplate_dlpInspectTemplate_withMinLikelihoodPerInfoTypeUpdate(context), + }, + { + ResourceName: "google_data_loss_prevention_inspect_template.basic", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccDataLossPreventionInspectTemplate_dlpInspectTemplate_withMinLikelihoodPerInfoType(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_data_loss_prevention_inspect_template" "basic" { + parent = "projects/%{project}" + description = "Description" + display_name = "Display" + + inspect_config { + info_types { + name = "EMAIL_ADDRESS" + } + info_types { + name = "PERSON_NAME" + version = "latest" + } + info_types { + name = "LAST_NAME" + } + info_types { + name = "DOMAIN_NAME" + } + info_types { + name = "PHONE_NUMBER" + } + info_types { + name = "FIRST_NAME" + } + min_likelihood_per_info_type { + info_type { + name = "PERSON_NAME" + version = "latest" + } + min_likelihood = "UNLIKELY" + } + } +} +`, context) +} + +func testAccDataLossPreventionInspectTemplate_dlpInspectTemplate_withMinLikelihoodPerInfoTypeUpdate(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_data_loss_prevention_inspect_template" "basic" { + parent = "projects/%{project}" + description = "Updated" + display_name = "Different" + + inspect_config { + info_types { + name = "PERSON_NAME" + version = "latest" + } + info_types { + name = "LAST_NAME" + } + info_types { + name = "DOMAIN_NAME" + } + info_types { + name = "PHONE_NUMBER" + } + info_types { + name = "FIRST_NAME" + } + min_likelihood_per_info_type { + info_type { + name = "PERSON_NAME" + version = "latest" + } + min_likelihood = "POSSIBLE" + } + min_likelihood_per_info_type { + info_type { + name = "PHONE_NUMBER" + } + min_likelihood = "VERY_LIKELY" + } + } +} +`, context) +} diff --git a/mmv1/third_party/terraform/services/datastream/resource_datastream_connection_profile_test.go b/mmv1/third_party/terraform/services/datastream/resource_datastream_connection_profile_test.go index f09d9bd35552..a608bd6906c7 100644 --- a/mmv1/third_party/terraform/services/datastream/resource_datastream_connection_profile_test.go +++ b/mmv1/third_party/terraform/services/datastream/resource_datastream_connection_profile_test.go @@ -730,7 +730,8 @@ resource "google_datastream_connection_profile" "default" { secret_manager_stored_client_key = google_secret_manager_secret_version.client_key_secret_version.id } additional_options = { - readPreference = "secondary" + readPreference = "primary" // <-- Changed + connectTimeoutMS = "5000" // <-- Added } standard_connection_format { direct_connection = true diff --git a/mmv1/third_party/terraform/services/dns/list_google_dns_managed_zone.go b/mmv1/third_party/terraform/services/dns/list_google_dns_managed_zone.go new file mode 100644 index 000000000000..c038fdc7a85e --- /dev/null +++ b/mmv1/third_party/terraform/services/dns/list_google_dns_managed_zone.go @@ -0,0 +1,163 @@ +package dns + +import ( + "context" + "errors" + "fmt" + + frameworkdiag "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/hashicorp/terraform-provider-google/google/registry" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +var errStreamClosed = errors.New("stream closed") + +func init() { + registry.FrameworkListResource{ + Name: "google_dns_managed_zone", + ProductName: "dns", + Func: NewGoogleDnsManagedZoneListResource, + }.Register() +} + +type GoogleDnsManagedZoneResource struct { + tpgresource.ListResourceMetadata +} + +type GoogleDnsManagedZoneListModel struct { + Project types.String `tfsdk:"project"` +} + +func NewGoogleDnsManagedZoneListResource() list.ListResource { + listR := &GoogleDnsManagedZoneResource{} + listR.TypeName = "google_dns_managed_zone" + listR.SDKv2Resource = ResourceDNSManagedZone() + listR.ListConfigFields = []tpgresource.ListConfigField{{Name: "project", Kind: tpgresource.ListConfigKindString, Optional: true}} + return listR +} + +func (listR *GoogleDnsManagedZoneResource) List(ctx context.Context, req list.ListRequest, stream *list.ListResultsStream) { + var data GoogleDnsManagedZoneListModel + diags := req.Config.Get(ctx, &data) + + if diags.HasError() { + stream.Results = list.ListResultsStreamDiagnostics(diags) + return + } + + if listR.Client == nil { + diags = append(diags, frameworkdiag.NewErrorDiagnostic( + "provider not configured", + "The Google provider client is not available; ensure the provider is configured.", + )) + stream.Results = list.ListResultsStreamDiagnostics(diags) + return + } + + tempData := listR.SDKv2Resource.Data(&terraform.InstanceState{}) + if !data.Project.IsNull() && !data.Project.IsUnknown() { + if err := tempData.Set("project", data.Project.ValueString()); err != nil { + diags.AddError("Failed to set project", err.Error()) + stream.Results = list.ListResultsStreamDiagnostics(diags) + return + } + } + + project, err := tpgresource.GetProject(tempData, listR.Client) + if err != nil { + diags.AddError("Failed to resolve project", err.Error()) + stream.Results = list.ListResultsStreamDiagnostics(diags) + return + } + + stream.Results = func(push func(list.ListResult) bool) { + var streamDiags frameworkdiag.Diagnostics + + err := ListDnsManagedZones(listR.Client, project, func(rd *schema.ResourceData) error { + result := req.NewListResult(ctx) + + if err := listR.SetResult(ctx, req.IncludeResource, &result, rd, "name"); err != nil { + streamDiags.AddError("Failed to set result", err.Error()) + return err + } + + if !push(result) { + // A closed stream means the consumer has enough results; stop cleanly. + return errStreamClosed + } + return nil + }) + + if err != nil { + // Stream closure is expected and should not be reported as an API error. + if errors.Is(err, errStreamClosed) { + return + } + streamDiags.AddError("API Error listing DNS managed zones", fmt.Sprintf("Failed to list DNS managed zones in project %q: %v", project, err)) + result := req.NewListResult(ctx) + result.Diagnostics = streamDiags + push(result) + return + } + + if streamDiags.HasError() { + stream.Results = list.ListResultsStreamDiagnostics(streamDiags) + } + } +} + +func ListDnsManagedZones(config *transport_tpg.Config, project string, callback func(*schema.ResourceData) error) error { + if config == nil { + return fmt.Errorf("provider client is not configured") + } + + managedZoneSchema := ResourceDNSManagedZone() + tempData := managedZoneSchema.Data(&terraform.InstanceState{}) + if project != "" { + if err := tempData.Set("project", project); err != nil { + return fmt.Errorf("error setting project on temporary resource data: %w", err) + } + } + + userAgent, err := tpgresource.GenerateUserAgentString(tempData, config.UserAgent) + if err != nil { + return err + } + + url, err := tpgresource.ReplaceVars(tempData, config, "{{DNSBasePath}}projects/{{project}}/managedZones") + if err != nil { + return err + } + + return transport_tpg.ListPages(transport_tpg.ListPagesOptions{ + Config: config, + TempData: tempData, + Resource: managedZoneSchema, + ListURL: url, + Filter: "", + ItemName: "managedZones", + UserAgent: userAgent, + Flattener: func(res map[string]interface{}, d *schema.ResourceData, config *transport_tpg.Config) error { + name, _ := res["name"].(string) + + d.SetId(fmt.Sprintf("projects/%s/managedZones/%s", project, name)) + + if err := ResourceDNSManagedZoneFlatten(d, config, res, config, project, "", "", "", nil); err != nil { + return err + } + + if err := d.Set("project", project); err != nil { + return fmt.Errorf("error setting project: %w", err) + } + + return nil + }, + Callback: callback, + }) +} diff --git a/mmv1/third_party/terraform/services/dns/list_google_dns_managed_zone_test.go b/mmv1/third_party/terraform/services/dns/list_google_dns_managed_zone_test.go new file mode 100644 index 000000000000..6f9ab89f7e33 --- /dev/null +++ b/mmv1/third_party/terraform/services/dns/list_google_dns_managed_zone_test.go @@ -0,0 +1,112 @@ +package dns_test + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" + tpgdns "github.com/hashicorp/terraform-provider-google/google/services/dns" +) + +func TestAccDnsManagedZoneListResource_queryIdentity(t *testing.T) { + t.Parallel() + + zoneName := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10)) + dnsName := fmt.Sprintf("tf-test-%s.hashicorptest.com.", acctest.RandString(t, 10)) + project := envvar.GetTestProjectFromEnv() + t.Logf("Using project %s for testing", project) + + acctest.VcrTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccDnsManagedZoneListResource_basic(zoneName, dnsName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_dns_managed_zone.foobar", "project", project), + resource.TestCheckResourceAttr("google_dns_managed_zone.foobar", "name", zoneName), + resource.TestCheckResourceAttr("google_dns_managed_zone.foobar", "dns_name", dnsName), + testAccCheckDnsManagedZoneVisibleInList(t, project, zoneName), + ), + }, + { + Query: true, + Config: testAccDnsManagedZoneListQuery(project), + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectLengthAtLeast("google_dns_managed_zone.all", 1), + querycheck.ExpectIdentity("google_dns_managed_zone.all", map[string]knownvalue.Check{ + "name": knownvalue.StringExact(zoneName), + "project": knownvalue.StringExact(project), + }), + }, + }, + }, + }) +} + +func testAccCheckDnsManagedZoneVisibleInList(t *testing.T, project, zoneName string) resource.TestCheckFunc { + return func(_ *terraform.State) error { + config := acctest.GoogleProviderConfig(t) + + observedNames := make([]string, 0) + err := resource.Retry(30*time.Second, func() *resource.RetryError { + resp, err := tpgdns.NewClient(config, config.UserAgent).ManagedZones.List(project).Do() + if err != nil { + return resource.RetryableError(fmt.Errorf("error listing managed zones for project %q: %w", project, err)) + } + + observedNames = observedNames[:0] + for _, zone := range resp.ManagedZones { + if zone == nil { + continue + } + observedNames = append(observedNames, zone.Name) + if zone.Name == zoneName { + return nil + } + } + + return resource.RetryableError(fmt.Errorf("managed zone %q not visible yet; currently observed zones: %v", zoneName, observedNames)) + }) + if err != nil { + return fmt.Errorf("managed zone %q did not become query-visible before list step in project %q; last observed zones: %v; retry error: %w", zoneName, project, observedNames, err) + } + + return nil + } +} + +func testAccDnsManagedZoneListResource_basic(name, dnsName string) string { + return fmt.Sprintf(` +resource "google_dns_managed_zone" "foobar" { + name = %q + dns_name = %q + visibility = "public" +} +`, name, dnsName) +} + +func testAccDnsManagedZoneListQuery(project string) string { + return fmt.Sprintf(` +provider "google" {} + +list "google_dns_managed_zone" "all" { + provider = google + limit = 1000 + config { + project = %q + } +} +`, project) +} diff --git a/mmv1/third_party/terraform/services/eventarc/flatten_eventarc_trigger_conditions_test.go b/mmv1/third_party/terraform/services/eventarc/flatten_eventarc_trigger_conditions_test.go new file mode 100644 index 000000000000..b81b7fee0b28 --- /dev/null +++ b/mmv1/third_party/terraform/services/eventarc/flatten_eventarc_trigger_conditions_test.go @@ -0,0 +1,26 @@ +package eventarc + +import ( + "testing" +) + +func TestFlattenEventarcTriggerConditions(t *testing.T) { + mockApiResponse := map[string]interface{}{ + "transport.pubsub.topic": map[string]interface{}{ + "code": "UNKNOWN", + "message": "Pub/Sub topic status is unknown. Try requesting the trigger description again.", + }, + } + + result := flattenEventarcTriggerConditions(mockApiResponse, nil, nil) + + flatResult, ok := result.(map[string]string) + if !ok { + t.Fatalf("Flattener returned %T, expected map[string]string", result) + } + + expectedMessage := "Pub/Sub topic status is unknown. Try requesting the trigger description again." + if flatResult["transport.pubsub.topic"] != expectedMessage { + t.Fatalf("Expected message %q, got %q", expectedMessage, flatResult["transport.pubsub.topic"]) + } +} diff --git a/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_cloud_vm_clusters.go b/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_cloud_vm_clusters.go index 442d12886e65..2276dadfce44 100644 --- a/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_cloud_vm_clusters.go +++ b/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_cloud_vm_clusters.go @@ -96,18 +96,19 @@ func flattenOracleDatabaseCloudVmClusters(v interface{}, d *schema.ResourceData, for _, raw := range l { original := raw.(map[string]interface{}) transformed = append(transformed, map[string]interface{}{ - "name": flattenOracleDatabaseCloudVmClusterName(original["name"], d, config), - "exadata_infrastructure": flattenOracleDatabaseCloudVmClusterExadataInfrastructure(original["exadataInfrastructure"], d, config), - "display_name": flattenOracleDatabaseCloudVmClusterDisplayName(original["displayName"], d, config), - "gcp_oracle_zone": flattenOracleDatabaseCloudVmClusterGcpOracleZone(original["gcpOracleZone"], d, config), - "properties": flattenOracleDatabaseCloudVmClusterProperties(original["properties"], d, config), - "labels": flattenOracleDatabaseCloudVmClusterLabels(original["labels"], d, config), - "create_time": flattenOracleDatabaseCloudVmClusterCreateTime(original["createTime"], d, config), - "cidr": flattenOracleDatabaseCloudVmClusterCidr(original["cidr"], d, config), - "backup_subnet_cidr": flattenOracleDatabaseCloudVmClusterBackupSubnetCidr(original["backupSubnetCidr"], d, config), - "network": flattenOracleDatabaseCloudVmClusterNetwork(original["network"], d, config), - "terraform_labels": flattenOracleDatabaseCloudVmClusterTerraformLabels(original["labels"], d, config), - "effective_labels": flattenOracleDatabaseCloudVmClusterEffectiveLabels(original["labels"], d, config), + "name": flattenOracleDatabaseCloudVmClusterName(original["name"], d, config), + "exadata_infrastructure": flattenOracleDatabaseCloudVmClusterExadataInfrastructure(original["exadataInfrastructure"], d, config), + "display_name": flattenOracleDatabaseCloudVmClusterDisplayName(original["displayName"], d, config), + "gcp_oracle_zone": flattenOracleDatabaseCloudVmClusterGcpOracleZone(original["gcpOracleZone"], d, config), + "properties": flattenOracleDatabaseCloudVmClusterProperties(original["properties"], d, config), + "labels": flattenOracleDatabaseCloudVmClusterLabels(original["labels"], d, config), + "create_time": flattenOracleDatabaseCloudVmClusterCreateTime(original["createTime"], d, config), + "cidr": flattenOracleDatabaseCloudVmClusterCidr(original["cidr"], d, config), + "backup_subnet_cidr": flattenOracleDatabaseCloudVmClusterBackupSubnetCidr(original["backupSubnetCidr"], d, config), + "network": flattenOracleDatabaseCloudVmClusterNetwork(original["network"], d, config), + "exascale_db_storage_vault": flattenOracleDatabaseCloudVmClusterExascaleDbStorageVault(original["exascaleDbStorageVault"], d, config), + "terraform_labels": flattenOracleDatabaseCloudVmClusterTerraformLabels(original["labels"], d, config), + "effective_labels": flattenOracleDatabaseCloudVmClusterEffectiveLabels(original["labels"], d, config), }) } return transformed diff --git a/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vault.go b/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vault.go new file mode 100644 index 000000000000..e3bcef5d36c3 --- /dev/null +++ b/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vault.go @@ -0,0 +1,49 @@ +package oracledatabase + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-google/google/registry" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +func DataSourceOracleDatabaseExascaleDbStorageVault() *schema.Resource { + dsSchema := tpgresource.DatasourceSchemaFromResourceSchema(ResourceOracleDatabaseExascaleDbStorageVault().Schema) + tpgresource.AddRequiredFieldsToSchema(dsSchema, "location", "exascale_db_storage_vault_id") + tpgresource.AddOptionalFieldsToSchema(dsSchema, "project") + return &schema.Resource{ + Read: dataSourceOracleDatabaseExascaleDbStorageVaultRead, + Schema: dsSchema, + } +} + +func dataSourceOracleDatabaseExascaleDbStorageVaultRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + + id, err := tpgresource.ReplaceVars(d, config, "projects/{{project}}/locations/{{location}}/exascaleDbStorageVaults/{{exascale_db_storage_vault_id}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + err = resourceOracleDatabaseExascaleDbStorageVaultRead(d, meta) + if err != nil { + return err + } + if d.Id() == "" { + return fmt.Errorf("%s not found", id) + } + return nil + + return nil +} + +func init() { + registry.Schema{ + Name: "google_oracle_database_exascale_db_storage_vault", + ProductName: "oracledatabase", + Type: registry.SchemaTypeDataSource, + Schema: DataSourceOracleDatabaseExascaleDbStorageVault(), + }.Register() +} diff --git a/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vault_test.go b/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vault_test.go new file mode 100644 index 000000000000..9041fe293394 --- /dev/null +++ b/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vault_test.go @@ -0,0 +1,60 @@ +package oracledatabase_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-google/google/acctest" + _ "github.com/hashicorp/terraform-provider-google/google/services/oracledatabase" +) + +func TestAccOracleDatabaseExascaleDbStorageVaultDataSource_basic(t *testing.T) { + t.Parallel() + + vaultId := fmt.Sprintf("ofake-tf-test-vault-ds-%s", acctest.RandString(t, 10)) + + context := map[string]interface{}{ + "vault_id": vaultId, + "project": "oci-terraform-testing-prod", + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccOracleDatabaseExascaleDbStorageVaultDataSource_basic(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("data.google_oracle_database_exascale_db_storage_vault.ds_vault", "name"), + resource.TestCheckResourceAttrSet("data.google_oracle_database_exascale_db_storage_vault.ds_vault", "display_name"), + resource.TestCheckResourceAttr("data.google_oracle_database_exascale_db_storage_vault.ds_vault", "display_name", fmt.Sprintf("%s displayname", vaultId)), + resource.TestCheckResourceAttr("data.google_oracle_database_exascale_db_storage_vault.ds_vault", "properties.0.exascale_db_storage_details.0.total_size_gbs", "512"), + ), + }, + }, + }) +} + +func testAccOracleDatabaseExascaleDbStorageVaultDataSource_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_oracle_database_exascale_db_storage_vault" "vault" { + exascale_db_storage_vault_id = "%{vault_id}" + display_name = "%{vault_id} displayname" + location = "us-east4" + project = "%{project}" + properties { + exascale_db_storage_details { + total_size_gbs = 512 + } + } + deletion_protection = false +} + +data "google_oracle_database_exascale_db_storage_vault" "ds_vault" { + exascale_db_storage_vault_id = google_oracle_database_exascale_db_storage_vault.vault.exascale_db_storage_vault_id + location = google_oracle_database_exascale_db_storage_vault.vault.location + project = google_oracle_database_exascale_db_storage_vault.vault.project +} +`, context) +} diff --git a/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vaults.go b/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vaults.go new file mode 100644 index 000000000000..5e3bd92b8d20 --- /dev/null +++ b/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vaults.go @@ -0,0 +1,118 @@ +package oracledatabase + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-google/google/registry" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +func DataSourceOracleDatabaseExascaleDbStorageVaults() *schema.Resource { + dsSchema := map[string]*schema.Schema{ + "project": { + Type: schema.TypeString, + Optional: true, + Description: "The ID of the project in which the dataset is located. If it is not provided, the provider project is used.", + }, + "location": { + Type: schema.TypeString, + Required: true, + Description: "location", + }, + "exascale_db_storage_vaults": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: tpgresource.DatasourceSchemaFromResourceSchema(ResourceOracleDatabaseExascaleDbStorageVault().Schema), + }, + }, + } + return &schema.Resource{ + Read: dataSourceOracleDatabaseExascaleDbStorageVaultsRead, + Schema: dsSchema, + } +} + +func dataSourceOracleDatabaseExascaleDbStorageVaultsRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + url, err := tpgresource.ReplaceVars(d, config, transport_tpg.BaseUrl(Product, config)+"projects/{{project}}/locations/{{location}}/exascaleDbStorageVaults") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + + billingProject := "" + project, err := tpgresource.GetProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for exascaleDbStorageVaults: %s", err) + } + billingProject = project + if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp + } + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + }) + + if err != nil { + return fmt.Errorf("Error reading exascaleDbStorageVaults: %s", err) + } + + if err := d.Set("project", project); err != nil { + return fmt.Errorf("Error setting exascaleDbStorageVaults project: %s", err) + } + + if err := d.Set("exascale_db_storage_vaults", flattenOracleDatabaseExascaleDbStorageVaults(res["exascaleDbStorageVaults"], d, config)); err != nil { + return fmt.Errorf("Error setting exascaleDbStorageVaults: %s", err) + } + + id, err := tpgresource.ReplaceVars(d, config, "projects/{{project}}/locations/{{location}}/exascaleDbStorageVaults") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + return nil +} + +func flattenOracleDatabaseExascaleDbStorageVaults(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) []map[string]interface{} { + if v == nil { + return nil + } + l := v.([]interface{}) + transformed := make([]map[string]interface{}, 0) + for _, raw := range l { + original := raw.(map[string]interface{}) + transformed = append(transformed, map[string]interface{}{ + "name": flattenOracleDatabaseExascaleDbStorageVaultName(original["name"], d, config), + "display_name": flattenOracleDatabaseExascaleDbStorageVaultDisplayName(original["displayName"], d, config), + "gcp_oracle_zone": flattenOracleDatabaseExascaleDbStorageVaultGcpOracleZone(original["gcpOracleZone"], d, config), + "entitlement_id": flattenOracleDatabaseExascaleDbStorageVaultEntitlementId(original["entitlementId"], d, config), + "properties": flattenOracleDatabaseExascaleDbStorageVaultProperties(original["properties"], d, config), + "labels": flattenOracleDatabaseExascaleDbStorageVaultLabels(original["labels"], d, config), + "create_time": flattenOracleDatabaseExascaleDbStorageVaultCreateTime(original["createTime"], d, config), + "exadata_infrastructure": flattenOracleDatabaseExascaleDbStorageVaultExadataInfrastructure(original["exadataInfrastructure"], d, config), + "terraform_labels": flattenOracleDatabaseExascaleDbStorageVaultTerraformLabels(original["labels"], d, config), + "effective_labels": flattenOracleDatabaseExascaleDbStorageVaultEffectiveLabels(original["labels"], d, config), + }) + } + return transformed +} + +func init() { + registry.Schema{ + Name: "google_oracle_database_exascale_db_storage_vaults", + ProductName: "oracledatabase", + Type: registry.SchemaTypeDataSource, + Schema: DataSourceOracleDatabaseExascaleDbStorageVaults(), + }.Register() +} diff --git a/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vaults_test.go b/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vaults_test.go new file mode 100644 index 000000000000..3e4712c01f8a --- /dev/null +++ b/mmv1/third_party/terraform/services/oracledatabase/data_source_oracle_database_exascale_db_storage_vaults_test.go @@ -0,0 +1,59 @@ +package oracledatabase_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-google/google/acctest" + _ "github.com/hashicorp/terraform-provider-google/google/services/oracledatabase" +) + +func TestAccOracleDatabaseExascaleDbStorageVaultsDataSource_basic(t *testing.T) { + t.Parallel() + + vaultId := fmt.Sprintf("ofake-tf-test-vault-ds-%s", acctest.RandString(t, 10)) + + context := map[string]interface{}{ + "vault_id": vaultId, + "project": "oci-terraform-testing-prod", + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccOracleDatabaseExascaleDbStorageVaultsDataSource_basic(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("data.google_oracle_database_exascale_db_storage_vaults.ds_vaults", "exascale_db_storage_vaults.#"), + resource.TestCheckResourceAttrSet("data.google_oracle_database_exascale_db_storage_vaults.ds_vaults", "exascale_db_storage_vaults.0.name"), + resource.TestCheckResourceAttrSet("data.google_oracle_database_exascale_db_storage_vaults.ds_vaults", "exascale_db_storage_vaults.0.display_name"), + resource.TestCheckResourceAttrSet("data.google_oracle_database_exascale_db_storage_vaults.ds_vaults", "exascale_db_storage_vaults.0.properties.#"), + ), + }, + }, + }) +} + +func testAccOracleDatabaseExascaleDbStorageVaultsDataSource_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_oracle_database_exascale_db_storage_vault" "vault" { + exascale_db_storage_vault_id = "%{vault_id}" + display_name = "%{vault_id} displayname" + location = "us-east4" + project = "%{project}" + properties { + exascale_db_storage_details { + total_size_gbs = 512 + } + } + deletion_protection = false +} + +data "google_oracle_database_exascale_db_storage_vaults" "ds_vaults" { + location = google_oracle_database_exascale_db_storage_vault.vault.location + project = google_oracle_database_exascale_db_storage_vault.vault.project +} +`, context) +} diff --git a/mmv1/third_party/terraform/services/privilegedaccessmanager/resource_privileged_access_manager_entitlement_test.go.tmpl b/mmv1/third_party/terraform/services/privilegedaccessmanager/resource_privileged_access_manager_entitlement_test.go.tmpl index e5ae3f989288..37a3638f4a08 100644 --- a/mmv1/third_party/terraform/services/privilegedaccessmanager/resource_privileged_access_manager_entitlement_test.go.tmpl +++ b/mmv1/third_party/terraform/services/privilegedaccessmanager/resource_privileged_access_manager_entitlement_test.go.tmpl @@ -10,6 +10,32 @@ import ( "github.com/hashicorp/terraform-provider-google/google/envvar" ) +func TestAccPrivilegedAccessManagerEntitlement_noApprovalWorkflow(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + "project_name": envvar.GetTestProjectFromEnv(), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckPrivilegedAccessManagerEntitlementDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccPrivilegedAccessManagerEntitlement_noApprovalWorkflow(context), + }, + { + ResourceName: "google_privileged_access_manager_entitlement.tfentitlement", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"location", "entitlement_id", "parent"}, + }, + }, + }) +} + func TestAccPrivilegedAccessManagerEntitlement_privilegedAccessManagerEntitlementProjectExample_update(t *testing.T) { t.Parallel() @@ -203,3 +229,29 @@ resource "google_privileged_access_manager_entitlement" "tfentitlement" { } `, context) } + +func testAccPrivilegedAccessManagerEntitlement_noApprovalWorkflow(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_privileged_access_manager_entitlement" "tfentitlement" { + entitlement_id = "tf-test-example-entitlement%{random_suffix}" + location = "global" + max_request_duration = "43200s" + parent = "projects/%{project_name}" + requester_justification_config { + unstructured {} + } + eligible_users { + principals = ["group:test@google.com"] + } + privileged_access { + gcp_iam_access { + role_bindings { + role = "roles/storage.admin" + } + resource = "//cloudresourcemanager.googleapis.com/projects/%{project_name}" + resource_type = "cloudresourcemanager.googleapis.com/Project" + } + } +} +`, context) +} diff --git a/mmv1/third_party/terraform/services/resourcemanager/resource_google_project.go b/mmv1/third_party/terraform/services/resourcemanager/resource_google_project.go index f513003c5df7..2f049dc944f3 100644 --- a/mmv1/third_party/terraform/services/resourcemanager/resource_google_project.go +++ b/mmv1/third_party/terraform/services/resourcemanager/resource_google_project.go @@ -399,11 +399,6 @@ func populateGoogleProjectResourceData(d *schema.ResourceData, p *cloudresourcem if err := tpgresource.SetLabels(p.Labels, d, "terraform_labels"); err != nil { return fmt.Errorf("Error setting terraform_labels: %s", err) } - if v := d.Get("terraform_labels").(map[string]interface{}); len(v) == 0 && len(p.Labels) > 0 { - if err := d.Set("terraform_labels", p.Labels); err != nil { - return fmt.Errorf("Error setting terraform_labels: %s", err) - } - } if err := d.Set("effective_labels", p.Labels); err != nil { return fmt.Errorf("Error setting effective_labels: %s", err) } diff --git a/mmv1/third_party/terraform/services/resourcemanager/resource_google_project_iam_custom_role.go b/mmv1/third_party/terraform/services/resourcemanager/resource_google_project_iam_custom_role.go index f6314b8a26c0..d9fead3f97fb 100644 --- a/mmv1/third_party/terraform/services/resourcemanager/resource_google_project_iam_custom_role.go +++ b/mmv1/third_party/terraform/services/resourcemanager/resource_google_project_iam_custom_role.go @@ -159,7 +159,21 @@ func resourceGoogleProjectIamCustomRoleRead(d *schema.ResourceData, meta interfa return transport_tpg.HandleNotFoundError(err, d, d.Id()) } - if err := d.Set("role_id", tpgresource.GetResourceNameFromSelfLink(role.Name)); err != nil { + if err := FlattenProjectIamCustomRole(d, role, project); err != nil { + return err + } + + if err := tpgresource.DeletionPolicyReadDefault(d, config, "DELETE"); err != nil { + return err + } + + return nil +} + +func FlattenProjectIamCustomRole(d *schema.ResourceData, role *iam.Role, project string) error { + roleID := tpgresource.GetResourceNameFromSelfLink(role.Name) + + if err := d.Set("role_id", roleID); err != nil { return fmt.Errorf("Error setting role_id: %s", err) } if err := d.Set("title", role.Title); err != nil { @@ -184,10 +198,6 @@ func resourceGoogleProjectIamCustomRoleRead(d *schema.ResourceData, meta interfa return fmt.Errorf("Error setting project: %s", err) } - if err := tpgresource.DeletionPolicyReadDefault(d, config, "DELETE"); err != nil { - return err - } - return nil } diff --git a/mmv1/third_party/terraform/services/resourcemanager/resource_google_project_internal_test.go b/mmv1/third_party/terraform/services/resourcemanager/resource_google_project_internal_test.go new file mode 100644 index 000000000000..c4e6ce68d81b --- /dev/null +++ b/mmv1/third_party/terraform/services/resourcemanager/resource_google_project_internal_test.go @@ -0,0 +1,45 @@ +package resourcemanager + +import ( + "reflect" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" + "google.golang.org/api/cloudresourcemanager/v1" +) + +func TestPopulateGoogleProjectResourceData_DoesNotCopyAllLabelsToTerraformLabelsWhenUnset(t *testing.T) { + d := schema.TestResourceDataRaw(t, ResourceGoogleProject().Schema, map[string]interface{}{ + "project_id": "example-project", + "name": "Example Project", + }) + + project := &cloudresourcemanager.Project{ + ProjectNumber: 123456789, + Name: "Example Project", + Labels: map[string]string{ + "firebase": "enabled", + "earth-engine": "", + }, + } + + if err := populateGoogleProjectResourceData(d, project, "example-project", &transport_tpg.Config{}); err != nil { + t.Fatalf("populateGoogleProjectResourceData() returned error: %v", err) + } + + if got := d.Get("labels").(map[string]interface{}); len(got) != 0 { + t.Fatalf("expected labels to remain empty when unset in config, got %#v", got) + } + + if got := d.Get("terraform_labels").(map[string]interface{}); len(got) != 0 { + t.Fatalf("expected terraform_labels to remain empty when unset in config, got %#v", got) + } + + if got := d.Get("effective_labels").(map[string]interface{}); !reflect.DeepEqual(got, map[string]interface{}{ + "firebase": "enabled", + "earth-engine": "", + }) { + t.Fatalf("expected effective_labels to contain all project labels, got %#v", got) + } +} diff --git a/mmv1/third_party/terraform/services/resourcemanager/resource_google_project_test.go b/mmv1/third_party/terraform/services/resourcemanager/resource_google_project_test.go index addc853b0f4a..b01f2ad22ec7 100644 --- a/mmv1/third_party/terraform/services/resourcemanager/resource_google_project_test.go +++ b/mmv1/third_party/terraform/services/resourcemanager/resource_google_project_test.go @@ -53,6 +53,7 @@ func TestAccProject_createWithoutOrg(t *testing.T) { // Test that a Project resource can be imported using an identity block (Terraform 1.12+). func TestAccProject_importBlockWithResourceIdentity(t *testing.T) { t.Parallel() + t.Skip("terraform_labels cannot be ignored during a resource identity import test") org := envvar.GetTestOrgFromEnv(t) pid := fmt.Sprintf("%s-%d", TestPrefix, acctest.RandInt(t)) diff --git a/mmv1/third_party/terraform/services/sql/data_source_sql_database_instances.go b/mmv1/third_party/terraform/services/sql/data_source_sql_database_instances.go index 4b81d81a33a8..68bfed4bdad2 100644 --- a/mmv1/third_party/terraform/services/sql/data_source_sql_database_instances.go +++ b/mmv1/third_party/terraform/services/sql/data_source_sql_database_instances.go @@ -147,6 +147,7 @@ func flattenDatasourceGoogleDatabaseInstancesList(fetchedInstances []*sqladmin.D instance["available_maintenance_versions"] = rawInstance.AvailableMaintenanceVersions instance["instance_type"] = rawInstance.InstanceType instance["service_account_email_address"] = rawInstance.ServiceAccountEmailAddress + instance["enforce_new_sql_network_architecture"] = rawInstance.SqlNetworkArchitecture == "NEW_NETWORK_ARCHITECTURE" instance["settings"] = flattenSettings(rawInstance.Settings, rawInstance.InstanceType, d) if rawInstance.DiskEncryptionConfiguration != nil { diff --git a/mmv1/third_party/terraform/services/sql/resource_sql_database_instance.go.tmpl b/mmv1/third_party/terraform/services/sql/resource_sql_database_instance.go.tmpl index 245dbcc43e99..ce59736c0b2a 100644 --- a/mmv1/third_party/terraform/services/sql/resource_sql_database_instance.go.tmpl +++ b/mmv1/third_party/terraform/services/sql/resource_sql_database_instance.go.tmpl @@ -263,6 +263,13 @@ func ResourceSqlDatabaseInstance() *schema.Resource { Optional: true, Description: `Used to block Terraform from deleting a SQL Instance. Defaults to true.`, }, + "enforce_new_sql_network_architecture": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + DiffSuppressFunc: sqlNetworkArchitectureDiffSuppress, + Description: `Whether to enforce the new SQL network architecture.`, + }, "final_backup_description": { Type: schema.TypeString, Optional: true, @@ -677,6 +684,12 @@ API (for read pools, effective_availability_type may differ from availability_ty Computed: true, Description: `Whether PSC write endpoint DNS is enabled for this instance.`, }, + "psc_auto_connection_policy_enabled": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: `Whether a service connection policy is created for the auto connections configured for the instance.`, + }, "allowed_consumer_projects": { Type: schema.TypeSet, Optional: true, @@ -699,6 +712,7 @@ API (for read pools, effective_availability_type may differ from availability_ty "consumer_service_project_id": { Type: schema.TypeString, Optional: true, + Computed: true, Description: `The project ID of consumer service project of this consumer endpoint.`, }, "consumer_network": { @@ -721,6 +735,16 @@ API (for read pools, effective_availability_type may differ from availability_ty Computed: true, Description: `The connection status of the consumer endpoint.`, }, + "service_connection_policy": { + Type: schema.TypeString, + Computed: true, + Description: `The service connection policy created for the auto connection.`, + }, + "service_connection_policy_creation_result": { + Type: schema.TypeString, + Computed: true, + Description: `The result of the service connection policy creation.`, + }, }, }, Description: `A comma-separated list of networks or a comma-separated list of network-project pairs. Each project in this list is represented by a project number (numeric) or by a project ID (alphanumeric). This allows Private Service Connect connections to be created automatically for the specified networks.`, @@ -1676,6 +1700,14 @@ func pitrSupportDbCustomizeDiff(_ context.Context, diff *schema.ResourceDiff, v return nil } +func sqlNetworkArchitectureDiffSuppress(k, old, new string, d *schema.ResourceData) bool { + // By default, new Cloud SQL instances created in projects created after August 2021 use the new network architecture. + // If the state is true and the config is false, suppress the diff. + // This follows the gcloud pattern where the flag is an irreversible opt-in. + // See https://docs.cloud.google.com/sql/docs/mysql/upgrade-cloud-sql-instance-new-network-architecture#new-arch + return old == "true" && new == "false" +} + func resourceSqlDatabaseInstanceCreate(d *schema.ResourceData, meta interface{}) error { config := meta.(*transport_tpg.Config) userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) @@ -1724,6 +1756,10 @@ func resourceSqlDatabaseInstanceCreate(d *schema.ResourceData, meta interface{}) ReplicaConfiguration: expandReplicaConfiguration(d.Get("replica_configuration").([]interface{})), } + if d.Get("enforce_new_sql_network_architecture").(bool) { + instance.SqlNetworkArchitecture = "NEW_NETWORK_ARCHITECTURE" + } + cloneContext, cloneSourceInstance := expandCloneContext(d.Get("clone").([]interface{})) cloneSourceProject := expandCloneSourceProject(d.Get("clone").([]interface{})) @@ -2137,6 +2173,7 @@ func expandPscConfig(configured []interface{}) *sqladmin.PscConfig { PscEnabled: _entry["psc_enabled"].(bool), PscAutoDnsEnabled: _entry["psc_auto_dns_enabled"].(bool), PscWriteEndpointDnsEnabled: _entry["psc_write_endpoint_dns_enabled"].(bool), + PscAutoConnectionPolicyEnabled: _entry["psc_auto_connection_policy_enabled"].(bool), AllowedConsumerProjects: tpgresource.ConvertStringArr(_entry["allowed_consumer_projects"].(*schema.Set).List()), NetworkAttachmentUri: _entry["network_attachment_uri"].(string), PscAutoConnections: expandPscAutoConnectionConfig(_entry["psc_auto_connections"].([]interface{})), @@ -2499,6 +2536,9 @@ func resourceSqlDatabaseInstanceRead(d *schema.ResourceData, meta interface{}) e if err := d.Set("instance_type", instance.InstanceType); err != nil { return fmt.Errorf("Error setting instance_type: %s", err) } + if err := d.Set("enforce_new_sql_network_architecture", instance.SqlNetworkArchitecture == "NEW_NETWORK_ARCHITECTURE"); err != nil { + return fmt.Errorf("Error setting enforce_new_sql_network_architecture: %s", err) + } if err := d.Set("node_count", instance.NodeCount); err != nil { return fmt.Errorf("Error setting node_count: %s", err) } @@ -2661,6 +2701,32 @@ func resourceSqlDatabaseInstanceUpdate(d *schema.ResourceData, meta interface{}) } } + // We check for HasChange because we only want to trigger the network architecture upgrade + // if the configuration has explicitly changed. We also check the current value to ensure + // it's true, as this is an irreversible opt-in. + if d.HasChange("enforce_new_sql_network_architecture") && d.Get("enforce_new_sql_network_architecture").(bool) { + instance = &sqladmin.DatabaseInstance{SqlNetworkArchitecture: "NEW_NETWORK_ARCHITECTURE"} + err = transport_tpg.Retry(transport_tpg.RetryOptions{ + RetryFunc: func() (rerr error) { + op, rerr = NewClient(config, userAgent).Instances.Patch(project, d.Get("name").(string), instance).Do() + return rerr + }, + Timeout: d.Timeout(schema.TimeoutUpdate), + ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{transport_tpg.IsSqlOperationInProgressError}, + }) + if err != nil { + return fmt.Errorf("Error, failed to patch instance settings for %s: %s", d.Get("name").(string), err) + } + err = SqlAdminOperationWaitTime(config, op, project, "Patch Instance", userAgent, d.Timeout(schema.TimeoutUpdate)) + if err != nil { + return err + } + err = resourceSqlDatabaseInstanceRead(d, meta) + if err != nil { + return err + } + } + // Check if the database version is being updated, because patching database version is an atomic operation and can not be // performed with other fields, we first patch database version before updating the rest of the fields. if d.HasChange("database_version") { @@ -3533,6 +3599,8 @@ func flattenPscAutoConnections(pscAutoConnections []*sqladmin.PscAutoConnectionC "consumer_service_project_id": flag.ConsumerProject, "ip_address": flag.IpAddress, "status": flag.Status, + "service_connection_policy": flag.ServiceConnectionPolicy, + "service_connection_policy_creation_result": flag.ServiceConnectionPolicyCreationResult, } flags = append(flags, data) @@ -3546,6 +3614,7 @@ func flattenPscConfigs(pscConfig *sqladmin.PscConfig) interface{} { "psc_enabled": pscConfig.PscEnabled, "psc_auto_dns_enabled": pscConfig.PscAutoDnsEnabled, "psc_write_endpoint_dns_enabled": pscConfig.PscWriteEndpointDnsEnabled, + "psc_auto_connection_policy_enabled": pscConfig.PscAutoConnectionPolicyEnabled, "allowed_consumer_projects": schema.NewSet(schema.HashString, tpgresource.ConvertStringArrToInterface(pscConfig.AllowedConsumerProjects)), "network_attachment_uri": pscConfig.NetworkAttachmentUri, "psc_auto_connections": flattenPscAutoConnections(pscConfig.PscAutoConnections), diff --git a/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_meta.yaml.tmpl b/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_meta.yaml.tmpl index 1dc442491f97..5f59440fed5f 100644 --- a/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_meta.yaml.tmpl +++ b/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_meta.yaml.tmpl @@ -74,6 +74,8 @@ fields: - api_field: 'replicaConfiguration.mysqlReplicaConfiguration.verifyServerCertificate' field: 'replica_configuration.verify_server_certificate' - api_field: 'replicaNames' + - api_field: 'sqlNetworkArchitecture' + field: 'enforce_new_sql_network_architecture' - api_field: 'replicationCluster.drReplica' - api_field: 'replicationCluster.failoverDrReplicaName' - api_field: 'replicationCluster.psaWriteEndpoint' @@ -175,8 +177,11 @@ fields: field: 'settings.ip_configuration.psc_config.psc_auto_connections.consumer_service_project_id' - api_field: 'settings.ipConfiguration.pscConfig.pscAutoConnections.ipAddress' - api_field: 'settings.ipConfiguration.pscConfig.pscAutoConnections.status' + - api_field: 'settings.ipConfiguration.pscConfig.pscAutoConnections.serviceConnectionPolicy' + - api_field: 'settings.ipConfiguration.pscConfig.pscAutoConnections.serviceConnectionPolicyCreationResult' - api_field: 'settings.ipConfiguration.pscConfig.pscAutoDnsEnabled' - api_field: 'settings.ipConfiguration.pscConfig.pscWriteEndpointDnsEnabled' + - api_field: 'settings.ipConfiguration.pscConfig.pscAutoConnectionPolicyEnabled' - api_field: 'settings.ipConfiguration.pscConfig.pscEnabled' - api_field: 'settings.ipConfiguration.serverCaMode' - api_field: 'settings.ipConfiguration.serverCaPool' diff --git a/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_test.go.tmpl b/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_test.go.tmpl index e466ee274bf1..d83398ca9e0c 100644 --- a/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_test.go.tmpl +++ b/mmv1/third_party/terraform/services/sql/resource_sql_database_instance_test.go.tmpl @@ -262,6 +262,40 @@ func TestAccSqlDatabaseInstance_basicSecondGen(t *testing.T) { }) } +func TestAccSqlDatabaseInstance_enforceNewSqlNetworkArchitecture(t *testing.T) { + t.Parallel() + + instanceName := "tf-test-" + acctest.RandString(t, 10) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccSqlDatabaseInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccSqlDatabaseInstance_enforceNewSqlNetworkArchitecture(instanceName, true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_sql_database_instance.instance", "enforce_new_sql_network_architecture", "true"), + ), + }, + { + ResourceName: "google_sql_database_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: testAccSqlDatabaseInstance_enforceNewSqlNetworkArchitecture(instanceName, false), + Check: resource.ComposeTestCheckFunc( + // Verify that even though we set it to false in HCL, the state stays true + // and no diff was generated (otherwise this step would fail or show a plan). + resource.TestCheckResourceAttr("google_sql_database_instance.instance", "enforce_new_sql_network_architecture", "true"), + ), + }, + }, + }) +} + func TestAccSqlDatabaseInstance_basicMSSQL(t *testing.T) { t.Parallel() @@ -1238,6 +1272,37 @@ func TestAccSqlDatabaseInstance_updateMCPEnabled(t *testing.T) { }) } +func TestAccSqlDatabaseInstance_withPSCEnabled_withAutoConnectionPolicy(t *testing.T) { + t.Parallel() + + instanceName := "tf-test-" + acctest.RandString(t, 10) + networkName := "tf-test-" + acctest.RandString(t, 10) + projectId := envvar.GetTestProjectFromEnv() + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccSqlDatabaseInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccSqlDatabaseInstance_withPSCEnabled_withAutoConnectionPolicy(instanceName, networkName, projectId), + Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, nil, false, false, true, "", "COMPLETED")), + }, + { + ResourceName: "google_sql_database_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateIdPrefix: fmt.Sprintf("%s/", projectId), + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: testAccSqlDatabaseInstance_withPSCEnabled_withAutoConnectionPolicy_deleted(networkName, projectId), + Check: testAccCheckSqlDatabaseInstanceDeleteServiceConnectionPolicyByNetwork(t, projectId, "us-south1", networkName), + }, + }, + }) +} + func TestAccSqlDatabaseInstance_withPSCEnabled_withoutAllowedConsumerProjects(t *testing.T) { t.Parallel() @@ -1253,7 +1318,7 @@ func TestAccSqlDatabaseInstance_withPSCEnabled_withoutAllowedConsumerProjects(t Steps: []resource.TestStep{ { Config: testAccSqlDatabaseInstance_withPSCEnabled_withoutAllowedConsumerProjects(instanceName, projectId, orgId, billingAccount), - Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, nil, false, false)), + Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, nil, false, false, false, "", "")), }, { ResourceName: "google_sql_database_instance.instance", @@ -1281,7 +1346,7 @@ func TestAccSqlDatabaseInstance_withPSCEnabled_withEmptyAllowedConsumerProjects( Steps: []resource.TestStep{ { Config: testAccSqlDatabaseInstance_withPSCEnabled_withEmptyAllowedConsumerProjects(instanceName, projectId, orgId, billingAccount), - Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, []string{}, false, false)), + Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, []string{}, false, false, false, "", "")), }, { ResourceName: "google_sql_database_instance.instance", @@ -1309,7 +1374,7 @@ func TestAccSqlDatabaseInstance_withPSCEnabled_withAllowedConsumerProjects(t *te Steps: []resource.TestStep{ { Config: testAccSqlDatabaseInstance_withPSCEnabled_withAllowedConsumerProjects(instanceName, projectId, orgId, billingAccount), - Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, []string{envvar.GetTestProjectFromEnv()}, false, false)), + Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, []string{envvar.GetTestProjectFromEnv()}, false, false, false, "", "")), }, { ResourceName: "google_sql_database_instance.instance", @@ -1336,7 +1401,7 @@ func TestAccSqlDatabaseInstance_pscDnsConfig(t *testing.T) { { Config: testAccSqlDatabaseInstance_withPSCDnsEnabled(instanceName, projectId), Check: resource.ComposeTestCheckFunc( - verifyPscOperation("google_sql_database_instance.instance", true, true, []string{projectId}, true, true), + verifyPscOperation("google_sql_database_instance.instance", true, true, []string{projectId}, true, true, false, "", ""), ), }, { @@ -1348,7 +1413,7 @@ func TestAccSqlDatabaseInstance_pscDnsConfig(t *testing.T) { { Config: testAccSqlDatabaseInstance_withPSCDnsDisabled(instanceName, projectId), Check: resource.ComposeTestCheckFunc( - verifyPscOperation("google_sql_database_instance.instance", true, true, []string{projectId}, false, false), + verifyPscOperation("google_sql_database_instance.instance", true, true, []string{projectId}, false, false, false, "", ""), ), }, { @@ -1360,7 +1425,7 @@ func TestAccSqlDatabaseInstance_pscDnsConfig(t *testing.T) { { Config: testAccSqlDatabaseInstance_withPSCOnlyAutoDnsEnabled(instanceName, projectId), Check: resource.ComposeTestCheckFunc( - verifyPscOperation("google_sql_database_instance.instance", true, true, []string{projectId}, true, false), + verifyPscOperation("google_sql_database_instance.instance", true, true, []string{projectId}, true, false, false, "", ""), ), }, { @@ -1388,7 +1453,7 @@ func TestAccSqlDatabaseInstance_withPSCEnabled_thenAddAllowedConsumerProjects_th Steps: []resource.TestStep{ { Config: testAccSqlDatabaseInstance_withPSCEnabled_withoutAllowedConsumerProjects(instanceName, projectId, orgId, billingAccount), - Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, nil, false, false)), + Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, nil, false, false, false, "", "")), }, { ResourceName: "google_sql_database_instance.instance", @@ -1399,7 +1464,7 @@ func TestAccSqlDatabaseInstance_withPSCEnabled_thenAddAllowedConsumerProjects_th }, { Config: testAccSqlDatabaseInstance_withPSCEnabled_withAllowedConsumerProjects(instanceName, projectId, orgId, billingAccount), - Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, []string{envvar.GetTestProjectFromEnv()}, false, false)), + Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, []string{envvar.GetTestProjectFromEnv()}, false, false, false, "", "")), }, { ResourceName: "google_sql_database_instance.instance", @@ -1410,7 +1475,7 @@ func TestAccSqlDatabaseInstance_withPSCEnabled_thenAddAllowedConsumerProjects_th }, { Config: testAccSqlDatabaseInstance_withPSCEnabled_withoutAllowedConsumerProjects(instanceName, projectId, orgId, billingAccount), - Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, []string{}, false, false)), + Check: resource.ComposeTestCheckFunc(verifyPscOperation("google_sql_database_instance.instance", true, true, []string{}, false, false, false, "", "")), }, { ResourceName: "google_sql_database_instance.instance", @@ -7213,6 +7278,95 @@ resource "google_sql_database_instance" "instance" { `, projectId, projectId, orgId, billingAccount, instanceName) } +func testAccSqlDatabaseInstance_withPSCEnabled_withAutoConnectionPolicy(instanceName string, networkName string, projectId string) string { + return fmt.Sprintf(` +resource "google_project_service_identity" "gcp_sa_cloud_sql" { + project = "%s" + service = "sqladmin.googleapis.com" +} + +resource "google_project_iam_member" "sa_consumer_network_admin" { + project = "%s" + role = "roles/networkconnectivity.consumerNetworkAdmin" + member = google_project_service_identity.gcp_sa_cloud_sql.member +} + +resource "google_project_iam_member" "sa_network_viewer" { + project = "%s" + role = "roles/compute.networkViewer" + member = google_project_service_identity.gcp_sa_cloud_sql.member +} + +resource "google_compute_network" "default" { + project = "%s" + name = "%s" + auto_create_subnetworks = true +} + +resource "google_sql_database_instance" "instance" { + project = "%s" + name = "%s" + region = "us-south1" + database_version = "MYSQL_8_0" + deletion_protection = false + settings { + tier = "db-g1-small" + ip_configuration { + psc_config { + psc_enabled = true + psc_auto_connection_policy_enabled = true + psc_auto_connections { + consumer_network = google_compute_network.default.id + consumer_service_project_id = "%s" + } + } + ipv4_enabled = false + } + backup_configuration { + enabled = true + binary_log_enabled = true + } + database_flags { + name = "cloudsql_iam_authentication" + value = "on" + } + availability_type = "REGIONAL" + } + depends_on = [ + google_project_iam_member.sa_consumer_network_admin, + google_project_iam_member.sa_network_viewer, + ] +} +`, projectId, projectId, projectId, projectId, networkName, projectId, instanceName, projectId) +} + +func testAccSqlDatabaseInstance_withPSCEnabled_withAutoConnectionPolicy_deleted(networkName string, projectId string) string { + return fmt.Sprintf(` +resource "google_project_service_identity" "gcp_sa_cloud_sql" { + project = "%s" + service = "sqladmin.googleapis.com" +} + +resource "google_project_iam_member" "sa_consumer_network_admin" { + project = "%s" + role = "roles/networkconnectivity.consumerNetworkAdmin" + member = google_project_service_identity.gcp_sa_cloud_sql.member +} + +resource "google_project_iam_member" "sa_network_viewer" { + project = "%s" + role = "roles/compute.networkViewer" + member = google_project_service_identity.gcp_sa_cloud_sql.member +} + +resource "google_compute_network" "default" { + project = "%s" + name = "%s" + auto_create_subnetworks = true +} +`, projectId, projectId, projectId, projectId, networkName) +} + func testAccSqlDatabaseInstance_withPSCEnabled_withoutAllowedConsumerProjects(instanceName string, projectId string, orgId string, billingAccount string) string { return fmt.Sprintf(` resource "google_project" "testproject" { @@ -7407,7 +7561,7 @@ resource "google_sql_database_instance" "instance" { `, projectId, instanceName, projectId) } -func verifyPscOperation(resourceName string, isPscConfigExpected bool, expectedPscEnabled bool, expectedAllowedConsumerProjects []string, expectedPscAutoDnsEnabled bool, expectedPscWriteEndpointDnsEnabled bool) func(*terraform.State) error { +func verifyPscOperation(resourceName string, isPscConfigExpected bool, expectedPscEnabled bool, expectedAllowedConsumerProjects []string, expectedPscAutoDnsEnabled bool, expectedPscWriteEndpointDnsEnabled bool, expectedEnablePscAutoConnectionPolicy bool, expectedServiceConnectionPolicy string, expectedServiceConnectionPolicyCreationResult string) func(*terraform.State) error { return func(s *terraform.State) error { resource, ok := s.RootModule().Resources[resourceName] if !ok { @@ -7444,6 +7598,26 @@ func verifyPscOperation(resourceName string, isPscConfigExpected bool, expectedP return fmt.Errorf("settings.0.ip_configuration.0.psc_config.0.psc_write_endpoint_dns_enabled property value is not set as expected in state of %s, expected %v, actual %v", resourceName, expectedPscWriteEndpointDnsEnabled, pscWriteEndpointDnsEnabled) } + enablePscAutoConnectionPolicyStr, ok := resourceAttributes["settings.0.ip_configuration.0.psc_config.0.psc_auto_connection_policy_enabled"] + enablePscAutoConnectionPolicy, err := strconv.ParseBool(enablePscAutoConnectionPolicyStr) + if err != nil || enablePscAutoConnectionPolicy != expectedEnablePscAutoConnectionPolicy { + return fmt.Errorf("settings.0.ip_configuration.0.psc_config.0.psc_auto_connection_policy_enabled property value is not set as expected in state of %s, expected %v, actual %v", resourceName, expectedEnablePscAutoConnectionPolicy, enablePscAutoConnectionPolicy) + } + + if expectedServiceConnectionPolicy != "" { + serviceConnectionPolicy, ok := resourceAttributes["settings.0.ip_configuration.0.psc_config.0.psc_auto_connections.0.service_connection_policy"] + if !ok || serviceConnectionPolicy != expectedServiceConnectionPolicy { + return fmt.Errorf("settings.0.ip_configuration.0.psc_config.0.psc_auto_connections.0.service_connection_policy property value is not set as expected in state of %s, expected %v, actual %v", resourceName, expectedServiceConnectionPolicy, serviceConnectionPolicy) + } + } + + if expectedServiceConnectionPolicyCreationResult != "" { + serviceConnectionPolicyCreationResult, ok := resourceAttributes["settings.0.ip_configuration.0.psc_config.0.psc_auto_connections.0.service_connection_policy_creation_result"] + if !ok || serviceConnectionPolicyCreationResult != expectedServiceConnectionPolicyCreationResult { + return fmt.Errorf("settings.0.ip_configuration.0.psc_config.0.psc_auto_connections.0.service_connection_policy_creation_result property value is not set as expected in state of %s, expected %v, actual %v", resourceName, expectedServiceConnectionPolicyCreationResult, serviceConnectionPolicyCreationResult) + } + } + allowedConsumerProjectsStr, ok := resourceAttributes["settings.0.ip_configuration.0.psc_config.0.allowed_consumer_projects.#"] allowedConsumerProjects, err := strconv.Atoi(allowedConsumerProjectsStr) if !ok || allowedConsumerProjects != len(expectedAllowedConsumerProjects) { @@ -7529,6 +7703,80 @@ func verifyPscAutoConnectionsOperation(resourceName string, isPscConfigExpected } } +func testAccCheckSqlDatabaseInstanceDeleteServiceConnectionPolicyByNetwork(t *testing.T, project string, region string, networkName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := acctest.GoogleProviderConfig(t) + url := fmt.Sprintf("https://networkconnectivity.googleapis.com/v1/projects/%s/locations/%s/serviceConnectionPolicies", project, region) + + billingProject := "" + if config.BillingProject != "" { + billingProject = config.BillingProject + } + + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: billingProject, + RawURL: url, + UserAgent: config.UserAgent, + }) + if err != nil { + log.Printf("[DEBUG] Error listing existing service connection policies: %s", err) + return nil + } + if res == nil || res["serviceConnectionPolicies"] == nil { + return nil + } + + policies, ok := res["serviceConnectionPolicies"].([]interface{}) + if !ok { + return nil + } + for _, p := range policies { + policy, ok := p.(map[string]interface{}) + if !ok { + continue + } + network, ok := policy["network"].(string) + if !ok || (!strings.HasSuffix(network, "/networks/"+networkName) && !strings.Contains(network, "/"+networkName+"")) { + continue + } + name, ok := policy["name"].(string) + if !ok || name == "" { + continue + } + deleteUrl := fmt.Sprintf("https://networkconnectivity.googleapis.com/v1/%s", strings.TrimPrefix(name, "/")) + log.Printf("[DEBUG] Deleting service connection policy %q before deleting network", name) + _, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "DELETE", + Project: billingProject, + RawURL: deleteUrl, + UserAgent: config.UserAgent, + }) + if err != nil && !transport_tpg.IsGoogleApiErrorWithCode(err, 404) { + log.Printf("[DEBUG] Error deleting service connection policy %s: %s", name, err) + } + + for i := 0; i < 30; i++ { + time.Sleep(5 * time.Second) + _, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: billingProject, + RawURL: deleteUrl, + UserAgent: config.UserAgent, + }) + if err != nil && transport_tpg.IsGoogleApiErrorWithCode(err, 404) { + log.Printf("[DEBUG] Service connection policy %q deleted successfully", name) + break + } + } + } + return nil + } +} + func verifyPscNetorkAttachmentOperation(resourceName string, isPscConfigExpected bool, expectedPscEnabled bool, expectedNetworkAttachmentUri string) func(*terraform.State) error { return func(s *terraform.State) error { resource, ok := s.RootModule().Resources[resourceName] @@ -9989,3 +10237,20 @@ func testGoogleSqlDatabaseInstanceCheckNodeCount(t *testing.T, instance string, return nil } } + +func testAccSqlDatabaseInstance_enforceNewSqlNetworkArchitecture(instanceName string, enforce bool) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "instance" { + name = "%s" + region = "us-central1" + database_version = "POSTGRES_14" + enforce_new_sql_network_architecture = %t + settings { + tier = "db-f1-micro" + } + + deletion_protection = false +} +`, instanceName, enforce) +} + diff --git a/mmv1/third_party/terraform/services/storage/resource_storage_anywhere_cache_test.go b/mmv1/third_party/terraform/services/storage/resource_storage_anywhere_cache_test.go index ede31a228673..d37d046f4301 100644 --- a/mmv1/third_party/terraform/services/storage/resource_storage_anywhere_cache_test.go +++ b/mmv1/third_party/terraform/services/storage/resource_storage_anywhere_cache_test.go @@ -39,6 +39,9 @@ func TestAccStorageAnywhereCache_update(t *testing.T) { plancheck.ExpectResourceAction("google_storage_anywhere_cache.cache", plancheck.ResourceActionUpdate), }, }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_storage_anywhere_cache.cache", "admission_policy", "admit-on-first-miss"), + ), }, { ResourceName: "google_storage_anywhere_cache.cache", diff --git a/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job.go b/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job.go index b5e0ad7e3a42..21ef2d20b629 100644 --- a/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job.go +++ b/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job.go @@ -917,6 +917,11 @@ func azureBlobStorageDataSchema() *schema.Resource { }, Description: ` Workload Identity Details used to authenticate API requests to Azure.`, }, + "private_network_service": { + Type: schema.TypeString, + Optional: true, + Description: `Service Directory Service used as the endpoint for transfers from a customer-managed VPC. Format: projects/{projectId}/locations/{location}/namespaces/{namespace}/services/{service}`, + }, }, } } @@ -1782,6 +1787,7 @@ func expandAzureBlobStorageData(azureBlobStorageDatas []interface{}) *storagetra AzureCredentials: expandAzureCredentials(azureBlobStorageData["azure_credentials"].([]interface{})), CredentialsSecret: azureBlobStorageData["credentials_secret"].(string), FederatedIdentityConfig: expandAzureFederatedIdentifyConfig(azureBlobStorageData["federated_identity_config"].([]interface{})), + PrivateNetworkService: azureBlobStorageData["private_network_service"].(string), } } @@ -1793,6 +1799,7 @@ func flattenAzureBlobStorageData(azureBlobStorageData *storagetransfer.AzureBlob "azure_credentials": flattenAzureCredentials(d), "federated_identity_config": flattenAzureFederatedIdentifyConfig(d), "credentials_secret": azureBlobStorageData.CredentialsSecret, + "private_network_service": azureBlobStorageData.PrivateNetworkService, } return []map[string]interface{}{data} diff --git a/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job_meta.yaml b/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job_meta.yaml index 9bf9ba4ae234..ac10b9a1d31f 100644 --- a/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job_meta.yaml +++ b/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job_meta.yaml @@ -70,6 +70,7 @@ fields: - api_field: 'transferSpec.azureBlobStorageDataSource.federatedIdentityConfig.clientId' - api_field: 'transferSpec.azureBlobStorageDataSource.federatedIdentityConfig.tenantId' - api_field: 'transferSpec.azureBlobStorageDataSource.path' + - api_field: 'transferSpec.azureBlobStorageDataSource.privateNetworkService' - api_field: 'transferSpec.azureBlobStorageDataSource.storageAccount' - api_field: 'transferSpec.awsS3CompatibleDataSource.bucketName' - api_field: 'transferSpec.awsS3CompatibleDataSource.path' diff --git a/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job_test.go b/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job_test.go index 6294e75ea556..22d91d18da56 100644 --- a/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job_test.go +++ b/mmv1/third_party/terraform/services/storagetransfer/resource_storage_transfer_job_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-provider-google/google/acctest" "github.com/hashicorp/terraform-provider-google/google/envvar" @@ -681,6 +682,52 @@ func TestAccStorageTransferJob_awsS3CompatibleDataSource(t *testing.T) { }) } +func TestAccStorageTransferJob_azureBlobStorageDataSourcePrivateNetwork(t *testing.T) { + t.Skip("only works locally: requires a private cross-cloud interconnect between Azure and GCP that is not available in the test environment") + t.Parallel() + + project := envvar.GetTestProjectFromEnv() + testDataSinkName := acctest.RandString(t, 10) + suffix := acctest.RandString(t, 10) + namespaceId := "tf-test-sts-azure-" + suffix + serviceIdA := "tf-test-sts-azure-svc-a-" + suffix + serviceIdB := "tf-test-sts-azure-svc-b-" + suffix + location := "us-central1" + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccStorageTransferJobDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccStorageTransferJob_azureBlobStorageDataSourcePrivateNetwork(project, testDataSinkName, namespaceId, serviceIdA, serviceIdB, location, "a"), + }, + { + ResourceName: "google_storage_transfer_job.transfer_job", + ImportState: true, + ImportStateVerify: true, + // azure_credentials is input-only on the API and not returned on Read. + ImportStateVerifyIgnore: []string{"transfer_spec.0.azure_blob_storage_data_source.0.azure_credentials"}, + }, + { + Config: testAccStorageTransferJob_azureBlobStorageDataSourcePrivateNetwork(project, testDataSinkName, namespaceId, serviceIdA, serviceIdB, location, "b"), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + // private_network_service is mutable; switching it must update in-place, not recreate. + plancheck.ExpectResourceAction("google_storage_transfer_job.transfer_job", plancheck.ResourceActionUpdate), + }, + }, + }, + { + ResourceName: "google_storage_transfer_job.transfer_job", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"transfer_spec.0.azure_blob_storage_data_source.0.azure_credentials"}, + }, + }, + }) +} + func TestAccStorageTransferJob_transferManifest(t *testing.T) { t.Parallel() @@ -3071,6 +3118,85 @@ resource "google_storage_transfer_job" "transfer_job" { `, project, dataSourceBucketName, dataSinkBucketName, transferJobDescription, manifestObjectName) } +func testAccStorageTransferJob_azureBlobStorageDataSourcePrivateNetwork(project, dataSinkBucketName, namespaceId, serviceIdA, serviceIdB, location, which string) string { + pnsRef := "google_service_directory_service.sts_azure_a.id" + if which == "b" { + pnsRef = "google_service_directory_service.sts_azure_b.id" + } + return fmt.Sprintf(` +data "google_storage_transfer_project_service_account" "default" { + project = "%[1]s" +} + +resource "google_storage_bucket" "data_sink" { + name = "%[2]s" + project = "%[1]s" + location = "US" + force_destroy = true +} + +resource "google_storage_bucket_iam_member" "data_sink" { + bucket = google_storage_bucket.data_sink.name + role = "roles/storage.admin" + member = "serviceAccount:${data.google_storage_transfer_project_service_account.default.email}" +} + +resource "google_service_directory_namespace" "sts_azure" { + project = "%[1]s" + namespace_id = "%[3]s" + location = "%[6]s" +} + +resource "google_service_directory_service" "sts_azure_a" { + service_id = "%[4]s" + namespace = google_service_directory_namespace.sts_azure.id +} + +resource "google_service_directory_service" "sts_azure_b" { + service_id = "%[5]s" + namespace = google_service_directory_namespace.sts_azure.id +} + +resource "google_storage_transfer_job" "transfer_job" { + description = "Azure source with private network service" + project = "%[1]s" + + transfer_spec { + azure_blob_storage_data_source { + storage_account = "examplestorageaccount" + container = "example-container" + path = "foo/bar/" + azure_credentials { + sas_token = "fakesastoken" + } + private_network_service = %[7]s + } + gcs_data_sink { + bucket_name = google_storage_bucket.data_sink.name + path = "foo/bar/" + } + } + + schedule { + schedule_start_date { + year = 2024 + month = 1 + day = 1 + } + schedule_end_date { + year = 2024 + month = 1 + day = 1 + } + } + + depends_on = [ + google_storage_bucket_iam_member.data_sink, + ] +} +`, project, dataSinkBucketName, namespaceId, serviceIdA, serviceIdB, location, pnsRef) +} + func testAccStorageTransferJob_withoutTransferManifest(project, dataSourceBucketName, dataSinkBucketName, transferJobDescription string) string { return fmt.Sprintf(` data "google_storage_transfer_project_service_account" "default" { diff --git a/mmv1/third_party/terraform/services/tags/resource_tags_tag_binding_collection.go.tmpl b/mmv1/third_party/terraform/services/tags/resource_tags_tag_binding_collection.go.tmpl new file mode 100644 index 000000000000..0bbba05b9091 --- /dev/null +++ b/mmv1/third_party/terraform/services/tags/resource_tags_tag_binding_collection.go.tmpl @@ -0,0 +1,472 @@ +package tags + +{{- if ne $.TargetVersionName "ga" }} + +import ( + "context" + "fmt" + "log" + "net/http" + "net/url" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-provider-google/google/registry" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +func ResourceTagsTagBindingCollection() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceTagsTagBindingCollectionUpsert, // Use Upsert + ReadContext: resourceTagsTagBindingCollectionRead, + UpdateContext: resourceTagsTagBindingCollectionUpsert, // Use Upsert + DeleteContext: resourceTagsTagBindingCollectionDelete, + + Importer: &schema.ResourceImporter{ + StateContext: resourceTagsTagBindingCollectionImport, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(20 * time.Minute), + Update: schema.DefaultTimeout(20 * time.Minute), + Delete: schema.DefaultTimeout(20 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "location": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "global", + Description: `The location of the TagBindingCollection.`, + }, + "full_resource_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + DiffSuppressFunc: tpgresource.ProjectNumberDiffSuppress, + Description: `The full resource name of the resource to which the tags are bound. E.g. //cloudresourcemanager.googleapis.com/projects/123`, + }, + "tags": { + Type: schema.TypeMap, + Required: true, + Description: `A map of tag keys to values directly bound to this resource, specified in namespaced format. +For example: + "123/environment": "production" +Keys must be namespaced names of TagKeys, and values must be short names of TagValues. +This field is non-authoritative. Terraform will only manage the precise tags present in this map.`, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "active_tags": { + Type: schema.TypeMap, + Computed: true, + Description: `(Output) The most recent state of all direct tags on the resource, as reported by the API. +This includes the tags configured through Terraform, Google system tags, and tags attached by other clients.`, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: `The name of the TagBindingCollection, in the format: +locations/{location}/tagBindingCollections/{encoded_full_resource_name}`, + }, + }, + + UseJSONNumber: true, + } +} + +// Custom Read Function - Essential for non-authoritative sync +func resourceTagsTagBindingCollectionRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return diag.FromErr(err) + } + + location, resourceName, encodedResourceName := getTagBindingCollectionInfo(d) + currentRes, currentTags, _, err := getCurrentTagBindingCollectionState(d, config, userAgent, location, encodedResourceName) + if err != nil { + return diag.FromErr(transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("TagBindingCollection %q", d.Id()))) + } + + if err := d.Set("location", location); err != nil { + return diag.Errorf("error setting location: %s", err) + } + if err := d.Set("full_resource_name", resourceName); err != nil { + return diag.Errorf("error setting full_resource_name: %s", err) + } + if err := d.Set("name", currentRes["name"]); err != nil { + return diag.Errorf("error setting name: %s", err) + } + + if err := d.Set("active_tags", currentTags); err != nil { + return diag.Errorf("error setting active_tags: %s", err) + } + + // Sync "tags" to detect drift for configured keys + if currentTags != nil { + transformedTags := make(map[string]interface{}) + if v, ok := d.GetOk("tags"); ok { + for k := range v.(map[string]interface{}) { + if val, exists := currentTags[k]; exists { + transformedTags[k] = val + } + } + } + if err := d.Set("tags", transformedTags); err != nil { + return diag.Errorf("error setting tags: %s", err) + } + } + + return nil +} + +// Custom Upsert Function for Create and Update +func resourceTagsTagBindingCollectionUpsert(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return diag.FromErr(err) + } + + location, resourceName, encodedResourceName := getTagBindingCollectionInfo(d) + + configuredTags, diags := getStringMapFromSchema(d, "tags") + if diags != nil { + return diags + } + + timeout := d.Timeout(schema.TimeoutUpdate) + opDesc := "Updating TagBindingCollection" + if d.IsNewResource() { + timeout = d.Timeout(schema.TimeoutCreate) + opDesc = "Creating TagBindingCollection" + } + + backoff := time.Second + for { + // Step 1: Read current state + _, currentTags, currentEtag, err := getCurrentTagBindingCollectionState(d, config, userAgent, location, encodedResourceName) + if err != nil { + return diag.Errorf("error reading current state for upsert: %s", err) + } + + // Step 2: Prepare PATCH object + obj := make(map[string]interface{}) + obj["full_resource_name"] = resourceName + if currentEtag != "" { + obj["etag"] = currentEtag + } + + log.Printf("[DEBUG] Current Tags %#v", currentTags) + log.Printf("[DEBUG] Configured Tags %#v", configuredTags) + + mergedTags := make(map[string]interface{}) + for k, v := range currentTags { + mergedTags[k] = v + } + for k, v := range configuredTags { + // This will also overwrite the a key with value as configured in terraform, nullifying any updates to the same binding outside of terraform + mergedTags[k] = v + } + + // Remove tags that were in the old state but not in the new config + if !d.IsNewResource() { + oldTagsIFace, _ := d.GetChange("tags") + oldTags, diags := convertInterfaceToStringMap(oldTagsIFace) + if diags != nil { + return diags + } + log.Printf("[DEBUG] Old Tags %#v", oldTags) + for k := range oldTags { + if _, existsInNewConfig := configuredTags[k]; !existsInNewConfig { + delete(mergedTags, k) + } + } + } + log.Printf("[DEBUG] Merged Tags %#v", mergedTags) + obj["tags"] = mergedTags + + // Step 3: Write + err = patchTagBindingCollection(config, userAgent, d, location, encodedResourceName, obj, timeout, opDesc) + if err != nil { + if transport_tpg.IsGoogleApiErrorWithCode(err, 412) || transport_tpg.IsGoogleApiErrorWithCode(err, 409) { + log.Printf("[DEBUG] Concurrent ETag collision detected during %s, restarting read-modify-write cycle after %s", opDesc, backoff) + time.Sleep(backoff) + backoff = backoff * 2 + if backoff > 30*time.Second { + return diag.Errorf("Error %s: Too many concurrent ETag modifications on parent %q. Final error: %s", opDesc, resourceName, err) + } + continue + } + return diag.Errorf("Error executing PATCH request for %s: %s", opDesc, err) + } + break + } + + if d.IsNewResource() { + d.SetId(fmt.Sprintf("locations/%s/tagBindingCollections/%s", location, encodedResourceName)) + } + + return resourceTagsTagBindingCollectionRead(ctx, d, meta) +} + +func resourceTagsTagBindingCollectionDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return diag.FromErr(err) + } + + location, resourceName, encodedResourceName := getTagBindingCollectionInfo(d) + + // Step 1: Get Terraform-managed old tags from state + oldTagsIFace, _ := d.GetChange("tags") + oldTags, diags := convertInterfaceToStringMap(oldTagsIFace) + if diags != nil { + return diags + } + + timeout := d.Timeout(schema.TimeoutDelete) + opDesc := "Deleting Terraform-managed tags" + backoff := time.Second + + for { + // Read current state + _, currentTags, currentEtag, err := getCurrentTagBindingCollectionState(d, config, userAgent, location, encodedResourceName) + if err != nil { + return diag.FromErr(transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("TagBindingCollection %q", resourceName))) + } + + // Compute the final set of tags for the PATCH request + // (Start with all current tags, then remove the ones TF managed) + finalTags := make(map[string]interface{}) + for k, v := range currentTags { + finalTags[k] = v + } + + hasTagsToDelete := false + for k := range oldTags { + if _, existsInCurrent := currentTags[k]; existsInCurrent { + delete(finalTags, k) + hasTagsToDelete = true + } + } + + if !hasTagsToDelete { + log.Printf("[DEBUG] No Terraform-managed tags to remove for %s.", resourceName) + d.SetId("") + return nil + } + + // Send PATCH request + obj := make(map[string]interface{}) + obj["full_resource_name"] = resourceName + if currentEtag != "" { + obj["etag"] = currentEtag + } + obj["tags"] = finalTags + + err = patchTagBindingCollection(config, userAgent, d, location, encodedResourceName, obj, timeout, opDesc) + if err != nil { + if transport_tpg.IsGoogleApiErrorWithCode(err, 412) || transport_tpg.IsGoogleApiErrorWithCode(err, 409) { + log.Printf("[DEBUG] Concurrent ETag collision detected during %s, restarting read-modify-write cycle after %s", opDesc, backoff) + time.Sleep(backoff) + backoff = backoff * 2 + if backoff > 30*time.Second { + return diag.Errorf("Error %s: Too many concurrent ETag modifications on parent %q. Final error: %s", opDesc, resourceName, err) + } + continue + } + return diag.Errorf("Error executing PATCH request for %s: %s", opDesc, err) + } + break + } + + d.SetId("") + log.Printf("[INFO] Successfully removed Terraform-managed tags for %s", resourceName) + return nil +} + +func resourceTagsTagBindingCollectionImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + // Import ID format: locations/{location}/tagBindingCollections/{encoded_full_resource_name} + // The {encoded_full_resource_name} part is a encoded full resource name. + id := d.Id() + parts := strings.SplitN(id, "/", 4) + if len(parts) != 4 || parts[0] != "locations" || parts[2] != "tagBindingCollections" { + return nil, fmt.Errorf("invalid import ID: %s. Expected format: locations/{location}/tagBindingCollections/{encoded_full_resource_name}", id) + } + + location := parts[1] + encodedResourceName := parts[3] + + if location == "" || encodedResourceName == "" { + return nil, fmt.Errorf("invalid import ID: %s. Location and encoded_full_resource_name cannot be empty", id) + } + + resourceName, err := url.PathUnescape(encodedResourceName) + if err != nil { + return nil, fmt.Errorf("error decoding full_resource_name from import ID %q: %s", encodedResourceName, err) + } + + if err := d.Set("location", location); err != nil { + return nil, err + } + if err := d.Set("full_resource_name", resourceName); err != nil { + return nil, err + } + + // The ID is already in the desired format, so no need to d.SetId(id) again + return []*schema.ResourceData{d}, nil +} + +func getTagBindingCollectionInfo(d *schema.ResourceData) (location, resourceName, encodedResourceName string) { + location = d.Get("location").(string) + resourceName = d.Get("full_resource_name").(string) + encodedResourceName = url.PathEscape(resourceName) + return location, resourceName, encodedResourceName +} + +func buildTagBindingCollectionUrl(d *schema.ResourceData, config *transport_tpg.Config, location, encodedResourceName string) (string, error) { + basePath := "{{"{{"}}TagsLocationBasePath{{"}}"}}" + if location == "global" { + basePath = "{{"{{"}}TagsBasePath{{"}}"}}" + } + return tpgresource.ReplaceVars(d, config, fmt.Sprintf("%slocations/%s/tagBindingCollections/%s", basePath, location, encodedResourceName)) +} + +func flattenTagsTagBindingCollectionTags(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +// Helper to fetch the current state of the TagBindingCollection from GCP +func getCurrentTagBindingCollectionState(d *schema.ResourceData, config *transport_tpg.Config, userAgent, location, encodedResourceName string) (currentRes map[string]interface{}, currentTags map[string]string, currentEtag string, err error) { + readUrl, err := buildTagBindingCollectionUrl(d, config, location, encodedResourceName) + if err != nil { + return nil, nil, "", err + } + headers := make(http.Header) + + log.Printf("[DEBUG] Reading current TagBindingCollection state with URL %s", readUrl) + currentRes, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + RawURL: readUrl, + UserAgent: userAgent, + Headers: headers, + }) + + if err != nil { + return nil, nil, "", err + } + + currentEtag, _ = currentRes["etag"].(string) + + currentTags = make(map[string]string) + currentApiTags := currentRes["tags"] + if currentApiTags != nil { + currentTagsIFace, ok := currentApiTags.(map[string]interface{}) + if !ok { + return nil, nil, currentEtag, fmt.Errorf("API returned 'tags' in an unexpected format: %T", currentApiTags) + } + currentTags, err = convertToStringMap(currentTagsIFace) + if err != nil { + return nil, nil, currentEtag, fmt.Errorf("error converting current API tags: %s", err) + } + } + + return currentRes, currentTags, currentEtag, nil +} + +// Helper to send a PATCH request for TagBindingCollection and wait for the operation +func patchTagBindingCollection(config *transport_tpg.Config, userAgent string, d *schema.ResourceData, location, encodedResourceName string, patchObj map[string]interface{}, timeout time.Duration, operationDesc string) error { + patchUrl, err := buildTagBindingCollectionUrl(d, config, location, encodedResourceName) + if err != nil { + return err + } + patchUrl, err = transport_tpg.AddQueryParams(patchUrl, map[string]string{"updateMask": "*"}) + if err != nil { + return err + } + + billingProject := "" + if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp + } + + headers := make(http.Header) + + log.Printf("[DEBUG] Patching TagBindingCollection with URL %s (%s): %#v", patchUrl, operationDesc, patchObj) + initialOp, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "PATCH", + Project: billingProject, + RawURL: patchUrl, + UserAgent: userAgent, + Body: patchObj, + Headers: headers, + }) + if err != nil { + return fmt.Errorf("error starting PATCH request for %s: %w", operationDesc, err) + } + + err = TagsOperationWaitTime(config, initialOp, operationDesc, userAgent, timeout) + if err != nil { + return fmt.Errorf("error waiting for PATCH operation %s: %w", operationDesc, err) + } + + log.Printf("[INFO] Successfully completed PATCH request for %s", operationDesc) + return nil +} + +// Helper to get a map[string]string from schema.ResourceData +func getStringMapFromSchema(d *schema.ResourceData, key string) (map[string]string, diag.Diagnostics) { + val := d.Get(key) + return convertInterfaceToStringMap(val) +} + +// Helper to safely convert interface{} to map[string]string +func convertInterfaceToStringMap(iface interface{}) (map[string]string, diag.Diagnostics) { + if iface == nil { + return make(map[string]string), nil + } + mapIFace, ok := iface.(map[string]interface{}) + if !ok { + return nil, diag.Errorf("input is not a map[string]interface{}: %T", iface) + } + res, err := convertToStringMap(mapIFace) + if err != nil { + return nil, diag.FromErr(err) + } + return res, nil +} + +// convertToStringMap safely converts a map[string]interface{} to map[string]string. +// It returns an error if any value in the map is not a string. +func convertToStringMap(in map[string]interface{}) (map[string]string, error) { + out := make(map[string]string) + for k, v := range in { + s, ok := v.(string) + if !ok { + return nil, fmt.Errorf("value for key %q is not a string (got %T)", k, v) + } + out[k] = s + } + return out, nil +} + +func init() { + registry.Schema{ + Name: "google_tags_tag_binding_collection", + ProductName: "tags", + Type: registry.SchemaTypeResource, + Schema: ResourceTagsTagBindingCollection(), + }.Register() +} + +{{- end }} diff --git a/mmv1/third_party/terraform/services/tags/resource_tags_tag_binding_collection_meta.yaml.tmpl b/mmv1/third_party/terraform/services/tags/resource_tags_tag_binding_collection_meta.yaml.tmpl new file mode 100644 index 000000000000..91273881735c --- /dev/null +++ b/mmv1/third_party/terraform/services/tags/resource_tags_tag_binding_collection_meta.yaml.tmpl @@ -0,0 +1,15 @@ +{{ if ne $.TargetVersionName "ga" -}} +resource: 'google_tags_tag_binding_collection' +generation_type: 'handwritten' +api_service_name: 'cloudresourcemanager.googleapis.com' +api_version: 'v3' +api_resource_type_kind: 'TagBindingCollection' +fields: + - field: 'location' + - field: 'full_resource_name' + - api_field: 'name' + - field: 'active_tags' + provider_only: true + - field: 'tags' + provider_only: true +{{ end -}} diff --git a/mmv1/third_party/terraform/services/tags/resource_tags_tag_binding_collection_test.go.tmpl b/mmv1/third_party/terraform/services/tags/resource_tags_tag_binding_collection_test.go.tmpl new file mode 100644 index 000000000000..d5ba95cd9f16 --- /dev/null +++ b/mmv1/third_party/terraform/services/tags/resource_tags_tag_binding_collection_test.go.tmpl @@ -0,0 +1,1820 @@ +package tags_test + +{{- if ne $.TargetVersionName "ga" }} + +import ( + "context" + "fmt" + "net/http" + "net/url" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + + "github.com/hashicorp/terraform-provider-google/google/acctest" + "github.com/hashicorp/terraform-provider-google/google/envvar" + "github.com/hashicorp/terraform-provider-google/google/services/tags" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +// Acceptance Tests +func TestAccTagsTagBindingCollection_basic(t *testing.T) { + t.Parallel() + + orgId := envvar.GetTestOrgFromEnv(t) + context := map[string]interface{}{ + "org_id": orgId, + "cc_key_short_name": "tf-test-cc-" + acctest.RandString(t, 6), + "cc_marketing_value_short_name": "tf-test-mkt-" + acctest.RandString(t, 6), + "cc_finance_value_short_name": "tf-test-fin-" + acctest.RandString(t, 6), + "env_key_short_name": "tf-test-env-" + acctest.RandString(t, 6), + "env_prod_value_short_name": "tf-test-prod-" + acctest.RandString(t, 6), + "new_key_short_name": "tf-test-new-" + acctest.RandString(t, 6), + "new_value_short_name": "tf-test-newval-" + acctest.RandString(t, 6), + "project_id": "tf-test-" + acctest.RandString(t, 10), + } + + // Expected namespaced keys + envKeyNamespaced := fmt.Sprintf("%s/%s", orgId, context["env_key_short_name"]) + ccKeyNamespaced := fmt.Sprintf("%s/%s", orgId, context["cc_key_short_name"]) + newKeyNamespaced := fmt.Sprintf("%s/%s", orgId, context["new_key_short_name"]) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), + Steps: []resource.TestStep{ + // Step 1: Create with initial set of tags + { + Config: testAccTagsTagBindingCollection_basic(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", "tags.%", "2"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", "active_tags.%", "2"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", fmt.Sprintf("tags.%s", envKeyNamespaced), context["env_prod_value_short_name"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", fmt.Sprintf("tags.%s", ccKeyNamespaced), context["cc_marketing_value_short_name"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", fmt.Sprintf("active_tags.%s", envKeyNamespaced), context["env_prod_value_short_name"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", fmt.Sprintf("active_tags.%s", ccKeyNamespaced), context["cc_marketing_value_short_name"].(string)), + resource.TestMatchResourceAttr("google_tags_tag_binding_collection.basic", "id", regexp.MustCompile(fmt.Sprintf("locations/global/tagBindingCollections/.+"))), + resource.TestMatchResourceAttr("google_tags_tag_binding_collection.basic", "name", regexp.MustCompile(fmt.Sprintf("locations/global/tagBindingCollections/.+"))), + ), + }, + // Step 2: Import + { + ResourceName: "google_tags_tag_binding_collection.basic", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"tags"}, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", "tags.%", "0"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", "active_tags.%", "2"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", "location", "global"), + ), + }, + // Step 3: Update - modify one tag, remove one tag, add one tag + { + Config: testAccTagsTagBindingCollection_update(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", "tags.%", "2"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", "active_tags.%", "2"), + // env_key was removed from config, should be gone + resource.TestCheckNoResourceAttr("google_tags_tag_binding_collection.basic", fmt.Sprintf("tags.%s", envKeyNamespaced)), + // cc_key value changed + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", fmt.Sprintf("tags.%s", ccKeyNamespaced), context["cc_finance_value_short_name"].(string)), + // new_key was added + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", fmt.Sprintf("tags.%s", newKeyNamespaced), context["new_value_short_name"].(string)), + // Check active_tags + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", fmt.Sprintf("active_tags.%s", ccKeyNamespaced), context["cc_finance_value_short_name"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", fmt.Sprintf("active_tags.%s", newKeyNamespaced), context["new_value_short_name"].(string)), + ), + }, + // Step 4: Empty - Remove all Terraform-managed tags + { + Config: testAccTagsTagBindingCollection_empty(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", "tags.%", "0"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.basic", "active_tags.%", "0"), + ), + }, + }, + }) +} + +func TestAccTagsTagBindingCollection_locationDefault(t *testing.T) { + t.Parallel() + + orgId := envvar.GetTestOrgFromEnv(t) + context := map[string]interface{}{ + "org_id": orgId, + "cc_key_short_name": "tf-test-cc-" + acctest.RandString(t, 6), + "cc_marketing_value_short_name": "tf-test-mkt-" + acctest.RandString(t, 6), + "env_key_short_name": "tf-test-env-" + acctest.RandString(t, 6), + "env_prod_value_short_name": "tf-test-prod-" + acctest.RandString(t, 6), + "project_id": "tf-test-" + acctest.RandString(t, 10), + } + + envKeyNamespaced := fmt.Sprintf("%s/%s", orgId, context["env_key_short_name"]) + ccKeyNamespaced := fmt.Sprintf("%s/%s", orgId, context["cc_key_short_name"]) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccTagsTagBindingCollection_locationDefault(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.location_default", "location", "global"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.location_default", "tags.%", "2"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.location_default", fmt.Sprintf("tags.%s", envKeyNamespaced), context["env_prod_value_short_name"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.location_default", fmt.Sprintf("tags.%s", ccKeyNamespaced), context["cc_marketing_value_short_name"].(string)), + resource.TestMatchResourceAttr("google_tags_tag_binding_collection.location_default", "id", regexp.MustCompile(fmt.Sprintf("locations/global/tagBindingCollections/.+"))), + resource.TestMatchResourceAttr("google_tags_tag_binding_collection.location_default", "name", regexp.MustCompile(fmt.Sprintf("locations/global/tagBindingCollections/.+"))), + ), + }, + }, + }) +} + +func TestAccTagsTagBindingCollection_destroy(t *testing.T) { + t.Parallel() + + orgId := envvar.GetTestOrgFromEnv(t) + context := map[string]interface{}{ + "org_id": orgId, + "key_a_short": "tf-test-ka-" + acctest.RandString(t, 6), + "val_a_short": "tf-test-va-" + acctest.RandString(t, 6), + "key_b_short": "tf-test-kb-" + acctest.RandString(t, 6), + "val_b_short": "tf-test-vb-" + acctest.RandString(t, 6), + "project_id": "tf-test-" + acctest.RandString(t, 10), + } + + keyANamespaced := fmt.Sprintf("%s/%s", orgId, context["key_a_short"]) + keyBNamespaced := fmt.Sprintf("%s/%s", orgId, context["key_b_short"]) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), + Steps: []resource.TestStep{ + // Step 1: Create collection with TagA and a separate binding with TagB + { + Config: testAccTagsTagBindingCollection_destroyStep1(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test", "tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test", "active_tags.%", "2"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test", fmt.Sprintf("tags.%s", keyANamespaced), context["val_a_short"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test", fmt.Sprintf("active_tags.%s", keyANamespaced), context["val_a_short"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test", fmt.Sprintf("active_tags.%s", keyBNamespaced), context["val_b_short"].(string)), + resource.TestMatchResourceAttr("google_tags_tag_binding_collection.test", "id", regexp.MustCompile(fmt.Sprintf("locations/global/tagBindingCollections/.+"))), + resource.TestMatchResourceAttr("google_tags_tag_binding_collection.test", "name", regexp.MustCompile(fmt.Sprintf("locations/global/tagBindingCollections/.+"))), + ), + }, + // Step 2: Destroy the collection resource + { + Config: testAccTagsTagBindingCollection_destroyStep2(context), + Check: resource.ComposeTestCheckFunc( + // Collection is deleted from state, so we need a custom check on the API + // We check that the "other_binding" binding (TagB) still exists. + // This check is implicitly done by Terraform not erroring on the google_tags_tag_binding.other_binding resource. + + // We also need to verify TagA is deleted. This requires a custom API check + // within a CheckFunc, as the collection resource is no longer in the state. + testAccCheckTagADeleted(t, context), + ), + }, + }, + }) +} + +func TestAccTagsTagBindingCollection_outOfBandUpdate(t *testing.T) { + t.Parallel() + + orgId := envvar.GetTestOrgFromEnv(t) + context := map[string]interface{}{ + "org_id": orgId, + "project_id": envvar.GetTestProjectFromEnv(), + "instance_name": "tf-test-vm-oob-" + acctest.RandString(t, 10), + "zone": "us-central1-a", + "key_1_short": "tf-test-k1-" + acctest.RandString(t, 6), + "val_1_short": "tf-test-v1-" + acctest.RandString(t, 6), // Initial value + "val_2_short": "tf-test-v2-" + acctest.RandString(t, 6), // Value to change to externally + } + + keyNamespaced := fmt.Sprintf("%s/%s", orgId, context["key_1_short"]) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), + Steps: []resource.TestStep{ + // Step 1: Create the collection with initial tag value (val_1) on Compute VM + { + Config: testAccTagsTagBindingCollection_oob_config(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_oob", "tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_oob", "active_tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_oob", fmt.Sprintf("tags.%s", keyNamespaced), context["val_1_short"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_oob", fmt.Sprintf("active_tags.%s", keyNamespaced), context["val_1_short"].(string)), + ), + }, + // Step 2: Intermediate PlanOnly step that mutates the physical server in its Check helper! + { + PlanOnly: true, + ExpectNonEmptyPlan: false, + Config: testAccTagsTagBindingCollection_oob_config(context), + Check: resource.ComposeTestCheckFunc( + testAccCheckBindingUpdatedExternally_VM(t, context, keyNamespaced, context["val_2_short"].(string)), + testAccCheckBindingValueViaAPI_VM(t, context, keyNamespaced, context["val_2_short"].(string)), + ), + }, + // Step 3: Let Terraform execute apply to flawlessly reconcile the drift back to val_1! + { + Config: testAccTagsTagBindingCollection_oob_config(context), + PlanOnly: false, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_oob", "tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_oob", "active_tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_oob", fmt.Sprintf("tags.%s", keyNamespaced), context["val_1_short"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_oob", fmt.Sprintf("active_tags.%s", keyNamespaced), context["val_1_short"].(string)), + testAccCheckBindingValueViaAPI_VM(t, context, keyNamespaced, context["val_1_short"].(string)), + ), + }, + }, + }) +} + +func TestAccTagsTagBindingCollection_curatedAndDynamicMix(t *testing.T) { + t.Parallel() + + orgId := envvar.GetTestOrgFromEnv(t) + context := map[string]interface{}{ + "org_id": orgId, + "curated_key_short": "tf-test-cur-" + acctest.RandString(t, 6), + "curated_val_short": "tf-test-cval-" + acctest.RandString(t, 6), + "dynamic_key_short": "tf-test-dyn-" + acctest.RandString(t, 6), + "dynamic_bind_val": "dynamic" + acctest.RandString(t, 4), + "project_id": "tf-test-" + acctest.RandString(t, 10), + } + + curatedKeyNamespaced := fmt.Sprintf("%s/%s", orgId, context["curated_key_short"]) + dynamicKeyNamespaced := fmt.Sprintf("%s/%s", orgId, context["dynamic_key_short"]) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), + Steps: []resource.TestStep{ + // Step 1: Create Collection with a mix of Curated and Dynamic Tag Bindings + { + Config: testAccTagsTagBindingCollection_mixConfig(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.mix", "tags.%", "2"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.mix", "active_tags.%", "2"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.mix", fmt.Sprintf("tags.%s", curatedKeyNamespaced), context["curated_val_short"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.mix", fmt.Sprintf("tags.%s", dynamicKeyNamespaced), context["dynamic_bind_val"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.mix", fmt.Sprintf("active_tags.%s", curatedKeyNamespaced), context["curated_val_short"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.mix", fmt.Sprintf("active_tags.%s", dynamicKeyNamespaced), context["dynamic_bind_val"].(string)), + ), + }, + }, + }) +} + +func TestAccTagsTagBindingCollection_restoration(t *testing.T) { + t.Parallel() + + orgId := envvar.GetTestOrgFromEnv(t) + context := map[string]interface{}{ + "org_id": orgId, + "project_id": envvar.GetTestProjectFromEnv(), + "instance_name": "tf-test-vm-rest-" + acctest.RandString(t, 10), + "zone": "us-central1-a", + "key_short": "tf-test-rest-" + acctest.RandString(t, 6), + "val_short": "tf-test-rval-" + acctest.RandString(t, 6), + } + + keyNamespaced := fmt.Sprintf("%s/%s", orgId, context["key_short"]) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), + Steps: []resource.TestStep{ + // Step 1: Create TagBindingCollection managing 1 tag on Compute VM + { + Config: testAccTagsTagBindingCollection_restorationConfig(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.rest", "tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.rest", fmt.Sprintf("tags.%s", keyNamespaced), context["val_short"].(string)), + ), + }, + // Step 2: Intermediate PlanOnly step that deletes the physical server tag in its Check helper! + { + PlanOnly: true, + ExpectNonEmptyPlan: false, + Config: testAccTagsTagBindingCollection_restorationConfig(context), + Check: resource.ComposeTestCheckFunc( + testAccCheckBindingDeletedExternally_VM(t, context, keyNamespaced), + ), + }, + // Step 3: Let Terraform execute apply to fully and flawlessly restore the deleted tag! + { + Config: testAccTagsTagBindingCollection_restorationConfig(context), + PlanOnly: false, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.rest", "tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.rest", fmt.Sprintf("tags.%s", keyNamespaced), context["val_short"].(string)), + testAccCheckBindingValueViaAPI_VM(t, context, keyNamespaced, context["val_short"].(string)), + ), + }, + }, + }) +} + +func testAccTagsTagBindingCollection_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_project" "project" { + provider = google-beta + project_id = "%{project_id}" + name = "%{project_id}" + org_id = "%{org_id}" + deletion_policy = "DELETE" +} + +resource "google_tags_tag_key" "env_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{env_key_short_name}" +} +resource "google_tags_tag_value" "env_prod_value" { + provider = google-beta + parent = google_tags_tag_key.env_key.id + short_name = "%{env_prod_value_short_name}" +} + +resource "google_tags_tag_key" "cost_center_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{cc_key_short_name}" +} +resource "google_tags_tag_value" "cc_marketing_value" { + provider = google-beta + parent = google_tags_tag_key.cost_center_key.id + short_name = "%{cc_marketing_value_short_name}" +} + +# Resources for update test +resource "google_tags_tag_value" "cc_finance_value" { + provider = google-beta + parent = google_tags_tag_key.cost_center_key.id + short_name = "%{cc_finance_value_short_name}" +} +resource "google_tags_tag_key" "new_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{new_key_short_name}" +} +resource "google_tags_tag_value" "new_value" { + provider = google-beta + parent = google_tags_tag_key.new_key.id + short_name = "%{new_value_short_name}" +} + +resource "google_tags_tag_binding_collection" "basic" { + provider = google-beta + full_resource_name = "//cloudresourcemanager.googleapis.com/projects/${google_project.project.number}" + location = "global" + tags = { + "${google_tags_tag_key.env_key.namespaced_name}" = google_tags_tag_value.env_prod_value.short_name + "${google_tags_tag_key.cost_center_key.namespaced_name}" = google_tags_tag_value.cc_marketing_value.short_name + } + + depends_on = [ + google_tags_tag_value.env_prod_value, + google_tags_tag_value.cc_marketing_value, + google_tags_tag_value.cc_finance_value, + google_tags_tag_value.new_value, + ] +} +`, context) +} + +func testAccTagsTagBindingCollection_update(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_project" "project" { + provider = google-beta + project_id = "%{project_id}" + name = "%{project_id}" + org_id = "%{org_id}" + deletion_policy = "DELETE" +} + +resource "google_tags_tag_key" "env_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{env_key_short_name}" +} +resource "google_tags_tag_value" "env_prod_value" { + provider = google-beta + parent = google_tags_tag_key.env_key.id + short_name = "%{env_prod_value_short_name}" +} + +resource "google_tags_tag_key" "cost_center_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{cc_key_short_name}" +} +resource "google_tags_tag_value" "cc_marketing_value" { + provider = google-beta + parent = google_tags_tag_key.cost_center_key.id + short_name = "%{cc_marketing_value_short_name}" +} +resource "google_tags_tag_value" "cc_finance_value" { + provider = google-beta + parent = google_tags_tag_key.cost_center_key.id + short_name = "%{cc_finance_value_short_name}" +} + +resource "google_tags_tag_key" "new_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{new_key_short_name}" +} +resource "google_tags_tag_value" "new_value" { + provider = google-beta + parent = google_tags_tag_key.new_key.id + short_name = "%{new_value_short_name}" +} + +resource "google_tags_tag_binding_collection" "basic" { + provider = google-beta + full_resource_name = "//cloudresourcemanager.googleapis.com/projects/${google_project.project.number}" + location = "global" + tags = { + // env_key removed + "${google_tags_tag_key.cost_center_key.namespaced_name}" = google_tags_tag_value.cc_finance_value.short_name # Changed value + "${google_tags_tag_key.new_key.namespaced_name}" = google_tags_tag_value.new_value.short_name # Added tag + } + + depends_on = [ + google_tags_tag_value.env_prod_value, + google_tags_tag_value.cc_marketing_value, + google_tags_tag_value.cc_finance_value, + google_tags_tag_value.new_value, + ] +} +`, context) +} + +func testAccTagsTagBindingCollection_empty(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_project" "project" { + provider = google-beta + project_id = "%{project_id}" + name = "%{project_id}" + org_id = "%{org_id}" + deletion_policy = "DELETE" +} + +resource "google_tags_tag_key" "env_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{env_key_short_name}" +} +resource "google_tags_tag_value" "env_prod_value" { + provider = google-beta + parent = google_tags_tag_key.env_key.id + short_name = "%{env_prod_value_short_name}" +} + +resource "google_tags_tag_key" "cost_center_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{cc_key_short_name}" +} +resource "google_tags_tag_value" "cc_marketing_value" { + provider = google-beta + parent = google_tags_tag_key.cost_center_key.id + short_name = "%{cc_marketing_value_short_name}" +} +resource "google_tags_tag_value" "cc_finance_value" { + provider = google-beta + parent = google_tags_tag_key.cost_center_key.id + short_name = "%{cc_finance_value_short_name}" +} + +resource "google_tags_tag_key" "new_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{new_key_short_name}" +} +resource "google_tags_tag_value" "new_value" { + provider = google-beta + parent = google_tags_tag_key.new_key.id + short_name = "%{new_value_short_name}" +} + +resource "google_tags_tag_binding_collection" "basic" { + provider = google-beta + full_resource_name = "//cloudresourcemanager.googleapis.com/projects/${google_project.project.number}" + location = "global" + tags = {} # Empty map + + depends_on = [ + google_tags_tag_value.env_prod_value, + google_tags_tag_value.cc_marketing_value, + google_tags_tag_value.cc_finance_value, + google_tags_tag_value.new_value, + ] +} +`, context) +} + +func testAccTagsTagBindingCollection_destroyStep1(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_project" "project" { + provider = google-beta + project_id = "%{project_id}" + name = "%{project_id}" + org_id = "%{org_id}" + deletion_policy = "DELETE" +} + +resource "google_tags_tag_key" "key_a" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_a_short}" +} +resource "google_tags_tag_value" "val_a" { + provider = google-beta + parent = google_tags_tag_key.key_a.id + short_name = "%{val_a_short}" +} +resource "google_tags_tag_key" "key_b" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_b_short}" +} +resource "google_tags_tag_value" "val_b" { + provider = google-beta + parent = google_tags_tag_key.key_b.id + short_name = "%{val_b_short}" +} + +resource "google_tags_tag_binding" "other_binding" { + provider = google-beta + parent = "//cloudresourcemanager.googleapis.com/projects/${google_project.project.number}" + tag_value = google_tags_tag_value.val_b.id + depends_on = [google_tags_tag_value.val_a, google_tags_tag_value.val_b] +} + +resource "google_tags_tag_binding_collection" "test" { + provider = google-beta + full_resource_name = "//cloudresourcemanager.googleapis.com/projects/${google_project.project.number}" + location = "global" + tags = { + "${google_tags_tag_key.key_a.namespaced_name}" = google_tags_tag_value.val_a.short_name + } + depends_on = [google_tags_tag_value.val_a, google_tags_tag_value.val_b, google_tags_tag_binding.other_binding] +} +`, context) +} + +func testAccTagsTagBindingCollection_destroyStep2(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_project" "project" { + provider = google-beta + project_id = "%{project_id}" + name = "%{project_id}" + org_id = "%{org_id}" + deletion_policy = "DELETE" +} + +resource "google_tags_tag_key" "key_a" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_a_short}" +} +resource "google_tags_tag_value" "val_a" { + provider = google-beta + parent = google_tags_tag_key.key_a.id + short_name = "%{val_a_short}" +} +resource "google_tags_tag_key" "key_b" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_b_short}" +} +resource "google_tags_tag_value" "val_b" { + provider = google-beta + parent = google_tags_tag_key.key_b.id + short_name = "%{val_b_short}" +} + +resource "google_tags_tag_binding" "other_binding" { + provider = google-beta + parent = "//cloudresourcemanager.googleapis.com/projects/${google_project.project.number}" + tag_value = google_tags_tag_value.val_b.id + depends_on = [google_tags_tag_value.val_a, google_tags_tag_value.val_b] +} +`, context) +} + +func testAccTagsTagBindingCollection_oob_config(context map[string]interface{}) string { + return acctest.Nprintf(` +data "google_compute_image" "my_image" { + provider = google-beta + family = "debian-11" + project = "debian-cloud" +} + +resource "google_compute_instance" "instance" { + provider = google-beta + name = "%{instance_name}" + machine_type = "e2-micro" + zone = "%{zone}" + + boot_disk { + initialize_params { + image = data.google_compute_image.my_image.self_link + } + } + + network_interface { + network = "default" + } +} + +resource "google_tags_tag_key" "key_1" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_1_short}" +} + +resource "google_tags_tag_value" "key_1_val_1" { + provider = google-beta + parent = google_tags_tag_key.key_1.id + short_name = "%{val_1_short}" +} + +resource "google_tags_tag_value" "key_1_val_2" { + provider = google-beta + parent = google_tags_tag_key.key_1.id + short_name = "%{val_2_short}" +} + +resource "google_tags_tag_binding_collection" "test_oob" { + provider = google-beta + full_resource_name = "//compute.googleapis.com/projects/%{project_id}/zones/%{zone}/instances/${google_compute_instance.instance.instance_id}" + location = "us-central1-a" + + tags = { + "${google_tags_tag_key.key_1.namespaced_name}" = google_tags_tag_value.key_1_val_1.short_name + } + + depends_on = [ + google_tags_tag_value.key_1_val_1, + google_tags_tag_value.key_1_val_2, + ] +} +`, context) +} + +func dummyResourceState() *terraform.ResourceState { + return &terraform.ResourceState{ + Primary: &terraform.InstanceState{}, + } +} + +func dummyResourceStateWithLocation(loc string) *terraform.ResourceState { + return &terraform.ResourceState{ + Primary: &terraform.InstanceState{ + Attributes: map[string]string{ + "location": loc, + }, + }, + } +} + +func testAccCheckBindingValueViaAPI_Direct(t *testing.T, location, encodedResourceName, keyNamespacedName, expectedValueShortName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := acctest.GoogleProviderConfig(t) + basePath := "{{"{{"}}TagsLocationBasePath{{"}}"}}" + if location == "global" { + basePath = "{{"{{"}}TagsBasePath{{"}}"}}" + } + apiURL, err := tpgresource.ReplaceVarsForTest(config, dummyResourceState(), fmt.Sprintf("%slocations/%s/tagBindingCollections/%s", basePath, location, encodedResourceName)) + if err != nil { + return err + } + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + RawURL: apiURL, + Headers: make(http.Header), + }) + if err != nil { + return fmt.Errorf("Error reading TagBindingCollection via API: %s", err) + } + tagsMap, ok := res["tags"].(map[string]interface{}) + if !ok { + return fmt.Errorf("API response does not contain 'tags' map") + } + currentValue, ok := tagsMap[keyNamespacedName].(string) + if !ok || currentValue != expectedValueShortName { + return fmt.Errorf("TagValue mismatch via direct API GET: expected %s, got %s", expectedValueShortName, currentValue) + } + return nil + } +} + +func testAccCheckTagADeleted(t *testing.T, contextMap map[string]interface{}) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := acctest.GoogleProviderConfig(t) + orgId := contextMap["org_id"].(string) + keyANamespaced := fmt.Sprintf("%s/%s", orgId, contextMap["key_a_short"]) + + projectResource, ok := s.RootModule().Resources["google_project.project"] + if !ok { + return fmt.Errorf("Resource 'google_project.project' not found in state") + } + projectNumber := projectResource.Primary.Attributes["number"] + + resourceName := fmt.Sprintf("//cloudresourcemanager.googleapis.com/projects/%s", projectNumber) + encodedResourceName := url.PathEscape(resourceName) + location := "global" + + headers := make(http.Header) + apiURL, err := tpgresource.ReplaceVarsForTest(config, dummyResourceState(), fmt.Sprintf("{{"{{"}}TagsBasePath{{"}}"}}locations/%s/tagBindingCollections/%s", location, encodedResourceName)) + if err != nil { + return err + } + + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + RawURL: apiURL, + Headers: headers, + }) + + if err != nil { + if transport_tpg.IsGoogleApiErrorWithCode(err, 404) { + return nil + } + return fmt.Errorf("Error reading TagBindingCollection: %s", err) + } + + if res != nil && res["tags"] != nil { + activeTagsIFace, ok := res["tags"].(map[string]interface{}) + if ok { + if _, exists := activeTagsIFace[keyANamespaced]; exists { + return fmt.Errorf("Tag %s should have been removed, but still exists in the collection", keyANamespaced) + } + } + } + return nil + } +} + +// testAccCheckBindingUpdatedExternally performs the out-of-band API call to change the TagValue. +func testAccCheckBindingUpdatedExternally(t *testing.T, contextMap map[string]interface{}, keyNamespacedName, newValueShortName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := acctest.GoogleProviderConfig(t) + + projectResource, ok := s.RootModule().Resources["google_project.project"] + if !ok { + return fmt.Errorf("Resource 'google_project.project' not found in state") + } + projectNumber := projectResource.Primary.Attributes["number"] + + resourceName := fmt.Sprintf("//cloudresourcemanager.googleapis.com/projects/%s", projectNumber) + encodedResourceName := url.PathEscape(resourceName) + location := "global" + + apiURL, err := tpgresource.ReplaceVarsForTest(config, dummyResourceState(), fmt.Sprintf("{{"{{"}}TagsBasePath{{"}}"}}locations/%s/tagBindingCollections/%s", location, encodedResourceName)) + if err != nil { + return err + } + + body := map[string]interface{}{ + "full_resource_name": resourceName, + "tags": map[string]string{ + keyNamespacedName: newValueShortName, + }, + } + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "PATCH", + RawURL: apiURL, + Body: body, + Headers: make(http.Header), + }) + + if err != nil { + return fmt.Errorf("Error updating TagBindingCollection via API: %s", err) + } + t.Logf("Successfully updated binding for %s to %s via API", keyNamespacedName, newValueShortName) + return nil + } +} + +// testAccCheckBindingDeletedExternally performs the out-of-band API call to delete the TagBinding. +func testAccCheckBindingDeletedExternally(t *testing.T, contextMap map[string]interface{}, keyNamespacedName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := acctest.GoogleProviderConfig(t) + + projectResource, ok := s.RootModule().Resources["google_project.project"] + if !ok { + return fmt.Errorf("Resource 'google_project.project' not found in state") + } + projectNumber := projectResource.Primary.Attributes["number"] + + resourceName := fmt.Sprintf("//cloudresourcemanager.googleapis.com/projects/%s", projectNumber) + encodedResourceName := url.PathEscape(resourceName) + location := "global" + + // 1. Get current TagBindingCollection to find Etag + apiURL, err := tpgresource.ReplaceVarsForTest(config, dummyResourceState(), fmt.Sprintf("{{"{{"}}TagsBasePath{{"}}"}}locations/%s/tagBindingCollections/%s", location, encodedResourceName)) + if err != nil { + return err + } + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + RawURL: apiURL, + UserAgent: config.UserAgent, + }) + if err != nil { + return fmt.Errorf("Error fetching parent TagBindingCollection before deletion: %v", err) + } + + currentEtag := res["etag"].(string) + + body := map[string]interface{}{ + "full_resource_name": resourceName, + "etag": currentEtag, + "tags": make(map[string]interface{}), + } + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "PATCH", + RawURL: apiURL + "?updateMask=*", + Body: body, + Headers: make(http.Header), + }) + + if err != nil { + return fmt.Errorf("Error deleting TagBindingCollection via API: %s", err) + } + t.Logf("Successfully deleted binding for %s externally via API", keyNamespacedName) + return nil + } +} + +// testAccCheckBindingValueViaAPI checks the current TagValue for a key by calling the GET API. +func testAccCheckBindingValueViaAPI(t *testing.T, contextMap map[string]interface{}, keyNamespacedName, expectedValueShortName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := acctest.GoogleProviderConfig(t) + + projectResource, ok := s.RootModule().Resources["google_project.project"] + if !ok { + return fmt.Errorf("Resource 'google_project.project' not found in state") + } + projectNumber := projectResource.Primary.Attributes["number"] + + resourceName := fmt.Sprintf("//cloudresourcemanager.googleapis.com/projects/%s", projectNumber) + encodedResourceName := url.PathEscape(resourceName) + location := "global" + + apiURL, err := tpgresource.ReplaceVarsForTest(config, dummyResourceState(), fmt.Sprintf("{{"{{"}}TagsBasePath{{"}}"}}locations/%s/tagBindingCollections/%s", location, encodedResourceName)) + if err != nil { + return err + } + + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + RawURL: apiURL, + Headers: make(http.Header), + }) + + if err != nil { + return fmt.Errorf("Error reading TagBindingCollection via API: %s", err) + } + + tagsMap, ok := res["tags"].(map[string]interface{}) + if !ok { + return fmt.Errorf("API response does not contain 'tags' map or it's not the expected type") + } + + currentValue, ok := tagsMap[keyNamespacedName].(string) + if !ok { + return fmt.Errorf("Tag key %s not found in the collection read from API", keyNamespacedName) + } + + if currentValue != expectedValueShortName { + return fmt.Errorf("Expected tag value for %s to be %s, but got %s from API", keyNamespacedName, expectedValueShortName, currentValue) + } + + t.Logf("Confirmed via API that binding for %s is %s", keyNamespacedName, currentValue) + return nil + } +} + +// Unit Tests + +func TestResourceTagsTagBindingCollectionImport(t *testing.T) { + // This test function is defined in the package 'tags', so we can call the function directly. + importer := tags.ResourceTagsTagBindingCollection().Importer + if importer == nil || importer.StateContext == nil { + t.Fatalf("Importer not configured for google_tags_tag_binding_collection") + } + StateContext := importer.StateContext + + cases := []struct { + Name string + ImportID string + ExpectedID string + ExpectedLoc string + ExpectedResourceName string + ExpectError bool + }{ + { + Name: "StandardImportProject", + ImportID: "locations/global/tagBindingCollections/%2F%2Fcloudresourcemanager.googleapis.com%2Fprojects%2F123", + ExpectedID: "locations/global/tagBindingCollections/%2F%2Fcloudresourcemanager.googleapis.com%2Fprojects%2F123", + ExpectedLoc: "global", + ExpectedResourceName: "//cloudresourcemanager.googleapis.com/projects/123", + ExpectError: false, + }, + { + Name: "ZonalImportComputeInstance", + ImportID: "locations/us-central1-a/tagBindingCollections/%2F%2Fcompute.googleapis.com/projects%2F123%2Fzones%2Fus-central1-a%2Finstances%2F456", + ExpectedID: "locations/us-central1-a/tagBindingCollections/%2F%2Fcompute.googleapis.com/projects%2F123%2Fzones%2Fus-central1-a%2Finstances%2F456", + ExpectedLoc: "us-central1-a", + ExpectedResourceName: "//compute.googleapis.com/projects/123/zones/us-central1-a/instances/456", + ExpectError: false, + }, + { + Name: "InvalidFormatMissingLocationPrefix", + ImportID: "global/tagBindingCollections/%2F%2Fcloudresourcemanager.googleapis.com%2Fprojects%2F123", + ExpectError: true, + }, + { + Name: "InvalidFormatMissingCollectionKeyword", + ImportID: "locations/global/%2F%2Fcloudresourcemanager.googleapis.com%2Fprojects%2F123", + ExpectError: true, + }, + { + Name: "InvalidEncodedResourceName", + ImportID: "locations/global/tagBindingCollections/%invalid", + ExpectError: true, + }, + { + Name: "EmptyResourceName", + ImportID: "locations/global/tagBindingCollections/", + ExpectError: true, + }, + } + + for _, tc := range cases { + t.Run(tc.Name, func(t *testing.T) { + d := schema.TestResourceDataRaw(t, tags.ResourceTagsTagBindingCollection().Schema, nil) + d.SetId(tc.ImportID) + + newData, err := StateContext(context.Background(), d, nil) + + if tc.ExpectError { + if err == nil { + t.Errorf("Expected error but got none") + } + return + } + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if len(newData) != 1 { + t.Fatalf("Expected 1 ResourceData to be returned, got %d", len(newData)) + } + + data := newData[0] + if data.Id() != tc.ExpectedID { + t.Errorf("Expected ID %q, got %q", tc.ExpectedID, data.Id()) + } + if got := data.Get("location").(string); got != tc.ExpectedLoc { + t.Errorf("Expected location %q, got %q", tc.ExpectedLoc, got) + } + if got := data.Get("full_resource_name").(string); got != tc.ExpectedResourceName { + t.Errorf("Expected full_resource_name %q, got %q", tc.ExpectedResourceName, got) + } + }) + } +} + +func testAccTagsTagBindingCollection_locationDefault(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_project" "project" { + provider = google-beta + project_id = "%{project_id}" + name = "%{project_id}" + org_id = "%{org_id}" + deletion_policy = "DELETE" +} + +resource "google_tags_tag_key" "env_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{env_key_short_name}" +} +resource "google_tags_tag_value" "env_prod_value" { + provider = google-beta + parent = google_tags_tag_key.env_key.id + short_name = "%{env_prod_value_short_name}" +} + +resource "google_tags_tag_key" "cost_center_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{cc_key_short_name}" +} +resource "google_tags_tag_value" "cc_marketing_value" { + provider = google-beta + parent = google_tags_tag_key.cost_center_key.id + short_name = "%{cc_marketing_value_short_name}" +} + +resource "google_tags_tag_binding_collection" "location_default" { + provider = google-beta + full_resource_name = "//cloudresourcemanager.googleapis.com/projects/${google_project.project.number}" + # location is omitted, should default to "global" + tags = { + "${google_tags_tag_key.env_key.namespaced_name}" = google_tags_tag_value.env_prod_value.short_name + "${google_tags_tag_key.cost_center_key.namespaced_name}" = google_tags_tag_value.cc_marketing_value.short_name + } + + depends_on = [ + google_tags_tag_value.env_prod_value, + google_tags_tag_value.cc_marketing_value, + ] +} +`, context) +} + +func testAccTagsTagBindingCollection_mixConfig(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_project" "project" { + provider = google-beta + project_id = "%{project_id}" + name = "%{project_id}" + org_id = "%{org_id}" + deletion_policy = "DELETE" +} + +resource "google_tags_tag_key" "curated_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{curated_key_short}" +} +resource "google_tags_tag_value" "curated_val" { + provider = google-beta + parent = google_tags_tag_key.curated_key.id + short_name = "%{curated_val_short}" +} + +resource "google_tags_tag_key" "dynamic_key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{dynamic_key_short}" + allowed_values_regex = "^[a-z0-9]+$" +} + +resource "google_tags_tag_binding_collection" "mix" { + provider = google-beta + full_resource_name = "//cloudresourcemanager.googleapis.com/projects/${google_project.project.number}" + location = "global" + tags = { + "${google_tags_tag_key.curated_key.namespaced_name}" = google_tags_tag_value.curated_val.short_name + "${google_tags_tag_key.dynamic_key.namespaced_name}" = "%{dynamic_bind_val}" + } + + depends_on = [ + google_tags_tag_value.curated_val, + ] +} +`, context) +} + +func testAccTagsTagBindingCollection_restorationConfig(context map[string]interface{}) string { + return acctest.Nprintf(` +data "google_compute_image" "my_image" { + provider = google-beta + family = "debian-11" + project = "debian-cloud" +} + +resource "google_compute_instance" "instance" { + provider = google-beta + name = "%{instance_name}" + machine_type = "e2-micro" + zone = "%{zone}" + + boot_disk { + initialize_params { + image = data.google_compute_image.my_image.self_link + } + } + + network_interface { + network = "default" + } +} + +resource "google_tags_tag_key" "key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_short}" +} + +resource "google_tags_tag_value" "val" { + provider = google-beta + parent = google_tags_tag_key.key.id + short_name = "%{val_short}" +} + +resource "google_tags_tag_binding_collection" "rest" { + provider = google-beta + full_resource_name = "//compute.googleapis.com/projects/%{project_id}/zones/%{zone}/instances/${google_compute_instance.instance.instance_id}" + location = "us-central1-a" + + tags = { + "${google_tags_tag_key.key.namespaced_name}" = google_tags_tag_value.val.short_name + } + + depends_on = [ + google_tags_tag_value.val, + ] +} +`, context) +} + +// TODO: Add more unit tests for Upsert and Delete logic, mocking API calls. + +func TestAccTagsTagBindingCollection_regional_basic(t *testing.T) { + t.Parallel() + + orgId := envvar.GetTestOrgFromEnv(t) + context := map[string]interface{}{ + "org_id": orgId, + "key_short_name": "tf-test-reg-" + acctest.RandString(t, 6), + "value_short_name": "tf-test-val-" + acctest.RandString(t, 6), + "project_id": envvar.GetTestProjectFromEnv(), + "instance_name": "tf-test-vm-" + acctest.RandString(t, 10), + "zone": "us-central1-a", + } + + keyNamespaced := fmt.Sprintf("%s/%s", orgId, context["key_short_name"]) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), + Steps: []resource.TestStep{ + // Step 1: Create regional TagBindingCollection on Compute VM + { + Config: testAccTagsTagBindingCollection_regional_basic(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.regional", "tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.regional", "active_tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.regional", fmt.Sprintf("tags.%s", keyNamespaced), context["value_short_name"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.regional", fmt.Sprintf("active_tags.%s", keyNamespaced), context["value_short_name"].(string)), + resource.TestMatchResourceAttr("google_tags_tag_binding_collection.regional", "id", regexp.MustCompile("locations/us-central1-a/tagBindingCollections/.+")), + resource.TestMatchResourceAttr("google_tags_tag_binding_collection.regional", "name", regexp.MustCompile("locations/us-central1-a/tagBindingCollections/.+")), + ), + }, + // Step 2: Import + { + ResourceName: "google_tags_tag_binding_collection.regional", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"full_resource_name", "location", "tags"}, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.regional", "tags.%", "0"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.regional", "active_tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.regional", "location", "us-central1-a"), + ), + }, + }, + }) +} + +func testAccTagsTagBindingCollection_regional_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +data "google_compute_image" "my_image" { + provider = google-beta + family = "debian-11" + project = "debian-cloud" +} + +resource "google_compute_instance" "instance" { + provider = google-beta + name = "%{instance_name}" + machine_type = "e2-micro" + zone = "%{zone}" + + boot_disk { + initialize_params { + image = data.google_compute_image.my_image.self_link + } + } + + network_interface { + network = "default" + } +} + +resource "google_tags_tag_key" "key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_short_name}" +} + +resource "google_tags_tag_value" "value" { + provider = google-beta + parent = google_tags_tag_key.key.id + short_name = "%{value_short_name}" +} + +resource "google_tags_tag_binding_collection" "regional" { + provider = google-beta + full_resource_name = "//compute.googleapis.com/projects/%{project_id}/zones/%{zone}/instances/${google_compute_instance.instance.instance_id}" + location = "us-central1-a" + + tags = { + "${google_tags_tag_key.key.namespaced_name}" = google_tags_tag_value.value.short_name + } +} +`, context) +} + +func TestAccTagsTagBindingCollection_forceNew(t *testing.T) { + t.Parallel() + + orgId := envvar.GetTestOrgFromEnv(t) + context := map[string]interface{}{ + "org_id": orgId, + "key_short_name": "tf-test-fn-" + acctest.RandString(t, 6), + "value_short_name": "tf-test-val-" + acctest.RandString(t, 6), + "project_id": envvar.GetTestProjectFromEnv(), + "instance_name": "tf-test-vm-fn-" + acctest.RandString(t, 10), + "zone_1": "us-central1-a", + "zone_2": "us-central1-b", + } + + keyNamespaced := fmt.Sprintf("%s/%s", orgId, context["key_short_name"]) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), + Steps: []resource.TestStep{ + // Step 1: Create TagBindingCollection on VM in zone_1 + { + Config: testAccTagsTagBindingCollection_forceNewConfig1(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.fn", "tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.fn", fmt.Sprintf("tags.%s", keyNamespaced), context["value_short_name"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.fn", "location", "us-central1-a"), + ), + }, + // Step 2: Update configuration to target VM in zone_2! This must trigger a perfect ForceNew replacement! + { + Config: testAccTagsTagBindingCollection_forceNewConfig2(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.fn", "tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.fn", fmt.Sprintf("tags.%s", keyNamespaced), context["value_short_name"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.fn", "location", "us-central1-b"), + ), + }, + }, + }) +} + +func testAccTagsTagBindingCollection_forceNewConfig1(context map[string]interface{}) string { + return acctest.Nprintf(` +data "google_compute_image" "my_image" { + provider = google-beta + family = "debian-11" + project = "debian-cloud" +} + +resource "google_compute_instance" "vm1" { + provider = google-beta + name = "%{instance_name}-1" + machine_type = "e2-micro" + zone = "%{zone_1}" + boot_disk { + initialize_params { + image = data.google_compute_image.my_image.self_link + } + } + network_interface { + network = "default" + } +} + +resource "google_compute_instance" "vm2" { + provider = google-beta + name = "%{instance_name}-2" + machine_type = "e2-micro" + zone = "%{zone_2}" + boot_disk { + initialize_params { + image = data.google_compute_image.my_image.self_link + } + } + network_interface { + network = "default" + } +} + +resource "google_tags_tag_key" "key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_short_name}" +} + +resource "google_tags_tag_value" "value" { + provider = google-beta + parent = google_tags_tag_key.key.id + short_name = "%{value_short_name}" +} + +resource "google_tags_tag_binding_collection" "fn" { + provider = google-beta + full_resource_name = "//compute.googleapis.com/projects/%{project_id}/zones/%{zone_1}/instances/${google_compute_instance.vm1.instance_id}" + location = "us-central1-a" + + tags = { + "${google_tags_tag_key.key.namespaced_name}" = google_tags_tag_value.value.short_name + } +} +`, context) +} + +func testAccTagsTagBindingCollection_forceNewConfig2(context map[string]interface{}) string { + return acctest.Nprintf(` +data "google_compute_image" "my_image" { + provider = google-beta + family = "debian-11" + project = "debian-cloud" +} + +resource "google_compute_instance" "vm1" { + provider = google-beta + name = "%{instance_name}-1" + machine_type = "e2-micro" + zone = "%{zone_1}" + boot_disk { + initialize_params { + image = data.google_compute_image.my_image.self_link + } + } + network_interface { + network = "default" + } +} + +resource "google_compute_instance" "vm2" { + provider = google-beta + name = "%{instance_name}-2" + machine_type = "e2-micro" + zone = "%{zone_2}" + boot_disk { + initialize_params { + image = data.google_compute_image.my_image.self_link + } + } + network_interface { + network = "default" + } +} + +resource "google_tags_tag_key" "key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_short_name}" +} + +resource "google_tags_tag_value" "value" { + provider = google-beta + parent = google_tags_tag_key.key.id + short_name = "%{value_short_name}" +} + +resource "google_tags_tag_binding_collection" "fn" { + provider = google-beta + full_resource_name = "//compute.googleapis.com/projects/%{project_id}/zones/%{zone_2}/instances/${google_compute_instance.vm2.instance_id}" + location = "us-central1-b" + + tags = { + "${google_tags_tag_key.key.namespaced_name}" = google_tags_tag_value.value.short_name + } +} +`, context) +} + +func TestAccTagsTagBindingCollection_regionalMismatch(t *testing.T) { + t.Parallel() + + orgId := envvar.GetTestOrgFromEnv(t) + context := map[string]interface{}{ + "org_id": orgId, + "key_short_name": "tf-test-err-" + acctest.RandString(t, 6), + "value_short_name": "tf-test-val-" + acctest.RandString(t, 6), + "project_id": envvar.GetTestProjectFromEnv(), + "instance_name": "tf-test-vm-err-" + acctest.RandString(t, 10), + "zone": "us-central1-a", + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), + Steps: []resource.TestStep{ + // Attempt to bind to a Zonal Compute VM but pass location = "global" + { + Config: testAccTagsTagBindingCollection_regionalMismatchConfig(context), + ExpectError: regexp.MustCompile("Must be a valid One Platform resource name of a tag-compatible global resource"), + }, + }, + }) +} + +func testAccTagsTagBindingCollection_regionalMismatchConfig(context map[string]interface{}) string { + return acctest.Nprintf(` +data "google_compute_image" "my_image" { + provider = google-beta + family = "debian-11" + project = "debian-cloud" +} + +resource "google_compute_instance" "instance" { + provider = google-beta + name = "%{instance_name}" + machine_type = "e2-micro" + zone = "%{zone}" + boot_disk { + initialize_params { + image = data.google_compute_image.my_image.self_link + } + } + network_interface { + network = "default" + } +} + +resource "google_tags_tag_key" "key" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_short_name}" +} + +resource "google_tags_tag_value" "value" { + provider = google-beta + parent = google_tags_tag_key.key.id + short_name = "%{value_short_name}" +} + +resource "google_tags_tag_binding_collection" "err" { + provider = google-beta + full_resource_name = "//compute.googleapis.com/projects/%{project_id}/zones/%{zone}/instances/${google_compute_instance.instance.instance_id}" + location = "global" + + tags = { + "${google_tags_tag_key.key.namespaced_name}" = google_tags_tag_value.value.short_name + } +} +`, context) +} + +func testAccCheckBindingUpdatedExternally_VM(t *testing.T, contextMap map[string]interface{}, keyNamespacedName, newValueShortName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := acctest.GoogleProviderConfig(t) + vmResource := s.RootModule().Resources["google_compute_instance.instance"] + if vmResource == nil { + return fmt.Errorf("Resource 'google_compute_instance.instance' not found in state") + } + instanceId := vmResource.Primary.Attributes["instance_id"] + projectId := contextMap["project_id"].(string) + zone := contextMap["zone"].(string) + + resourceName := fmt.Sprintf("//compute.googleapis.com/projects/%s/zones/%s/instances/%s", projectId, zone, instanceId) + encodedResourceName := url.PathEscape(resourceName) + location := zone + + apiURL, err := tpgresource.ReplaceVarsForTest(config, dummyResourceStateWithLocation(location), fmt.Sprintf("{{"{{"}}TagsLocationBasePath{{"}}"}}locations/%s/tagBindingCollections/%s", location, encodedResourceName)) + if err != nil { + return err + } + apiURL, err = tpgresource.ReplaceVarsForTest(config, dummyResourceStateWithLocation(location), apiURL) + if err != nil { + return err + } + + body := map[string]interface{}{ + "full_resource_name": resourceName, + "tags": map[string]string{ + keyNamespacedName: newValueShortName, + }, + } + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "PATCH", + RawURL: apiURL + "?updateMask=*", + Body: body, + Headers: make(http.Header), + }) + if err != nil { + return fmt.Errorf("Error updating TagBindingCollection via API: %s", err) + } + t.Logf("Successfully updated physical binding for %s to %s via API", keyNamespacedName, newValueShortName) + return nil + } +} + +func testAccCheckBindingDeletedExternally_VM(t *testing.T, contextMap map[string]interface{}, keyNamespacedName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := acctest.GoogleProviderConfig(t) + vmResource := s.RootModule().Resources["google_compute_instance.instance"] + if vmResource == nil { + return fmt.Errorf("Resource 'google_compute_instance.instance' not found in state") + } + instanceId := vmResource.Primary.Attributes["instance_id"] + projectId := contextMap["project_id"].(string) + zone := contextMap["zone"].(string) + + resourceName := fmt.Sprintf("//compute.googleapis.com/projects/%s/zones/%s/instances/%s", projectId, zone, instanceId) + encodedResourceName := url.PathEscape(resourceName) + location := zone + + apiURL, err := tpgresource.ReplaceVarsForTest(config, dummyResourceStateWithLocation(location), fmt.Sprintf("{{"{{"}}TagsLocationBasePath{{"}}"}}locations/%s/tagBindingCollections/%s", location, encodedResourceName)) + if err != nil { + return err + } + apiURL, err = tpgresource.ReplaceVarsForTest(config, dummyResourceStateWithLocation(location), apiURL) + if err != nil { + return err + } + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + RawURL: apiURL, + }) + if err != nil { + return fmt.Errorf("Error fetching parent TagBindingCollection before deletion: %v", err) + } + + currentEtag := res["etag"].(string) + + body := map[string]interface{}{ + "full_resource_name": resourceName, + "etag": currentEtag, + "tags": make(map[string]interface{}), + } + + _, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "PATCH", + RawURL: apiURL + "?updateMask=*", + Body: body, + Headers: make(http.Header), + }) + if err != nil { + return fmt.Errorf("Error deleting TagBindingCollection via API: %s", err) + } + t.Logf("Successfully deleted physical binding for %s externally via API", keyNamespacedName) + return nil + } +} + +func testAccCheckBindingValueViaAPI_VM(t *testing.T, contextMap map[string]interface{}, keyNamespacedName, expectedValueShortName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := acctest.GoogleProviderConfig(t) + vmResource := s.RootModule().Resources["google_compute_instance.instance"] + if vmResource == nil { + return fmt.Errorf("Resource 'google_compute_instance.instance' not found in state") + } + instanceId := vmResource.Primary.Attributes["instance_id"] + projectId := contextMap["project_id"].(string) + zone := contextMap["zone"].(string) + + resourceName := fmt.Sprintf("//compute.googleapis.com/projects/%s/zones/%s/instances/%s", projectId, zone, instanceId) + encodedResourceName := url.PathEscape(resourceName) + location := zone + + apiURL, err := tpgresource.ReplaceVarsForTest(config, dummyResourceStateWithLocation(location), fmt.Sprintf("{{"{{"}}TagsLocationBasePath{{"}}"}}locations/%s/tagBindingCollections/%s", location, encodedResourceName)) + if err != nil { + return err + } + apiURL, err = tpgresource.ReplaceVarsForTest(config, dummyResourceStateWithLocation(location), apiURL) + if err != nil { + return err + } + + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + RawURL: apiURL, + }) + if err != nil { + return fmt.Errorf("Error reading TagBindingCollection via API: %s", err) + } + var tagsMap map[string]interface{} + if res["tags"] != nil { + tagsMap = res["tags"].(map[string]interface{}) + } + var currentValue string + if tagsMap != nil { + currentValue, _ = tagsMap[keyNamespacedName].(string) + } + if currentValue != expectedValueShortName { + return fmt.Errorf("TagValue mismatch via direct API GET: expected %q, got %q", expectedValueShortName, currentValue) + } + return nil + } +} + +func TestAccTagsTagBindingCollection_nonAuthoritative(t *testing.T) { + t.Parallel() + + orgId := envvar.GetTestOrgFromEnv(t) + context := map[string]interface{}{ + "org_id": orgId, + "project_id": envvar.GetTestProjectFromEnv(), + "instance_name": "tf-test-vm-nonauth-" + acctest.RandString(t, 10), + "zone": "us-central1-a", + "key_1_short": "tf-test-k1-" + acctest.RandString(t, 6), + "val_1_short": "tf-test-v1-" + acctest.RandString(t, 6), // Managed by TF + "key_2_short": "tf-test-k2-" + acctest.RandString(t, 6), + "val_2_short": "tf-test-v2-" + acctest.RandString(t, 6), // Attached externally + } + + key1Namespaced := fmt.Sprintf("%s/%s", orgId, context["key_1_short"]) + key2Namespaced := fmt.Sprintf("%s/%s", orgId, context["key_2_short"]) + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderBetaFactories(t), + Steps: []resource.TestStep{ + // Step 1: Create the collection with 1 managed tag (key_1 = val_1) + { + Config: testAccTagsTagBindingCollection_nonAuthoritativeConfig(context), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_nonauth", "tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_nonauth", "active_tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_nonauth", fmt.Sprintf("tags.%s", key1Namespaced), context["val_1_short"].(string)), + ), + }, + // Step 2: Intermediate PlanOnly step that attaches key_2 = val_2 externally (out-of-band) + { + PlanOnly: true, + ExpectNonEmptyPlan: false, + Config: testAccTagsTagBindingCollection_nonAuthoritativeConfig(context), + Check: resource.ComposeTestCheckFunc( + testAccCheckBindingAddedExternally_VM(t, context, "google_tags_tag_value.key_2_val_2"), + testAccCheckBindingValueViaAPI_VM(t, context, key2Namespaced, context["val_2_short"].(string)), + ), + }, + // Step 3: Run plan again. It should show NO CHANGES (empty plan), + // proving that Terraform does not attempt to delete the externally attached key_2! + { + Config: testAccTagsTagBindingCollection_nonAuthoritativeConfig(context), + PlanOnly: true, + ExpectNonEmptyPlan: false, + Check: resource.ComposeTestCheckFunc( + // active_tags should now show both tags (managed + external) + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_nonauth", "active_tags.%", "2"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_nonauth", fmt.Sprintf("active_tags.%s", key1Namespaced), context["val_1_short"].(string)), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_nonauth", fmt.Sprintf("active_tags.%s", key2Namespaced), context["val_2_short"].(string)), + // tags should still only show the 1 managed tag + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_nonauth", "tags.%", "1"), + resource.TestCheckResourceAttr("google_tags_tag_binding_collection.test_nonauth", fmt.Sprintf("tags.%s", key1Namespaced), context["val_1_short"].(string)), + ), + }, + }, + }) +} + +func testAccTagsTagBindingCollection_nonAuthoritativeConfig(context map[string]interface{}) string { + return acctest.Nprintf(` +data "google_compute_image" "my_image" { + provider = google-beta + family = "debian-11" + project = "debian-cloud" +} + +resource "google_compute_instance" "instance" { + provider = google-beta + name = "%{instance_name}" + machine_type = "e2-micro" + zone = "%{zone}" + + boot_disk { + initialize_params { + image = data.google_compute_image.my_image.self_link + } + } + + network_interface { + network = "default" + } +} + +resource "google_tags_tag_key" "key_1" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_1_short}" +} + +resource "google_tags_tag_value" "key_1_val_1" { + provider = google-beta + parent = google_tags_tag_key.key_1.id + short_name = "%{val_1_short}" +} + +resource "google_tags_tag_key" "key_2" { + provider = google-beta + parent = "organizations/%{org_id}" + short_name = "%{key_2_short}" +} + +resource "google_tags_tag_value" "key_2_val_2" { + provider = google-beta + parent = google_tags_tag_key.key_2.id + short_name = "%{val_2_short}" +} + +resource "google_tags_tag_binding_collection" "test_nonauth" { + provider = google-beta + full_resource_name = "//compute.googleapis.com/projects/%{project_id}/zones/%{zone}/instances/${google_compute_instance.instance.instance_id}" + location = "us-central1-a" + + tags = { + "${google_tags_tag_key.key_1.namespaced_name}" = google_tags_tag_value.key_1_val_1.short_name + } + + depends_on = [ + google_tags_tag_value.key_1_val_1, + google_tags_tag_value.key_2_val_2, + ] +} +`, context) +} + +// testAccCheckBindingAddedExternally_VM performs an out-of-band API call to create an individual TagBinding. +// This simulates another client attaching a tag to the same resource. +func testAccCheckBindingAddedExternally_VM(t *testing.T, contextMap map[string]interface{}, tagValueResourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := acctest.GoogleProviderConfig(t) + + // Resolve tag value ID from state + tagValueRes, ok := s.RootModule().Resources[tagValueResourceName] + if !ok { + return fmt.Errorf("Resource %q not found in state", tagValueResourceName) + } + tagValueId := tagValueRes.Primary.ID // This is "tagValues/12345" + + vmResource := s.RootModule().Resources["google_compute_instance.instance"] + if vmResource == nil { + return fmt.Errorf("Resource 'google_compute_instance.instance' not found in state") + } + instanceId := vmResource.Primary.Attributes["instance_id"] + projectId := contextMap["project_id"].(string) + zone := contextMap["zone"].(string) + + resourceName := fmt.Sprintf("//compute.googleapis.com/projects/%s/zones/%s/instances/%s", projectId, zone, instanceId) + + // Create a single TagBinding via the API + apiURL := "https://cloudresourcemanager.googleapis.com/v3/tagBindings" + body := map[string]interface{}{ + "parent": resourceName, + "tagValue": tagValueId, + } + + _, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "POST", + RawURL: apiURL, + Body: body, + Headers: make(http.Header), + }) + + if err != nil { + return fmt.Errorf("Error creating external TagBinding via API: %s", err) + } + t.Logf("Successfully created external binding for %s to %s via API", resourceName, tagValueId) + return nil + } +} + +{{- end }} + diff --git a/mmv1/third_party/terraform/services/vectorsearch/resource_vector_search_index_test.go b/mmv1/third_party/terraform/services/vectorsearch/resource_vector_search_index_test.go new file mode 100644 index 000000000000..17074ba00c4f --- /dev/null +++ b/mmv1/third_party/terraform/services/vectorsearch/resource_vector_search_index_test.go @@ -0,0 +1,172 @@ +package vectorsearch_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + + "github.com/hashicorp/terraform-provider-google/google/acctest" + _ "github.com/hashicorp/terraform-provider-google/google/services/vectorsearch" +) + +func TestAccVectorSearchIndex_update(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + Steps: []resource.TestStep{ + { + Config: testAccVectorSearchIndex_basic(context), + }, + { + ResourceName: "google_vector_search_index.example-index", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"collection_id", "index_id", "labels", "location", "terraform_labels"}, + }, + { + Config: testAccVectorSearchIndex_updated(context), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("google_vector_search_index.example-index", plancheck.ResourceActionUpdate), + }, + }, + }, + { + ResourceName: "google_vector_search_index.example-index", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"collection_id", "index_id", "labels", "location", "terraform_labels"}, + }, + }, + }) +} + +func testAccVectorSearchIndex_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_vector_search_collection" "parent" { + location = "us-central1" + collection_id = "tf-test-example-collection-id%{random_suffix}" + + display_name = "My Awesome Collection" + + data_schema = < Before upgrading to version 8.0.0, it is recommended to upgrade to the most +recent `7.X` series release of the provider, make the changes noted in this guide, +and ensure that your environment successfully runs +[`terraform plan`](https://developer.hashicorp.com/terraform/cli/commands/plan) +without unexpected changes or deprecation notices. + +It is recommended to use [version constraints](https://developer.hashicorp.com/terraform/language/providers/requirements#requiring-providers) +when configuring Terraform providers. If you are following that recommendation, +update the version constraints in your Terraform configuration and run +[`terraform init`](https://developer.hashicorp.com/terraform/cli/commands/init) to download +the new version. + +If you aren't using version constraints, you can use `terraform init -upgrade` +in order to upgrade your provider to the latest released version. + +For example, given this previous configuration: + +```hcl +terraform { + required_providers { + google = { + version = "~> 7.30.0" + } + } +} +``` + +An updated configuration: + +```hcl +terraform { + required_providers { + google = { + version = "~> 8.0.0" + } + } +} +``` + +## Provider + +## Datasources + +## Resources diff --git a/mmv1/third_party/terraform/website/docs/list-resources/google_dns_managed_zone.html.markdown b/mmv1/third_party/terraform/website/docs/list-resources/google_dns_managed_zone.html.markdown new file mode 100644 index 000000000000..9f84bd674b95 --- /dev/null +++ b/mmv1/third_party/terraform/website/docs/list-resources/google_dns_managed_zone.html.markdown @@ -0,0 +1,50 @@ +--- +subcategory: "Cloud DNS" +page_title: "google_dns_managed_zone" +description: |- + Lists DNS managed zones in a project. +--- + +# google_dns_managed_zone (list) + +Lists **DNS managed zones** in a Google Cloud project for use with +[`terraform query`](https://developer.hashicorp.com/terraform/cli/commands/query) and +**`.tfquery.hcl`** files. Results correspond to existing +[`google_dns_managed_zone`](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/dns_managed_zone) +managed resources. + +For how list resources work in this provider, file layout, Terraform version requirements, and +shared `list` block arguments, refer to the guide +[Use list resources with terraform query (Google Cloud provider)](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/using_list_resources_with_terraform_query). + +## Example + +```hcl +list "google_dns_managed_zone" "all" { + provider = google + + config { + # Optional. Defaults to the provider project when omitted. + # project = "my-project" + } +} +``` + +Run `terraform query` from the directory that contains the `.tfquery.hcl` file. + +## Configuration (`config` block) + +* `project` - (Optional) Project ID whose DNS managed zones are listed. If unset, the provider's + configured default project is used. + +## Results + +By default each result includes **resource identity** for `google_dns_managed_zone` (see +[Resource identity](https://developer.hashicorp.com/terraform/language/resources/identities)): + +* `name` - Managed zone name. +* `project` - Project ID. + +With `include_resource = true` on the `list` block, results also include the full resource-style +attributes documented for the managed +[`google_dns_managed_zone` resource](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/dns_managed_zone#attributes-reference). \ No newline at end of file diff --git a/mmv1/third_party/terraform/website/docs/r/compute_instance_group_manager.html.markdown b/mmv1/third_party/terraform/website/docs/r/compute_instance_group_manager.html.markdown index 42acbf2e6668..4f217ccca5ef 100644 --- a/mmv1/third_party/terraform/website/docs/r/compute_instance_group_manager.html.markdown +++ b/mmv1/third_party/terraform/website/docs/r/compute_instance_group_manager.html.markdown @@ -313,7 +313,7 @@ instance_lifecycle_policy { force_update_on_repair = "YES" default_action_on_failure = "DO_NOTHING" on_failed_health_check = "DO_NOTHING" - on_repair { //google-beta only + on_repair { allow_changing_zone = "YES" } } @@ -322,7 +322,7 @@ instance_lifecycle_policy { * `force_update_on_repair` - (Optional), Specifies whether to apply the group's latest configuration when repairing a VM. Valid options are: `YES`, `NO`. If `YES` and you updated the group's instance template or per-instance configurations after the VM was created, then these changes are applied when VM is repaired. If `NO` (default), then updates are applied in accordance with the group's update policy type. * `default_action_on_failure` - (Optional), Specifies the action that a MIG performs on a failed VM. If the value of the `on_failed_health_check` field is `DEFAULT_ACTION`, then the same action also applies to the VMs on which your application fails a health check. Valid options are: `DO_NOTHING`, `REPAIR`. If `DO_NOTHING`, then MIG does not repair a failed VM. If `REPAIR` (default), then MIG automatically repairs a failed VM by recreating it. For more information, see about repairing VMs in a MIG. * `on_failed_health_check` - (Optional), Specifies the action that a MIG performs on an unhealthy VM. A VM is marked as unhealthy when the application running on that VM fails a health check. Valid options are: `DEFAULT_ACTION`, `DO_NOTHING`, `REPAIR`. If `DEFAULT_ACTION` (default), then MIG uses the same action configured for the `default_action_on_failure` field. If `DO_NOTHING`, then MIG does not repair unhealthy VM. If `REPAIR`, then MIG automatically repairs an unhealthy VM by recreating it. For more information, see about repairing VMs in a MIG. -* `on_repair` - (Optional, [Beta](../guides/provider_versions.html.markdown)), Configuration for VM repairs in the MIG. Structure is [documented below](#nested_on_repair). +* `on_repair` - (Optional), Configuration for VM repairs in the MIG. Structure is [documented below](#nested_on_repair). - - - The `on_repair` block supports: diff --git a/mmv1/third_party/terraform/website/docs/r/compute_region_instance_group_manager.html.markdown b/mmv1/third_party/terraform/website/docs/r/compute_region_instance_group_manager.html.markdown index 86ec2740d800..9cac3f0d0f01 100644 --- a/mmv1/third_party/terraform/website/docs/r/compute_region_instance_group_manager.html.markdown +++ b/mmv1/third_party/terraform/website/docs/r/compute_region_instance_group_manager.html.markdown @@ -268,7 +268,7 @@ instance_lifecycle_policy { force_update_on_repair = "YES" default_action_on_failure = "DO_NOTHING" on_failed_health_check = "DO_NOTHING" - on_repair { //google-beta only + on_repair { allow_changing_zone = "YES" } } @@ -277,7 +277,7 @@ instance_lifecycle_policy { * `force_update_on_repair` - (Optional), Specifies whether to apply the group's latest configuration when repairing a VM. Valid options are: `YES`, `NO`. If `YES` and you updated the group's instance template or per-instance configurations after the VM was created, then these changes are applied when VM is repaired. If `NO` (default), then updates are applied in accordance with the group's update policy type. * `default_action_on_failure` - (Optional), Specifies the action that a MIG performs on a failed VM. If the value of the `on_failed_health_check` field is `DEFAULT_ACTION`, then the same action also applies to the VMs on which your application fails a health check. Valid options are: `DO_NOTHING`, `REPAIR`. If `DO_NOTHING`, then MIG does not repair a failed VM. If `REPAIR` (default), then MIG automatically repairs a failed VM by recreating it. For more information, see about repairing VMs in a MIG. * `on_failed_health_check` - (Optional), Specifies the action that a MIG performs on an unhealthy VM. A VM is marked as unhealthy when the application running on that VM fails a health check. Valid options are: `DEFAULT_ACTION`, `DO_NOTHING`, `REPAIR`. If `DEFAULT_ACTION` (default), then MIG uses the same action configured for the `default_action_on_failure` field. If `DO_NOTHING`, then MIG does not repair unhealthy VM. If `REPAIR`, then MIG automatically repairs an unhealthy VM by recreating it. For more information, see about repairing VMs in a MIG. -* `on_repair` - (Optional, [Beta](../guides/provider_versions.html.markdown)), Configuration for VM repairs in the MIG. Structure is [documented below](#nested_on_repair). +* `on_repair` - (Optional), Configuration for VM repairs in the MIG. Structure is [documented below](#nested_on_repair). - - - The `on_repair` block supports: diff --git a/mmv1/third_party/terraform/website/docs/r/container_cluster.html.markdown b/mmv1/third_party/terraform/website/docs/r/container_cluster.html.markdown index 49ae98d1306e..6c260a6253fb 100644 --- a/mmv1/third_party/terraform/website/docs/r/container_cluster.html.markdown +++ b/mmv1/third_party/terraform/website/docs/r/container_cluster.html.markdown @@ -766,10 +766,11 @@ This block also contains several computed attributes, documented below. The `maintenance_policy` block supports: * `daily_maintenance_window` - (Optional) structure documented below. * `recurring_window` - (Optional) structure documented below +* `recurring_maintenance_window` - (Optional) structure documented below * `maintenance_exclusion` - (Optional) structure documented below * `disruption_budget` - (Optional) structure documented below -In beta, one or the other of `recurring_window` and `daily_maintenance_window` is required if a `maintenance_policy` block is supplied. +In beta, one of `recurring_window`, `recurring_maintenance_window` and `daily_maintenance_window` is required if a `maintenance_policy` block is supplied. * `daily_maintenance_window` - Time window specified for daily maintenance operations. Specify `start_time` in [RFC3339](https://www.ietf.org/rfc/rfc3339.txt) format "HH:MM”, @@ -812,6 +813,60 @@ maintenance_policy { } ``` +* `recurring_maintenance_window` - Defines a recurring window for maintenance operations. + * `delay_until`: (Optional) Specifies the initial date when the recurring window can start. + * `day`: The day of the month (integer value between 1 and 31). + * `month`: The month of the year (integer value between 1 and 12). + * `year`: The year (integer value). + + * `window_start_time`: The time of day when each maintenance window instance begins. + * `hours`: The hour of the day (integer value between 0 and 23). + * `minutes`: The minute of the hour (integer value between 0 and 59). + * `seconds`: The second of the minute (integer value between 0 and 59). + + * `window_duration`: The length of each maintenance window instance. Specified as a sequence of decimal numbers, each with an optional fraction and a unit suffix, such as `"300s"`, `"1.5m"`, and `"2h45m"`. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". The value must be a positive duration. + + * `recurrence`: Defines when the window recurs, using the [RFC5545](https://tools.ietf.org/html/rfc5545#section-3.8.5.3) RRULE format. + +Examples: +``` +maintenance_policy { + recurring_maintenance_window { + delay_until { + day = 1 + month = 8 + year = 2019 + } + window_start_time { + hours = 2 + minutes = 0 + seconds = 0 + } + window_duration = "4h" + recurrence = "FREQ=DAILY" + } +} +``` + +``` +maintenance_policy { + recurring_maintenance_window { + delay_until { + day = 1 + month = 1 + year = 2019 + } + window_start_time { + hours = 9 + minutes = 0 + seconds = 0 + } + window_duration = "8h" + recurrence = "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR" + } +} +``` + * `maintenance_exclusion` - Exceptions to maintenance window. Non-emergency maintenance should not occur in these windows. A cluster can have up to 20 maintenance exclusions at a time [Maintenance Window and Exclusions](https://cloud.google.com/kubernetes-engine/docs/concepts/maintenance-windows-and-exclusions) The `maintenance_exclusion` block supports: @@ -1507,8 +1562,9 @@ not. * `"UNSPECIFIED"`: Default value. This should not be used. * `"NO_RESERVATION"`: Do not consume from any reserved capacity. - * `"ANY_RESERVATION"`: Consume any reservation available. + * `"ANY_RESERVATION"`: Consume any non-specific reservation available, with a fallback to on-demand capacity in case of none reservaition being claimable. * `"SPECIFIC_RESERVATION"`: Must consume from a specific reservation. Must specify key value fields for specifying the reservations. + * `"ANY_RESERVATION_THEN_FAIL"`: Consume any non-specific reservation available, without a fallback to on-demand capacity in case of none reservaition being claimable. * `key` (Optional) The label key of a reservation resource. To target a SPECIFIC_RESERVATION by name, specify "compute.googleapis.com/reservation-name" as the key and specify the name of your reservation as its value. * `values` (Optional) The list of label values of reservation resources. For example: the name of the specific reservation when using a key of "compute.googleapis.com/reservation-name" @@ -1733,6 +1789,8 @@ linux_node_config { * `accurate_time_config` - (Optional) Accurate time configuration for the node. Structure is [documented below](#nested_accurate_time_config). +* `custom_node_init` - (Optional) Custom node init settings. Structure is [documented below](#nested_custom_node_init). + The `swap_config` block supports: * `enabled` - (Optional) Enables or disables swap for the node pool. @@ -1799,6 +1857,18 @@ linux_node_config { * `ENFORCE_SIGNED_MODULES`: Enforced signature verification: Node pools will use a Container-Optimized OS image configured to allow loading of *Google-signed* external kernel modules. Loadpin is enabled but configured to exclude modules, and kernel module signature checking is enforced. * `DO_NOT_ENFORCE_SIGNED_MODULES`: Mirrors existing DEFAULT behavior: For CPU and TPU nodes, the image will not allow loading external kernel modules. For GPU nodes, the image will allow loading any module, whether it is signed or not. +The `custom_node_init` block supports: + +* `init_script` - (Optional) The init script configuration. Structure is [documented below](#nested_init_script). + +The `init_script` block supports: + +* `gcs_uri` - (Optional) The Google Cloud Storage URI for storing the init script. Format: `gs://BUCKET_NAME/OBJECT_NAME`. The service account on the nodepool must have read access to the object. Conflicts with `gcp_secret_manager_secret_uri`. If `gcs_uri` is used, `gcs_generation` is required. + +* `gcs_generation` - (Optional) The generation of the init script in Google Cloud Storage. If `gcs_uri` is used, `gcs_generation` is required. + +* `gcp_secret_manager_secret_uri` - (Optional) The Google Cloud Secret Manager secret version URI for storing the init script. Format: `projects/PROJECT_ID/secrets/SECRET_NAME/versions/VERSION`. The service account on the nodepool must have access to the secret version. Conflicts with `gcs_uri`. + The `containerd_config` block supports: * `private_registry_access_config` (Optional) - Configuration for private container registries. There are two fields in this config: diff --git a/mmv1/third_party/terraform/website/docs/r/container_node_pool.html.markdown b/mmv1/third_party/terraform/website/docs/r/container_node_pool.html.markdown index 66af863477ac..d612e187ce81 100644 --- a/mmv1/third_party/terraform/website/docs/r/container_node_pool.html.markdown +++ b/mmv1/third_party/terraform/website/docs/r/container_node_pool.html.markdown @@ -159,6 +159,8 @@ cluster. * `node_drain_config` - (Optional) The node drain configuration of the pool. Structure is [documented below](#nested_node_drain_config). +* `maintenance_policy` - (Optional) The maintenance policy of the pool. Structure is [documented below](#nested_maintenance_policy). + * `project` - (Optional) The ID of the project in which to create the node pool. If blank, the provider-configured project will be used. @@ -267,6 +269,16 @@ cluster. * `respect_pdb_during_node_pool_deletion` - (Optional) Whether to respect PodDisruptionBudget policy during node pool deletion. +The `maintenance_policy` block supports: + +* `exclusion_until_end_of_support` - (Optional) When enabled, the node pool will not be automatically upgraded by GKE until the node pool version's end of support date. Structure is [documented below](#nested_exclusion_until_end_of_support). + +The `exclusion_until_end_of_support` block supports: + +* `enabled` - (Optional) When true, the node pool will not be automatically upgraded by GKE until the node pool version's end of support date. +* `start_time` - (Optional) The time when the maintenance policy is first created. +* `end_time` - (Optional) The time when the maintenance policy is no longer effective, i.e., the node pool version's end of support date. + The `upgrade_settings` block supports: * `max_surge` - (Optional) The number of additional nodes that can be added to the node pool during @@ -322,8 +334,9 @@ cluster. * `"UNSPECIFIED"`: Default value. This should not be used. * `"NO_RESERVATION"`: Do not consume from any reserved capacity. - * `"ANY_RESERVATION"`: Consume any reservation available. + * `"ANY_RESERVATION"`: Consume any non-specific reservation available, with a fallback to on-demand capacity in case of none reservaition being claimable. * `"SPECIFIC_RESERVATION"`: Must consume from a specific reservation. Must specify key value fields for specifying the reservations. + * `"ANY_RESERVATION_THEN_FAIL"`: Consume any non-specific reservation available, without a fallback to on-demand capacity in case of none reservaition being claimable. * `key` (Optional) The label key of a reservation resource. To target a SPECIFIC_RESERVATION by name, specify "compute.googleapis.com/reservation-name" as the key and specify the name of your reservation as its value. * `values` (Optional) The list of label values of reservation resources. For example: the name of the specific reservation when using a key of "compute.googleapis.com/reservation-name" diff --git a/mmv1/third_party/terraform/website/docs/r/sql_database_instance.html.markdown b/mmv1/third_party/terraform/website/docs/r/sql_database_instance.html.markdown index fb02b87d898e..92e771244d29 100644 --- a/mmv1/third_party/terraform/website/docs/r/sql_database_instance.html.markdown +++ b/mmv1/third_party/terraform/website/docs/r/sql_database_instance.html.markdown @@ -399,6 +399,11 @@ includes an up-to-date reference of supported versions. ~> **NOTE:** This flag only protects instances from deletion within Terraform. To protect your instances from accidental deletion across all surfaces (API, gcloud, Cloud Console and Terraform), use the API flag `settings.deletion_protection_enabled`. +* `enforce_new_sql_network_architecture` - (Optional) Whether to enforce the new SQL network architecture. + By default, new Cloud SQL instances created in projects created after August 2021 use the new network architecture. + This follows the gcloud pattern where the flag is an irreversible opt-in. + See [official documentation](https://docs.cloud.google.com/sql/docs/mysql/upgrade-cloud-sql-instance-new-network-architecture#new-arch) for more details. + * `final_backup_description` - (Optional) The description of final backup. Only set this field when `final_backup_config.enabled` is true. * `restore_backup_context` - (optional) The context needed to restore the database to a backup run. This field will @@ -620,6 +625,8 @@ The optional `settings.ip_configuration.psc_config` sublist supports: * `psc_write_endpoint_dns_enabled` - (Optional) Whether PSC write endpoint DNS is enabled for this instance. This is only supported for Enterprise Plus edition instances. +* `settings.ip_configuration.psc_config.psc_auto_connection_policy_enabled` - (Optional) Whether a service connection policy is created for the auto connections configured for the instance. + * `allowed_consumer_projects` - (Optional) List of consumer projects that are allow-listed for PSC connections to this instance. This instance can be connected to with PSC from any network in these projects. Each consumer project in this list may be represented by a project number (numeric) or by a project id (alphanumeric). * The optional `psc_config.psc_auto_connections` subblock - (Optional) A comma-separated list of networks or a comma-separated list of network-project pairs. Each project in this list is represented by a project number (numeric) or by a project ID (alphanumeric). This allows Private Service Connect connections to be created automatically for the specified networks. @@ -898,6 +905,10 @@ performing filtering in a Terraform config. * `settings.ip_configuration.psc_config.psc_auto_connections.status` - (Output) The connection status of the consumer endpoint. +* `settings.ip_configuration.psc_config.psc_auto_connections.service_connection_policy` - (Output) The service connection policy created for the auto connection. + +* `settings.ip_configuration.psc_config.psc_auto_connections.service_connection_policy_creation_result` - (Output) The result of the service connection policy creation. + * `settings.version` - Used to make sure changes to the `settings` block are atomic. diff --git a/mmv1/third_party/terraform/website/docs/r/storage_bucket.html.markdown b/mmv1/third_party/terraform/website/docs/r/storage_bucket.html.markdown index 5245e1e77203..042bc569216d 100644 --- a/mmv1/third_party/terraform/website/docs/r/storage_bucket.html.markdown +++ b/mmv1/third_party/terraform/website/docs/r/storage_bucket.html.markdown @@ -177,6 +177,21 @@ resource "google_storage_bucket" "hns-enabled" { } } ``` +## Example Usage - Enabling RAPID storage bucket + +```hcl +resource "google_storage_bucket" "zonal_bucket" { + location = "US-CENTRAL1" + custom_placement_config { + data_locations = ["US-CENTRAL1-B"] + } + name = "zonal-rapid-bucket" + storage_class = "RAPID" + hierarchical_namespace { + enabled = true + } +} +``` ## Argument Reference diff --git a/mmv1/third_party/terraform/website/docs/r/storage_transfer_job.html.markdown b/mmv1/third_party/terraform/website/docs/r/storage_transfer_job.html.markdown index a004ac000ab8..45b8df4fb385 100644 --- a/mmv1/third_party/terraform/website/docs/r/storage_transfer_job.html.markdown +++ b/mmv1/third_party/terraform/website/docs/r/storage_transfer_job.html.markdown @@ -333,6 +333,8 @@ The `aws_access_key` block supports: * `federated_identity_config` - (Optional) Federated identity config of a user registered Azure application. Structure [documented below](#nested_federated_identity_config). +* `private_network_service` - (Optional) Service Directory Service to be used as the endpoint for transfers from a customer-managed VPC. Format: `projects/{projectId}/locations/{location}/namespaces/{namespace}/services/{service}`. + The `azure_credentials` block supports: * `sas_token` - (Required) Azure shared access signature. See [Grant limited access to Azure Storage resources using shared access signatures (SAS)](https://docs.microsoft.com/en-us/azure/storage/common/storage-sas-overview). diff --git a/mmv1/third_party/terraform/website/docs/r/tags_tag_binding_collection.html.markdown.tmpl b/mmv1/third_party/terraform/website/docs/r/tags_tag_binding_collection.html.markdown.tmpl new file mode 100644 index 000000000000..4fb9bf45ae9f --- /dev/null +++ b/mmv1/third_party/terraform/website/docs/r/tags_tag_binding_collection.html.markdown.tmpl @@ -0,0 +1,127 @@ +{{- if ne $.TargetVersionName "ga" }} +--- +subcategory: "Tags" +description: |- + A TagBindingCollection represents a collection of tag bindings directly bound to a cloud resource. +--- + +# google_tags_tag_binding_collection + +A TagBindingCollection represents a collection of tag bindings directly bound to a cloud resource. + +> **Warning:** `google_tags_tag_binding_collection` is designed to perform non-authoritative bulk management of tags. It **cannot** be used in conjunction with individual `google_tags_tag_binding` or `google_tags_location_tag_binding` resources operating on the exact same parent resource. Doing so will cause overlapping state management, double adoptions, and unpredictable infrastructure convergence. + +To get more information about TagBindingCollection, see: + +* [API documentation](https://cloud.google.com/resource-manager/reference/rest/v3/locations.tagBindingCollections) +* How-to Guides + * [Official Documentation](https://cloud.google.com/resource-manager/docs/tags/tags-creating-and-managing) + +## Example Usage - Cloud Run Service + +To bind tags to a Cloud Run service: + +```hcl +resource "google_project" "project" { + project_id = "project_id" + name = "project_id" + org_id = "123456789" +} + +resource "google_tags_tag_key" "key1" { + parent = "organizations/123456789" + short_name = "keyname1" + description = "For keyname1 resources." +} + +resource "google_tags_tag_value" "value1" { + parent = google_tags_tag_key.key1.id + short_name = "valuename1" + description = "For valuename1 resources." +} + +resource "google_tags_tag_key" "key2" { + parent = "organizations/123456789" + short_name = "dynamic_keyname" + description = "For dynamic keyname resources." + allowed_values_regex = "^[a-z0-9]+$" +} + +resource "google_tags_tag_binding_collection" "bindingcollection" { + full_resource_name = "//run.googleapis.com/projects/${data.google_project.project.number}/locations/${google_cloud_run_service.default.location}/services/${google_cloud_run_service.default.name}" + location = "us-central1" + tags = { + # Format: "{TagKey.namespaced_name}" = "{TagValue.short_name}" + "${google_tags_tag_key.key1.namespaced_name}" = google_tags_tag_value.value1.short_name + "${google_tags_tag_key.key2.namespaced_name}" = "dynamicvalue123" + } +} +``` + +## Argument Reference + +The following arguments are supported: + + +* `full_resource_name` - + (Required) + The full resource name of the resource to which the tags are bound. E.g. `//cloudresourcemanager.googleapis.com/projects/123` + +* `tags` - + (Required) + A map of tag keys to values directly bound to this resource, specified in namespaced name format. E.g. `"123/environment": "production"`. Keys must be namespaced names of TagKeys, and values must be short names of TagValues. This field is non-authoritative. Terraform will only manage the precise tags present in this map. + +* `location` - + (Optional) + The location of the target resource. E.g. `"global"`, `"us-central1"`, `"us-east2-c"`. Defaults to `"global"`. + +- - - + + + +## Attributes Reference + +In addition to the arguments listed above, the following computed attributes are exported: + +* `id` - Identifier of the TagBindingCollection resource, in the format `locations/{location}/tagBindingCollections/{encoded_full_resource_name}` + +* `name` - + The name of the TagBindingCollection, in the format: `locations/{location}/tagBindingCollections/{encoded_full_resource_name}` + +* `active_tags` - + The most recent state of all direct tags on the resource, as reported by the API. + This includes the tags configured through Terraform, Google system tags, and tags attached by other clients. + + +## Timeouts + +This resource provides the following +[Timeouts](/docs/configuration/resources.html#timeouts) configuration options: + +- `create` - Default is 20 minutes. +- `update` - Default is 20 minutes. +- `delete` - Default is 20 minutes. + +## Import + +> **Note on Import and Non-Authoritative Behavior:** Because `tags` is a non-authoritative field, `terraform import` does not automatically populate the desired `tags` map in your configuration, to avoid inadvertently adopting system tags or unmanaged third-party tags. After running `terraform import`, inspect the computed `active_tags` attribute in your state file to discover all tags currently attached to the physical resource, and explicitly add only the specific keys you wish Terraform to manage into your `.tf` `tags` map. + +TagBindingCollection can be imported using any of these accepted formats: + +* `locations/{{location}}/tagBindingCollections/{{encoded_full_resource_name}}` + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import TagBindingCollection using one of the formats above. For example: + +```tf +import { + id = "locations/{{location}}/tagBindingCollections/{{encoded_full_resource_name}}" + to = google_tags_tag_binding_collection.default +} +``` + +When using the [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import), TagBindingCollection can be imported using one of the formats above. For example: + +``` +$ terraform import google_tags_tag_binding_collection.default locations/{{location}}/tagBindingCollections/{{encoded_full_resource_name}} +``` +{{- end }} diff --git a/mmv1/third_party/tgc/services/compute/compute_instance.go.tmpl b/mmv1/third_party/tgc/services/compute/compute_instance.go.tmpl index 2570d7859f4d..563361506d82 100644 --- a/mmv1/third_party/tgc/services/compute/compute_instance.go.tmpl +++ b/mmv1/third_party/tgc/services/compute/compute_instance.go.tmpl @@ -172,7 +172,7 @@ func expandComputeInstance(project string, d tpgresource.TerraformResourceData, DeletionProtection: d.Get("deletion_protection").(bool), Hostname: d.Get("hostname").(string), ForceSendFields: []string{"CanIpForward", "DeletionProtection"}, - AdvancedMachineFeatures: expandAdvancedMachineFeatures(d), + AdvancedMachineFeatures: expandAdvancedMachineFeaturesTyped(d), } if sicMap := expandShieldedVmConfigs(d); sicMap != nil { instance.ShieldedInstanceConfig = &compute.ShieldedInstanceConfig{ diff --git a/mmv1/third_party/tgc_next/pkg/services/compute/compute_instance_tfplan2cai.go b/mmv1/third_party/tgc_next/pkg/services/compute/compute_instance_tfplan2cai.go index 8c4fc194d333..0d2bb6a214c5 100644 --- a/mmv1/third_party/tgc_next/pkg/services/compute/compute_instance_tfplan2cai.go +++ b/mmv1/third_party/tgc_next/pkg/services/compute/compute_instance_tfplan2cai.go @@ -658,7 +658,7 @@ func expandComputeLocalSsdRecoveryTimeoutTgc(v interface{}) (*compute.Duration, } func expandAdvancedMachineFeaturesTgcNext(d tpgresource.TerraformResourceData) *compute.AdvancedMachineFeatures { - features := expandAdvancedMachineFeatures(d) + features := expandAdvancedMachineFeaturesTyped(d) if features != nil && features.PerformanceMonitoringUnit == "" { features.PerformanceMonitoringUnit = "STANDARD" } diff --git a/mmv1/third_party/tgc_next/pkg/services/container/node_config.go b/mmv1/third_party/tgc_next/pkg/services/container/node_config.go index 49ffc0dac3de..c82bd7c39932 100644 --- a/mmv1/third_party/tgc_next/pkg/services/container/node_config.go +++ b/mmv1/third_party/tgc_next/pkg/services/container/node_config.go @@ -1160,6 +1160,42 @@ func schemaNodeConfig() *schema.Schema { }, }, }, + "custom_node_init": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: `The custom node init settings.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "init_script": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: `The init script configuration.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "gcs_uri": { + Type: schema.TypeString, + Optional: true, + Description: `The GCS URI of the init script.`, + }, + "gcs_generation": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: `The GCS generation of the init script.`, + }, + "gcp_secret_manager_secret_uri": { + Type: schema.TypeString, + Optional: true, + Description: `The Secret Manager secret URI of the init script.`, + }, + }, + }, + }, + }, + }, + }, }, }, }, @@ -2164,6 +2200,10 @@ func expandLinuxNodeConfig(v interface{}) *container.LinuxNodeConfig { linuxNodeConfig.SwapConfig = expandSwapConfig(v) } + if v, ok := cfg["custom_node_init"]; ok { + linuxNodeConfig.CustomNodeInit = expandCustomNodeInit(v) + } + return linuxNodeConfig } @@ -3347,6 +3387,7 @@ func flattenLinuxNodeConfig(v interface{}) []map[string]interface{} { "node_kernel_module_loading": flattenNodeKernelModuleLoading(c["nodeKernelModuleLoading"]), "swap_config": flattenSwapConfig(c["swapConfig"]), "accurate_time_config": flattenAccurateTimeConfig(c["accurateTimeConfig"]), + "custom_node_init": flattenCustomNodeInit(c["customNodeInit"]), } return []map[string]interface{}{transformed} @@ -3797,3 +3838,93 @@ func flattenGpuDirectConfig(v interface{}) string { } return strategy } + +func expandCustomNodeInit(v interface{}) *container.CustomNodeInit { + if v == nil { + return nil + } + ls := v.([]interface{}) + if len(ls) == 0 { + return nil + } + if ls[0] == nil { + return &container.CustomNodeInit{} + } + cfg := ls[0].(map[string]interface{}) + + customNodeInit := &container.CustomNodeInit{} + if v, ok := cfg["init_script"]; ok { + customNodeInit.InitScript = expandInitScript(v) + } + return customNodeInit +} + +func expandInitScript(v interface{}) *container.InitScript { + if v == nil { + return nil + } + ls := v.([]interface{}) + if len(ls) == 0 { + return nil + } + if ls[0] == nil { + return &container.InitScript{} + } + cfg := ls[0].(map[string]interface{}) + + initScript := &container.InitScript{} + if v, ok := cfg["gcs_uri"]; ok { + initScript.GcsUri = v.(string) + } + if v, ok := cfg["gcs_generation"]; ok { + initScript.GcsGeneration = int64(v.(int)) + } + if v, ok := cfg["gcp_secret_manager_secret_uri"]; ok { + initScript.GcpSecretManagerSecretUri = v.(string) + } + return initScript +} + +func flattenCustomNodeInit(v interface{}) []map[string]interface{} { + if v == nil { + return nil + } + c, ok := v.(map[string]interface{}) + if !ok { + return nil + } + transformed := map[string]interface{}{} + if is, ok := c["initScript"].(map[string]interface{}); ok { + transformed["init_script"] = flattenInitScript(is) + } + return []map[string]interface{}{transformed} +} + +func flattenInitScript(v interface{}) []map[string]interface{} { + if v == nil { + return nil + } + c, ok := v.(map[string]interface{}) + if !ok { + return nil + } + transformed := map[string]interface{}{} + if val, ok := c["gcsUri"]; ok { + transformed["gcs_uri"] = val + } + if val, ok := c["gcsGeneration"]; ok { + var gen interface{} = val + if strGen, ok := val.(string); ok { + if intGen, err := strconv.Atoi(strGen); err == nil { + gen = intGen + } + } else if floatGen, ok := val.(float64); ok { + gen = int(floatGen) + } + transformed["gcs_generation"] = gen + } + if val, ok := c["gcpSecretManagerSecretUri"]; ok { + transformed["gcp_secret_manager_secret_uri"] = val + } + return []map[string]interface{}{transformed} +} diff --git a/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster.go b/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster.go index 45087d866978..3ebec9ec00d1 100644 --- a/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster.go +++ b/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster.go @@ -7,6 +7,7 @@ import ( "reflect" "regexp" "strings" + "time" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -41,6 +42,15 @@ func Rfc3339TimeDiffSuppress(k, old, new string, d *schema.ResourceData) bool { return false } +func validateDuration(v interface{}, k string) (ws []string, errors []error) { + s := v.(string) + _, err := time.ParseDuration(s) + if err != nil { + errors = append(errors, fmt.Errorf("%q is not a valid duration: %s", k, err)) + } + return +} + var ( instanceGroupManagerURL = regexp.MustCompile(fmt.Sprintf("projects/(%s)/zones/([a-z0-9-]*)/instanceGroupManagers/([^/]*)", verify.ProjectRegex)) @@ -1159,6 +1169,7 @@ func ResourceContainerCluster() *schema.Resource { ExactlyOneOf: []string{ "maintenance_policy.0.daily_maintenance_window", "maintenance_policy.0.recurring_window", + "maintenance_policy.0.recurring_maintenance_window", }, MaxItems: 1, Description: `Time window specified for daily maintenance operations. Specify start_time in RFC3339 format "HH:MM”, where HH : [00-23] and MM : [00-59] GMT.`, @@ -1184,6 +1195,7 @@ func ResourceContainerCluster() *schema.Resource { ExactlyOneOf: []string{ "maintenance_policy.0.daily_maintenance_window", "maintenance_policy.0.recurring_window", + "maintenance_policy.0.recurring_maintenance_window", }, Description: `Time window for recurring maintenance operations.`, Elem: &schema.Resource{ @@ -1206,6 +1218,74 @@ func ResourceContainerCluster() *schema.Resource { }, }, }, + "recurring_maintenance_window": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ExactlyOneOf: []string{ + "maintenance_policy.0.daily_maintenance_window", + "maintenance_policy.0.recurring_window", + "maintenance_policy.0.recurring_maintenance_window", + }, + Description: `Time window for recurring maintenance operations.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "delay_until": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "year": { + Type: schema.TypeInt, + Required: true, + }, + "month": { + Type: schema.TypeInt, + Required: true, + }, + "day": { + Type: schema.TypeInt, + Required: true, + }, + }, + }, + }, + "window_duration": { + Required: true, + Type: schema.TypeString, + DiffSuppressFunc: tpgresource.DurationDiffSuppress, + ValidateFunc: validateDuration, + }, + "window_start_time": { + Required: true, + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "hours": { + Type: schema.TypeInt, + Required: true, + }, + "minutes": { + Type: schema.TypeInt, + Required: true, + }, + "seconds": { + Type: schema.TypeInt, + Required: true, + }, + }, + }, + }, + "recurrence": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: rfc5545RecurrenceDiffSuppress, + }, + }, + }, + }, "maintenance_exclusion": { Type: schema.TypeSet, Optional: true, diff --git a/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster_cai2hcl.go b/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster_cai2hcl.go index ffe21a342e72..2e6ac4f00dbe 100644 --- a/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster_cai2hcl.go +++ b/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster_cai2hcl.go @@ -1122,6 +1122,36 @@ func flattenMaintenancePolicy(v interface{}) []map[string]interface{} { transformed["recurring_window"] = []map[string]interface{}{windowMap} } + + if recurringMaintenanceWindow, ok := window["recurringMaintenanceWindow"].(map[string]interface{}); ok && recurringMaintenanceWindow != nil { + windowMap := map[string]interface{}{} + windowMap["window_start_time"] = []map[string]interface{}{} + startTime, _ := recurringMaintenanceWindow["windowStartTime"].(map[string]interface{}) + windowMap["window_start_time"] = []map[string]interface{}{ + { + "hours": startTime["hours"], + "minutes": startTime["minutes"], + "seconds": startTime["seconds"], + }, + } + + delay, _ := recurringMaintenanceWindow["delayUntil"].(map[string]interface{}) + if delay != nil { + windowMap["delayUntil"] = []map[string]interface{}{ + { + "year": delay["year"], + "month": delay["month"], + "day": delay["day"], + }, + } + } + + windowMap["window_duration"] = recurringMaintenanceWindow["windowDuration"] + windowMap["recurrence"] = recurringMaintenanceWindow["recurrence"] + + transformed["recurring_maintenance_window"] = []map[string]interface{}{windowMap} + } + } if disruptionBudget, ok := mp["disruptionBudget"].(map[string]interface{}); ok && disruptionBudget != nil { @@ -1140,8 +1170,9 @@ func flattenMaintenancePolicy(v interface{}) []map[string]interface{} { } _, hasDaily := transformed["daily_maintenance_window"] - _, hasRecurring := transformed["recurring_window"] - if !hasDaily && !hasRecurring { + _, hasRecurringWindow := transformed["recurring_window"] + _, hasRecurringMaintenance := transformed["recurring_maintenance_window"] + if !hasDaily && !hasRecurringWindow && !hasRecurringMaintenance { return nil } diff --git a/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster_tfplan2cai.go b/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster_tfplan2cai.go index b5cd9e011ed7..c26c956aa2ca 100644 --- a/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster_tfplan2cai.go +++ b/mmv1/third_party/tgc_next/pkg/services/container/resource_container_cluster_tfplan2cai.go @@ -2,7 +2,9 @@ package container import ( "fmt" + "log" "strings" + "time" "github.com/GoogleCloudPlatform/terraform-google-conversion/v7/pkg/caiasset" "github.com/GoogleCloudPlatform/terraform-google-conversion/v7/pkg/tfplan2cai/converters/cai" @@ -642,11 +644,17 @@ func expandMaintenancePolicy(d tpgresource.TerraformResourceData, config *transp if err != nil { return nil } - clusterGetCall := NewClient(config, userAgent).Projects.Locations.Clusters.Get(name) - if config.UserProjectOverride { - clusterGetCall.Header().Add("X-Goog-User-Project", project) + var cluster *container.Cluster + client := NewClient(config, userAgent) + if client != nil { + clusterGetCall := client.Projects.Locations.Clusters.Get(name) + if config.UserProjectOverride { + clusterGetCall.Header().Add("X-Goog-User-Project", project) + } + cluster, _ = clusterGetCall.Do() + } else { + log.Printf("[WARN] GKE client is nil, skipping cluster GET call") } - cluster, _ := clusterGetCall.Do() resourceVersion := "" exclusions := make(map[string]container.TimeWindow) if cluster != nil && cluster.MaintenancePolicy != nil { @@ -729,6 +737,35 @@ func expandMaintenancePolicy(d tpgresource.TerraformResourceData, config *transp ResourceVersion: resourceVersion, } } + if recurringMaintenanceWindow, ok := maintenancePolicy["recurring_maintenance_window"]; ok && len(recurringMaintenanceWindow.([]interface{})) > 0 { + rmw := recurringMaintenanceWindow.([]interface{})[0].(map[string]interface{}) + duration, _ := time.ParseDuration(rmw["window_duration"].(string)) + + policy := &container.MaintenancePolicy{ + Window: &container.MaintenanceWindow{ + MaintenanceExclusions: exclusions, + RecurringMaintenanceWindow: &container.RecurringMaintenanceWindow{ + WindowStartTime: &container.TimeOfDay{ + Hours: int64(rmw["window_start_time"].([]interface{})[0].(map[string]interface{})["hours"].(int)), + Minutes: int64(rmw["window_start_time"].([]interface{})[0].(map[string]interface{})["minutes"].(int)), + Seconds: int64(rmw["window_start_time"].([]interface{})[0].(map[string]interface{})["seconds"].(int)), + }, + WindowDuration: fmt.Sprintf("%ds", int(duration.Seconds())), + Recurrence: rmw["recurrence"].(string), + }, + }, + ResourceVersion: resourceVersion, + } + + if _, ok := rmw["delay_until"]; ok && len(rmw["delay_until"].([]interface{})) == 1 { + policy.Window.RecurringMaintenanceWindow.DelayUntil = &container.Date{ + Year: int64(rmw["delay_until"].([]interface{})[0].(map[string]interface{})["year"].(int)), + Month: int64(rmw["delay_until"].([]interface{})[0].(map[string]interface{})["month"].(int)), + Day: int64(rmw["delay_until"].([]interface{})[0].(map[string]interface{})["day"].(int)), + } + } + return policy + } return nil } diff --git a/mmv1/third_party/tgc_next/pkg/tfplan2cai/convert_test.go b/mmv1/third_party/tgc_next/pkg/tfplan2cai/convert_test.go index 778d1fb41649..5a4e360c1bae 100644 --- a/mmv1/third_party/tgc_next/pkg/tfplan2cai/convert_test.go +++ b/mmv1/third_party/tgc_next/pkg/tfplan2cai/convert_test.go @@ -213,3 +213,49 @@ func TestConvert_ComputeAddress(t *testing.T) { assert.NotEmpty(t, assets) assert.Equal(t, "https://www.googleapis.com/compute/v1/projects/terraform-dev-haonan/regions/us-east1/subnetworks/subnetwork-test", assets[1].Resource.Data["subnetwork"]) } + +func TestConvert_GkeCustomNodeInit(t *testing.T) { + logger, _ := newTestErrorLogger() + o := &Options{ + ErrorLogger: logger, + Offline: true, + DefaultProject: testProject, + DefaultZone: "us-central1-a", + NoOpAncestryManager: true, + } + + jsonPlan, err := os.ReadFile("resolvers/gke_custom_node_init.tfplan.json") + if err != nil { + t.Fatalf("Error reading test file: %v", err) + } + + assets, err := Convert(context.Background(), jsonPlan, o) + if err != nil { + t.Fatalf("Error marshaling assets: %v", err) + } + assert.Nil(t, err) + assert.NotEmpty(t, assets) + + var clusterAsset *caiasset.Asset + for _, asset := range assets { + if asset.Type == "container.googleapis.com/Cluster" { + clusterAsset = &asset + break + } + } + assert.NotNil(t, clusterAsset, "Cluster asset not found") + + nodeConfig, ok := clusterAsset.Resource.Data["nodeConfig"].(map[string]interface{}) + assert.True(t, ok, "nodeConfig should be a map") + + linuxNodeConfig, ok := nodeConfig["linuxNodeConfig"].(map[string]interface{}) + assert.True(t, ok, "linuxNodeConfig should be a map") + + customNodeInit, ok := linuxNodeConfig["customNodeInit"].(map[string]interface{}) + assert.True(t, ok, "customNodeInit should be a map") + + initScript, ok := customNodeInit["initScript"].(map[string]interface{}) + assert.True(t, ok, "initScript should be a map") + + assert.Equal(t, "gs://my-bucket/init.sh", initScript["gcsUri"]) +} diff --git a/mmv1/third_party/tgc_next/pkg/tfplan2cai/resolvers/gke_custom_node_init.tfplan.json b/mmv1/third_party/tgc_next/pkg/tfplan2cai/resolvers/gke_custom_node_init.tfplan.json new file mode 100644 index 000000000000..985b9af88e24 --- /dev/null +++ b/mmv1/third_party/tgc_next/pkg/tfplan2cai/resolvers/gke_custom_node_init.tfplan.json @@ -0,0 +1 @@ +{"format_version":"1.2","terraform_version":"1.9.0","planned_values":{"root_module":{"resources":[{"address":"google_container_cluster.primary","mode":"managed","type":"google_container_cluster","name":"primary","provider_name":"registry.terraform.io/hashicorp/google-beta","schema_version":2,"values":{"allow_net_admin":null,"binary_authorization":[],"deletion_policy":"DELETE","deletion_protection":false,"description":null,"disable_l4_lb_firewall_reconciliation":false,"dns_config":[],"effective_labels":{"goog-terraform-provisioned":"true"},"enable_autopilot":null,"enable_cilium_clusterwide_network_policy":false,"enable_fqdn_network_policy":false,"enable_k8s_beta_apis":[],"enable_kubernetes_alpha":false,"enable_legacy_abac":false,"enable_multi_networking":false,"enable_shielded_nodes":true,"fleet":[],"in_transit_encryption_config":null,"initial_node_count":1,"location":"us-central1-a","maintenance_policy":[],"min_master_version":null,"name":"marss-cluster","network":"default","network_performance_config":[],"network_policy":[],"node_config":[{"advanced_machine_features":[],"boot_disk_kms_key":null,"enable_confidential_storage":null,"ephemeral_storage_config":[],"ephemeral_storage_local_ssd_config":[],"fast_socket":[],"flex_start":null,"gvnic":[],"host_maintenance_policy":[],"linux_node_config":[{"accurate_time_config":[],"custom_node_init":[{"init_script":[{"gcp_secret_manager_secret_uri":null,"gcs_uri":"gs://my-bucket/init.sh"}]}],"hugepages_config":[],"node_kernel_module_loading":[],"swap_config":[],"sysctls":null,"transparent_hugepage_defrag":null}],"local_nvme_ssd_block_config":[],"local_ssd_encryption_mode":null,"max_run_duration":null,"node_group":null,"preemptible":false,"reservation_affinity":[],"resource_labels":null,"resource_manager_tags":null,"sandbox_config":[],"secondary_boot_disks":[],"sole_tenant_config":[],"spot":false,"storage_pools":null,"tags":null,"taint":[]}],"pod_security_policy_config":[],"private_cluster_config":[],"remove_default_node_pool":null,"resource_labels":null,"resource_usage_export_config":[],"secret_manager_config":[],"secret_sync_config":[],"terraform_labels":{"goog-terraform-provisioned":"true"},"timeouts":null,"user_managed_keys_config":[]},"sensitive_values":{"addons_config":[],"anonymous_authentication_config":[],"authenticator_groups_config":[],"autopilot_cluster_policy_config":[],"autopilot_privileged_admission":[],"binary_authorization":[],"cluster_autoscaling":[],"cluster_telemetry":[],"confidential_nodes":[],"control_plane_endpoints_config":[],"cost_management_config":[],"database_encryption":[],"default_snat_status":[],"dns_config":[],"effective_labels":{},"enable_k8s_beta_apis":[],"enterprise_config":[],"fleet":[],"gateway_api_config":[],"gke_auto_upgrade_config":[],"identity_service_config":[],"ip_allocation_policy":[],"logging_config":[],"maintenance_policy":[],"managed_machine_learning_diagnostics_config":[],"managed_opentelemetry_config":[],"master_auth":[],"master_authorized_networks_config":[],"mesh_certificates":[],"monitoring_config":[],"network_performance_config":[],"network_policy":[],"node_config":[{"advanced_machine_features":[],"boot_disk":[],"confidential_nodes":[],"containerd_config":[],"effective_taints":[],"ephemeral_storage_config":[],"ephemeral_storage_local_ssd_config":[],"fast_socket":[],"gcfs_config":[],"guest_accelerator":[],"gvnic":[],"host_maintenance_policy":[],"kubelet_config":[],"labels":{},"linux_node_config":[{"accurate_time_config":[],"custom_node_init":[{"init_script":[{}]}],"hugepages_config":[],"node_kernel_module_loading":[],"swap_config":[]}],"local_nvme_ssd_block_config":[],"metadata":{},"oauth_scopes":[],"reservation_affinity":[],"sandbox_config":[],"secondary_boot_disks":[],"shielded_instance_config":[],"sole_tenant_config":[],"taint":[],"windows_node_config":[],"workload_metadata_config":[]}],"node_locations":[],"node_pool":[],"node_pool_auto_config":[],"node_pool_defaults":[],"notification_config":[],"pod_autoscaling":[],"pod_security_policy_config":[],"private_cluster_config":[],"protect_config":[],"rbac_binding_config":[],"release_channel":[],"resource_usage_export_config":[],"secret_manager_config":[],"secret_sync_config":[],"security_posture_config":[],"service_external_ips_config":[],"terraform_labels":{},"tpu_config":[],"user_managed_keys_config":[],"vertical_pod_autoscaling":[],"workload_alts_config":[],"workload_identity_config":[]}}]}},"resource_changes":[{"address":"google_container_cluster.primary","mode":"managed","type":"google_container_cluster","name":"primary","provider_name":"registry.terraform.io/hashicorp/google-beta","change":{"actions":["create"],"before":null,"after":{"allow_net_admin":null,"binary_authorization":[],"deletion_policy":"DELETE","deletion_protection":false,"description":null,"disable_l4_lb_firewall_reconciliation":false,"dns_config":[],"effective_labels":{"goog-terraform-provisioned":"true"},"enable_autopilot":null,"enable_cilium_clusterwide_network_policy":false,"enable_fqdn_network_policy":false,"enable_k8s_beta_apis":[],"enable_kubernetes_alpha":false,"enable_legacy_abac":false,"enable_multi_networking":false,"enable_shielded_nodes":true,"fleet":[],"in_transit_encryption_config":null,"initial_node_count":1,"location":"us-central1-a","maintenance_policy":[],"min_master_version":null,"name":"marss-cluster","network":"default","network_performance_config":[],"network_policy":[],"node_config":[{"advanced_machine_features":[],"boot_disk_kms_key":null,"enable_confidential_storage":null,"ephemeral_storage_config":[],"ephemeral_storage_local_ssd_config":[],"fast_socket":[],"flex_start":null,"gvnic":[],"host_maintenance_policy":[],"linux_node_config":[{"accurate_time_config":[],"custom_node_init":[{"init_script":[{"gcp_secret_manager_secret_uri":null,"gcs_uri":"gs://my-bucket/init.sh"}]}],"hugepages_config":[],"node_kernel_module_loading":[],"swap_config":[],"sysctls":null,"transparent_hugepage_defrag":null}],"local_nvme_ssd_block_config":[],"local_ssd_encryption_mode":null,"max_run_duration":null,"node_group":null,"preemptible":false,"reservation_affinity":[],"resource_labels":null,"resource_manager_tags":null,"sandbox_config":[],"secondary_boot_disks":[],"sole_tenant_config":[],"spot":false,"storage_pools":null,"tags":null,"taint":[]}],"pod_security_policy_config":[],"private_cluster_config":[],"remove_default_node_pool":null,"resource_labels":null,"resource_usage_export_config":[],"secret_manager_config":[],"secret_sync_config":[],"terraform_labels":{"goog-terraform-provisioned":"true"},"timeouts":null,"user_managed_keys_config":[]},"after_unknown":{"addons_config":true,"anonymous_authentication_config":true,"authenticator_groups_config":true,"autopilot_cluster_policy_config":true,"autopilot_privileged_admission":true,"binary_authorization":[],"cluster_autoscaling":true,"cluster_ipv4_cidr":true,"cluster_telemetry":true,"confidential_nodes":true,"control_plane_endpoints_config":true,"cost_management_config":true,"database_encryption":true,"datapath_provider":true,"default_max_pods_per_node":true,"default_snat_status":true,"dns_config":[],"effective_labels":{},"enable_intranode_visibility":true,"enable_k8s_beta_apis":[],"enable_l4_ilb_subsetting":true,"enable_tpu":true,"endpoint":true,"enterprise_config":true,"fleet":[],"gateway_api_config":true,"gke_auto_upgrade_config":true,"id":true,"identity_service_config":true,"ip_allocation_policy":true,"label_fingerprint":true,"logging_config":true,"logging_service":true,"maintenance_policy":[],"managed_machine_learning_diagnostics_config":true,"managed_opentelemetry_config":true,"master_auth":true,"master_authorized_networks_config":true,"master_version":true,"mesh_certificates":true,"monitoring_config":true,"monitoring_service":true,"network_performance_config":[],"network_policy":[],"networking_mode":true,"node_config":[{"advanced_machine_features":[],"boot_disk":true,"confidential_nodes":true,"containerd_config":true,"disk_size_gb":true,"disk_type":true,"effective_taints":true,"ephemeral_storage_config":[],"ephemeral_storage_local_ssd_config":[],"fast_socket":[],"gcfs_config":true,"gpudirect_strategy":true,"guest_accelerator":true,"gvnic":[],"host_maintenance_policy":[],"image_type":true,"kubelet_config":true,"labels":true,"linux_node_config":[{"accurate_time_config":[],"cgroup_mode":true,"custom_node_init":[{"init_script":[{"gcs_generation":true}]}],"hugepages_config":[],"node_kernel_module_loading":[],"swap_config":[],"transparent_hugepage_enabled":true}],"local_nvme_ssd_block_config":[],"local_ssd_count":true,"logging_variant":true,"machine_type":true,"metadata":true,"min_cpu_platform":true,"oauth_scopes":true,"reservation_affinity":[],"sandbox_config":[],"secondary_boot_disks":[],"service_account":true,"shielded_instance_config":true,"sole_tenant_config":[],"taint":[],"windows_node_config":true,"workload_metadata_config":true}],"node_locations":true,"node_pool":true,"node_pool_auto_config":true,"node_pool_defaults":true,"node_version":true,"notification_config":true,"operation":true,"pod_autoscaling":true,"pod_security_policy_config":[],"private_cluster_config":[],"private_ipv6_google_access":true,"project":true,"protect_config":true,"rbac_binding_config":true,"release_channel":true,"resource_usage_export_config":[],"secret_manager_config":[],"secret_sync_config":[],"security_posture_config":true,"self_link":true,"service_external_ips_config":true,"services_ipv4_cidr":true,"subnetwork":true,"terraform_labels":{},"tpu_config":true,"tpu_ipv4_cidr_block":true,"user_managed_keys_config":[],"vertical_pod_autoscaling":true,"workload_alts_config":true,"workload_identity_config":true},"before_sensitive":false,"after_sensitive":{"addons_config":[],"anonymous_authentication_config":[],"authenticator_groups_config":[],"autopilot_cluster_policy_config":[],"autopilot_privileged_admission":[],"binary_authorization":[],"cluster_autoscaling":[],"cluster_telemetry":[],"confidential_nodes":[],"control_plane_endpoints_config":[],"cost_management_config":[],"database_encryption":[],"default_snat_status":[],"dns_config":[],"effective_labels":{},"enable_k8s_beta_apis":[],"enterprise_config":[],"fleet":[],"gateway_api_config":[],"gke_auto_upgrade_config":[],"identity_service_config":[],"ip_allocation_policy":[],"logging_config":[],"maintenance_policy":[],"managed_machine_learning_diagnostics_config":[],"managed_opentelemetry_config":[],"master_auth":[],"master_authorized_networks_config":[],"mesh_certificates":[],"monitoring_config":[],"network_performance_config":[],"network_policy":[],"node_config":[{"advanced_machine_features":[],"boot_disk":[],"confidential_nodes":[],"containerd_config":[],"effective_taints":[],"ephemeral_storage_config":[],"ephemeral_storage_local_ssd_config":[],"fast_socket":[],"gcfs_config":[],"guest_accelerator":[],"gvnic":[],"host_maintenance_policy":[],"kubelet_config":[],"labels":{},"linux_node_config":[{"accurate_time_config":[],"custom_node_init":[{"init_script":[{}]}],"hugepages_config":[],"node_kernel_module_loading":[],"swap_config":[]}],"local_nvme_ssd_block_config":[],"metadata":{},"oauth_scopes":[],"reservation_affinity":[],"sandbox_config":[],"secondary_boot_disks":[],"shielded_instance_config":[],"sole_tenant_config":[],"taint":[],"windows_node_config":[],"workload_metadata_config":[]}],"node_locations":[],"node_pool":[],"node_pool_auto_config":[],"node_pool_defaults":[],"notification_config":[],"pod_autoscaling":[],"pod_security_policy_config":[],"private_cluster_config":[],"protect_config":[],"rbac_binding_config":[],"release_channel":[],"resource_usage_export_config":[],"secret_manager_config":[],"secret_sync_config":[],"security_posture_config":[],"service_external_ips_config":[],"terraform_labels":{},"tpu_config":[],"user_managed_keys_config":[],"vertical_pod_autoscaling":[],"workload_alts_config":[],"workload_identity_config":[]}}}],"configuration":{"provider_config":{"google-beta":{"name":"google-beta","full_name":"registry.terraform.io/hashicorp/google-beta","expressions":{"project":{"constant_value":"zicong-gke-dev"},"region":{"constant_value":"us-central1"},"zone":{"constant_value":"us-central1-a"}}}},"root_module":{"resources":[{"address":"google_container_cluster.primary","mode":"managed","type":"google_container_cluster","name":"primary","provider_config_key":"google-beta","expressions":{"deletion_protection":{"constant_value":false},"initial_node_count":{"constant_value":1},"location":{"constant_value":"us-central1-a"},"name":{"constant_value":"marss-cluster"},"node_config":[{"linux_node_config":[{"custom_node_init":[{"init_script":[{"gcs_uri":{"constant_value":"gs://my-bucket/init.sh"}}]}]}]}]},"schema_version":2}]}},"timestamp":"2026-06-02T20:44:49Z","applyable":true,"complete":true,"errored":false} diff --git a/tools/issue-labeler/labeler/enrolled_teams.yml b/tools/issue-labeler/labeler/enrolled_teams.yml index c6502889b1f0..ad97f6873900 100755 --- a/tools/issue-labeler/labeler/enrolled_teams.yml +++ b/tools/issue-labeler/labeler/enrolled_teams.yml @@ -7,10 +7,15 @@ service/accessapproval: service/accesscontextmanager: resources: - google_access_context_manager_.* +service/agentidentity: + resources: + - google_agent_identity_.* +service/agentregistry: + resources: + - google_agent_registry_.* service/aiplatform-agent-engine: resources: - google_vertex_ai_reasoning_engine.* - - google_vertex_ai_online_evaluator service/aiplatform-colab-enterprise: resources: - google_colab_.* @@ -28,6 +33,7 @@ service/aiplatform-matching-engine: service/aiplatform-metadata: resources: - google_vertex_ai_metadata_store + - google_vertex_ai_schedule service/aiplatform-prediction: resources: - google_vertex_ai_cache_config @@ -35,12 +41,16 @@ service/aiplatform-prediction: - google_vertex_ai_deployment_resource_pool - google_ml_engine_model - google_vertex_ai_endpoint_with_model_garden_deployment + - google_vertex_ai_model_garden_enable_model service/aiplatform-rag-engine: resources: - google_vertex_ai_rag_engine_config +service/aiplatform-semantic-governance: + resources: + - google_vertex_ai_semantic_governance.* service/aiplatform-tensorboard: resources: - - google_vertex_ai_tensorboard + - google_vertex_ai_tensorboard.* service/alloydb: resources: - google_alloydb_.* @@ -120,7 +130,6 @@ service/cloudbilling: service/cloudbuild: resources: - google_cloudbuild_.* - - google_cloudbuildv2_.* service/clouddeploy: resources: - google_clouddeploy_.* @@ -246,6 +255,7 @@ service/compute-managed: - google_compute_region_instance_group_manager.* - google_compute_per_instance_config.* - google_compute_region_per_instance_config.* + - google_compute_bulk_per_instance_config.* - google_compute_resize_request.* - google_compute_region_resize_request.* service/compute-nat: @@ -378,9 +388,11 @@ service/deploymentmanager: service/developerconnect: resources: - google_developer_connect_.* + - google_cloudbuildv2_.* service/dialogflow: resources: - google_dialogflow_conversation_profile + - google_dialogflow_sip_trunk service/dialogflow-cx: resources: - google_dialogflow_cx_.* @@ -496,6 +508,7 @@ service/iam-core: - google_iam_role - google_iam_.*_policy_binding - google_iam_principal_access_boundary_policy + - google_iam_.*_access_policy - google_iam_testable_permissions service/iam-serviceaccount: resources: @@ -527,6 +540,9 @@ service/integrationconnectors: service/integrations: resources: - google_integrations_.* +service/licensemanager: + resources: + - google_license_manager_configuration service/logging: resources: - google_logging_.* @@ -605,6 +621,9 @@ service/network-security-distributed-firewall: - google_compute_firewall.* - google_compute_network_firewall_policy.* - google_compute_region_network_firewall_policy.* +service/networkconnectivity-customhardware: + resources: + - google_network_connectivity_custom_hardware_.* service/networkconnectivity-private-range: resources: - google_network_connectivity_internal_range @@ -636,6 +655,9 @@ service/networksecurity-swp: - google_network_security_gateway_security_policy_rule - google_network_security_url_lists - google_network_security_tls_inspection_policy +service/networkservices-agent-gateway: + resources: + - google_network_services_agent_gateway service/networkservices-media-cdn: resources: - google_network_services_edge_cache_.* @@ -672,6 +694,7 @@ service/orgpolicy: service/osconfig: resources: - google_os_config_.* + - google_compute_.*_vm_extension_policy service/oslogin: resources: - google_os_login_.* diff --git a/tools/resource-template-converter/README.md b/tools/resource-template-converter/README.md index 17716913bc70..1cfcf35b7655 100644 --- a/tools/resource-template-converter/README.md +++ b/tools/resource-template-converter/README.md @@ -40,6 +40,8 @@ go build -o bin/convert-resource-template main.go * `-P, --skip-product ` (Optional): Comma-separated list of product directories to skip from migration. * `--only-migration` (Optional): Run only the migration steps (examples -> samples conversion, copy and migrate templates). Do not sort keys or format string quotes. Cannot be combined with `--only-format`. * `--only-format` (Optional): Run only the formatting steps (sort keys, strip string quotes). Do not migrate examples to samples or copy templates. Cannot be combined with `--only-migration`. +* `--explicit-config-path` (Optional): Force writing `config_path` in step definition mappings during migration, even if they match the default generated template path or were not explicitly defined in the original YAML file. +* `--eap` (Optional): Enable EAP private overrides repository migration. Forces EAP folder layout resolution and forces `--explicit-config-path` to be true. * `--skip-open-pr` (Optional): Skip files modified by active open PRs updated in the last N days (configured by `--skip-open-pr-days`). * `--skip-open-pr-days ` (Optional): Number of days of open PR history to verify when checking open PRs (defaults to `60`). diff --git a/tools/resource-template-converter/cmd/convert_resource_template.go b/tools/resource-template-converter/cmd/convert_resource_template.go index 65b10182faf5..8ed3b8703a71 100644 --- a/tools/resource-template-converter/cmd/convert_resource_template.go +++ b/tools/resource-template-converter/cmd/convert_resource_template.go @@ -24,6 +24,8 @@ var onlyMigration bool var onlyFormat bool var skipOpenPR bool var skipOpenPRDays int +var explicitConfigPath bool +var eapFlag bool var convertResourceTemplateCmd = &cobra.Command{ Use: "convert-resource-template", @@ -83,19 +85,30 @@ func exeCconvertResourceTemplate(basePath string, targetFile string) error { var productsPath, examplesSourceDir, samplesDestDir string - if _, err := os.Stat(filepath.Join(basePath, "mmv1")); err == nil { + isEAP := eapFlag + if !isEAP { + if _, err := os.Stat(filepath.Join(basePath, "products")); err == nil { + if _, err := os.Stat(filepath.Join(basePath, "mmv1")); err != nil { + isEAP = true + } + } + } + + if isEAP { + // EAP private overrides repository + productsPath = filepath.Join(basePath, "products") + examplesSourceDir = basePath + samplesDestDir = filepath.Join(basePath, "samples") + explicitConfigPath = true + } else { // Public magic-modules repository + if _, err := os.Stat(filepath.Join(basePath, "mmv1")); err != nil { + log.Fatalf("Neither 'mmv1' nor 'products' directory structure found. Please ensure this tool is run from a magic-modules or magic-modules-private-overrides directory.") + } productsPath = filepath.Join(basePath, "mmv1", "products") templatesPath := filepath.Join(basePath, "mmv1", "templates", "terraform") examplesSourceDir = filepath.Join(templatesPath, "examples") samplesDestDir = filepath.Join(templatesPath, "samples", "services") - } else if _, err := os.Stat(filepath.Join(basePath, "products")); err == nil { - // EAP private overrides repository - productsPath = filepath.Join(basePath, "products") - examplesSourceDir = basePath - samplesDestDir = filepath.Join(basePath, "templates", "terraform", "samples", "services") - } else { - log.Fatalf("Neither 'mmv1' nor 'products' directory structure found. Please ensure this tool is run from a magic-modules or magic-modules-private-overrides directory.") } var pathsToWalk []string @@ -194,7 +207,7 @@ func exeCconvertResourceTemplate(basePath string, targetFile string) error { return fmt.Errorf("error copying templates for %s: %w", t.resolvedPath, err) } } - if err := migrate.MigrateFile(t.resolvedPath, t.serviceName, onlyMigration, onlyFormat); err != nil { + if err := migrate.MigrateFile(t.resolvedPath, t.serviceName, onlyMigration, onlyFormat, explicitConfigPath, isEAP); err != nil { return fmt.Errorf("failed to migrate file %s: %w", t.resolvedPath, err) } } @@ -246,7 +259,7 @@ func exeCconvertResourceTemplate(basePath string, targetFile string) error { // Continue processing other files even if one fails. } } - if err := migrate.MigrateFile(path, serviceName, onlyMigration, onlyFormat); err != nil { + if err := migrate.MigrateFile(path, serviceName, onlyMigration, onlyFormat, explicitConfigPath, isEAP); err != nil { log.Printf("Failed to migrate file %s: %v\n", path, err) // Continue migrating other files even if one fails. } @@ -270,6 +283,8 @@ func init() { convertResourceTemplateCmd.Flags().StringVarP(&skipProductsFlag, "skip-product", "P", "", "Comma-separated list of product directories to skip from migration") convertResourceTemplateCmd.Flags().BoolVar(&onlyMigration, "only-migration", false, "Only run migration steps (examples -> samples, copy templates), skip formatting") convertResourceTemplateCmd.Flags().BoolVar(&onlyFormat, "only-format", false, "Only run formatting steps (sort keys, strip quotes), skip migration") + convertResourceTemplateCmd.Flags().BoolVar(&explicitConfigPath, "explicit-config-path", false, "Explicitly write config_path for all samples during migration, even if they match default paths") + convertResourceTemplateCmd.Flags().BoolVar(&eapFlag, "eap", false, "Enable EAP private overrides repository migration (forces EAP directory structure and explicit config_path)") convertResourceTemplateCmd.Flags().BoolVar(&skipOpenPR, "skip-open-pr", false, "Skip files modified by active open PRs updated in the last N days (configured by --skip-open-pr-days)") convertResourceTemplateCmd.Flags().IntVar(&skipOpenPRDays, "skip-open-pr-days", 60, "Number of days of open PR history to verify when checking open PRs") rootCmd.AddCommand(convertResourceTemplateCmd) diff --git a/tools/resource-template-converter/migrate/format.go b/tools/resource-template-converter/migrate/format.go index b3bd5ab91cce..4f7d64115c7d 100644 --- a/tools/resource-template-converter/migrate/format.go +++ b/tools/resource-template-converter/migrate/format.go @@ -58,6 +58,7 @@ func init() { nestedKeyOrders["operation"] = getYamlStructFieldOrder(api.Operation{}) nestedKeyOrders["iam_policy"] = getYamlStructFieldOrder(resource.IamPolicy{}) nestedKeyOrders["custom_code"] = getYamlStructFieldOrder(resource.CustomCode{}) + nestedKeyOrders["steps"] = getYamlStructFieldOrder(resource.Step{}) } func sortMappingNode(mappingNode *yaml.Node, keyOrder []string) { diff --git a/tools/resource-template-converter/migrate/migrate.go b/tools/resource-template-converter/migrate/migrate.go index 5fa7cd13b6c1..3ee8a71b9c86 100644 --- a/tools/resource-template-converter/migrate/migrate.go +++ b/tools/resource-template-converter/migrate/migrate.go @@ -15,7 +15,7 @@ import ( // PATCH-START: existing structs // PATCH-END: existing structs -func MigrateFile(filePath, serviceName string, onlyMigration, onlyFormat bool) error { +func MigrateFile(filePath, serviceName string, onlyMigration, onlyFormat, explicitConfigPath, isEAP bool) error { originalBytes, err := ioutil.ReadFile(filePath) if err != nil { return fmt.Errorf("failed to read file: %w", err) @@ -83,17 +83,29 @@ func MigrateFile(filePath, serviceName string, onlyMigration, onlyFormat bool) e } var newConfigPath string - if configPathVal != "" && serviceName != "" { - templateName := filepath.Base(configPathVal) - calculatedPath := path.Join("templates/terraform/samples/services", serviceName, templateName) - defaultPath := path.Join("templates/terraform/samples/services", serviceName, fmt.Sprintf("%s.tf.tmpl", nameVal)) - if calculatedPath != defaultPath { + if serviceName != "" { + var templateName string + if configPathVal != "" { + templateName = filepath.Base(configPathVal) + } else { + templateName = fmt.Sprintf("%s.tf.tmpl", nameVal) + } + var calculatedPath, defaultPath string + if isEAP { + calculatedPath = path.Join("samples", serviceName, templateName) + defaultPath = path.Join("samples", serviceName, fmt.Sprintf("%s.tf.tmpl", nameVal)) + } else { + calculatedPath = path.Join("templates/terraform/samples/services", serviceName, templateName) + defaultPath = path.Join("templates/terraform/samples/services", serviceName, fmt.Sprintf("%s.tf.tmpl", nameVal)) + } + if explicitConfigPath || calculatedPath != defaultPath { newConfigPath = calculatedPath } } samplesContent := []*yaml.Node{} stepContent := []*yaml.Node{} + hasConfigPath := false for i := 0; i < len(exampleMapNode.Content); i += 2 { keyNode := exampleMapNode.Content[i] @@ -109,6 +121,7 @@ func MigrateFile(filePath, serviceName string, onlyMigration, onlyFormat bool) e stepContent = append(stepContent, cloneNode(keyNode), cloneNode(valNode)) case "config_path": + hasConfigPath = true if newConfigPath != "" { valNode.Value = newConfigPath valNode.Style = 0 @@ -136,6 +149,20 @@ func MigrateFile(filePath, serviceName string, onlyMigration, onlyFormat bool) e } } + if explicitConfigPath && !hasConfigPath && newConfigPath != "" { + keyNode := &yaml.Node{ + Kind: yaml.ScalarNode, + Tag: "!!str", + Value: "config_path", + } + valNode := &yaml.Node{ + Kind: yaml.ScalarNode, + Tag: "!!str", + Value: newConfigPath, + } + stepContent = append(stepContent, keyNode, valNode) + } + // Construct Step Mapping Node stepMapNode := &yaml.Node{ Kind: yaml.MappingNode, @@ -143,6 +170,8 @@ func MigrateFile(filePath, serviceName string, onlyMigration, onlyFormat bool) e Content: stepContent, } + sortMappingNode(stepMapNode, nestedKeyOrders["steps"]) + // Construct Steps Sequence Node stepsSeqNode := &yaml.Node{ Kind: yaml.SequenceNode, diff --git a/tools/resource-template-converter/migrate/migrate_test.go b/tools/resource-template-converter/migrate/migrate_test.go index 4da1e24e796d..92c1c3d19b79 100644 --- a/tools/resource-template-converter/migrate/migrate_test.go +++ b/tools/resource-template-converter/migrate/migrate_test.go @@ -53,7 +53,7 @@ examples: } // Run migration - err = MigrateFile(yamlPath, "accesscontextmanager", false, false) + err = MigrateFile(yamlPath, "accesscontextmanager", false, false, false, false) if err != nil { t.Fatalf("MigrateFile failed: %v", err) } @@ -122,7 +122,7 @@ examples: } // Run migration - err = MigrateFile(yamlPath, "accesscontextmanager", false, false) + err = MigrateFile(yamlPath, "accesscontextmanager", false, false, false, false) if err != nil { t.Fatalf("MigrateFile failed: %v", err) } @@ -187,7 +187,7 @@ examples: } // Run migration - err = MigrateFile(yamlPath, "accesscontextmanager", false, false) + err = MigrateFile(yamlPath, "accesscontextmanager", false, false, false, false) if err != nil { t.Fatalf("MigrateFile failed: %v", err) } @@ -249,7 +249,7 @@ examples: } // Run migration - err = MigrateFile(yamlPath, "datacatalog", false, false) + err = MigrateFile(yamlPath, "datacatalog", false, false, false, false) if err != nil { t.Fatalf("MigrateFile failed: %v", err) } @@ -305,7 +305,7 @@ examples: } // Run migration - err = MigrateFile(yamlPath, "artifactregistry", false, false) + err = MigrateFile(yamlPath, "artifactregistry", false, false, false, false) if err != nil { t.Fatalf("MigrateFile failed: %v", err) } @@ -354,7 +354,7 @@ examples: ioutil.WriteFile(tmplPath, []byte(`resource "google" "test" {}`), 0644) // Run migration only - err = MigrateFile(yamlPath, "accesscontextmanager", true, false) + err = MigrateFile(yamlPath, "accesscontextmanager", true, false, false, false) if err != nil { t.Fatalf("MigrateFile failed: %v", err) } @@ -394,7 +394,7 @@ description: "An AccessLevel is a label." ioutil.WriteFile(yamlPath, []byte(yamlContent), 0644) // Run formatting only - err = MigrateFile(yamlPath, "accesscontextmanager", false, true) + err = MigrateFile(yamlPath, "accesscontextmanager", false, true, false, false) if err != nil { t.Fatalf("MigrateFile failed: %v", err) } @@ -414,3 +414,153 @@ description: "An AccessLevel is a label." t.Errorf("expected string quotes to be stripped under only-format, got: %s", updatedYaml) } } + +func TestMigrateFile_ExplicitConfigPath(t *testing.T) { + // Tests that when explicitConfigPath is enabled, config_path is explicitly written + // even if it matches the default templates path, or even if the original yaml didn't have it. + tmpDir, err := ioutil.TempDir("", "mm-explicit-config-test") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + productsDir := filepath.Join(tmpDir, "mmv1", "products", "accesscontextmanager") + os.MkdirAll(productsDir, 0755) + + examplesDir := filepath.Join(tmpDir, "mmv1", "templates", "terraform", "examples") + os.MkdirAll(examplesDir, 0755) + + // Create resource YAML file (without config_path explicitly set) + yamlPath := filepath.Join(productsDir, "AccessLevel.yaml") + yamlContent := `--- +name: AccessLevel +examples: + - name: access_context_manager_access_level_basic + primary_resource_id: access-level + vars: + access_level_name: chromeos_no_lock +` + ioutil.WriteFile(yamlPath, []byte(yamlContent), 0644) + + tmplPath := filepath.Join(examplesDir, "access_context_manager_access_level_basic.tf.tmpl") + ioutil.WriteFile(tmplPath, []byte(`resource "google" "test" {}`), 0644) + + // Run migration with explicitConfigPath = true + err = MigrateFile(yamlPath, "accesscontextmanager", false, false, true, false) + if err != nil { + t.Fatalf("MigrateFile failed: %v", err) + } + + updatedYamlBytes, _ := ioutil.ReadFile(yamlPath) + updatedYaml := string(updatedYamlBytes) + + // Verify YAML content has config_path explicitly set to default samples path, ordered right after name: + expectedSubstr := ` steps: + - name: access_context_manager_access_level_basic + config_path: templates/terraform/samples/services/accesscontextmanager/access_context_manager_access_level_basic.tf.tmpl + resource_id_vars:` + if !strings.Contains(updatedYaml, expectedSubstr) { + t.Errorf("expected sorted keys and config_path right after name. Expected substring:\n%s\n\nGot:\n%s", expectedSubstr, updatedYaml) + } +} + +func TestMigrateFile_OnlyMigrationWithExplicitConfigPath(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "mm-only-migration-explicit-config-test") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + productsDir := filepath.Join(tmpDir, "mmv1", "products", "accesscontextmanager") + os.MkdirAll(productsDir, 0755) + + examplesDir := filepath.Join(tmpDir, "mmv1", "templates", "terraform", "examples") + os.MkdirAll(examplesDir, 0755) + + // Create resource YAML file + yamlPath := filepath.Join(productsDir, "AccessLevel.yaml") + yamlContent := `--- +name: "AccessLevel" +examples: + - name: access_context_manager_access_level_basic + primary_resource_id: access-level + vars: + access_level_name: chromeos_no_lock +` + ioutil.WriteFile(yamlPath, []byte(yamlContent), 0644) + + tmplPath := filepath.Join(examplesDir, "access_context_manager_access_level_basic.tf.tmpl") + ioutil.WriteFile(tmplPath, []byte(`resource "google" "test" {}`), 0644) + + // Run migration only + err = MigrateFile(yamlPath, "accesscontextmanager", true, false, true, false) + if err != nil { + t.Fatalf("MigrateFile failed: %v", err) + } + + updatedYamlBytes, _ := ioutil.ReadFile(yamlPath) + updatedYaml := string(updatedYamlBytes) + + // Quotes should still be preserved: + if !strings.Contains(updatedYaml, `name: "AccessLevel"`) { + t.Errorf("expected name quotes to be preserved, got: %s", updatedYaml) + } + + // But step key sorting should still happen (config_path sorted right after name): + expectedSubstr := ` steps: + - name: access_context_manager_access_level_basic + config_path: templates/terraform/samples/services/accesscontextmanager/access_context_manager_access_level_basic.tf.tmpl + resource_id_vars:` + if !strings.Contains(updatedYaml, expectedSubstr) { + t.Errorf("expected sorted keys inside steps under only-migration. Expected substring:\n%s\n\nGot:\n%s", expectedSubstr, updatedYaml) + } +} + +func TestMigrateFile_EAP(t *testing.T) { + // Tests that when isEAP is true, config_path is resolved flatly as samples/ + // and explicitly written. + tmpDir, err := ioutil.TempDir("", "mm-eap-test") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + productsDir := filepath.Join(tmpDir, "products", "accesscontextmanager") + os.MkdirAll(productsDir, 0755) + + examplesDir := filepath.Join(tmpDir, "examples") + os.MkdirAll(examplesDir, 0755) + + // Create resource YAML file (without config_path explicitly set) + yamlPath := filepath.Join(productsDir, "AccessLevel.yaml") + yamlContent := `--- +name: AccessLevel +examples: + - name: access_context_manager_access_level_basic + primary_resource_id: access-level + vars: + access_level_name: chromeos_no_lock +` + ioutil.WriteFile(yamlPath, []byte(yamlContent), 0644) + + tmplPath := filepath.Join(examplesDir, "access_context_manager_access_level_basic.tf.tmpl") + ioutil.WriteFile(tmplPath, []byte(`resource "google" "test" {}`), 0644) + + // Run migration with isEAP = true, explicitConfigPath = true + err = MigrateFile(yamlPath, "accesscontextmanager", false, false, true, true) + if err != nil { + t.Fatalf("MigrateFile failed: %v", err) + } + + updatedYamlBytes, _ := ioutil.ReadFile(yamlPath) + updatedYaml := string(updatedYamlBytes) + + // Verify YAML content has nested config_path under samples/serviceName/: + expectedSubstr := ` steps: + - name: access_context_manager_access_level_basic + config_path: samples/accesscontextmanager/access_context_manager_access_level_basic.tf.tmpl + resource_id_vars:` + if !strings.Contains(updatedYaml, expectedSubstr) { + t.Errorf("expected nested EAP config_path under samples/. Expected substring:\n%s\n\nGot:\n%s", expectedSubstr, updatedYaml) + } +}