feat: initial terraform provider for artifactapi v0.0.1

Resources:
- artifactapi_remote: CRUD for remote proxy repositories
- artifactapi_virtual: CRUD for virtual (merged) repositories

Data sources:
- data.artifactapi_remote: read remote config
- data.artifactapi_virtual: read virtual config

Supports all 10 package types (generic, docker, helm, pypi, npm,
rpm, alpine, puppet, terraform, goproxy), allowlist/blocklist,
tag banning, quarantine, and terraform import.
This commit is contained in:
2026-06-07 14:30:20 +10:00
commit ad50a06b33
13 changed files with 1204 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
terraform-provider-artifactapi
.terraform/
*.tfstate
*.tfstate.backup
+36
View File
@@ -0,0 +1,36 @@
.PHONY: build install test lint fmt clean tidy
BINARY := terraform-provider-artifactapi
VERSION ?= 0.0.1
OS_ARCH := linux_amd64
INSTALL_DIR := ~/.terraform.d/plugins/git.unkin.net/unkin/artifactapi/$(VERSION)/$(OS_ARCH)
GO_VERSION_REQUIRED := 1.23
GO_VERSION_ACTUAL := $(shell go version | sed 's/go version go\([0-9]*\.[0-9]*\).*/\1/')
check-go:
@if [ "$$(printf '%s\n%s' "$(GO_VERSION_REQUIRED)" "$(GO_VERSION_ACTUAL)" | sort -V | head -1)" != "$(GO_VERSION_REQUIRED)" ]; then \
echo "ERROR: Go >= $(GO_VERSION_REQUIRED) required, found $(GO_VERSION_ACTUAL)"; exit 1; \
fi
build: check-go tidy
go build -ldflags="-s -w -X main.version=$(VERSION)" -o $(BINARY)
install: build
mkdir -p $(INSTALL_DIR)
cp $(BINARY) $(INSTALL_DIR)/
test: check-go
go test -race -count=1 ./...
lint: check-go
go vet ./...
fmt: check-go
gofmt -w .
clean:
rm -f $(BINARY)
tidy:
go mod tidy
+112
View File
@@ -0,0 +1,112 @@
terraform {
required_providers {
artifactapi = {
source = "git.unkin.net/unkin/artifactapi"
}
}
}
provider "artifactapi" {
endpoint = "https://artifactapi.k8s.syd1.au.unkin.net"
}
resource "artifactapi_remote" "dockerhub" {
name = "dockerhub"
package_type = "docker"
base_url = "https://registry-1.docker.io"
description = "Docker Hub registry"
immutable_ttl = 0
mutable_ttl = 300
immutable_patterns = [
"^library/almalinux",
"^library/postgres",
"^library/redis",
]
}
resource "artifactapi_remote" "hashicorp_releases" {
name = "hashicorp-releases"
package_type = "generic"
base_url = "https://releases.hashicorp.com"
description = "HashiCorp product releases"
immutable_ttl = 0
mutable_ttl = 7200
immutable_patterns = [
"terraform/.*terraform_.*_linux_amd64\\.zip$",
"vault/.*vault_.*_linux_amd64\\.zip$",
]
}
resource "artifactapi_remote" "terraform_registry" {
name = "terraform-registry"
package_type = "terraform"
base_url = "https://registry.terraform.io"
description = "Terraform provider registry"
releases_remote = artifactapi_remote.hashicorp_releases.name
immutable_ttl = 0
mutable_ttl = 300
immutable_patterns = [
"[^/]+/[^/]+/[^/]+/download/[^/]+/[^/]+$",
]
}
resource "artifactapi_remote" "goproxy" {
name = "goproxy"
package_type = "goproxy"
base_url = "https://proxy.golang.org"
description = "Go module proxy"
immutable_ttl = 0
mutable_ttl = 300
}
resource "artifactapi_remote" "jetstack" {
name = "jetstack"
package_type = "helm"
base_url = "https://charts.jetstack.io"
description = "Jetstack Helm charts (cert-manager)"
immutable_ttl = 0
mutable_ttl = 3600
check_mutable = true
immutable_patterns = ["\\.tgz$"]
}
resource "artifactapi_remote" "hashicorp_helm" {
name = "hashicorp-helm"
package_type = "helm"
base_url = "https://helm.releases.hashicorp.com"
description = "HashiCorp Helm charts"
immutable_ttl = 0
mutable_ttl = 3600
check_mutable = true
immutable_patterns = ["\\.tgz$"]
}
resource "artifactapi_virtual" "helm" {
name = "helm"
package_type = "helm"
description = "All helm repos merged"
members = [
artifactapi_remote.jetstack.name,
artifactapi_remote.hashicorp_helm.name,
]
}
data "artifactapi_remote" "dockerhub" {
name = artifactapi_remote.dockerhub.name
}
output "dockerhub_base_url" {
value = data.artifactapi_remote.dockerhub.base_url
}
+30
View File
@@ -0,0 +1,30 @@
module git.unkin.net/unkin/terraform-provider-artifactapi
go 1.25.9
require github.com/hashicorp/terraform-plugin-framework v1.15.0
require (
github.com/fatih/color v1.13.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/hashicorp/go-hclog v1.5.0 // indirect
github.com/hashicorp/go-plugin v1.6.3 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/terraform-plugin-go v0.28.0 // indirect
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
github.com/hashicorp/terraform-registry-address v0.2.5 // indirect
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/oklog/run v1.0.0 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
google.golang.org/grpc v1.72.1 // indirect
google.golang.org/protobuf v1.36.6 // indirect
)
+91
View File
@@ -0,0 +1,91 @@
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg=
github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/terraform-plugin-framework v1.15.0 h1:LQ2rsOfmDLxcn5EeIwdXFtr03FVsNktbbBci8cOKdb4=
github.com/hashicorp/terraform-plugin-framework v1.15.0/go.mod h1:hxrNI/GY32KPISpWqlCoTLM9JZsGH3CyYlir09bD/fI=
github.com/hashicorp/terraform-plugin-go v0.28.0 h1:zJmu2UDwhVN0J+J20RE5huiF3XXlTYVIleaevHZgKPA=
github.com/hashicorp/terraform-plugin-go v0.28.0/go.mod h1:FDa2Bb3uumkTGSkTFpWSOwWJDwA7bf3vdP3ltLDTH6o=
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
github.com/hashicorp/terraform-registry-address v0.2.5 h1:2GTftHqmUhVOeuu9CW3kwDkRe4pcBDq0uuK5VJngU1M=
github.com/hashicorp/terraform-registry-address v0.2.5/go.mod h1:PpzXWINwB5kuVS5CA7m1+eO2f1jKb5ZDIxrOPfpnGkg=
github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+93
View File
@@ -0,0 +1,93 @@
package provider
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
)
type apiClient struct {
baseURL string
httpClient *http.Client
}
func newAPIClient(baseURL string) *apiClient {
return &apiClient{
baseURL: baseURL,
httpClient: &http.Client{},
}
}
func (c *apiClient) get(ctx context.Context, path string, out any) error {
return c.do(ctx, http.MethodGet, path, nil, out)
}
func (c *apiClient) post(ctx context.Context, path string, body, out any) error {
return c.do(ctx, http.MethodPost, path, body, out)
}
func (c *apiClient) put(ctx context.Context, path string, body, out any) error {
return c.do(ctx, http.MethodPut, path, body, out)
}
func (c *apiClient) del(ctx context.Context, path string) error {
return c.do(ctx, http.MethodDelete, path, nil, nil)
}
func (c *apiClient) do(ctx context.Context, method, path string, body, out any) error {
var bodyReader io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("marshal request: %w", err)
}
bodyReader = bytes.NewReader(b)
}
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bodyReader)
if err != nil {
return fmt.Errorf("create request: %w", err)
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("http request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return &notFoundError{path: path}
}
if resp.StatusCode >= 400 {
b, _ := io.ReadAll(resp.Body)
return fmt.Errorf("api error %d: %s", resp.StatusCode, string(b))
}
if out != nil && resp.StatusCode != http.StatusNoContent {
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
return fmt.Errorf("decode response: %w", err)
}
}
return nil
}
type notFoundError struct {
path string
}
func (e *notFoundError) Error() string {
return fmt.Sprintf("not found: %s", e.path)
}
func isNotFound(err error) bool {
_, ok := err.(*notFoundError)
return ok
}
+119
View File
@@ -0,0 +1,119 @@
package provider
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ datasource.DataSource = &remoteDataSource{}
type remoteDataSource struct {
client *apiClient
}
func NewRemoteDataSource() datasource.DataSource {
return &remoteDataSource{}
}
func (d *remoteDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_remote"
}
func (d *remoteDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Read an existing ArtifactAPI remote.",
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{Required: true, Description: "Remote name."},
"package_type": schema.StringAttribute{Computed: true},
"base_url": schema.StringAttribute{Computed: true},
"description": schema.StringAttribute{Computed: true},
"immutable_ttl": schema.Int64Attribute{Computed: true},
"mutable_ttl": schema.Int64Attribute{Computed: true},
"check_mutable": schema.BoolAttribute{Computed: true},
"immutable_patterns": schema.ListAttribute{Computed: true, ElementType: types.StringType},
"mutable_patterns": schema.ListAttribute{Computed: true, ElementType: types.StringType},
"allowlist": schema.ListAttribute{Computed: true, ElementType: types.StringType},
"blocklist": schema.ListAttribute{Computed: true, ElementType: types.StringType},
"ban_tags_enabled": schema.BoolAttribute{Computed: true},
"ban_tags": schema.ListAttribute{Computed: true, ElementType: types.StringType},
"quarantine_enabled": schema.BoolAttribute{Computed: true},
"quarantine_days": schema.Int64Attribute{Computed: true},
"stale_on_error": schema.BoolAttribute{Computed: true},
"releases_remote": schema.StringAttribute{Computed: true},
"managed_by": schema.StringAttribute{Computed: true},
},
}
}
type remoteDataSourceModel struct {
Name types.String `tfsdk:"name"`
PackageType types.String `tfsdk:"package_type"`
BaseURL types.String `tfsdk:"base_url"`
Description types.String `tfsdk:"description"`
ImmutableTTL types.Int64 `tfsdk:"immutable_ttl"`
MutableTTL types.Int64 `tfsdk:"mutable_ttl"`
CheckMutable types.Bool `tfsdk:"check_mutable"`
ImmutablePatterns types.List `tfsdk:"immutable_patterns"`
MutablePatterns types.List `tfsdk:"mutable_patterns"`
Allowlist types.List `tfsdk:"allowlist"`
Blocklist types.List `tfsdk:"blocklist"`
BanTagsEnabled types.Bool `tfsdk:"ban_tags_enabled"`
BanTags types.List `tfsdk:"ban_tags"`
QuarantineEnabled types.Bool `tfsdk:"quarantine_enabled"`
QuarantineDays types.Int64 `tfsdk:"quarantine_days"`
StaleOnError types.Bool `tfsdk:"stale_on_error"`
ReleasesRemote types.String `tfsdk:"releases_remote"`
ManagedBy types.String `tfsdk:"managed_by"`
}
func (d *remoteDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*apiClient)
if !ok {
resp.Diagnostics.AddError("unexpected provider data type", fmt.Sprintf("got %T", req.ProviderData))
return
}
d.client = client
}
func (d *remoteDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config remoteDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
var remote remoteAPI
if err := d.client.get(ctx, "/api/v2/remotes/"+config.Name.ValueString(), &remote); err != nil {
resp.Diagnostics.AddError("read remote failed", err.Error())
return
}
state := remoteDataSourceModel{
Name: types.StringValue(remote.Name),
PackageType: types.StringValue(remote.PackageType),
BaseURL: types.StringValue(remote.BaseURL),
Description: types.StringValue(remote.Description),
ImmutableTTL: types.Int64Value(remote.ImmutableTTL),
MutableTTL: types.Int64Value(remote.MutableTTL),
CheckMutable: types.BoolValue(remote.CheckMutable),
ImmutablePatterns: stringsToList(ctx, remote.ImmutablePatterns),
MutablePatterns: stringsToList(ctx, remote.MutablePatterns),
Allowlist: stringsToList(ctx, remote.Allowlist),
Blocklist: stringsToList(ctx, remote.Blocklist),
BanTagsEnabled: types.BoolValue(remote.BanTagsEnabled),
BanTags: stringsToList(ctx, remote.BanTags),
QuarantineEnabled: types.BoolValue(remote.QuarantineEnabled),
QuarantineDays: types.Int64Value(remote.QuarantineDays),
StaleOnError: types.BoolValue(remote.StaleOnError),
ReleasesRemote: types.StringValue(remote.ReleasesRemote),
ManagedBy: types.StringValue(remote.ManagedBy),
}
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}
+80
View File
@@ -0,0 +1,80 @@
package provider
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ datasource.DataSource = &virtualDataSource{}
type virtualDataSource struct {
client *apiClient
}
func NewVirtualDataSource() datasource.DataSource {
return &virtualDataSource{}
}
func (d *virtualDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_virtual"
}
func (d *virtualDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Read an existing ArtifactAPI virtual repository.",
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{Required: true, Description: "Virtual repository name."},
"package_type": schema.StringAttribute{Computed: true},
"description": schema.StringAttribute{Computed: true},
"members": schema.ListAttribute{Computed: true, ElementType: types.StringType},
"managed_by": schema.StringAttribute{Computed: true},
},
}
}
type virtualDataSourceModel struct {
Name types.String `tfsdk:"name"`
PackageType types.String `tfsdk:"package_type"`
Description types.String `tfsdk:"description"`
Members types.List `tfsdk:"members"`
ManagedBy types.String `tfsdk:"managed_by"`
}
func (d *virtualDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*apiClient)
if !ok {
resp.Diagnostics.AddError("unexpected provider data type", fmt.Sprintf("got %T", req.ProviderData))
return
}
d.client = client
}
func (d *virtualDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config virtualDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
var virt virtualAPI
if err := d.client.get(ctx, "/api/v2/virtuals/"+config.Name.ValueString(), &virt); err != nil {
resp.Diagnostics.AddError("read virtual failed", err.Error())
return
}
state := virtualDataSourceModel{
Name: types.StringValue(virt.Name),
PackageType: types.StringValue(virt.PackageType),
Description: types.StringValue(virt.Description),
Members: stringsToList(ctx, virt.Members),
ManagedBy: types.StringValue(virt.ManagedBy),
}
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}
+32
View File
@@ -0,0 +1,32 @@
package provider
type remoteAPI struct {
Name string `json:"name"`
PackageType string `json:"package_type"`
BaseURL string `json:"base_url"`
Description string `json:"description,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
ImmutableTTL int64 `json:"immutable_ttl"`
MutableTTL int64 `json:"mutable_ttl"`
CheckMutable bool `json:"check_mutable"`
ImmutablePatterns []string `json:"immutable_patterns"`
MutablePatterns []string `json:"mutable_patterns"`
Allowlist []string `json:"allowlist"`
Blocklist []string `json:"blocklist"`
BanTagsEnabled bool `json:"ban_tags_enabled"`
BanTags []string `json:"ban_tags"`
QuarantineEnabled bool `json:"quarantine_enabled"`
QuarantineDays int64 `json:"quarantine_days"`
StaleOnError bool `json:"stale_on_error"`
ReleasesRemote string `json:"releases_remote,omitempty"`
ManagedBy string `json:"managed_by,omitempty"`
}
type virtualAPI struct {
Name string `json:"name"`
PackageType string `json:"package_type"`
Description string `json:"description,omitempty"`
Members []string `json:"members"`
ManagedBy string `json:"managed_by,omitempty"`
}
+70
View File
@@ -0,0 +1,70 @@
package provider
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"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"
)
var _ provider.Provider = &ArtifactAPIProvider{}
type ArtifactAPIProvider struct {
version string
}
type artifactAPIProviderModel struct {
Endpoint types.String `tfsdk:"endpoint"`
}
func New(version string) func() provider.Provider {
return func() provider.Provider {
return &ArtifactAPIProvider{version: version}
}
}
func (p *ArtifactAPIProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "artifactapi"
resp.Version = p.version
}
func (p *ArtifactAPIProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Manage ArtifactAPI remotes and virtual repositories.",
Attributes: map[string]schema.Attribute{
"endpoint": schema.StringAttribute{
Description: "The ArtifactAPI server endpoint URL (e.g. https://artifactapi.example.com).",
Required: true,
},
},
}
}
func (p *ArtifactAPIProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config artifactAPIProviderModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
client := newAPIClient(config.Endpoint.ValueString())
resp.DataSourceData = client
resp.ResourceData = client
}
func (p *ArtifactAPIProvider) Resources(_ context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewRemoteResource,
NewVirtualResource,
}
}
func (p *ArtifactAPIProvider) DataSources(_ context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewRemoteDataSource,
NewVirtualDataSource,
}
}
+332
View File
@@ -0,0 +1,332 @@
package provider
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ resource.Resource = &remoteResource{}
_ resource.ResourceWithImportState = &remoteResource{}
)
type remoteResource struct {
client *apiClient
}
type remoteResourceModel struct {
Name types.String `tfsdk:"name"`
PackageType types.String `tfsdk:"package_type"`
BaseURL types.String `tfsdk:"base_url"`
Description types.String `tfsdk:"description"`
Username types.String `tfsdk:"username"`
Password types.String `tfsdk:"password"`
ImmutableTTL types.Int64 `tfsdk:"immutable_ttl"`
MutableTTL types.Int64 `tfsdk:"mutable_ttl"`
CheckMutable types.Bool `tfsdk:"check_mutable"`
ImmutablePatterns types.List `tfsdk:"immutable_patterns"`
MutablePatterns types.List `tfsdk:"mutable_patterns"`
Allowlist types.List `tfsdk:"allowlist"`
Blocklist types.List `tfsdk:"blocklist"`
BanTagsEnabled types.Bool `tfsdk:"ban_tags_enabled"`
BanTags types.List `tfsdk:"ban_tags"`
QuarantineEnabled types.Bool `tfsdk:"quarantine_enabled"`
QuarantineDays types.Int64 `tfsdk:"quarantine_days"`
StaleOnError types.Bool `tfsdk:"stale_on_error"`
ReleasesRemote types.String `tfsdk:"releases_remote"`
}
func NewRemoteResource() resource.Resource {
return &remoteResource{}
}
func (r *remoteResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_remote"
}
func (r *remoteResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Manages an ArtifactAPI remote proxy repository.",
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Description: "Unique name of the remote.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"package_type": schema.StringAttribute{
Description: "Package type: generic, docker, helm, pypi, npm, rpm, alpine, puppet, terraform, goproxy.",
Required: true,
},
"base_url": schema.StringAttribute{
Description: "Upstream repository base URL.",
Required: true,
},
"description": schema.StringAttribute{
Description: "Human-readable description.",
Optional: true,
Computed: true,
Default: stringdefault.StaticString(""),
},
"username": schema.StringAttribute{
Description: "Username for upstream authentication.",
Optional: true,
Computed: true,
Sensitive: true,
Default: stringdefault.StaticString(""),
},
"password": schema.StringAttribute{
Description: "Password for upstream authentication.",
Optional: true,
Computed: true,
Sensitive: true,
Default: stringdefault.StaticString(""),
},
"immutable_ttl": schema.Int64Attribute{
Description: "TTL in seconds for immutable artifacts (0 = forever).",
Optional: true,
Computed: true,
Default: int64default.StaticInt64(0),
},
"mutable_ttl": schema.Int64Attribute{
Description: "TTL in seconds for mutable artifacts.",
Optional: true,
Computed: true,
Default: int64default.StaticInt64(3600),
},
"check_mutable": schema.BoolAttribute{
Description: "Enable conditional revalidation (ETag/If-None-Match) for mutable artifacts.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
"immutable_patterns": schema.ListAttribute{
Description: "Regex patterns that identify immutable artifacts.",
Optional: true,
ElementType: types.StringType,
},
"mutable_patterns": schema.ListAttribute{
Description: "Additional regex patterns for mutable artifacts (merged with provider built-ins).",
Optional: true,
ElementType: types.StringType,
},
"allowlist": schema.ListAttribute{
Description: "If non-empty, only paths matching these patterns are proxied. Empty = allow all.",
Optional: true,
ElementType: types.StringType,
},
"blocklist": schema.ListAttribute{
Description: "Paths matching these patterns are always denied (checked before allowlist).",
Optional: true,
ElementType: types.StringType,
},
"ban_tags_enabled": schema.BoolAttribute{
Description: "Enable tag banning (Docker only).",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
},
"ban_tags": schema.ListAttribute{
Description: "Tags to ban (Docker only).",
Optional: true,
ElementType: types.StringType,
},
"quarantine_enabled": schema.BoolAttribute{
Description: "Enable quarantine for newly published artifacts.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
},
"quarantine_days": schema.Int64Attribute{
Description: "Number of days to quarantine new artifacts.",
Optional: true,
Computed: true,
Default: int64default.StaticInt64(3),
},
"stale_on_error": schema.BoolAttribute{
Description: "Serve stale cached content when upstream is unreachable.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
"releases_remote": schema.StringAttribute{
Description: "Name of the CDN remote for download URL rewriting (terraform package type).",
Optional: true,
Computed: true,
Default: stringdefault.StaticString(""),
},
},
}
}
func (r *remoteResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*apiClient)
if !ok {
resp.Diagnostics.AddError("unexpected provider data type", fmt.Sprintf("got %T", req.ProviderData))
return
}
r.client = client
}
func (r *remoteResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan remoteResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
api := modelToAPI(ctx, plan)
api.ManagedBy = "terraform"
var created remoteAPI
if err := r.client.post(ctx, "/api/v2/remotes", api, &created); err != nil {
resp.Diagnostics.AddError("create remote failed", err.Error())
return
}
state := apiToModel(ctx, created)
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}
func (r *remoteResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state remoteResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
var remote remoteAPI
err := r.client.get(ctx, "/api/v2/remotes/"+state.Name.ValueString(), &remote)
if err != nil {
if isNotFound(err) {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("read remote failed", err.Error())
return
}
newState := apiToModel(ctx, remote)
resp.Diagnostics.Append(resp.State.Set(ctx, newState)...)
}
func (r *remoteResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan remoteResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
api := modelToAPI(ctx, plan)
api.ManagedBy = "terraform"
var updated remoteAPI
if err := r.client.put(ctx, "/api/v2/remotes/"+plan.Name.ValueString(), api, &updated); err != nil {
resp.Diagnostics.AddError("update remote failed", err.Error())
return
}
state := apiToModel(ctx, updated)
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}
func (r *remoteResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state remoteResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
if err := r.client.del(ctx, "/api/v2/remotes/"+state.Name.ValueString()); err != nil {
resp.Diagnostics.AddError("delete remote failed", err.Error())
return
}
}
func (r *remoteResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp)
}
func modelToAPI(ctx context.Context, m remoteResourceModel) remoteAPI {
api := remoteAPI{
Name: m.Name.ValueString(),
PackageType: m.PackageType.ValueString(),
BaseURL: m.BaseURL.ValueString(),
Description: m.Description.ValueString(),
Username: m.Username.ValueString(),
Password: m.Password.ValueString(),
ImmutableTTL: m.ImmutableTTL.ValueInt64(),
MutableTTL: m.MutableTTL.ValueInt64(),
CheckMutable: m.CheckMutable.ValueBool(),
BanTagsEnabled: m.BanTagsEnabled.ValueBool(),
QuarantineEnabled: m.QuarantineEnabled.ValueBool(),
QuarantineDays: m.QuarantineDays.ValueInt64(),
StaleOnError: m.StaleOnError.ValueBool(),
ReleasesRemote: m.ReleasesRemote.ValueString(),
}
api.ImmutablePatterns = listToStrings(ctx, m.ImmutablePatterns)
api.MutablePatterns = listToStrings(ctx, m.MutablePatterns)
api.Allowlist = listToStrings(ctx, m.Allowlist)
api.Blocklist = listToStrings(ctx, m.Blocklist)
api.BanTags = listToStrings(ctx, m.BanTags)
return api
}
func apiToModel(ctx context.Context, api remoteAPI) remoteResourceModel {
return remoteResourceModel{
Name: types.StringValue(api.Name),
PackageType: types.StringValue(api.PackageType),
BaseURL: types.StringValue(api.BaseURL),
Description: types.StringValue(api.Description),
Username: types.StringValue(api.Username),
Password: types.StringValue(api.Password),
ImmutableTTL: types.Int64Value(api.ImmutableTTL),
MutableTTL: types.Int64Value(api.MutableTTL),
CheckMutable: types.BoolValue(api.CheckMutable),
ImmutablePatterns: stringsToList(ctx, api.ImmutablePatterns),
MutablePatterns: stringsToList(ctx, api.MutablePatterns),
Allowlist: stringsToList(ctx, api.Allowlist),
Blocklist: stringsToList(ctx, api.Blocklist),
BanTagsEnabled: types.BoolValue(api.BanTagsEnabled),
BanTags: stringsToList(ctx, api.BanTags),
QuarantineEnabled: types.BoolValue(api.QuarantineEnabled),
QuarantineDays: types.Int64Value(api.QuarantineDays),
StaleOnError: types.BoolValue(api.StaleOnError),
ReleasesRemote: types.StringValue(api.ReleasesRemote),
}
}
func listToStrings(ctx context.Context, l types.List) []string {
if l.IsNull() || l.IsUnknown() {
return nil
}
var result []string
l.ElementsAs(ctx, &result, false)
return result
}
func stringsToList(ctx context.Context, ss []string) types.List {
if ss == nil {
ss = []string{}
}
elems := make([]types.String, len(ss))
for i, s := range ss {
elems[i] = types.StringValue(s)
}
list, _ := types.ListValueFrom(ctx, types.StringType, elems)
return list
}
+177
View File
@@ -0,0 +1,177 @@
package provider
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ resource.Resource = &virtualResource{}
_ resource.ResourceWithImportState = &virtualResource{}
)
type virtualResource struct {
client *apiClient
}
type virtualResourceModel struct {
Name types.String `tfsdk:"name"`
PackageType types.String `tfsdk:"package_type"`
Description types.String `tfsdk:"description"`
Members types.List `tfsdk:"members"`
}
func NewVirtualResource() resource.Resource {
return &virtualResource{}
}
func (r *virtualResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_virtual"
}
func (r *virtualResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Manages an ArtifactAPI virtual repository that merges multiple remotes.",
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Description: "Unique name of the virtual repository.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"package_type": schema.StringAttribute{
Description: "Package type (must match member remotes): helm, pypi.",
Required: true,
},
"description": schema.StringAttribute{
Description: "Human-readable description.",
Optional: true,
Computed: true,
Default: stringdefault.StaticString(""),
},
"members": schema.ListAttribute{
Description: "Ordered list of member remote names. Earlier members have higher priority for duplicate entries.",
Required: true,
ElementType: types.StringType,
},
},
}
}
func (r *virtualResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*apiClient)
if !ok {
resp.Diagnostics.AddError("unexpected provider data type", fmt.Sprintf("got %T", req.ProviderData))
return
}
r.client = client
}
func (r *virtualResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan virtualResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
api := virtualModelToAPI(ctx, plan)
api.ManagedBy = "terraform"
var created virtualAPI
if err := r.client.post(ctx, "/api/v2/virtuals", api, &created); err != nil {
resp.Diagnostics.AddError("create virtual failed", err.Error())
return
}
state := virtualAPIToModel(ctx, created)
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}
func (r *virtualResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state virtualResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
var virt virtualAPI
err := r.client.get(ctx, "/api/v2/virtuals/"+state.Name.ValueString(), &virt)
if err != nil {
if isNotFound(err) {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("read virtual failed", err.Error())
return
}
newState := virtualAPIToModel(ctx, virt)
resp.Diagnostics.Append(resp.State.Set(ctx, newState)...)
}
func (r *virtualResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan virtualResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
api := virtualModelToAPI(ctx, plan)
api.ManagedBy = "terraform"
var updated virtualAPI
if err := r.client.put(ctx, "/api/v2/virtuals/"+plan.Name.ValueString(), api, &updated); err != nil {
resp.Diagnostics.AddError("update virtual failed", err.Error())
return
}
state := virtualAPIToModel(ctx, updated)
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}
func (r *virtualResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state virtualResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
if err := r.client.del(ctx, "/api/v2/virtuals/"+state.Name.ValueString()); err != nil {
resp.Diagnostics.AddError("delete virtual failed", err.Error())
return
}
}
func (r *virtualResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp)
}
func virtualModelToAPI(ctx context.Context, m virtualResourceModel) virtualAPI {
return virtualAPI{
Name: m.Name.ValueString(),
PackageType: m.PackageType.ValueString(),
Description: m.Description.ValueString(),
Members: listToStrings(ctx, m.Members),
}
}
func virtualAPIToModel(ctx context.Context, api virtualAPI) virtualResourceModel {
return virtualResourceModel{
Name: types.StringValue(api.Name),
PackageType: types.StringValue(api.PackageType),
Description: types.StringValue(api.Description),
Members: stringsToList(ctx, api.Members),
}
}
+28
View File
@@ -0,0 +1,28 @@
package main
import (
"context"
"flag"
"log"
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"git.unkin.net/unkin/terraform-provider-artifactapi/internal/provider"
)
var version = "0.0.1"
func main() {
var debug bool
flag.BoolVar(&debug, "debug", false, "enable debug mode")
flag.Parse()
opts := providerserver.ServeOpts{
Address: "git.unkin.net/unkin/artifactapi",
Debug: debug,
}
if err := providerserver.Serve(context.Background(), provider.New(version), opts); err != nil {
log.Fatal(err)
}
}