feat: initial terraform provider for artifactapi v0.0.1

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

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

Supports all 10 package types (generic, docker, helm, pypi, npm,
rpm, alpine, puppet, terraform, goproxy), allowlist/blocklist,
tag banning, quarantine, and terraform import.
This commit is contained in:
2026-06-07 14:30:20 +10:00
commit ad50a06b33
13 changed files with 1204 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
package provider
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
)
type apiClient struct {
baseURL string
httpClient *http.Client
}
func newAPIClient(baseURL string) *apiClient {
return &apiClient{
baseURL: baseURL,
httpClient: &http.Client{},
}
}
func (c *apiClient) get(ctx context.Context, path string, out any) error {
return c.do(ctx, http.MethodGet, path, nil, out)
}
func (c *apiClient) post(ctx context.Context, path string, body, out any) error {
return c.do(ctx, http.MethodPost, path, body, out)
}
func (c *apiClient) put(ctx context.Context, path string, body, out any) error {
return c.do(ctx, http.MethodPut, path, body, out)
}
func (c *apiClient) del(ctx context.Context, path string) error {
return c.do(ctx, http.MethodDelete, path, nil, nil)
}
func (c *apiClient) do(ctx context.Context, method, path string, body, out any) error {
var bodyReader io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("marshal request: %w", err)
}
bodyReader = bytes.NewReader(b)
}
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bodyReader)
if err != nil {
return fmt.Errorf("create request: %w", err)
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("http request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return &notFoundError{path: path}
}
if resp.StatusCode >= 400 {
b, _ := io.ReadAll(resp.Body)
return fmt.Errorf("api error %d: %s", resp.StatusCode, string(b))
}
if out != nil && resp.StatusCode != http.StatusNoContent {
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
return fmt.Errorf("decode response: %w", err)
}
}
return nil
}
type notFoundError struct {
path string
}
func (e *notFoundError) Error() string {
return fmt.Sprintf("not found: %s", e.path)
}
func isNotFound(err error) bool {
_, ok := err.(*notFoundError)
return ok
}
+119
View File
@@ -0,0 +1,119 @@
package provider
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ datasource.DataSource = &remoteDataSource{}
type remoteDataSource struct {
client *apiClient
}
func NewRemoteDataSource() datasource.DataSource {
return &remoteDataSource{}
}
func (d *remoteDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_remote"
}
func (d *remoteDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Read an existing ArtifactAPI remote.",
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{Required: true, Description: "Remote name."},
"package_type": schema.StringAttribute{Computed: true},
"base_url": schema.StringAttribute{Computed: true},
"description": schema.StringAttribute{Computed: true},
"immutable_ttl": schema.Int64Attribute{Computed: true},
"mutable_ttl": schema.Int64Attribute{Computed: true},
"check_mutable": schema.BoolAttribute{Computed: true},
"immutable_patterns": schema.ListAttribute{Computed: true, ElementType: types.StringType},
"mutable_patterns": schema.ListAttribute{Computed: true, ElementType: types.StringType},
"allowlist": schema.ListAttribute{Computed: true, ElementType: types.StringType},
"blocklist": schema.ListAttribute{Computed: true, ElementType: types.StringType},
"ban_tags_enabled": schema.BoolAttribute{Computed: true},
"ban_tags": schema.ListAttribute{Computed: true, ElementType: types.StringType},
"quarantine_enabled": schema.BoolAttribute{Computed: true},
"quarantine_days": schema.Int64Attribute{Computed: true},
"stale_on_error": schema.BoolAttribute{Computed: true},
"releases_remote": schema.StringAttribute{Computed: true},
"managed_by": schema.StringAttribute{Computed: true},
},
}
}
type remoteDataSourceModel struct {
Name types.String `tfsdk:"name"`
PackageType types.String `tfsdk:"package_type"`
BaseURL types.String `tfsdk:"base_url"`
Description types.String `tfsdk:"description"`
ImmutableTTL types.Int64 `tfsdk:"immutable_ttl"`
MutableTTL types.Int64 `tfsdk:"mutable_ttl"`
CheckMutable types.Bool `tfsdk:"check_mutable"`
ImmutablePatterns types.List `tfsdk:"immutable_patterns"`
MutablePatterns types.List `tfsdk:"mutable_patterns"`
Allowlist types.List `tfsdk:"allowlist"`
Blocklist types.List `tfsdk:"blocklist"`
BanTagsEnabled types.Bool `tfsdk:"ban_tags_enabled"`
BanTags types.List `tfsdk:"ban_tags"`
QuarantineEnabled types.Bool `tfsdk:"quarantine_enabled"`
QuarantineDays types.Int64 `tfsdk:"quarantine_days"`
StaleOnError types.Bool `tfsdk:"stale_on_error"`
ReleasesRemote types.String `tfsdk:"releases_remote"`
ManagedBy types.String `tfsdk:"managed_by"`
}
func (d *remoteDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*apiClient)
if !ok {
resp.Diagnostics.AddError("unexpected provider data type", fmt.Sprintf("got %T", req.ProviderData))
return
}
d.client = client
}
func (d *remoteDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config remoteDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
var remote remoteAPI
if err := d.client.get(ctx, "/api/v2/remotes/"+config.Name.ValueString(), &remote); err != nil {
resp.Diagnostics.AddError("read remote failed", err.Error())
return
}
state := remoteDataSourceModel{
Name: types.StringValue(remote.Name),
PackageType: types.StringValue(remote.PackageType),
BaseURL: types.StringValue(remote.BaseURL),
Description: types.StringValue(remote.Description),
ImmutableTTL: types.Int64Value(remote.ImmutableTTL),
MutableTTL: types.Int64Value(remote.MutableTTL),
CheckMutable: types.BoolValue(remote.CheckMutable),
ImmutablePatterns: stringsToList(ctx, remote.ImmutablePatterns),
MutablePatterns: stringsToList(ctx, remote.MutablePatterns),
Allowlist: stringsToList(ctx, remote.Allowlist),
Blocklist: stringsToList(ctx, remote.Blocklist),
BanTagsEnabled: types.BoolValue(remote.BanTagsEnabled),
BanTags: stringsToList(ctx, remote.BanTags),
QuarantineEnabled: types.BoolValue(remote.QuarantineEnabled),
QuarantineDays: types.Int64Value(remote.QuarantineDays),
StaleOnError: types.BoolValue(remote.StaleOnError),
ReleasesRemote: types.StringValue(remote.ReleasesRemote),
ManagedBy: types.StringValue(remote.ManagedBy),
}
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}
+80
View File
@@ -0,0 +1,80 @@
package provider
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ datasource.DataSource = &virtualDataSource{}
type virtualDataSource struct {
client *apiClient
}
func NewVirtualDataSource() datasource.DataSource {
return &virtualDataSource{}
}
func (d *virtualDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_virtual"
}
func (d *virtualDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Read an existing ArtifactAPI virtual repository.",
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{Required: true, Description: "Virtual repository name."},
"package_type": schema.StringAttribute{Computed: true},
"description": schema.StringAttribute{Computed: true},
"members": schema.ListAttribute{Computed: true, ElementType: types.StringType},
"managed_by": schema.StringAttribute{Computed: true},
},
}
}
type virtualDataSourceModel struct {
Name types.String `tfsdk:"name"`
PackageType types.String `tfsdk:"package_type"`
Description types.String `tfsdk:"description"`
Members types.List `tfsdk:"members"`
ManagedBy types.String `tfsdk:"managed_by"`
}
func (d *virtualDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*apiClient)
if !ok {
resp.Diagnostics.AddError("unexpected provider data type", fmt.Sprintf("got %T", req.ProviderData))
return
}
d.client = client
}
func (d *virtualDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config virtualDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
var virt virtualAPI
if err := d.client.get(ctx, "/api/v2/virtuals/"+config.Name.ValueString(), &virt); err != nil {
resp.Diagnostics.AddError("read virtual failed", err.Error())
return
}
state := virtualDataSourceModel{
Name: types.StringValue(virt.Name),
PackageType: types.StringValue(virt.PackageType),
Description: types.StringValue(virt.Description),
Members: stringsToList(ctx, virt.Members),
ManagedBy: types.StringValue(virt.ManagedBy),
}
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}
+32
View File
@@ -0,0 +1,32 @@
package provider
type remoteAPI struct {
Name string `json:"name"`
PackageType string `json:"package_type"`
BaseURL string `json:"base_url"`
Description string `json:"description,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
ImmutableTTL int64 `json:"immutable_ttl"`
MutableTTL int64 `json:"mutable_ttl"`
CheckMutable bool `json:"check_mutable"`
ImmutablePatterns []string `json:"immutable_patterns"`
MutablePatterns []string `json:"mutable_patterns"`
Allowlist []string `json:"allowlist"`
Blocklist []string `json:"blocklist"`
BanTagsEnabled bool `json:"ban_tags_enabled"`
BanTags []string `json:"ban_tags"`
QuarantineEnabled bool `json:"quarantine_enabled"`
QuarantineDays int64 `json:"quarantine_days"`
StaleOnError bool `json:"stale_on_error"`
ReleasesRemote string `json:"releases_remote,omitempty"`
ManagedBy string `json:"managed_by,omitempty"`
}
type virtualAPI struct {
Name string `json:"name"`
PackageType string `json:"package_type"`
Description string `json:"description,omitempty"`
Members []string `json:"members"`
ManagedBy string `json:"managed_by,omitempty"`
}
+70
View File
@@ -0,0 +1,70 @@
package provider
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ provider.Provider = &ArtifactAPIProvider{}
type ArtifactAPIProvider struct {
version string
}
type artifactAPIProviderModel struct {
Endpoint types.String `tfsdk:"endpoint"`
}
func New(version string) func() provider.Provider {
return func() provider.Provider {
return &ArtifactAPIProvider{version: version}
}
}
func (p *ArtifactAPIProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "artifactapi"
resp.Version = p.version
}
func (p *ArtifactAPIProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Manage ArtifactAPI remotes and virtual repositories.",
Attributes: map[string]schema.Attribute{
"endpoint": schema.StringAttribute{
Description: "The ArtifactAPI server endpoint URL (e.g. https://artifactapi.example.com).",
Required: true,
},
},
}
}
func (p *ArtifactAPIProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var config artifactAPIProviderModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
client := newAPIClient(config.Endpoint.ValueString())
resp.DataSourceData = client
resp.ResourceData = client
}
func (p *ArtifactAPIProvider) Resources(_ context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewRemoteResource,
NewVirtualResource,
}
}
func (p *ArtifactAPIProvider) DataSources(_ context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewRemoteDataSource,
NewVirtualDataSource,
}
}
+332
View File
@@ -0,0 +1,332 @@
package provider
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ resource.Resource = &remoteResource{}
_ resource.ResourceWithImportState = &remoteResource{}
)
type remoteResource struct {
client *apiClient
}
type remoteResourceModel struct {
Name types.String `tfsdk:"name"`
PackageType types.String `tfsdk:"package_type"`
BaseURL types.String `tfsdk:"base_url"`
Description types.String `tfsdk:"description"`
Username types.String `tfsdk:"username"`
Password types.String `tfsdk:"password"`
ImmutableTTL types.Int64 `tfsdk:"immutable_ttl"`
MutableTTL types.Int64 `tfsdk:"mutable_ttl"`
CheckMutable types.Bool `tfsdk:"check_mutable"`
ImmutablePatterns types.List `tfsdk:"immutable_patterns"`
MutablePatterns types.List `tfsdk:"mutable_patterns"`
Allowlist types.List `tfsdk:"allowlist"`
Blocklist types.List `tfsdk:"blocklist"`
BanTagsEnabled types.Bool `tfsdk:"ban_tags_enabled"`
BanTags types.List `tfsdk:"ban_tags"`
QuarantineEnabled types.Bool `tfsdk:"quarantine_enabled"`
QuarantineDays types.Int64 `tfsdk:"quarantine_days"`
StaleOnError types.Bool `tfsdk:"stale_on_error"`
ReleasesRemote types.String `tfsdk:"releases_remote"`
}
func NewRemoteResource() resource.Resource {
return &remoteResource{}
}
func (r *remoteResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_remote"
}
func (r *remoteResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Manages an ArtifactAPI remote proxy repository.",
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Description: "Unique name of the remote.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"package_type": schema.StringAttribute{
Description: "Package type: generic, docker, helm, pypi, npm, rpm, alpine, puppet, terraform, goproxy.",
Required: true,
},
"base_url": schema.StringAttribute{
Description: "Upstream repository base URL.",
Required: true,
},
"description": schema.StringAttribute{
Description: "Human-readable description.",
Optional: true,
Computed: true,
Default: stringdefault.StaticString(""),
},
"username": schema.StringAttribute{
Description: "Username for upstream authentication.",
Optional: true,
Computed: true,
Sensitive: true,
Default: stringdefault.StaticString(""),
},
"password": schema.StringAttribute{
Description: "Password for upstream authentication.",
Optional: true,
Computed: true,
Sensitive: true,
Default: stringdefault.StaticString(""),
},
"immutable_ttl": schema.Int64Attribute{
Description: "TTL in seconds for immutable artifacts (0 = forever).",
Optional: true,
Computed: true,
Default: int64default.StaticInt64(0),
},
"mutable_ttl": schema.Int64Attribute{
Description: "TTL in seconds for mutable artifacts.",
Optional: true,
Computed: true,
Default: int64default.StaticInt64(3600),
},
"check_mutable": schema.BoolAttribute{
Description: "Enable conditional revalidation (ETag/If-None-Match) for mutable artifacts.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
"immutable_patterns": schema.ListAttribute{
Description: "Regex patterns that identify immutable artifacts.",
Optional: true,
ElementType: types.StringType,
},
"mutable_patterns": schema.ListAttribute{
Description: "Additional regex patterns for mutable artifacts (merged with provider built-ins).",
Optional: true,
ElementType: types.StringType,
},
"allowlist": schema.ListAttribute{
Description: "If non-empty, only paths matching these patterns are proxied. Empty = allow all.",
Optional: true,
ElementType: types.StringType,
},
"blocklist": schema.ListAttribute{
Description: "Paths matching these patterns are always denied (checked before allowlist).",
Optional: true,
ElementType: types.StringType,
},
"ban_tags_enabled": schema.BoolAttribute{
Description: "Enable tag banning (Docker only).",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
},
"ban_tags": schema.ListAttribute{
Description: "Tags to ban (Docker only).",
Optional: true,
ElementType: types.StringType,
},
"quarantine_enabled": schema.BoolAttribute{
Description: "Enable quarantine for newly published artifacts.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
},
"quarantine_days": schema.Int64Attribute{
Description: "Number of days to quarantine new artifacts.",
Optional: true,
Computed: true,
Default: int64default.StaticInt64(3),
},
"stale_on_error": schema.BoolAttribute{
Description: "Serve stale cached content when upstream is unreachable.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
"releases_remote": schema.StringAttribute{
Description: "Name of the CDN remote for download URL rewriting (terraform package type).",
Optional: true,
Computed: true,
Default: stringdefault.StaticString(""),
},
},
}
}
func (r *remoteResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*apiClient)
if !ok {
resp.Diagnostics.AddError("unexpected provider data type", fmt.Sprintf("got %T", req.ProviderData))
return
}
r.client = client
}
func (r *remoteResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan remoteResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
api := modelToAPI(ctx, plan)
api.ManagedBy = "terraform"
var created remoteAPI
if err := r.client.post(ctx, "/api/v2/remotes", api, &created); err != nil {
resp.Diagnostics.AddError("create remote failed", err.Error())
return
}
state := apiToModel(ctx, created)
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}
func (r *remoteResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state remoteResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
var remote remoteAPI
err := r.client.get(ctx, "/api/v2/remotes/"+state.Name.ValueString(), &remote)
if err != nil {
if isNotFound(err) {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("read remote failed", err.Error())
return
}
newState := apiToModel(ctx, remote)
resp.Diagnostics.Append(resp.State.Set(ctx, newState)...)
}
func (r *remoteResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan remoteResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
api := modelToAPI(ctx, plan)
api.ManagedBy = "terraform"
var updated remoteAPI
if err := r.client.put(ctx, "/api/v2/remotes/"+plan.Name.ValueString(), api, &updated); err != nil {
resp.Diagnostics.AddError("update remote failed", err.Error())
return
}
state := apiToModel(ctx, updated)
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}
func (r *remoteResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state remoteResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
if err := r.client.del(ctx, "/api/v2/remotes/"+state.Name.ValueString()); err != nil {
resp.Diagnostics.AddError("delete remote failed", err.Error())
return
}
}
func (r *remoteResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp)
}
func modelToAPI(ctx context.Context, m remoteResourceModel) remoteAPI {
api := remoteAPI{
Name: m.Name.ValueString(),
PackageType: m.PackageType.ValueString(),
BaseURL: m.BaseURL.ValueString(),
Description: m.Description.ValueString(),
Username: m.Username.ValueString(),
Password: m.Password.ValueString(),
ImmutableTTL: m.ImmutableTTL.ValueInt64(),
MutableTTL: m.MutableTTL.ValueInt64(),
CheckMutable: m.CheckMutable.ValueBool(),
BanTagsEnabled: m.BanTagsEnabled.ValueBool(),
QuarantineEnabled: m.QuarantineEnabled.ValueBool(),
QuarantineDays: m.QuarantineDays.ValueInt64(),
StaleOnError: m.StaleOnError.ValueBool(),
ReleasesRemote: m.ReleasesRemote.ValueString(),
}
api.ImmutablePatterns = listToStrings(ctx, m.ImmutablePatterns)
api.MutablePatterns = listToStrings(ctx, m.MutablePatterns)
api.Allowlist = listToStrings(ctx, m.Allowlist)
api.Blocklist = listToStrings(ctx, m.Blocklist)
api.BanTags = listToStrings(ctx, m.BanTags)
return api
}
func apiToModel(ctx context.Context, api remoteAPI) remoteResourceModel {
return remoteResourceModel{
Name: types.StringValue(api.Name),
PackageType: types.StringValue(api.PackageType),
BaseURL: types.StringValue(api.BaseURL),
Description: types.StringValue(api.Description),
Username: types.StringValue(api.Username),
Password: types.StringValue(api.Password),
ImmutableTTL: types.Int64Value(api.ImmutableTTL),
MutableTTL: types.Int64Value(api.MutableTTL),
CheckMutable: types.BoolValue(api.CheckMutable),
ImmutablePatterns: stringsToList(ctx, api.ImmutablePatterns),
MutablePatterns: stringsToList(ctx, api.MutablePatterns),
Allowlist: stringsToList(ctx, api.Allowlist),
Blocklist: stringsToList(ctx, api.Blocklist),
BanTagsEnabled: types.BoolValue(api.BanTagsEnabled),
BanTags: stringsToList(ctx, api.BanTags),
QuarantineEnabled: types.BoolValue(api.QuarantineEnabled),
QuarantineDays: types.Int64Value(api.QuarantineDays),
StaleOnError: types.BoolValue(api.StaleOnError),
ReleasesRemote: types.StringValue(api.ReleasesRemote),
}
}
func listToStrings(ctx context.Context, l types.List) []string {
if l.IsNull() || l.IsUnknown() {
return nil
}
var result []string
l.ElementsAs(ctx, &result, false)
return result
}
func stringsToList(ctx context.Context, ss []string) types.List {
if ss == nil {
ss = []string{}
}
elems := make([]types.String, len(ss))
for i, s := range ss {
elems[i] = types.StringValue(s)
}
list, _ := types.ListValueFrom(ctx, types.StringType, elems)
return list
}
+177
View File
@@ -0,0 +1,177 @@
package provider
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ resource.Resource = &virtualResource{}
_ resource.ResourceWithImportState = &virtualResource{}
)
type virtualResource struct {
client *apiClient
}
type virtualResourceModel struct {
Name types.String `tfsdk:"name"`
PackageType types.String `tfsdk:"package_type"`
Description types.String `tfsdk:"description"`
Members types.List `tfsdk:"members"`
}
func NewVirtualResource() resource.Resource {
return &virtualResource{}
}
func (r *virtualResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_virtual"
}
func (r *virtualResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Manages an ArtifactAPI virtual repository that merges multiple remotes.",
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Description: "Unique name of the virtual repository.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"package_type": schema.StringAttribute{
Description: "Package type (must match member remotes): helm, pypi.",
Required: true,
},
"description": schema.StringAttribute{
Description: "Human-readable description.",
Optional: true,
Computed: true,
Default: stringdefault.StaticString(""),
},
"members": schema.ListAttribute{
Description: "Ordered list of member remote names. Earlier members have higher priority for duplicate entries.",
Required: true,
ElementType: types.StringType,
},
},
}
}
func (r *virtualResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*apiClient)
if !ok {
resp.Diagnostics.AddError("unexpected provider data type", fmt.Sprintf("got %T", req.ProviderData))
return
}
r.client = client
}
func (r *virtualResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan virtualResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
api := virtualModelToAPI(ctx, plan)
api.ManagedBy = "terraform"
var created virtualAPI
if err := r.client.post(ctx, "/api/v2/virtuals", api, &created); err != nil {
resp.Diagnostics.AddError("create virtual failed", err.Error())
return
}
state := virtualAPIToModel(ctx, created)
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}
func (r *virtualResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state virtualResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
var virt virtualAPI
err := r.client.get(ctx, "/api/v2/virtuals/"+state.Name.ValueString(), &virt)
if err != nil {
if isNotFound(err) {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("read virtual failed", err.Error())
return
}
newState := virtualAPIToModel(ctx, virt)
resp.Diagnostics.Append(resp.State.Set(ctx, newState)...)
}
func (r *virtualResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan virtualResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
api := virtualModelToAPI(ctx, plan)
api.ManagedBy = "terraform"
var updated virtualAPI
if err := r.client.put(ctx, "/api/v2/virtuals/"+plan.Name.ValueString(), api, &updated); err != nil {
resp.Diagnostics.AddError("update virtual failed", err.Error())
return
}
state := virtualAPIToModel(ctx, updated)
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}
func (r *virtualResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state virtualResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
if err := r.client.del(ctx, "/api/v2/virtuals/"+state.Name.ValueString()); err != nil {
resp.Diagnostics.AddError("delete virtual failed", err.Error())
return
}
}
func (r *virtualResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp)
}
func virtualModelToAPI(ctx context.Context, m virtualResourceModel) virtualAPI {
return virtualAPI{
Name: m.Name.ValueString(),
PackageType: m.PackageType.ValueString(),
Description: m.Description.ValueString(),
Members: listToStrings(ctx, m.Members),
}
}
func virtualAPIToModel(ctx context.Context, api virtualAPI) virtualResourceModel {
return virtualResourceModel{
Name: types.StringValue(api.Name),
PackageType: types.StringValue(api.PackageType),
Description: types.StringValue(api.Description),
Members: stringsToList(ctx, api.Members),
}
}