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