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
}