mcpField
P
provider-actions

provider-actions

github.com/hashicorp开发9093.9k 次安装今天更新
代码仓库

关于 provider-actions

使用 Plugin Framework 实现 Terraform Provider Actions 的开发指南,涵盖 Provider 配置、Schema、调用、进度、错误处理、测试及文档,适用于生命周期触发的操作。

主要功能

  1. 01配置 ProviderWithActions
  2. 02定义并校验 Action Schema
  3. 03实现调用与进度报告
  4. 04处理超时和错误
  5. 05测试生命周期触发的 Action
  6. 06编写 Action 文档

适用场景

  1. 01为 Provider 添加首个 Action
  2. 02实现由 Terraform 生命周期事件触发的 API 操作
  3. 03测试 Action 结果及失败场景

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/hashicorp/agent-skills --skill provider-actions

安装后在 Claude Code 中输入/provider-actions确认已加载。

详细介绍

Terraform 提供程序操作实现指南

概述

Terraform 操作可在 Terraform 生命周期中执行命令式操作。操作是实验性功能,允许在特定生命周期事件(创建、更新、销毁前后)执行提供程序操作。

参考资料:

首个操作的设置

首次向尚未包含任何操作的提供程序添加操作时,需要执行若干一次性脚手架设置步骤:

  1. 实现 ProviderWithActions — 向提供程序添加 Actions() 方法,使其返回 []func() action.Action。
  2. 在 Configure 中设置 ActionData — 提供程序的 Configure 方法必须设置 resp.ActionData = v,与现有的 ResourceData、DataSourceData 和 EphemeralResourceData 赋值并列。
  3. 创建 ActionWithConfigure 基础类型 — 如果提供程序使用嵌入式基础类型(例如 ResourceWithConfigure),则创建等效的 ActionWithConfigure 类型,实现 action.ConfigureRequest / action.ConfigureResponse。
  4. 操作架构辅助函数变体 — 如果提供程序通过辅助函数注入常见架构属性(例如 namespace),则需要操作架构变体,因为 action/schema 类型与 resource/schema 类型不同。

文件结构

大多数提供程序将操作与资源放在提供程序包中:

internal/provider/
├── <action_name>_action.go       # Action implementation
└── <action_name>_action_test.go  # Action tests

(大型多服务提供程序会改用 internal/service/<service>/ 包, 请遵循目标仓库的布局。)

文档与其他生成的文档放在一起:

docs/actions/
└── <action_name>.md              # User-facing documentation

(某些较旧的大型提供程序会手动编写 website/docs/actions/<name>.html.markdown,请遵循仓库的做法。)

操作架构定义

操作使用 Terraform Plugin Framework,并遵循标准架构模式:

func (a *actionType) Schema(ctx context.Context, req action.SchemaRequest, resp *action.SchemaResponse) {
    resp.Schema = schema.Schema{
        Attributes: map[string]schema.Attribute{
            // Required configuration parameters
            "resource_id": schema.StringAttribute{
                Required:    true,
                Description: "ID of the resource to operate on",
            },
            // Optional parameters with defaults
            "timeout": schema.Int64Attribute{
                Optional:    true,
                Description: "Operation timeout in seconds",
                Default:     int64default.StaticInt64(1800),
                Computed:    true,
            },
        },
    }
}
常见架构问题

请特别留意架构定义 —— 初稿完成后常见的问题包括:

  1. 类型不匹配

    • 模型结构体使用 types.String/types.Int64,架构使用来自 github.com/hashicorp/terraform-plugin-framework/types 的 types.StringType —— 不要混用其他包中的类型
    • 一些大型提供程序会在其基础上封装自定义类型包(例如 terraform-provider-aws 的内部 fwtypes);在此类仓库中, 应始终遵循其约定,而不是使用普通类型
  2. 列表/映射元素类型

    // WRONG - missing ElementType
    "items": schema.ListAttribute{
        Optional: true,
    }
    
    // CORRECT
    "items": schema.ListAttribute{
        Optional:    true,
        ElementType: types.StringType,
    }
    
  3. 计算属性与可选属性

    • 带默认值的属性必须同时设置 Optional: true 和 Computed: true
    • 除非操作输入有默认值,否则不要将其标记为 Computed
  4. 验证器导入

    // Ensure proper imports
    "github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
    "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
    
  5. 区域/提供程序属性(多区域提供程序,例如 AWS)

    • 如果提供程序有共享的区域处理逻辑,请使用该逻辑
    • 不要在操作架构中手动重新定义提供程序级配置
  6. 嵌套属性

    • 对复杂结构使用合适的嵌套对象类型
    • 确保正确定义嵌套类型
架构验证清单

提交前,请确认:

  • 所有属性均有描述
  • 列表/映射属性已定义 ElementType
  • 已正确导入并应用验证器
  • 模型结构体使用了正确的框架类型
  • 带默认值的可选属性已标记为 Computed
  • 代码编译时没有类型错误
  • 运行 go build 以发现类型不匹配

操作 Invoke 方法

Invoke 方法包含操作逻辑:

func (a *actionType) Invoke(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) {
    var data actionModel
    resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
    if resp.Diagnostics.HasError() {
        return
    }

    // a.client was stored by Configure (from req.ProviderData), the same
    // pattern resources use.
    resp.SendProgress(action.InvokeProgressEvent{Message: "Starting operation..."})

    // Implement action logic with error handling
    // Use context for timeout management
    // Poll for completion if async operation

    resp.SendProgress(action.InvokeProgressEvent{Message: "Operation completed"})
}

关键实现要求

1. 进度报告
  • 使用 resp.SendProgress(action.InvokeProgressEvent{...}) 实时更新进度
  • 在耗时操作期间提供有意义的进度消息
  • 在关键节点更新进度
  • 对耗时操作注明已用时间
2. 超时管理
  • 始终包含可配置的超时参数(默认值:1800s)
  • 对 API 调用使用 context.WithTimeout()
  • 妥善处理超时错误
  • 验证超时范围(通常为 60-7200 秒)
3. 错误处理
  • 使用 resp.Diagnostics.AddError() 添加诊断信息
  • 提供清晰且包含上下文的错误消息
  • 在相关情况下包含 API 错误详情
  • 将提供商错误类型映射为用户友好的消息
  • 记录所有可能的错误情况

错误处理示例:

// Handle specific errors
var notFound *types.ResourceNotFoundException
if errors.As(err, &notFound) {
    resp.Diagnostics.AddError(
        "Resource Not Found",
        fmt.Sprintf("Resource %s was not found", resourceID),
    )
    return
}

// Generic error handling
resp.Diagnostics.AddError(
    "Operation Failed",
    fmt.Sprintf("Could not complete operation for %s: %s", resourceID, err),
)
4. 提供商 SDK 集成
  • 使用 Configure 时保存的 API 客户端(a.client),并与资源和数据源共享
  • 为列表操作处理分页
  • 为瞬时故障实现重试逻辑
  • 使用适当的错误类型
5. 参数验证
  • 使用框架验证器验证输入
  • 在操作前验证资源是否存在
  • 检查是否存在冲突参数
  • 根据提供商的命名要求进行验证
6. 轮询与等待

对于需要等待完成的操作,应在上下文截止时间内使用定时器进行轮询,并在过程中报告进度。(或者使用 github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry 中的 retry.StateChangeConf,这是资源使用的相同等待器原语。)

ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()

// Poll fast, report slow: progress events cross the plugin protocol, so
// throttle them instead of emitting one per poll.
start := time.Now()
var lastProgress time.Time
for {
    res, err := findResource(ctx, a.client, id)
    if err != nil {
        resp.Diagnostics.AddError("Error polling operation", fmt.Sprintf("checking status of %s: %s", id, err))
        return
    }
    switch res.Status {
    case "AVAILABLE", "COMPLETED":
        resp.SendProgress(action.InvokeProgressEvent{Message: "Operation completed"})
        return
    case "CREATING", "PENDING":
        if time.Since(lastProgress) >= 30*time.Second {
            lastProgress = time.Now()
            resp.SendProgress(action.InvokeProgressEvent{
                Message: fmt.Sprintf("Status: %s, Elapsed: %v", res.Status, time.Since(start).Round(time.Second)),
            })
        }
    default:
        resp.Diagnostics.AddError("Operation Failed", fmt.Sprintf("%s entered unexpected status %q", id, res.Status))
        return
    }

    select {
    case <-ctx.Done():
        resp.Diagnostics.AddError("Operation Timed Out", fmt.Sprintf("%s did not complete within %v", id, timeout))
        return
    case <-ticker.C:
    }
}

常见操作模式

批量操作
  • 按可配置的批次处理项目
  • 按批次报告进度
  • 妥善处理部分失败
  • 支持前缀/筛选参数
命令执行
  • 提交命令并获取操作 ID
  • 轮询完成状态
  • 获取并报告输出
  • 处理轮询期间的超时
  • 执行前验证资源是否存在
服务调用
  • 使用参数调用服务
  • 等待完成(如果为同步调用)
  • 返回输出/结果
  • 处理服务特定错误
资源状态变更
  • 验证当前状态
  • 应用状态变更
  • 轮询目标状态
  • 处理过渡状态
异步作业提交
  • 使用配置提交作业
  • 获取作业 ID
  • 可选择等待完成
  • 报告作业状态

操作触发器

在 Terraform 配置中,通过 action_trigger 生命周期块调用操作。没有对应触发器的独立 action 块只会声明操作,但不会执行。

HCL 语法

操作参数必须放在 config {} 块中。触发器引用使用 action. 前缀,actions 是一个列表。事件使用不带引号的标识符。

action "provider_service_action" "name" {
  config {
    parameter = value
  }
}

resource "terraform_data" "trigger" {
  lifecycle {
    action_trigger {
      events  = [after_create]
      actions = [action.provider_service_action.name]
    }
  }
}
可用的触发器事件

支持的事件(截至 Terraform 1.14):

  • before_create - 创建资源之前
  • after_create - 创建资源之后
  • before_update - 更新资源之前
  • after_update - 更新资源之后

不支持的事件(截至 Terraform 1.14;请查看最新发行说明):

  • before_destroy - 不可用(将导致验证错误)
  • after_destroy - 不可用(将导致验证错误)

测试操作

验收测试
  • 使用有效参数测试操作调用
  • 测试超时场景
  • 测试错误情况
  • 验证提供商状态变更
  • 测试进度报告
  • 使用自定义参数进行测试
  • 测试基于触发器的调用
测试模式
func TestAccExampleAction_basic(t *testing.T) {
    resource.ParallelTest(t, resource.TestCase{
        PreCheck:                 func() { testAccPreCheck(t) },
        ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
        TerraformVersionChecks: []tfversion.TerraformVersionCheck{
            tfversion.SkipBelow(tfversion.Version1_14_0),
        },
        Steps: []resource.TestStep{
            {
                Config: testAccActionConfig_basic(),
                ConfigStateChecks: []statecheck.StateCheck{
                    // assert the observable effect of the action on the
                    // triggering resource
                },
            },
        },
    })
}
使用清理函数清理测试

测试中调用的操作可能会遗留真实资源;应注册清理器(列出 → 筛选名称带有测试前缀的资源 → 删除),以便清理泄漏的资源。清理器并非操作专用——请使用 provider-test-patterns 技能(如果可用),了解清理函数模式、注册方式、TestMain 和依赖顺序。

使用 terraform_data 作为无操作触发器

对于不需要真实基础设施的操作测试,terraform_data 可用作无操作触发器资源。这对错误情况和验证测试很有帮助:

resource "terraform_data" "trigger" {
  lifecycle {
    action_trigger {
      events  = [after_create]
      actions = [action.provider_service_action.test]
    }
  }
}

action "provider_service_action" "test" {
  config {
    param = "invalid-value"
  }
}
使用 PostApplyFunc 验证副作用

操作不会生成可通过 resource.TestCheckResourceAttr 检查的状态。使用 resource.TestStep 上的 PostApplyFunc 在应用后查询 API,并确认操作产生了预期的副作用:

Steps: []resource.TestStep{
    {
        Config: testConfig,
        PostApplyFunc: func() {
            // query the API to verify the action's side effect occurred
        },
    },
},
测试最佳实践

特定服务的前置条件

  • 始终检查操作成功执行前必须满足的特定服务前置条件
  • 在操作文档和测试配置中记录前置条件

错误模式匹配

  • Terraform 会为操作错误添加额外上下文
  • 使用灵活的正则表达式模式:regexp.MustCompile(\(?s)Error Title.*key phrase`)`

不适用于操作的测试模式

  1. 操作由生命周期事件触发,而非重新应用配置
  2. 销毁前/后测试:截至 Terraform 1.14 尚不支持
运行测试

先进行编译检查,然后运行针对性的验收测试:

go test -c -o /dev/null ./internal/provider
TF_ACC=1 go test ./internal/provider -run TestAccExampleAction_ -timeout 60m

如可用,请使用 run-acceptance-tests 技能来设置环境变量、 调试失败的测试以及运行清理器。

文档规范

如果提供程序使用 tfplugindocs,请用它生成操作文档(如可用,请使用 provider-docs 技能完成此工作流)。每个操作文档页面必须包含:

  1. 前置元数据(仅限手写的旧版布局)

    ---
    subcategory: "Service Name"
    layout: "provider"
    page_title: "Provider: provider_service_action"
    description: |-
      Brief description of what the action does.
    ---
    
  2. 带警告的标题

    • 关于实验状态的 Beta/Alpha 通知
    • 关于可能产生意外后果的警告
    • 提供程序文档链接
  3. 使用示例

    • 基本用法示例
    • 包含所有选项的高级用法示例
    • 使用 terraform_data 的基于触发器的示例
    • 真实用例示例
  4. 参数参考

    • 列出所有必需和可选参数
    • 包含说明和默认值
    • 注明所有验证规则
  5. 文档检查(可选工具)

    • 如果仓库使用 terrafmt,请在提交前运行 terrafmt fmt, 并使用 terrafmt diff 进行验证

变更日志条目格式(提供程序特定约定)

一些提供程序(例如 terraform-provider-aws)使用 go-changelog 跟踪发行说明:每个 PR 在 .changelog/ 目录中对应一个文件。请查看目标仓库的 CONTRIBUTING 指南; 如果仓库不使用此工具,则跳过此项。

.changelog/<pr_number>.txt

内容格式:

action/provider_service_action: Brief description of the action

提交前检查清单

提交操作实现前:

  • 代码可编译:go build -o /dev/null .
  • 测试可编译:go test -c -o /dev/null ./internal/provider
  • 代码已格式化:gofmt(或仓库的 make fmt)
  • 已按仓库约定生成或格式化文档
  • 已创建变更日志条目(如果仓库使用)
  • 架构使用正确的类型
  • 所有 List/Map 属性均有 ElementType
  • 已为长时间运行的操作实现进度更新
  • 错误消息包含上下文和资源标识符
  • 文档包含多个示例
  • 文档包含前置条件和警告

参考资料

评论

支持 Markdown