Contents

TF-dev

Dev terraform provider

基于新框架tpf开发

开发示例:

参考示例:

一、调试 provider

1、debug terraform

# makefile
default: install

build:
        go build -v ./...

install: 
        go install -v ./...

vscode 调试 terraform provider

1)vscode launch
cat .vscode/launch.json 
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Debug Terraform Provider",
            "type": "go",
            "request": "launch",
            "mode": "debug",
            // this assumes your workspace is the root of the repo
            "program": "${workspaceFolder}",
            "env": {},
            "args": [
                "-debug",
            ]
        }
    ]
}

run debug

2)ready terraform code
3)run terraform
TF_REATTACH_PROVIDERS='{"registry.terraform.io/serialt/message":{"Protocol":"grpc","ProtocolVersion":5,"Pid":50972,"Test":true,"Addr":{"Network":"unix","String":"/var/folders/vm/zlhwbdyj2031f088_w9q3f8w0000gn/T/plugin3117659601"}}}' terraform apply

dev模式覆盖

provider

cat ~/.terraformrc
provider_installation {
    dev_overrides {
      "serialt/message" = "/Users/serialt/go/bin"
    }
    direct {}
  }
# TF_LOG=TRACE terraform apply
terraform apply

二、TPF框架

目前官方在维护的开发 provider sdk 有两个版本:

  • terraform-plugin-sdk/v2
  • terraform-plugin-framework

如果要支持老版本的terraform,建议是使用 terraform-plugin-sdk/v2 开发,否则还是建议使用terraform-plugin-framework开发。

TPF开发示例:https://github.com/hashicorp/terraform-provider-hashicups

解释说明

插件开发主要涉及的文件分为三种类型:

  • provider
  • datasource
  • resource
provider

文件中主要是一些 配置文件的读取和对第三分 api 客户端的初始化,主要由五个方法组成:

  • Metadata:provider的名字
  • Schema:provider的配置参数
  • Configure:读取配置provider中的值和环境变量的值,初始化调用第三方api的client
  • Resources:定义的resource资源
  • DataSources:定义的datasource资源
datasource

通过第三方api的client调用,查询已存在的资源信息:

  • Metadata:定义datasource的名字
  • Schema:datasource配置的参数
  • Read:client调用api,读取具体的数据,生成state
  • Configure:从provider中获取传入的调用api的client
resource

通过第三方api的client调用,对资源进行增删改查和导入资源对象

  • Metadata:定义resource的名字

  • Schema:resource配置的参数

  • Configure:从provider中获取传入的调用api的client

  • Create:创建资源

  • Read:查询资源

  • Update:更新资源

  • Delete:删除资源,用于在销毁时删除资源

  • ImportState:导入资源,用terraform管理存量的资源对象。

main.go

package main

import (
	"context"
	"flag"
	"log"

	"github.com/hashicorp/terraform-plugin-framework/providerserver"
	"github.com/serialt/terraform-provider-harbor/internal/provider"
)

//go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs

func main() {
	var debug bool

	flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve")
	flag.Parse()

	err := providerserver.Serve(context.Background(), provider.New, providerserver.ServeOpts{
		Address: "registry.terraform.io/serialt/harbor",
		Debug:   debug,
	})
	if err != nil {
		log.Fatal(err)
	}
}

provider.go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package provider

import (
	"context"
	"os"
	"strings"

	"github.com/goharbor/go-client/pkg/harbor"
	"github.com/hashicorp/terraform-plugin-framework/datasource"
	"github.com/hashicorp/terraform-plugin-framework/path"
	"github.com/hashicorp/terraform-plugin-framework/provider"
	"github.com/hashicorp/terraform-plugin-framework/provider/schema"
	"github.com/hashicorp/terraform-plugin-framework/resource"
	"github.com/hashicorp/terraform-plugin-framework/types"
	"github.com/hashicorp/terraform-plugin-log/tflog"
)

var _ provider.Provider = &HarborProvider{}

func New() provider.Provider {
	return &HarborProvider{}
}

type HarborProvider struct {
}

type HarborProviderModel struct {
	URL         types.String `tfsdk:"url"`
	Username    types.String `tfsdk:"username"`
	Password    types.String `tfsdk:"password"`
	BearerToken types.String `tfsdk:"bearer_token"`
	Insecure    types.Bool   `tfsdk:"insecure"`
}

func (p *HarborProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
	resp.TypeName = "harbor"
}

func (p *HarborProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
	resp.Schema = schema.Schema{
		Description: "Harbor config",
		Attributes: map[string]schema.Attribute{
			"url": schema.StringAttribute{
				Description: "Harbor url.",
				Optional:    true,
			},
			"username": schema.StringAttribute{
				Description: "Harbor username.",
				Optional:    true,
			},
			"password": schema.StringAttribute{
				Description: "Harbor.",
				Optional:    true,
				Sensitive:   true,
			},
			"bearer_token": schema.StringAttribute{
				Description: "Harbor.",
				Optional:    true,
			},
			"insecure": schema.BoolAttribute{
				Description: "Harbor.",
				Optional:    true,
			},
		},
	}
}

func (p *HarborProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
	tflog.Info(ctx, "Configuring harbor client")

	// Retrieve provider data from configuration
	var config HarborProviderModel
	diags := req.Config.Get(ctx, &config)
	resp.Diagnostics.Append(diags...)
	if resp.Diagnostics.HasError() {
		return
	}

	if config.URL.IsUnknown() {
		resp.Diagnostics.AddAttributeError(
			path.Root("url"),
			"Unknown url",
			"",
		)
	}
	if resp.Diagnostics.HasError() {
		return
	}
	url := os.Getenv("HARBOR_URL")
	username := os.Getenv("HARBOR_USERNAME")
	password := os.Getenv("HARBOR_PASSWORD")
	insecure := false
	if strings.HasSuffix(url, "/") {
		url = strings.Trim(url, "/")
	}
	if !config.URL.IsNull() {
		url = config.URL.ValueString()
	}
	if !config.Username.IsNull() {
		username = config.Username.ValueString()
	}
	if !config.Password.IsNull() {
		password = config.Password.ValueString()
	}
	if !config.Insecure.IsNull() {
		insecure = config.Insecure.ValueBool()
	}

	harborConfig := &harbor.ClientSetConfig{
		URL:      url,
		Insecure: insecure,
		Username: username,
		Password: password,
	}

	client, err := harbor.NewClientSet(harborConfig)

	if err != nil {
		resp.Diagnostics.AddError(
			"Unable to create harbor client",
			err.Error(),
		)
	}
	resp.DataSourceData = client
	resp.ResourceData = client
	tflog.Info(ctx, "Configured harbor client", map[string]any{"success": true})

}

func (p *HarborProvider) Resources(ctx context.Context) []func() resource.Resource {
	return []func() resource.Resource{
		NewEmailResource,
	}
}

func (p *HarborProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
	return []func() datasource.DataSource{
		NewProjectDataSource,
	}
}

data_project.go

package provider

import (
	"context"
	"fmt"
	"strconv"

	"github.com/goharbor/go-client/pkg/harbor"
	"github.com/goharbor/go-client/pkg/sdk/v2.0/client/project"
	"github.com/hashicorp/terraform-plugin-framework/datasource"
	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
	"github.com/hashicorp/terraform-plugin-framework/types"
)

// Ensure the implementation satisfies the expected interfaces.
var (
	_ datasource.DataSource              = &ProjectDataSource{}
	_ datasource.DataSourceWithConfigure = &ProjectDataSource{}
)

// NewProjectDataSource is a helper function to simplify the provider implementation.
func NewProjectDataSource() datasource.DataSource {
	return &ProjectDataSource{}
}

// ProjectDataSource is the data source implementation.
type ProjectDataSource struct {
	client *harbor.ClientSet
}

// ProjectDataSourceModel maps the data source schema data.
type ProjectDataSourceModel struct {
	ID                    types.Int64  `tfsdk:"id"`
	Name                  types.String `tfsdk:"name"`
	Type                  types.String `tfsdk:"type"`
	ProjectID             types.Int64  `tfsdk:"project_id"`
	Public                types.Bool   `tfsdk:"public"`
	VulnerabilityScanning types.Bool   `tfsdk:"vulnerability_scanning"`
}

// Metadata returns the data source type name.
func (d *ProjectDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
	resp.TypeName = req.ProviderTypeName + "_project"
}

// Schema defines the schema for the data source.
func (d *ProjectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
	resp.Schema = schema.Schema{

		Description: "Fetches the data of project.",
		Attributes: map[string]schema.Attribute{
			"id": schema.Int64Attribute{
				Description: "Project id.",
				Optional:    true,
			},
			"name": schema.StringAttribute{
				Description: "Project name.",
				Optional:    true,
			},
			"type": schema.StringAttribute{
				Description: "Project type.",
				Optional:    true,
			},
			"project_id": schema.Int64Attribute{
				Description: "Project id.",
				Optional:    true,
			},
			"public": schema.BoolAttribute{
				Description: "Project type.",
				Optional:    true,
			},
			"vulnerability_scanning": schema.BoolAttribute{
				Description: "Project vulnerability scanning.",
				Optional:    true,
			},
		},
	}
}

// Read refreshes the Terraform state with the latest data.
func (d *ProjectDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
	var state ProjectDataSourceModel

	resp.Diagnostics.Append(req.Config.Get(ctx, &state)...)
	if resp.Diagnostics.HasError() {
		return
	}

	var projectNameOrID string

	if !state.Name.IsNull() {
		projectNameOrID = state.Name.ValueString()
	} else if !state.ID.IsNull() {
		projectNameOrID = fmt.Sprint(state.ID.ValueInt64())
	} else if !state.ProjectID.IsNull() {
		projectNameOrID = fmt.Sprint(state.ProjectID.ValueInt64())
	} else {
		resp.Diagnostics.AddError(
			"Get project id or name failed from tf", "",
		)
		return
	}

	params := &project.GetProjectParams{
		ProjectNameOrID: projectNameOrID,
	}

	project, err := d.client.V2().Project.GetProject(ctx, params)
	if err != nil {
		resp.Diagnostics.AddError(
			"Get project msg from harbor failed",
			err.Error(),
		)
		return
	}
	public := getboolfromstring(project.Payload.Metadata.Public)
	var autoScan bool

	// if len(*project.Payload.Metadata.AutoScan) ==
	myAutoScan := project.Payload.Metadata.AutoScan
	if myAutoScan == nil {
		autoScan = false
	} else {
		autoScan = getboolfromstring(*project.Payload.Metadata.AutoScan)
	}

	var projectType string
	if project.Payload.RegistryID != 0 {
		projectType = "ProxyCache"

	} else {
		projectType = "Project"
	}

	newState := &ProjectDataSourceModel{
		ID:                    types.Int64Value(int64(project.Payload.ProjectID)),
		ProjectID:             types.Int64Value(int64(project.Payload.ProjectID)),
		Name:                  types.StringValue(project.Payload.Name),
		Public:                types.BoolValue(public),
		VulnerabilityScanning: types.BoolValue(autoScan),
		Type:                  types.StringValue(projectType),
	}
	// Set state
	diags := resp.State.Set(ctx, &newState)
	resp.Diagnostics.Append(diags...)
	if resp.Diagnostics.HasError() {
		return
	}
}

// Configure adds the provider configured client to the data source.
func (d *ProjectDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
	if req.ProviderData == nil {
		return
	}

	client, ok := req.ProviderData.(*harbor.ClientSet)
	if !ok {
		resp.Diagnostics.AddError(
			"Unexpected Data Source Configure Type",
			fmt.Sprintf("Expected Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
		)

		return
	}

	d.client = client
}

func getboolfromstring(stringbool string) bool {
	boolbool, err := strconv.ParseBool(stringbool)
	if err != nil {
		return false
	}
	return boolbool
}

resource/schema 介绍,以schema.StringAttribute为例

CustomType  允许使用自定义属性类型来代替
Required   resource config 必填项
Optional resource config 可选项
Computed  resource config 只读
Sensitive  resource  config 敏感数据不输出
Description
MarkdownDescription 
DeprecationMessage   resource  config 弃用说明
Validators
PlanModifiers
Default

三、Terraform apply说明

1、apply,如果state文件中没有对应的资源则会触发create,create后会写入state文件

2、删除资源会先触发read,yes后触发删除操作

3、resource中定义的有改动导致资源某些配置会被replace,先触发read,yes 后会触发更新

4、create、update、read 最后都会获取资源状态数据并更新到state文件中