docs: add README, per-resource examples, unit tests, CI, and pre-commit
- Add README.md with provider docs, resource/data-source reference, and development instructions - Reorganize examples into per-resource-type subdirectories following Terraform provider conventions, add missing pypi/npm/puppet examples - Add unit tests for helpers, HTTP client, model conversions, and provider registration - Add Woodpecker CI pipelines for lint, test, and build - Add pre-commit config with standard and Go-specific hooks
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v5.0.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: check-yaml
|
||||||
|
- id: check-added-large-files
|
||||||
|
|
||||||
|
- repo: https://github.com/dnephin/pre-commit-golang
|
||||||
|
rev: v0.5.1
|
||||||
|
hooks:
|
||||||
|
- id: go-fmt
|
||||||
|
- id: go-vet
|
||||||
|
- id: go-mod-tidy
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
when:
|
||||||
|
- event: [push, pull_request]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build
|
||||||
|
image: golang:1.25
|
||||||
|
commands:
|
||||||
|
- make build
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
when:
|
||||||
|
- event: [push, pull_request]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: lint
|
||||||
|
image: golang:1.25
|
||||||
|
commands:
|
||||||
|
- make lint
|
||||||
|
|
||||||
|
- name: test
|
||||||
|
image: golang:1.25
|
||||||
|
commands:
|
||||||
|
- make test
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
# terraform-provider-artifactapi
|
||||||
|
|
||||||
|
Terraform provider for managing [ArtifactAPI](https://git.unkin.net/unkin/artifactapi) remotes and virtual repositories.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Go >= 1.23
|
||||||
|
- Terraform >= 1.0
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Install the provider to your local Terraform plugin directory:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make install
|
||||||
|
```
|
||||||
|
|
||||||
|
This places the binary at `~/.terraform.d/plugins/git.unkin.net/unkin/artifactapi/<version>/<os_arch>/`.
|
||||||
|
|
||||||
|
## Provider Configuration
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
artifactapi = {
|
||||||
|
source = "git.unkin.net/unkin/artifactapi"
|
||||||
|
version = "0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "artifactapi" {
|
||||||
|
endpoint = "https://artifactapi.example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Attribute | Required | Description |
|
||||||
|
|------------|----------|--------------------------------------|
|
||||||
|
| `endpoint` | Yes | ArtifactAPI server endpoint URL |
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
### Remote Resources
|
||||||
|
|
||||||
|
Per-type remote resources manage upstream repository proxies. Each type applies its own mutability classification rules automatically (e.g., Docker classifies tag manifests as mutable and blobs as immutable; Helm classifies `index.yaml` as mutable).
|
||||||
|
|
||||||
|
Available resource types:
|
||||||
|
|
||||||
|
- `artifactapi_remote_generic`
|
||||||
|
- `artifactapi_remote_docker`
|
||||||
|
- `artifactapi_remote_helm`
|
||||||
|
- `artifactapi_remote_pypi`
|
||||||
|
- `artifactapi_remote_npm`
|
||||||
|
- `artifactapi_remote_rpm`
|
||||||
|
- `artifactapi_remote_alpine`
|
||||||
|
- `artifactapi_remote_puppet`
|
||||||
|
- `artifactapi_remote_terraform`
|
||||||
|
- `artifactapi_remote_goproxy`
|
||||||
|
|
||||||
|
#### Common Attributes
|
||||||
|
|
||||||
|
| Attribute | Required | Default | Description |
|
||||||
|
|----------------------|----------|---------|-------------------------------------------------------------------|
|
||||||
|
| `name` | Yes | | Unique name (forces replacement on change) |
|
||||||
|
| `base_url` | Yes | | Upstream repository URL |
|
||||||
|
| `description` | No | `""` | Human-readable description |
|
||||||
|
| `username` | No | `""` | Upstream auth username (sensitive) |
|
||||||
|
| `password` | No | `""` | Upstream auth password (sensitive) |
|
||||||
|
| `immutable_ttl` | No | `0` | TTL in seconds for immutable artifacts (0 = cache forever) |
|
||||||
|
| `mutable_ttl` | No | `3600` | TTL in seconds for mutable artifacts |
|
||||||
|
| `check_mutable` | No | `true` | Enable conditional revalidation for mutable artifacts |
|
||||||
|
| `patterns` | No | | Allowlist of path patterns to proxy (empty = all) |
|
||||||
|
| `blocklist` | No | | Paths to always deny (checked before patterns) |
|
||||||
|
| `mutable_patterns` | No | | Override: treat matching paths as mutable |
|
||||||
|
| `immutable_patterns` | No | | Override: treat matching paths as immutable |
|
||||||
|
| `quarantine_enabled` | No | `false` | Enable quarantine for new artifacts |
|
||||||
|
| `quarantine_days` | No | `3` | Days to quarantine new artifacts |
|
||||||
|
| `stale_on_error` | No | `true` | Serve stale cache when upstream is unreachable |
|
||||||
|
|
||||||
|
#### Docker-specific Attributes
|
||||||
|
|
||||||
|
| Attribute | Default | Description |
|
||||||
|
|--------------------|---------|----------------------------|
|
||||||
|
| `ban_tags_enabled` | `false` | Enable tag banning |
|
||||||
|
| `ban_tags` | | List of tags to ban |
|
||||||
|
|
||||||
|
#### Terraform-specific Attributes
|
||||||
|
|
||||||
|
| Attribute | Default | Description |
|
||||||
|
|-------------------|---------|----------------------------------------------------------|
|
||||||
|
| `releases_remote` | `""` | Name of a generic remote for download URL rewriting |
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
resource "artifactapi_remote_docker" "dockerhub" {
|
||||||
|
name = "dockerhub"
|
||||||
|
base_url = "https://registry-1.docker.io"
|
||||||
|
|
||||||
|
immutable_ttl = 0
|
||||||
|
mutable_ttl = 300
|
||||||
|
ban_tags_enabled = true
|
||||||
|
ban_tags = ["latest"]
|
||||||
|
|
||||||
|
patterns = [
|
||||||
|
"^library/postgres",
|
||||||
|
"^library/redis",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Virtual Resources
|
||||||
|
|
||||||
|
Virtual repositories merge multiple remotes of the same package type into a single endpoint.
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
resource "artifactapi_virtual" "helm" {
|
||||||
|
name = "helm"
|
||||||
|
package_type = "helm"
|
||||||
|
description = "All helm repos merged"
|
||||||
|
|
||||||
|
members = [
|
||||||
|
artifactapi_remote_helm.jetstack.name,
|
||||||
|
artifactapi_remote_helm.hashicorp_helm.name,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Attribute | Required | Description |
|
||||||
|
|----------------|----------|-------------------------------------------|
|
||||||
|
| `name` | Yes | Unique name (forces replacement on change)|
|
||||||
|
| `package_type` | Yes | Package type of member remotes |
|
||||||
|
| `description` | No | Human-readable description |
|
||||||
|
| `members` | Yes | List of remote names to include |
|
||||||
|
|
||||||
|
## Data Sources
|
||||||
|
|
||||||
|
### `artifactapi_remote`
|
||||||
|
|
||||||
|
Read an existing remote's configuration.
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
data "artifactapi_remote" "dockerhub" {
|
||||||
|
name = "dockerhub"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `artifactapi_virtual`
|
||||||
|
|
||||||
|
Read an existing virtual repository's configuration.
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
data "artifactapi_virtual" "helm" {
|
||||||
|
name = "helm"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Import
|
||||||
|
|
||||||
|
Resources can be imported by name:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
terraform import artifactapi_remote_docker.dockerhub dockerhub
|
||||||
|
terraform import artifactapi_virtual.helm helm
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make build # Build the provider binary
|
||||||
|
make install # Install to local plugin directory
|
||||||
|
make test # Run tests
|
||||||
|
make lint # Run go vet
|
||||||
|
make fmt # Format code
|
||||||
|
make clean # Remove binary
|
||||||
|
```
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
artifactapi = {
|
||||||
|
source = "git.unkin.net/unkin/artifactapi"
|
||||||
|
version = "0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "artifactapi" {
|
||||||
|
endpoint = "https://artifactapi.example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read the configuration of an existing remote by name.
|
||||||
|
# All fields (base_url, package_type, TTLs, patterns, etc.) are available
|
||||||
|
# as computed attributes.
|
||||||
|
data "artifactapi_remote" "dockerhub" {
|
||||||
|
name = "dockerhub"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "dockerhub_base_url" {
|
||||||
|
value = data.artifactapi_remote.dockerhub.base_url
|
||||||
|
}
|
||||||
|
|
||||||
|
output "dockerhub_package_type" {
|
||||||
|
value = data.artifactapi_remote.dockerhub.package_type
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
artifactapi = {
|
||||||
|
source = "git.unkin.net/unkin/artifactapi"
|
||||||
|
version = "0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "artifactapi" {
|
||||||
|
endpoint = "https://artifactapi.example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read the configuration of an existing virtual repository by name.
|
||||||
|
# Returns the package type, description, and ordered member list.
|
||||||
|
data "artifactapi_virtual" "helm" {
|
||||||
|
name = "helm"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "helm_members" {
|
||||||
|
value = data.artifactapi_virtual.helm.members
|
||||||
|
}
|
||||||
|
|
||||||
|
output "helm_package_type" {
|
||||||
|
value = data.artifactapi_virtual.helm.package_type
|
||||||
|
}
|
||||||
+87
-31
@@ -11,10 +11,13 @@ provider "artifactapi" {
|
|||||||
endpoint = "https://artifactapi.k8s.syd1.au.unkin.net"
|
endpoint = "https://artifactapi.k8s.syd1.au.unkin.net"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Generic — patterns act as allowlist, everything matching is immutable by default
|
# ---------------------------------------------------------------------------
|
||||||
|
# Generic remotes
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
resource "artifactapi_remote_generic" "github" {
|
resource "artifactapi_remote_generic" "github" {
|
||||||
name = "github"
|
name = "github"
|
||||||
base_url = "https://github.com"
|
base_url = "https://github.com"
|
||||||
description = "GitHub releases"
|
description = "GitHub releases"
|
||||||
|
|
||||||
immutable_ttl = 0
|
immutable_ttl = 0
|
||||||
@@ -26,15 +29,14 @@ resource "artifactapi_remote_generic" "github" {
|
|||||||
"neovim/neovim-releases/.*/nvim-linux-x86_64.tar.gz$",
|
"neovim/neovim-releases/.*/nvim-linux-x86_64.tar.gz$",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Override: branch archives are mutable
|
|
||||||
mutable_patterns = [
|
mutable_patterns = [
|
||||||
".*/archive/refs/heads/.*\\.tar\\.gz$",
|
".*/archive/refs/heads/.*\\.tar\\.gz$",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "artifactapi_remote_generic" "hashicorp_releases" {
|
resource "artifactapi_remote_generic" "hashicorp_releases" {
|
||||||
name = "hashicorp-releases"
|
name = "hashicorp-releases"
|
||||||
base_url = "https://releases.hashicorp.com"
|
base_url = "https://releases.hashicorp.com"
|
||||||
description = "HashiCorp product releases"
|
description = "HashiCorp product releases"
|
||||||
|
|
||||||
immutable_ttl = 0
|
immutable_ttl = 0
|
||||||
@@ -46,11 +48,13 @@ resource "artifactapi_remote_generic" "hashicorp_releases" {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Docker — patterns restrict which images are proxied
|
# ---------------------------------------------------------------------------
|
||||||
# Provider auto-classifies: tag manifests mutable, blobs immutable
|
# Docker
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
resource "artifactapi_remote_docker" "dockerhub" {
|
resource "artifactapi_remote_docker" "dockerhub" {
|
||||||
name = "dockerhub"
|
name = "dockerhub"
|
||||||
base_url = "https://registry-1.docker.io"
|
base_url = "https://registry-1.docker.io"
|
||||||
description = "Docker Hub registry"
|
description = "Docker Hub registry"
|
||||||
|
|
||||||
immutable_ttl = 0
|
immutable_ttl = 0
|
||||||
@@ -66,10 +70,13 @@ resource "artifactapi_remote_docker" "dockerhub" {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Helm — no patterns needed, provider knows index.yaml is mutable
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helm
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
resource "artifactapi_remote_helm" "jetstack" {
|
resource "artifactapi_remote_helm" "jetstack" {
|
||||||
name = "jetstack"
|
name = "jetstack"
|
||||||
base_url = "https://charts.jetstack.io"
|
base_url = "https://charts.jetstack.io"
|
||||||
description = "Jetstack Helm charts (cert-manager)"
|
description = "Jetstack Helm charts (cert-manager)"
|
||||||
|
|
||||||
immutable_ttl = 0
|
immutable_ttl = 0
|
||||||
@@ -77,25 +84,62 @@ resource "artifactapi_remote_helm" "jetstack" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resource "artifactapi_remote_helm" "hashicorp_helm" {
|
resource "artifactapi_remote_helm" "hashicorp_helm" {
|
||||||
name = "hashicorp-helm"
|
name = "hashicorp-helm"
|
||||||
base_url = "https://helm.releases.hashicorp.com"
|
base_url = "https://helm.releases.hashicorp.com"
|
||||||
description = "HashiCorp Helm charts"
|
description = "HashiCorp Helm charts"
|
||||||
|
|
||||||
immutable_ttl = 0
|
immutable_ttl = 0
|
||||||
mutable_ttl = 3600
|
mutable_ttl = 3600
|
||||||
}
|
}
|
||||||
|
|
||||||
# RPM — no patterns needed, provider knows repodata/* is mutable
|
# ---------------------------------------------------------------------------
|
||||||
|
# Language package managers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
resource "artifactapi_remote_pypi" "pypi" {
|
||||||
|
name = "pypi"
|
||||||
|
base_url = "https://pypi.org"
|
||||||
|
description = "Python Package Index"
|
||||||
|
|
||||||
|
immutable_ttl = 0
|
||||||
|
mutable_ttl = 900
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "artifactapi_remote_npm" "npmjs" {
|
||||||
|
name = "npmjs"
|
||||||
|
base_url = "https://registry.npmjs.org"
|
||||||
|
description = "npm public registry"
|
||||||
|
|
||||||
|
immutable_ttl = 0
|
||||||
|
mutable_ttl = 900
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# OS package managers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
resource "artifactapi_remote_rpm" "almalinux" {
|
resource "artifactapi_remote_rpm" "almalinux" {
|
||||||
name = "almalinux"
|
name = "almalinux"
|
||||||
base_url = "https://gsl-syd.mm.fcix.net/almalinux"
|
base_url = "https://gsl-syd.mm.fcix.net/almalinux"
|
||||||
description = "AlmaLinux RPM package repository"
|
description = "AlmaLinux RPM package repository"
|
||||||
|
|
||||||
immutable_ttl = 0
|
immutable_ttl = 0
|
||||||
mutable_ttl = 7200
|
mutable_ttl = 7200
|
||||||
}
|
}
|
||||||
|
|
||||||
# Terraform registry — needs releases_remote for URL rewriting
|
resource "artifactapi_remote_alpine" "alpine" {
|
||||||
|
name = "alpine"
|
||||||
|
base_url = "https://dl-cdn.alpinelinux.org"
|
||||||
|
description = "Alpine Linux APK package repository"
|
||||||
|
|
||||||
|
immutable_ttl = 0
|
||||||
|
mutable_ttl = 7200
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Infrastructure tooling
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
resource "artifactapi_remote_terraform" "terraform_registry" {
|
resource "artifactapi_remote_terraform" "terraform_registry" {
|
||||||
name = "terraform-registry"
|
name = "terraform-registry"
|
||||||
base_url = "https://registry.terraform.io"
|
base_url = "https://registry.terraform.io"
|
||||||
@@ -106,31 +150,32 @@ resource "artifactapi_remote_terraform" "terraform_registry" {
|
|||||||
mutable_ttl = 300
|
mutable_ttl = 300
|
||||||
}
|
}
|
||||||
|
|
||||||
# Go module proxy — provider knows @v/list is mutable, .zip/.mod/.info immutable
|
|
||||||
resource "artifactapi_remote_goproxy" "goproxy" {
|
resource "artifactapi_remote_goproxy" "goproxy" {
|
||||||
name = "goproxy"
|
name = "goproxy"
|
||||||
base_url = "https://proxy.golang.org"
|
base_url = "https://proxy.golang.org"
|
||||||
description = "Go module proxy"
|
description = "Go module proxy"
|
||||||
|
|
||||||
immutable_ttl = 0
|
immutable_ttl = 0
|
||||||
mutable_ttl = 300
|
mutable_ttl = 300
|
||||||
}
|
}
|
||||||
|
|
||||||
# Alpine — provider knows APKINDEX.tar.gz is mutable
|
resource "artifactapi_remote_puppet" "puppet_forge" {
|
||||||
resource "artifactapi_remote_alpine" "alpine" {
|
name = "puppet-forge"
|
||||||
name = "alpine"
|
base_url = "https://forgeapi.puppet.com"
|
||||||
base_url = "https://dl-cdn.alpinelinux.org"
|
description = "Puppet Forge module repository"
|
||||||
description = "Alpine Linux APK package repository"
|
|
||||||
|
|
||||||
immutable_ttl = 0
|
immutable_ttl = 0
|
||||||
mutable_ttl = 7200
|
mutable_ttl = 3600
|
||||||
}
|
}
|
||||||
|
|
||||||
# Virtual — merges multiple helm repos into one index
|
# ---------------------------------------------------------------------------
|
||||||
|
# Virtual repositories
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
resource "artifactapi_virtual" "helm" {
|
resource "artifactapi_virtual" "helm" {
|
||||||
name = "helm"
|
name = "helm"
|
||||||
package_type = "helm"
|
package_type = "helm"
|
||||||
description = "All helm repos merged"
|
description = "All Helm repos merged"
|
||||||
|
|
||||||
members = [
|
members = [
|
||||||
artifactapi_remote_helm.jetstack.name,
|
artifactapi_remote_helm.jetstack.name,
|
||||||
@@ -138,11 +183,22 @@ resource "artifactapi_virtual" "helm" {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Data source — read a remote's config
|
# ---------------------------------------------------------------------------
|
||||||
|
# Data sources
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
data "artifactapi_remote" "dockerhub" {
|
data "artifactapi_remote" "dockerhub" {
|
||||||
name = artifactapi_remote_docker.dockerhub.name
|
name = artifactapi_remote_docker.dockerhub.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data "artifactapi_virtual" "helm" {
|
||||||
|
name = artifactapi_virtual.helm.name
|
||||||
|
}
|
||||||
|
|
||||||
output "dockerhub_base_url" {
|
output "dockerhub_base_url" {
|
||||||
value = data.artifactapi_remote.dockerhub.base_url
|
value = data.artifactapi_remote.dockerhub.base_url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output "helm_members" {
|
||||||
|
value = data.artifactapi_virtual.helm.members
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
artifactapi = {
|
||||||
|
source = "git.unkin.net/unkin/artifactapi"
|
||||||
|
version = "0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "artifactapi" {
|
||||||
|
endpoint = "https://artifactapi.example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Alpine remote proxies an Alpine Linux APK repository.
|
||||||
|
# The provider knows APKINDEX.tar.gz is mutable; .apk packages are immutable.
|
||||||
|
resource "artifactapi_remote_alpine" "alpine" {
|
||||||
|
name = "alpine"
|
||||||
|
base_url = "https://dl-cdn.alpinelinux.org"
|
||||||
|
description = "Alpine Linux APK package repository"
|
||||||
|
|
||||||
|
immutable_ttl = 0
|
||||||
|
mutable_ttl = 7200
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
artifactapi = {
|
||||||
|
source = "git.unkin.net/unkin/artifactapi"
|
||||||
|
version = "0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "artifactapi" {
|
||||||
|
endpoint = "https://artifactapi.example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Docker remote proxies a container registry.
|
||||||
|
# The provider auto-classifies: tag manifests are mutable, blobs are immutable.
|
||||||
|
resource "artifactapi_remote_docker" "dockerhub" {
|
||||||
|
name = "dockerhub"
|
||||||
|
base_url = "https://registry-1.docker.io"
|
||||||
|
description = "Docker Hub registry"
|
||||||
|
|
||||||
|
immutable_ttl = 0
|
||||||
|
mutable_ttl = 300
|
||||||
|
ban_tags_enabled = true
|
||||||
|
ban_tags = ["latest"]
|
||||||
|
|
||||||
|
patterns = [
|
||||||
|
"^library/almalinux",
|
||||||
|
"^library/postgres",
|
||||||
|
"^library/redis",
|
||||||
|
"^bitnami/",
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
artifactapi = {
|
||||||
|
source = "git.unkin.net/unkin/artifactapi"
|
||||||
|
version = "0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "artifactapi" {
|
||||||
|
endpoint = "https://artifactapi.example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
# A generic remote proxies arbitrary HTTP endpoints.
|
||||||
|
# Patterns act as an allowlist; everything matching is immutable by default.
|
||||||
|
resource "artifactapi_remote_generic" "github" {
|
||||||
|
name = "github"
|
||||||
|
base_url = "https://github.com"
|
||||||
|
description = "GitHub releases"
|
||||||
|
|
||||||
|
immutable_ttl = 0
|
||||||
|
mutable_ttl = 7200
|
||||||
|
|
||||||
|
patterns = [
|
||||||
|
"ducaale/xh/.*/xh-.*-x86_64-unknown-linux-musl.tar.gz$",
|
||||||
|
"mikefarah/yq/.*/yq_linux_amd64$",
|
||||||
|
"neovim/neovim-releases/.*/nvim-linux-x86_64.tar.gz$",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Override: branch archives are mutable
|
||||||
|
mutable_patterns = [
|
||||||
|
".*/archive/refs/heads/.*\\.tar\\.gz$",
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
artifactapi = {
|
||||||
|
source = "git.unkin.net/unkin/artifactapi"
|
||||||
|
version = "0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "artifactapi" {
|
||||||
|
endpoint = "https://artifactapi.example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Go module proxy remote proxies a Go module proxy.
|
||||||
|
# The provider knows @v/list is mutable; .zip, .mod, and .info files are immutable.
|
||||||
|
resource "artifactapi_remote_goproxy" "goproxy" {
|
||||||
|
name = "goproxy"
|
||||||
|
base_url = "https://proxy.golang.org"
|
||||||
|
description = "Go module proxy"
|
||||||
|
|
||||||
|
immutable_ttl = 0
|
||||||
|
mutable_ttl = 300
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
artifactapi = {
|
||||||
|
source = "git.unkin.net/unkin/artifactapi"
|
||||||
|
version = "0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "artifactapi" {
|
||||||
|
endpoint = "https://artifactapi.example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helm remote proxies a Helm chart repository.
|
||||||
|
# The provider knows index.yaml is mutable; chart tarballs are immutable.
|
||||||
|
resource "artifactapi_remote_helm" "jetstack" {
|
||||||
|
name = "jetstack"
|
||||||
|
base_url = "https://charts.jetstack.io"
|
||||||
|
description = "Jetstack Helm charts (cert-manager)"
|
||||||
|
|
||||||
|
immutable_ttl = 0
|
||||||
|
mutable_ttl = 3600
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
artifactapi = {
|
||||||
|
source = "git.unkin.net/unkin/artifactapi"
|
||||||
|
version = "0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "artifactapi" {
|
||||||
|
endpoint = "https://artifactapi.example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
# NPM remote proxies an npm registry.
|
||||||
|
# The provider knows package metadata is mutable; tarballs are immutable.
|
||||||
|
resource "artifactapi_remote_npm" "npmjs" {
|
||||||
|
name = "npmjs"
|
||||||
|
base_url = "https://registry.npmjs.org"
|
||||||
|
description = "npm public registry"
|
||||||
|
|
||||||
|
immutable_ttl = 0
|
||||||
|
mutable_ttl = 900
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
artifactapi = {
|
||||||
|
source = "git.unkin.net/unkin/artifactapi"
|
||||||
|
version = "0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "artifactapi" {
|
||||||
|
endpoint = "https://artifactapi.example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Puppet remote proxies a Puppet Forge repository.
|
||||||
|
# The provider knows module metadata is mutable; release tarballs are immutable.
|
||||||
|
resource "artifactapi_remote_puppet" "puppet_forge" {
|
||||||
|
name = "puppet-forge"
|
||||||
|
base_url = "https://forgeapi.puppet.com"
|
||||||
|
description = "Puppet Forge module repository"
|
||||||
|
|
||||||
|
immutable_ttl = 0
|
||||||
|
mutable_ttl = 3600
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
artifactapi = {
|
||||||
|
source = "git.unkin.net/unkin/artifactapi"
|
||||||
|
version = "0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "artifactapi" {
|
||||||
|
endpoint = "https://artifactapi.example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
# PyPI remote proxies a Python package index.
|
||||||
|
# The provider knows /simple/ index pages are mutable; sdists and wheels are immutable.
|
||||||
|
resource "artifactapi_remote_pypi" "pypi" {
|
||||||
|
name = "pypi"
|
||||||
|
base_url = "https://pypi.org"
|
||||||
|
description = "Python Package Index"
|
||||||
|
|
||||||
|
immutable_ttl = 0
|
||||||
|
mutable_ttl = 900
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
artifactapi = {
|
||||||
|
source = "git.unkin.net/unkin/artifactapi"
|
||||||
|
version = "0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "artifactapi" {
|
||||||
|
endpoint = "https://artifactapi.example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
# RPM remote proxies an RPM package repository.
|
||||||
|
# The provider knows repodata/* is mutable; RPM packages are immutable.
|
||||||
|
resource "artifactapi_remote_rpm" "almalinux" {
|
||||||
|
name = "almalinux"
|
||||||
|
base_url = "https://gsl-syd.mm.fcix.net/almalinux"
|
||||||
|
description = "AlmaLinux RPM package repository"
|
||||||
|
|
||||||
|
immutable_ttl = 0
|
||||||
|
mutable_ttl = 7200
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
artifactapi = {
|
||||||
|
source = "git.unkin.net/unkin/artifactapi"
|
||||||
|
version = "0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "artifactapi" {
|
||||||
|
endpoint = "https://artifactapi.example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
# A generic remote is needed for the releases CDN that the Terraform registry
|
||||||
|
# redirects provider downloads to.
|
||||||
|
resource "artifactapi_remote_generic" "hashicorp_releases" {
|
||||||
|
name = "hashicorp-releases"
|
||||||
|
base_url = "https://releases.hashicorp.com"
|
||||||
|
description = "HashiCorp product releases"
|
||||||
|
|
||||||
|
immutable_ttl = 0
|
||||||
|
mutable_ttl = 7200
|
||||||
|
|
||||||
|
patterns = [
|
||||||
|
"terraform/.*terraform_.*_linux_amd64\\.zip$",
|
||||||
|
"vault/.*vault_.*_linux_amd64\\.zip$",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Terraform registry remote proxies the Terraform provider registry.
|
||||||
|
# The releases_remote attribute points to a generic remote that serves
|
||||||
|
# the actual provider/module archives after URL rewriting.
|
||||||
|
resource "artifactapi_remote_terraform" "terraform_registry" {
|
||||||
|
name = "terraform-registry"
|
||||||
|
base_url = "https://registry.terraform.io"
|
||||||
|
description = "Terraform provider registry"
|
||||||
|
releases_remote = artifactapi_remote_generic.hashicorp_releases.name
|
||||||
|
|
||||||
|
immutable_ttl = 0
|
||||||
|
mutable_ttl = 300
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
artifactapi = {
|
||||||
|
source = "git.unkin.net/unkin/artifactapi"
|
||||||
|
version = "0.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "artifactapi" {
|
||||||
|
endpoint = "https://artifactapi.example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helm remotes to be merged into a virtual repository
|
||||||
|
resource "artifactapi_remote_helm" "jetstack" {
|
||||||
|
name = "jetstack"
|
||||||
|
base_url = "https://charts.jetstack.io"
|
||||||
|
description = "Jetstack Helm charts (cert-manager)"
|
||||||
|
|
||||||
|
immutable_ttl = 0
|
||||||
|
mutable_ttl = 3600
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "artifactapi_remote_helm" "hashicorp_helm" {
|
||||||
|
name = "hashicorp-helm"
|
||||||
|
base_url = "https://helm.releases.hashicorp.com"
|
||||||
|
description = "HashiCorp Helm charts"
|
||||||
|
|
||||||
|
immutable_ttl = 0
|
||||||
|
mutable_ttl = 3600
|
||||||
|
}
|
||||||
|
|
||||||
|
# Virtual repository merges multiple remotes of the same package type
|
||||||
|
# into a single endpoint. Earlier members have higher priority for
|
||||||
|
# duplicate entries.
|
||||||
|
resource "artifactapi_virtual" "helm" {
|
||||||
|
name = "helm"
|
||||||
|
package_type = "helm"
|
||||||
|
description = "All Helm repos merged"
|
||||||
|
|
||||||
|
members = [
|
||||||
|
artifactapi_remote_helm.jetstack.name,
|
||||||
|
artifactapi_remote_helm.hashicorp_helm.name,
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewAPIClient(t *testing.T) {
|
||||||
|
c := newAPIClient("http://example.com")
|
||||||
|
if c.baseURL != "http://example.com" {
|
||||||
|
t.Errorf("expected baseURL http://example.com, got %s", c.baseURL)
|
||||||
|
}
|
||||||
|
if c.httpClient == nil {
|
||||||
|
t.Error("expected non-nil httpClient")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGet_Success(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
t.Errorf("expected GET, got %s", r.Method)
|
||||||
|
}
|
||||||
|
if r.URL.Path != "/api/v2/remotes/test" {
|
||||||
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"name": "test"})
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
c := newAPIClient(srv.URL)
|
||||||
|
var out map[string]string
|
||||||
|
err := c.get(context.Background(), "/api/v2/remotes/test", &out)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if out["name"] != "test" {
|
||||||
|
t.Errorf("expected name=test, got %s", out["name"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGet_NotFound(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
c := newAPIClient(srv.URL)
|
||||||
|
var out map[string]string
|
||||||
|
err := c.get(context.Background(), "/api/v2/remotes/missing", &out)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for 404")
|
||||||
|
}
|
||||||
|
if !isNotFound(err) {
|
||||||
|
t.Errorf("expected notFoundError, got %T: %v", err, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGet_ServerError(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
fmt.Fprint(w, "internal server error")
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
c := newAPIClient(srv.URL)
|
||||||
|
var out map[string]string
|
||||||
|
err := c.get(context.Background(), "/api/v2/remotes/fail", &out)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for 500")
|
||||||
|
}
|
||||||
|
if isNotFound(err) {
|
||||||
|
t.Error("500 should not be notFoundError")
|
||||||
|
}
|
||||||
|
expected := "api error 500: internal server error"
|
||||||
|
if err.Error() != expected {
|
||||||
|
t.Errorf("expected error %q, got %q", expected, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPost_Success(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
t.Errorf("expected POST, got %s", r.Method)
|
||||||
|
}
|
||||||
|
if r.Header.Get("Content-Type") != "application/json" {
|
||||||
|
t.Error("expected Content-Type application/json")
|
||||||
|
}
|
||||||
|
// Decode the request body and echo it back
|
||||||
|
var body map[string]string
|
||||||
|
json.NewDecoder(r.Body).Decode(&body)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(body)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
c := newAPIClient(srv.URL)
|
||||||
|
input := map[string]string{"name": "new-remote"}
|
||||||
|
var out map[string]string
|
||||||
|
err := c.post(context.Background(), "/api/v2/remotes", input, &out)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if out["name"] != "new-remote" {
|
||||||
|
t.Errorf("expected name=new-remote, got %s", out["name"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPut_Success(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPut {
|
||||||
|
t.Errorf("expected PUT, got %s", r.Method)
|
||||||
|
}
|
||||||
|
var body map[string]string
|
||||||
|
json.NewDecoder(r.Body).Decode(&body)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(body)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
c := newAPIClient(srv.URL)
|
||||||
|
input := map[string]string{"name": "updated"}
|
||||||
|
var out map[string]string
|
||||||
|
err := c.put(context.Background(), "/api/v2/remotes/updated", input, &out)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if out["name"] != "updated" {
|
||||||
|
t.Errorf("expected name=updated, got %s", out["name"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDel_Success(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodDelete {
|
||||||
|
t.Errorf("expected DELETE, got %s", r.Method)
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
c := newAPIClient(srv.URL)
|
||||||
|
err := c.del(context.Background(), "/api/v2/remotes/test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDel_NotFound(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
c := newAPIClient(srv.URL)
|
||||||
|
err := c.del(context.Background(), "/api/v2/remotes/missing")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for 404")
|
||||||
|
}
|
||||||
|
if !isNotFound(err) {
|
||||||
|
t.Errorf("expected notFoundError, got %T", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDo_BadRequest(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
fmt.Fprint(w, "invalid input")
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
c := newAPIClient(srv.URL)
|
||||||
|
err := c.do(context.Background(), http.MethodPost, "/api/v2/remotes", map[string]string{"bad": "data"}, nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for 400")
|
||||||
|
}
|
||||||
|
expected := "api error 400: invalid input"
|
||||||
|
if err.Error() != expected {
|
||||||
|
t.Errorf("expected %q, got %q", expected, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDo_NoContent(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
c := newAPIClient(srv.URL)
|
||||||
|
var out map[string]string
|
||||||
|
// Should not attempt to decode 204 even with out pointer
|
||||||
|
err := c.do(context.Background(), http.MethodDelete, "/test", nil, &out)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDo_JSONEncoding(t *testing.T) {
|
||||||
|
type payload struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
body, _ := io.ReadAll(r.Body)
|
||||||
|
var p payload
|
||||||
|
if err := json.Unmarshal(body, &p); err != nil {
|
||||||
|
t.Errorf("failed to decode request body: %v", err)
|
||||||
|
}
|
||||||
|
if p.Name != "test" || p.Count != 42 {
|
||||||
|
t.Errorf("unexpected payload: %+v", p)
|
||||||
|
}
|
||||||
|
// Return a different payload
|
||||||
|
resp := payload{Name: "response", Count: 99}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
c := newAPIClient(srv.URL)
|
||||||
|
input := payload{Name: "test", Count: 42}
|
||||||
|
var out payload
|
||||||
|
err := c.post(context.Background(), "/test", input, &out)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if out.Name != "response" || out.Count != 99 {
|
||||||
|
t.Errorf("unexpected response: %+v", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotFoundError_Error(t *testing.T) {
|
||||||
|
e := ¬FoundError{path: "/api/v2/remotes/test"}
|
||||||
|
expected := "not found: /api/v2/remotes/test"
|
||||||
|
if e.Error() != expected {
|
||||||
|
t.Errorf("expected %q, got %q", expected, e.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsNotFound_True(t *testing.T) {
|
||||||
|
err := ¬FoundError{path: "/test"}
|
||||||
|
if !isNotFound(err) {
|
||||||
|
t.Error("expected isNotFound to return true for *notFoundError")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsNotFound_False(t *testing.T) {
|
||||||
|
err := fmt.Errorf("some other error")
|
||||||
|
if isNotFound(err) {
|
||||||
|
t.Error("expected isNotFound to return false for non-notFoundError")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsNotFound_Nil(t *testing.T) {
|
||||||
|
if isNotFound(nil) {
|
||||||
|
t.Error("expected isNotFound to return false for nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsNotFound_WrappedError(t *testing.T) {
|
||||||
|
// isNotFound uses type assertion, not errors.As, so a wrapped notFoundError should NOT match
|
||||||
|
inner := ¬FoundError{path: "/test"}
|
||||||
|
wrapped := fmt.Errorf("wrapped: %w", inner)
|
||||||
|
// The current implementation uses type assertion, so wrapped should be false
|
||||||
|
if isNotFound(wrapped) {
|
||||||
|
t.Error("expected isNotFound to return false for wrapped notFoundError (uses type assertion)")
|
||||||
|
}
|
||||||
|
// But errors.As would find it
|
||||||
|
var nfe *notFoundError
|
||||||
|
if !errors.As(wrapped, &nfe) {
|
||||||
|
t.Error("errors.As should find wrapped notFoundError")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGet_DecodeError(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
fmt.Fprint(w, "not valid json")
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
c := newAPIClient(srv.URL)
|
||||||
|
var out map[string]string
|
||||||
|
err := c.get(context.Background(), "/test", &out)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for invalid JSON response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDo_NilBody(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Header.Get("Content-Type") != "" {
|
||||||
|
t.Error("Content-Type should not be set for nil body")
|
||||||
|
}
|
||||||
|
if r.Body != nil {
|
||||||
|
body, _ := io.ReadAll(r.Body)
|
||||||
|
if len(body) > 0 {
|
||||||
|
t.Errorf("expected empty body, got %s", string(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
fmt.Fprint(w, "{}")
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
c := newAPIClient(srv.URL)
|
||||||
|
var out map[string]string
|
||||||
|
err := c.get(context.Background(), "/test", &out)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestListToStrings_NullList(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
null := types.ListNull(types.StringType)
|
||||||
|
result := listToStrings(ctx, null)
|
||||||
|
if result != nil {
|
||||||
|
t.Fatalf("expected nil for null list, got %v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListToStrings_UnknownList(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
unknown := types.ListUnknown(types.StringType)
|
||||||
|
result := listToStrings(ctx, unknown)
|
||||||
|
if result != nil {
|
||||||
|
t.Fatalf("expected nil for unknown list, got %v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListToStrings_EmptyList(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
list, diags := types.ListValueFrom(ctx, types.StringType, []types.String{})
|
||||||
|
if diags.HasError() {
|
||||||
|
t.Fatalf("unexpected diags: %v", diags)
|
||||||
|
}
|
||||||
|
result := listToStrings(ctx, list)
|
||||||
|
if len(result) != 0 {
|
||||||
|
t.Fatalf("expected empty slice, got %v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListToStrings_WithValues(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
elems := []types.String{
|
||||||
|
types.StringValue("alpha"),
|
||||||
|
types.StringValue("beta"),
|
||||||
|
types.StringValue("gamma"),
|
||||||
|
}
|
||||||
|
list, diags := types.ListValueFrom(ctx, types.StringType, elems)
|
||||||
|
if diags.HasError() {
|
||||||
|
t.Fatalf("unexpected diags: %v", diags)
|
||||||
|
}
|
||||||
|
result := listToStrings(ctx, list)
|
||||||
|
if len(result) != 3 {
|
||||||
|
t.Fatalf("expected 3 elements, got %d", len(result))
|
||||||
|
}
|
||||||
|
expected := []string{"alpha", "beta", "gamma"}
|
||||||
|
for i, v := range result {
|
||||||
|
if v != expected[i] {
|
||||||
|
t.Errorf("index %d: expected %q, got %q", i, expected[i], v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringsToList_EmptySlice(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
result := stringsToList(ctx, []string{})
|
||||||
|
if !result.IsNull() {
|
||||||
|
t.Fatalf("expected null list for empty slice, got %v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringsToList_NilSlice(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
result := stringsToList(ctx, nil)
|
||||||
|
if !result.IsNull() {
|
||||||
|
t.Fatalf("expected null list for nil slice, got %v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringsToList_WithValues(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
result := stringsToList(ctx, []string{"one", "two", "three"})
|
||||||
|
if result.IsNull() || result.IsUnknown() {
|
||||||
|
t.Fatalf("expected non-null non-unknown list, got null=%v unknown=%v", result.IsNull(), result.IsUnknown())
|
||||||
|
}
|
||||||
|
// Round-trip: convert back and verify
|
||||||
|
back := listToStrings(ctx, result)
|
||||||
|
if len(back) != 3 {
|
||||||
|
t.Fatalf("expected 3 elements after round-trip, got %d", len(back))
|
||||||
|
}
|
||||||
|
expected := []string{"one", "two", "three"}
|
||||||
|
for i, v := range back {
|
||||||
|
if v != expected[i] {
|
||||||
|
t.Errorf("round-trip index %d: expected %q, got %q", i, expected[i], v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringsToList_SingleValue(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
result := stringsToList(ctx, []string{"solo"})
|
||||||
|
back := listToStrings(ctx, result)
|
||||||
|
if len(back) != 1 || back[0] != "solo" {
|
||||||
|
t.Fatalf("expected [\"solo\"], got %v", back)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||||
|
fwprovider "github.com/hashicorp/terraform-plugin-framework/provider"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNew_ReturnsFactory(t *testing.T) {
|
||||||
|
factory := New("1.0.0")
|
||||||
|
if factory == nil {
|
||||||
|
t.Fatal("expected non-nil factory")
|
||||||
|
}
|
||||||
|
p := factory()
|
||||||
|
if p == nil {
|
||||||
|
t.Fatal("expected non-nil provider")
|
||||||
|
}
|
||||||
|
_, ok := p.(*ArtifactAPIProvider)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected *ArtifactAPIProvider, got %T", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNew_VersionPropagated(t *testing.T) {
|
||||||
|
factory := New("2.3.4")
|
||||||
|
p := factory().(*ArtifactAPIProvider)
|
||||||
|
if p.version != "2.3.4" {
|
||||||
|
t.Errorf("expected version 2.3.4, got %s", p.version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_Metadata(t *testing.T) {
|
||||||
|
p := &ArtifactAPIProvider{version: "1.2.3"}
|
||||||
|
req := fwprovider.MetadataRequest{}
|
||||||
|
var resp fwprovider.MetadataResponse
|
||||||
|
p.Metadata(context.Background(), req, &resp)
|
||||||
|
|
||||||
|
if resp.TypeName != "artifactapi" {
|
||||||
|
t.Errorf("expected type name 'artifactapi', got %s", resp.TypeName)
|
||||||
|
}
|
||||||
|
if resp.Version != "1.2.3" {
|
||||||
|
t.Errorf("expected version '1.2.3', got %s", resp.Version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_Schema(t *testing.T) {
|
||||||
|
p := &ArtifactAPIProvider{version: "1.0.0"}
|
||||||
|
req := fwprovider.SchemaRequest{}
|
||||||
|
var resp fwprovider.SchemaResponse
|
||||||
|
p.Schema(context.Background(), req, &resp)
|
||||||
|
|
||||||
|
if resp.Schema.Description == "" {
|
||||||
|
t.Error("expected non-empty schema description")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := resp.Schema.Attributes["endpoint"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("missing 'endpoint' attribute in schema")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_Resources(t *testing.T) {
|
||||||
|
p := &ArtifactAPIProvider{version: "1.0.0"}
|
||||||
|
resources := p.Resources(context.Background())
|
||||||
|
|
||||||
|
// 10 remote resource types + 1 virtual = 11
|
||||||
|
expectedCount := 11
|
||||||
|
if len(resources) != expectedCount {
|
||||||
|
t.Fatalf("expected %d resources, got %d", expectedCount, len(resources))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify each factory produces a valid resource
|
||||||
|
for i, factory := range resources {
|
||||||
|
r := factory()
|
||||||
|
if r == nil {
|
||||||
|
t.Errorf("resource factory %d returned nil", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_Resources_ContainsExpectedTypes(t *testing.T) {
|
||||||
|
p := &ArtifactAPIProvider{version: "1.0.0"}
|
||||||
|
resources := p.Resources(context.Background())
|
||||||
|
|
||||||
|
// Collect type names by calling Metadata on each
|
||||||
|
typeNames := make(map[string]bool)
|
||||||
|
for _, factory := range resources {
|
||||||
|
r := factory()
|
||||||
|
req := resource.MetadataRequest{ProviderTypeName: "artifactapi"}
|
||||||
|
var resp resource.MetadataResponse
|
||||||
|
r.Metadata(context.Background(), req, &resp)
|
||||||
|
typeNames[resp.TypeName] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []string{
|
||||||
|
"artifactapi_remote_generic",
|
||||||
|
"artifactapi_remote_docker",
|
||||||
|
"artifactapi_remote_helm",
|
||||||
|
"artifactapi_remote_pypi",
|
||||||
|
"artifactapi_remote_npm",
|
||||||
|
"artifactapi_remote_rpm",
|
||||||
|
"artifactapi_remote_alpine",
|
||||||
|
"artifactapi_remote_puppet",
|
||||||
|
"artifactapi_remote_terraform",
|
||||||
|
"artifactapi_remote_goproxy",
|
||||||
|
"artifactapi_virtual",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range expected {
|
||||||
|
if !typeNames[name] {
|
||||||
|
t.Errorf("missing expected resource type: %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_DataSources(t *testing.T) {
|
||||||
|
p := &ArtifactAPIProvider{version: "1.0.0"}
|
||||||
|
dataSources := p.DataSources(context.Background())
|
||||||
|
|
||||||
|
expectedCount := 2
|
||||||
|
if len(dataSources) != expectedCount {
|
||||||
|
t.Fatalf("expected %d data sources, got %d", expectedCount, len(dataSources))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify each factory produces a valid data source
|
||||||
|
for i, factory := range dataSources {
|
||||||
|
ds := factory()
|
||||||
|
if ds == nil {
|
||||||
|
t.Errorf("data source factory %d returned nil", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_DataSources_ContainsExpectedTypes(t *testing.T) {
|
||||||
|
p := &ArtifactAPIProvider{version: "1.0.0"}
|
||||||
|
dataSources := p.DataSources(context.Background())
|
||||||
|
|
||||||
|
typeNames := make(map[string]bool)
|
||||||
|
for _, factory := range dataSources {
|
||||||
|
ds := factory()
|
||||||
|
req := datasource.MetadataRequest{ProviderTypeName: "artifactapi"}
|
||||||
|
var resp datasource.MetadataResponse
|
||||||
|
ds.Metadata(context.Background(), req, &resp)
|
||||||
|
typeNames[resp.TypeName] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []string{
|
||||||
|
"artifactapi_remote",
|
||||||
|
"artifactapi_virtual",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range expected {
|
||||||
|
if !typeNames[name] {
|
||||||
|
t.Errorf("missing expected data source type: %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvider_ImplementsInterface(t *testing.T) {
|
||||||
|
var _ fwprovider.Provider = &ArtifactAPIProvider{}
|
||||||
|
}
|
||||||
@@ -0,0 +1,447 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestModelToAPI_FullFields(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
r := &remoteResource{packageType: "docker"}
|
||||||
|
|
||||||
|
model := remoteResourceModel{
|
||||||
|
Name: types.StringValue("my-remote"),
|
||||||
|
BaseURL: types.StringValue("https://registry.example.com"),
|
||||||
|
Description: types.StringValue("A test remote"),
|
||||||
|
Username: types.StringValue("user"),
|
||||||
|
Password: types.StringValue("pass"),
|
||||||
|
ImmutableTTL: types.Int64Value(86400),
|
||||||
|
MutableTTL: types.Int64Value(3600),
|
||||||
|
CheckMutable: types.BoolValue(true),
|
||||||
|
Patterns: stringsToList(ctx, []string{"*.tar.gz", "*.whl"}),
|
||||||
|
Blocklist: stringsToList(ctx, []string{"blocked/*"}),
|
||||||
|
MutablePatterns: stringsToList(ctx, []string{"latest"}),
|
||||||
|
ImmutablePatterns: stringsToList(ctx, []string{"v*"}),
|
||||||
|
BanTagsEnabled: types.BoolValue(true),
|
||||||
|
BanTags: stringsToList(ctx, []string{"latest", "dev"}),
|
||||||
|
QuarantineEnabled: types.BoolValue(true),
|
||||||
|
QuarantineDays: types.Int64Value(7),
|
||||||
|
StaleOnError: types.BoolValue(false),
|
||||||
|
ReleasesRemote: types.StringValue("cdn-remote"),
|
||||||
|
}
|
||||||
|
|
||||||
|
api := r.modelToAPI(ctx, model)
|
||||||
|
|
||||||
|
if api.Name != "my-remote" {
|
||||||
|
t.Errorf("Name: expected my-remote, got %s", api.Name)
|
||||||
|
}
|
||||||
|
if api.PackageType != "docker" {
|
||||||
|
t.Errorf("PackageType: expected docker, got %s", api.PackageType)
|
||||||
|
}
|
||||||
|
if api.BaseURL != "https://registry.example.com" {
|
||||||
|
t.Errorf("BaseURL: expected https://registry.example.com, got %s", api.BaseURL)
|
||||||
|
}
|
||||||
|
if api.Description != "A test remote" {
|
||||||
|
t.Errorf("Description: expected 'A test remote', got %s", api.Description)
|
||||||
|
}
|
||||||
|
if api.Username != "user" {
|
||||||
|
t.Errorf("Username: expected user, got %s", api.Username)
|
||||||
|
}
|
||||||
|
if api.Password != "pass" {
|
||||||
|
t.Errorf("Password: expected pass, got %s", api.Password)
|
||||||
|
}
|
||||||
|
if api.ImmutableTTL != 86400 {
|
||||||
|
t.Errorf("ImmutableTTL: expected 86400, got %d", api.ImmutableTTL)
|
||||||
|
}
|
||||||
|
if api.MutableTTL != 3600 {
|
||||||
|
t.Errorf("MutableTTL: expected 3600, got %d", api.MutableTTL)
|
||||||
|
}
|
||||||
|
if !api.CheckMutable {
|
||||||
|
t.Error("CheckMutable: expected true")
|
||||||
|
}
|
||||||
|
if len(api.Patterns) != 2 || api.Patterns[0] != "*.tar.gz" {
|
||||||
|
t.Errorf("Patterns: expected [*.tar.gz *.whl], got %v", api.Patterns)
|
||||||
|
}
|
||||||
|
if len(api.Blocklist) != 1 || api.Blocklist[0] != "blocked/*" {
|
||||||
|
t.Errorf("Blocklist: expected [blocked/*], got %v", api.Blocklist)
|
||||||
|
}
|
||||||
|
if len(api.MutablePatterns) != 1 || api.MutablePatterns[0] != "latest" {
|
||||||
|
t.Errorf("MutablePatterns: expected [latest], got %v", api.MutablePatterns)
|
||||||
|
}
|
||||||
|
if len(api.ImmutablePatterns) != 1 || api.ImmutablePatterns[0] != "v*" {
|
||||||
|
t.Errorf("ImmutablePatterns: expected [v*], got %v", api.ImmutablePatterns)
|
||||||
|
}
|
||||||
|
if !api.BanTagsEnabled {
|
||||||
|
t.Error("BanTagsEnabled: expected true")
|
||||||
|
}
|
||||||
|
if len(api.BanTags) != 2 || api.BanTags[0] != "latest" {
|
||||||
|
t.Errorf("BanTags: expected [latest dev], got %v", api.BanTags)
|
||||||
|
}
|
||||||
|
if !api.QuarantineEnabled {
|
||||||
|
t.Error("QuarantineEnabled: expected true")
|
||||||
|
}
|
||||||
|
if api.QuarantineDays != 7 {
|
||||||
|
t.Errorf("QuarantineDays: expected 7, got %d", api.QuarantineDays)
|
||||||
|
}
|
||||||
|
if api.StaleOnError {
|
||||||
|
t.Error("StaleOnError: expected false")
|
||||||
|
}
|
||||||
|
if api.ReleasesRemote != "cdn-remote" {
|
||||||
|
t.Errorf("ReleasesRemote: expected cdn-remote, got %s", api.ReleasesRemote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModelToAPI_NullLists(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
r := &remoteResource{packageType: "generic"}
|
||||||
|
|
||||||
|
model := remoteResourceModel{
|
||||||
|
Name: types.StringValue("minimal"),
|
||||||
|
BaseURL: types.StringValue("https://example.com"),
|
||||||
|
Description: types.StringValue(""),
|
||||||
|
Username: types.StringValue(""),
|
||||||
|
Password: types.StringValue(""),
|
||||||
|
ImmutableTTL: types.Int64Value(0),
|
||||||
|
MutableTTL: types.Int64Value(3600),
|
||||||
|
CheckMutable: types.BoolValue(true),
|
||||||
|
Patterns: types.ListNull(types.StringType),
|
||||||
|
Blocklist: types.ListNull(types.StringType),
|
||||||
|
MutablePatterns: types.ListNull(types.StringType),
|
||||||
|
ImmutablePatterns: types.ListNull(types.StringType),
|
||||||
|
BanTagsEnabled: types.BoolValue(false),
|
||||||
|
BanTags: types.ListNull(types.StringType),
|
||||||
|
QuarantineEnabled: types.BoolValue(false),
|
||||||
|
QuarantineDays: types.Int64Value(3),
|
||||||
|
StaleOnError: types.BoolValue(true),
|
||||||
|
ReleasesRemote: types.StringValue(""),
|
||||||
|
}
|
||||||
|
|
||||||
|
api := r.modelToAPI(ctx, model)
|
||||||
|
|
||||||
|
if api.Patterns != nil {
|
||||||
|
t.Errorf("Patterns: expected nil, got %v", api.Patterns)
|
||||||
|
}
|
||||||
|
if api.Blocklist != nil {
|
||||||
|
t.Errorf("Blocklist: expected nil, got %v", api.Blocklist)
|
||||||
|
}
|
||||||
|
if api.MutablePatterns != nil {
|
||||||
|
t.Errorf("MutablePatterns: expected nil, got %v", api.MutablePatterns)
|
||||||
|
}
|
||||||
|
if api.ImmutablePatterns != nil {
|
||||||
|
t.Errorf("ImmutablePatterns: expected nil, got %v", api.ImmutablePatterns)
|
||||||
|
}
|
||||||
|
if api.BanTags != nil {
|
||||||
|
t.Errorf("BanTags: expected nil, got %v", api.BanTags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModelToAPI_PackageTypeFromResource(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
tests := []struct {
|
||||||
|
pkgType string
|
||||||
|
}{
|
||||||
|
{"generic"},
|
||||||
|
{"docker"},
|
||||||
|
{"helm"},
|
||||||
|
{"pypi"},
|
||||||
|
{"npm"},
|
||||||
|
{"rpm"},
|
||||||
|
{"alpine"},
|
||||||
|
{"puppet"},
|
||||||
|
{"terraform"},
|
||||||
|
{"goproxy"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.pkgType, func(t *testing.T) {
|
||||||
|
r := &remoteResource{packageType: tt.pkgType}
|
||||||
|
model := remoteResourceModel{
|
||||||
|
Name: types.StringValue("test"),
|
||||||
|
BaseURL: types.StringValue("https://example.com"),
|
||||||
|
Description: types.StringValue(""),
|
||||||
|
Username: types.StringValue(""),
|
||||||
|
Password: types.StringValue(""),
|
||||||
|
ImmutableTTL: types.Int64Value(0),
|
||||||
|
MutableTTL: types.Int64Value(0),
|
||||||
|
CheckMutable: types.BoolValue(false),
|
||||||
|
Patterns: types.ListNull(types.StringType),
|
||||||
|
Blocklist: types.ListNull(types.StringType),
|
||||||
|
MutablePatterns: types.ListNull(types.StringType),
|
||||||
|
ImmutablePatterns: types.ListNull(types.StringType),
|
||||||
|
BanTagsEnabled: types.BoolValue(false),
|
||||||
|
BanTags: types.ListNull(types.StringType),
|
||||||
|
QuarantineEnabled: types.BoolValue(false),
|
||||||
|
QuarantineDays: types.Int64Value(0),
|
||||||
|
StaleOnError: types.BoolValue(false),
|
||||||
|
ReleasesRemote: types.StringValue(""),
|
||||||
|
}
|
||||||
|
api := r.modelToAPI(ctx, model)
|
||||||
|
if api.PackageType != tt.pkgType {
|
||||||
|
t.Errorf("expected package_type %s, got %s", tt.pkgType, api.PackageType)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIToModel_FullFields(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
r := &remoteResource{packageType: "docker"}
|
||||||
|
|
||||||
|
api := remoteAPI{
|
||||||
|
Name: "my-remote",
|
||||||
|
PackageType: "docker",
|
||||||
|
BaseURL: "https://registry.example.com",
|
||||||
|
Description: "A test remote",
|
||||||
|
Username: "user",
|
||||||
|
Password: "pass",
|
||||||
|
ImmutableTTL: 86400,
|
||||||
|
MutableTTL: 3600,
|
||||||
|
CheckMutable: true,
|
||||||
|
Patterns: []string{"*.tar.gz"},
|
||||||
|
Blocklist: []string{"blocked/*"},
|
||||||
|
MutablePatterns: []string{"latest"},
|
||||||
|
ImmutablePatterns: []string{"v*"},
|
||||||
|
BanTagsEnabled: true,
|
||||||
|
BanTags: []string{"latest"},
|
||||||
|
QuarantineEnabled: true,
|
||||||
|
QuarantineDays: 7,
|
||||||
|
StaleOnError: false,
|
||||||
|
ReleasesRemote: "cdn-remote",
|
||||||
|
ManagedBy: "terraform",
|
||||||
|
}
|
||||||
|
|
||||||
|
model := r.apiToModel(ctx, api)
|
||||||
|
|
||||||
|
if model.Name.ValueString() != "my-remote" {
|
||||||
|
t.Errorf("Name: expected my-remote, got %s", model.Name.ValueString())
|
||||||
|
}
|
||||||
|
if model.BaseURL.ValueString() != "https://registry.example.com" {
|
||||||
|
t.Errorf("BaseURL: expected https://registry.example.com, got %s", model.BaseURL.ValueString())
|
||||||
|
}
|
||||||
|
if model.Description.ValueString() != "A test remote" {
|
||||||
|
t.Errorf("Description: expected 'A test remote', got %s", model.Description.ValueString())
|
||||||
|
}
|
||||||
|
if model.Username.ValueString() != "user" {
|
||||||
|
t.Errorf("Username: expected user, got %s", model.Username.ValueString())
|
||||||
|
}
|
||||||
|
if model.Password.ValueString() != "pass" {
|
||||||
|
t.Errorf("Password: expected pass, got %s", model.Password.ValueString())
|
||||||
|
}
|
||||||
|
if model.ImmutableTTL.ValueInt64() != 86400 {
|
||||||
|
t.Errorf("ImmutableTTL: expected 86400, got %d", model.ImmutableTTL.ValueInt64())
|
||||||
|
}
|
||||||
|
if model.MutableTTL.ValueInt64() != 3600 {
|
||||||
|
t.Errorf("MutableTTL: expected 3600, got %d", model.MutableTTL.ValueInt64())
|
||||||
|
}
|
||||||
|
if !model.CheckMutable.ValueBool() {
|
||||||
|
t.Error("CheckMutable: expected true")
|
||||||
|
}
|
||||||
|
if !model.QuarantineEnabled.ValueBool() {
|
||||||
|
t.Error("QuarantineEnabled: expected true")
|
||||||
|
}
|
||||||
|
if model.QuarantineDays.ValueInt64() != 7 {
|
||||||
|
t.Errorf("QuarantineDays: expected 7, got %d", model.QuarantineDays.ValueInt64())
|
||||||
|
}
|
||||||
|
if model.StaleOnError.ValueBool() {
|
||||||
|
t.Error("StaleOnError: expected false")
|
||||||
|
}
|
||||||
|
if !model.BanTagsEnabled.ValueBool() {
|
||||||
|
t.Error("BanTagsEnabled: expected true")
|
||||||
|
}
|
||||||
|
if model.ReleasesRemote.ValueString() != "cdn-remote" {
|
||||||
|
t.Errorf("ReleasesRemote: expected cdn-remote, got %s", model.ReleasesRemote.ValueString())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify list fields via round-trip
|
||||||
|
patterns := listToStrings(ctx, model.Patterns)
|
||||||
|
if len(patterns) != 1 || patterns[0] != "*.tar.gz" {
|
||||||
|
t.Errorf("Patterns: expected [*.tar.gz], got %v", patterns)
|
||||||
|
}
|
||||||
|
blocklist := listToStrings(ctx, model.Blocklist)
|
||||||
|
if len(blocklist) != 1 || blocklist[0] != "blocked/*" {
|
||||||
|
t.Errorf("Blocklist: expected [blocked/*], got %v", blocklist)
|
||||||
|
}
|
||||||
|
banTags := listToStrings(ctx, model.BanTags)
|
||||||
|
if len(banTags) != 1 || banTags[0] != "latest" {
|
||||||
|
t.Errorf("BanTags: expected [latest], got %v", banTags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIToModel_EmptyLists(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
r := &remoteResource{packageType: "generic"}
|
||||||
|
|
||||||
|
api := remoteAPI{
|
||||||
|
Name: "minimal",
|
||||||
|
PackageType: "generic",
|
||||||
|
BaseURL: "https://example.com",
|
||||||
|
// All lists are nil/empty
|
||||||
|
}
|
||||||
|
|
||||||
|
model := r.apiToModel(ctx, api)
|
||||||
|
|
||||||
|
if !model.Patterns.IsNull() {
|
||||||
|
t.Errorf("Patterns: expected null for nil input, got %v", model.Patterns)
|
||||||
|
}
|
||||||
|
if !model.Blocklist.IsNull() {
|
||||||
|
t.Errorf("Blocklist: expected null for nil input, got %v", model.Blocklist)
|
||||||
|
}
|
||||||
|
if !model.MutablePatterns.IsNull() {
|
||||||
|
t.Errorf("MutablePatterns: expected null for nil input, got %v", model.MutablePatterns)
|
||||||
|
}
|
||||||
|
if !model.ImmutablePatterns.IsNull() {
|
||||||
|
t.Errorf("ImmutablePatterns: expected null for nil input, got %v", model.ImmutablePatterns)
|
||||||
|
}
|
||||||
|
if !model.BanTags.IsNull() {
|
||||||
|
t.Errorf("BanTags: expected null for nil input, got %v", model.BanTags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModelToAPI_RoundTrip(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
r := &remoteResource{packageType: "helm"}
|
||||||
|
|
||||||
|
original := remoteAPI{
|
||||||
|
Name: "helm-remote",
|
||||||
|
PackageType: "helm",
|
||||||
|
BaseURL: "https://charts.example.com",
|
||||||
|
Description: "Helm chart mirror",
|
||||||
|
Username: "helmuser",
|
||||||
|
Password: "helmpass",
|
||||||
|
ImmutableTTL: 172800,
|
||||||
|
MutableTTL: 7200,
|
||||||
|
CheckMutable: true,
|
||||||
|
Patterns: []string{"stable/*", "incubator/*"},
|
||||||
|
Blocklist: []string{"deprecated/*"},
|
||||||
|
MutablePatterns: []string{"latest"},
|
||||||
|
ImmutablePatterns: []string{"v1.*"},
|
||||||
|
BanTagsEnabled: false,
|
||||||
|
BanTags: nil,
|
||||||
|
QuarantineEnabled: false,
|
||||||
|
QuarantineDays: 3,
|
||||||
|
StaleOnError: true,
|
||||||
|
ReleasesRemote: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
// API -> Model -> API round-trip
|
||||||
|
model := r.apiToModel(ctx, original)
|
||||||
|
result := r.modelToAPI(ctx, model)
|
||||||
|
|
||||||
|
if result.Name != original.Name {
|
||||||
|
t.Errorf("Name: expected %s, got %s", original.Name, result.Name)
|
||||||
|
}
|
||||||
|
if result.PackageType != original.PackageType {
|
||||||
|
t.Errorf("PackageType: expected %s, got %s", original.PackageType, result.PackageType)
|
||||||
|
}
|
||||||
|
if result.BaseURL != original.BaseURL {
|
||||||
|
t.Errorf("BaseURL: expected %s, got %s", original.BaseURL, result.BaseURL)
|
||||||
|
}
|
||||||
|
if result.Description != original.Description {
|
||||||
|
t.Errorf("Description: expected %s, got %s", original.Description, result.Description)
|
||||||
|
}
|
||||||
|
if result.ImmutableTTL != original.ImmutableTTL {
|
||||||
|
t.Errorf("ImmutableTTL: expected %d, got %d", original.ImmutableTTL, result.ImmutableTTL)
|
||||||
|
}
|
||||||
|
if result.MutableTTL != original.MutableTTL {
|
||||||
|
t.Errorf("MutableTTL: expected %d, got %d", original.MutableTTL, result.MutableTTL)
|
||||||
|
}
|
||||||
|
if result.CheckMutable != original.CheckMutable {
|
||||||
|
t.Errorf("CheckMutable: expected %v, got %v", original.CheckMutable, result.CheckMutable)
|
||||||
|
}
|
||||||
|
if len(result.Patterns) != len(original.Patterns) {
|
||||||
|
t.Errorf("Patterns length: expected %d, got %d", len(original.Patterns), len(result.Patterns))
|
||||||
|
}
|
||||||
|
for i := range original.Patterns {
|
||||||
|
if result.Patterns[i] != original.Patterns[i] {
|
||||||
|
t.Errorf("Patterns[%d]: expected %s, got %s", i, original.Patterns[i], result.Patterns[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result.QuarantineDays != original.QuarantineDays {
|
||||||
|
t.Errorf("QuarantineDays: expected %d, got %d", original.QuarantineDays, result.QuarantineDays)
|
||||||
|
}
|
||||||
|
if result.StaleOnError != original.StaleOnError {
|
||||||
|
t.Errorf("StaleOnError: expected %v, got %v", original.StaleOnError, result.StaleOnError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoteResource_Metadata(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
pkgType string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"generic", "artifactapi_remote_generic"},
|
||||||
|
{"docker", "artifactapi_remote_docker"},
|
||||||
|
{"helm", "artifactapi_remote_helm"},
|
||||||
|
{"pypi", "artifactapi_remote_pypi"},
|
||||||
|
{"npm", "artifactapi_remote_npm"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.pkgType, func(t *testing.T) {
|
||||||
|
r := &remoteResource{packageType: tt.pkgType}
|
||||||
|
req := resource.MetadataRequest{ProviderTypeName: "artifactapi"}
|
||||||
|
var resp resource.MetadataResponse
|
||||||
|
r.Metadata(context.Background(), req, &resp)
|
||||||
|
if resp.TypeName != tt.expected {
|
||||||
|
t.Errorf("expected %s, got %s", tt.expected, resp.TypeName)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoteResource_Schema(t *testing.T) {
|
||||||
|
r := &remoteResource{packageType: "docker"}
|
||||||
|
req := resource.SchemaRequest{}
|
||||||
|
var resp resource.SchemaResponse
|
||||||
|
r.Schema(context.Background(), req, &resp)
|
||||||
|
|
||||||
|
expectedAttrs := []string{
|
||||||
|
"name", "base_url", "description", "username", "password",
|
||||||
|
"immutable_ttl", "mutable_ttl", "check_mutable",
|
||||||
|
"patterns", "blocklist", "mutable_patterns", "immutable_patterns",
|
||||||
|
"ban_tags_enabled", "ban_tags",
|
||||||
|
"quarantine_enabled", "quarantine_days",
|
||||||
|
"stale_on_error", "releases_remote",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, attr := range expectedAttrs {
|
||||||
|
if _, ok := resp.Schema.Attributes[attr]; !ok {
|
||||||
|
t.Errorf("missing expected attribute: %s", attr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewRemoteResource_Constructors(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fn func() resource.Resource
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"generic", func() resource.Resource { return NewRemoteGeneric() }, "generic"},
|
||||||
|
{"docker", func() resource.Resource { return NewRemoteDocker() }, "docker"},
|
||||||
|
{"helm", func() resource.Resource { return NewRemoteHelm() }, "helm"},
|
||||||
|
{"pypi", func() resource.Resource { return NewRemotePyPI() }, "pypi"},
|
||||||
|
{"npm", func() resource.Resource { return NewRemoteNPM() }, "npm"},
|
||||||
|
{"rpm", func() resource.Resource { return NewRemoteRPM() }, "rpm"},
|
||||||
|
{"alpine", func() resource.Resource { return NewRemoteAlpine() }, "alpine"},
|
||||||
|
{"puppet", func() resource.Resource { return NewRemotePuppet() }, "puppet"},
|
||||||
|
{"terraform", func() resource.Resource { return NewRemoteTerraform() }, "terraform"},
|
||||||
|
{"goproxy", func() resource.Resource { return NewRemoteGoProxy() }, "goproxy"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
r := tt.fn()
|
||||||
|
rr, ok := r.(*remoteResource)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected *remoteResource")
|
||||||
|
}
|
||||||
|
if rr.packageType != tt.expected {
|
||||||
|
t.Errorf("expected packageType %s, got %s", tt.expected, rr.packageType)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVirtualModelToAPI(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
model := virtualResourceModel{
|
||||||
|
Name: types.StringValue("my-virtual"),
|
||||||
|
PackageType: types.StringValue("helm"),
|
||||||
|
Description: types.StringValue("Virtual helm repo"),
|
||||||
|
Members: stringsToList(ctx, []string{"remote-a", "remote-b"}),
|
||||||
|
}
|
||||||
|
|
||||||
|
api := virtualModelToAPI(ctx, model)
|
||||||
|
|
||||||
|
if api.Name != "my-virtual" {
|
||||||
|
t.Errorf("Name: expected my-virtual, got %s", api.Name)
|
||||||
|
}
|
||||||
|
if api.PackageType != "helm" {
|
||||||
|
t.Errorf("PackageType: expected helm, got %s", api.PackageType)
|
||||||
|
}
|
||||||
|
if api.Description != "Virtual helm repo" {
|
||||||
|
t.Errorf("Description: expected 'Virtual helm repo', got %s", api.Description)
|
||||||
|
}
|
||||||
|
if len(api.Members) != 2 {
|
||||||
|
t.Fatalf("Members: expected 2, got %d", len(api.Members))
|
||||||
|
}
|
||||||
|
if api.Members[0] != "remote-a" || api.Members[1] != "remote-b" {
|
||||||
|
t.Errorf("Members: expected [remote-a remote-b], got %v", api.Members)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVirtualModelToAPI_NullMembers(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
model := virtualResourceModel{
|
||||||
|
Name: types.StringValue("no-members"),
|
||||||
|
PackageType: types.StringValue("pypi"),
|
||||||
|
Description: types.StringValue(""),
|
||||||
|
Members: types.ListNull(types.StringType),
|
||||||
|
}
|
||||||
|
|
||||||
|
api := virtualModelToAPI(ctx, model)
|
||||||
|
|
||||||
|
if api.Members != nil {
|
||||||
|
t.Errorf("Members: expected nil for null list, got %v", api.Members)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVirtualAPIToModel(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
api := virtualAPI{
|
||||||
|
Name: "my-virtual",
|
||||||
|
PackageType: "helm",
|
||||||
|
Description: "Virtual helm repo",
|
||||||
|
Members: []string{"remote-a", "remote-b"},
|
||||||
|
ManagedBy: "terraform",
|
||||||
|
}
|
||||||
|
|
||||||
|
model := virtualAPIToModel(ctx, api)
|
||||||
|
|
||||||
|
if model.Name.ValueString() != "my-virtual" {
|
||||||
|
t.Errorf("Name: expected my-virtual, got %s", model.Name.ValueString())
|
||||||
|
}
|
||||||
|
if model.PackageType.ValueString() != "helm" {
|
||||||
|
t.Errorf("PackageType: expected helm, got %s", model.PackageType.ValueString())
|
||||||
|
}
|
||||||
|
if model.Description.ValueString() != "Virtual helm repo" {
|
||||||
|
t.Errorf("Description: expected 'Virtual helm repo', got %s", model.Description.ValueString())
|
||||||
|
}
|
||||||
|
|
||||||
|
members := listToStrings(ctx, model.Members)
|
||||||
|
if len(members) != 2 {
|
||||||
|
t.Fatalf("Members: expected 2, got %d", len(members))
|
||||||
|
}
|
||||||
|
if members[0] != "remote-a" || members[1] != "remote-b" {
|
||||||
|
t.Errorf("Members: expected [remote-a remote-b], got %v", members)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVirtualAPIToModel_EmptyMembers(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
api := virtualAPI{
|
||||||
|
Name: "empty-members",
|
||||||
|
PackageType: "pypi",
|
||||||
|
Members: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
model := virtualAPIToModel(ctx, api)
|
||||||
|
|
||||||
|
if !model.Members.IsNull() {
|
||||||
|
t.Errorf("Members: expected null for nil/empty slice, got %v", model.Members)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVirtualRoundTrip(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
original := virtualAPI{
|
||||||
|
Name: "roundtrip-virtual",
|
||||||
|
PackageType: "helm",
|
||||||
|
Description: "Round trip test",
|
||||||
|
Members: []string{"a", "b", "c"},
|
||||||
|
}
|
||||||
|
|
||||||
|
model := virtualAPIToModel(ctx, original)
|
||||||
|
result := virtualModelToAPI(ctx, model)
|
||||||
|
|
||||||
|
if result.Name != original.Name {
|
||||||
|
t.Errorf("Name: expected %s, got %s", original.Name, result.Name)
|
||||||
|
}
|
||||||
|
if result.PackageType != original.PackageType {
|
||||||
|
t.Errorf("PackageType: expected %s, got %s", original.PackageType, result.PackageType)
|
||||||
|
}
|
||||||
|
if result.Description != original.Description {
|
||||||
|
t.Errorf("Description: expected %s, got %s", original.Description, result.Description)
|
||||||
|
}
|
||||||
|
if len(result.Members) != len(original.Members) {
|
||||||
|
t.Fatalf("Members length: expected %d, got %d", len(original.Members), len(result.Members))
|
||||||
|
}
|
||||||
|
for i := range original.Members {
|
||||||
|
if result.Members[i] != original.Members[i] {
|
||||||
|
t.Errorf("Members[%d]: expected %s, got %s", i, original.Members[i], result.Members[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVirtualResource_Metadata(t *testing.T) {
|
||||||
|
r := NewVirtualResource()
|
||||||
|
req := resource.MetadataRequest{ProviderTypeName: "artifactapi"}
|
||||||
|
var resp resource.MetadataResponse
|
||||||
|
r.Metadata(context.Background(), req, &resp)
|
||||||
|
if resp.TypeName != "artifactapi_virtual" {
|
||||||
|
t.Errorf("expected artifactapi_virtual, got %s", resp.TypeName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVirtualResource_Schema(t *testing.T) {
|
||||||
|
r := NewVirtualResource()
|
||||||
|
req := resource.SchemaRequest{}
|
||||||
|
var resp resource.SchemaResponse
|
||||||
|
r.Schema(context.Background(), req, &resp)
|
||||||
|
|
||||||
|
expectedAttrs := []string{"name", "package_type", "description", "members"}
|
||||||
|
for _, attr := range expectedAttrs {
|
||||||
|
if _, ok := resp.Schema.Attributes[attr]; !ok {
|
||||||
|
t.Errorf("missing expected attribute: %s", attr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewVirtualResource_Type(t *testing.T) {
|
||||||
|
r := NewVirtualResource()
|
||||||
|
_, ok := r.(*virtualResource)
|
||||||
|
if !ok {
|
||||||
|
t.Error("expected *virtualResource")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user