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:
@@ -0,0 +1,4 @@
|
||||
terraform-provider-artifactapi
|
||||
.terraform/
|
||||
*.tfstate
|
||||
*.tfstate.backup
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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 ¬FoundError{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
|
||||
}
|
||||
@@ -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)...)
|
||||
}
|
||||
@@ -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)...)
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user