Add serve_stale, cache_repodata, sync_interval and parallel_fetch
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful

The artifactapi PUT is a full replace, so unmodelled fields reset on every apply.
This commit is contained in:
2026-10-25 23:03:02 +11:00
parent 10266f2b45
commit bb91098128
15 changed files with 297 additions and 33 deletions
+1
View File
@@ -21,6 +21,7 @@ Per-type remote resources manage upstream repository proxies. Each type applies
| `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 |
| `serve_stale` | No | `false` | Let rpm/deb/alpine virtuals serve this member's last good index while it refreshes |
| `upstream_dial_timeout` | No | `0` | Upstream TCP connect timeout in seconds (0 = server default) |
| `upstream_tls_timeout` | No | `0` | Upstream TLS handshake timeout in seconds (0 = server default) |
| `upstream_response_header_timeout` | No | `0` | Upstream response-header timeout in seconds (0 = server default) |
+1
View File
@@ -21,6 +21,7 @@ Per-type remote resources manage upstream repository proxies. Each type applies
| `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 |
| `serve_stale` | No | `false` | Let rpm/deb/alpine virtuals serve this member's last good index while it refreshes |
| `upstream_dial_timeout` | No | `0` | Upstream TCP connect timeout in seconds (0 = server default) |
| `upstream_tls_timeout` | No | `0` | Upstream TLS handshake timeout in seconds (0 = server default) |
| `upstream_response_header_timeout` | No | `0` | Upstream response-header timeout in seconds (0 = server default) |
+2
View File
@@ -21,6 +21,7 @@ Per-type remote resources manage upstream repository proxies. Each type applies
| `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 |
| `serve_stale` | No | `false` | Let rpm/deb/alpine virtuals serve this member's last good index while it refreshes |
| `upstream_dial_timeout` | No | `0` | Upstream TCP connect timeout in seconds (0 = server default) |
| `upstream_tls_timeout` | No | `0` | Upstream TLS handshake timeout in seconds (0 = server default) |
| `upstream_response_header_timeout` | No | `0` | Upstream response-header timeout in seconds (0 = server default) |
@@ -30,6 +31,7 @@ Per-type remote resources manage upstream repository proxies. Each type applies
| Attribute | Default | Description |
|-------------------|---------|----------------------------------------------------------|
| `releases_remote` | `""` | Name of a backend remote that serves the download bytes. Terraform uses it for download URL rewriting; `github_rpm`/`github_deb`/`github_alpine` 302-redirect `.rpm`/`.deb`/`.apk` downloads to it. |
| `sync_interval` | `0` | Release re-scan interval in seconds, `0`..`2592000` (`0` = `mutable_ttl`). `github_rpm`/`github_deb`/`github_alpine` only; rejected at plan time elsewhere. |
## Notes
+2
View File
@@ -21,6 +21,7 @@ Per-type remote resources manage upstream repository proxies. Each type applies
| `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 |
| `serve_stale` | No | `false` | Let rpm/deb/alpine virtuals serve this member's last good index while it refreshes |
| `upstream_dial_timeout` | No | `0` | Upstream TCP connect timeout in seconds (0 = server default) |
| `upstream_tls_timeout` | No | `0` | Upstream TLS handshake timeout in seconds (0 = server default) |
| `upstream_response_header_timeout` | No | `0` | Upstream response-header timeout in seconds (0 = server default) |
@@ -30,6 +31,7 @@ Per-type remote resources manage upstream repository proxies. Each type applies
| Attribute | Default | Description |
|-------------------|---------|----------------------------------------------------------|
| `releases_remote` | `""` | Name of a backend remote that serves the download bytes. Terraform uses it for download URL rewriting; `github_rpm`/`github_deb`/`github_alpine` 302-redirect `.rpm`/`.deb`/`.apk` downloads to it. |
| `sync_interval` | `0` | Release re-scan interval in seconds, `0`..`2592000` (`0` = `mutable_ttl`). `github_rpm`/`github_deb`/`github_alpine` only; rejected at plan time elsewhere. |
## Notes
+3
View File
@@ -21,6 +21,7 @@ Per-type remote resources manage upstream repository proxies. Each type applies
| `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 |
| `serve_stale` | No | `false` | Let rpm/deb/alpine virtuals serve this member's last good index while it refreshes |
| `upstream_dial_timeout` | No | `0` | Upstream TCP connect timeout in seconds (0 = server default) |
| `upstream_tls_timeout` | No | `0` | Upstream TLS handshake timeout in seconds (0 = server default) |
| `upstream_response_header_timeout` | No | `0` | Upstream response-header timeout in seconds (0 = server default) |
@@ -30,6 +31,8 @@ Per-type remote resources manage upstream repository proxies. Each type applies
| Attribute | Default | Description |
|-------------------|---------|----------------------------------------------------------|
| `releases_remote` | `""` | Name of a backend remote that serves the download bytes. Terraform uses it for download URL rewriting; `github_rpm`/`github_deb`/`github_alpine` 302-redirect `.rpm`/`.deb`/`.apk` downloads to it. |
| `cache_repodata` | `false` | Cache generated repodata (`github_rpm` only). |
| `sync_interval` | `0` | Release re-scan interval in seconds, `0`..`2592000` (`0` = `mutable_ttl`). `github_rpm`/`github_deb`/`github_alpine` only; rejected at plan time elsewhere. |
## Notes
+1
View File
@@ -21,6 +21,7 @@ Per-type remote resources manage upstream repository proxies. Each type applies
| `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 |
| `serve_stale` | No | `false` | Let rpm/deb/alpine virtuals serve this member's last good index while it refreshes |
| `upstream_dial_timeout` | No | `0` | Upstream TCP connect timeout in seconds (0 = server default) |
| `upstream_tls_timeout` | No | `0` | Upstream TLS handshake timeout in seconds (0 = server default) |
| `upstream_response_header_timeout` | No | `0` | Upstream response-header timeout in seconds (0 = server default) |
+1
View File
@@ -21,3 +21,4 @@ resource "artifactapi_virtual" "helm" {
| `package_type` | Yes | Package type of member remotes |
| `description` | No | Human-readable description |
| `members` | Yes | List of remote names to include |
| `parallel_fetch` | No | Fetch each member's index files concurrently (rpm/deb); default `false` |
@@ -33,6 +33,12 @@ resource "artifactapi_remote_github_rpm" "acme_tools" {
mutable_ttl = 3600
# Optional: re-scan releases every 15 minutes, cache the generated repodata,
# and let virtuals serve the last good index while this remote refreshes.
sync_interval = 900
cache_repodata = true
serve_stale = true
# Optional: restrict which release assets become packages.
patterns = [
".*\\.x86_64\\.rpm$",
+9
View File
@@ -47,6 +47,9 @@ func (d *remoteDataSource) Schema(_ context.Context, _ datasource.SchemaRequest,
"quarantine_enabled": schema.BoolAttribute{Computed: true},
"quarantine_days": schema.Int64Attribute{Computed: true},
"stale_on_error": schema.BoolAttribute{Computed: true},
"serve_stale": schema.BoolAttribute{Computed: true},
"cache_repodata": schema.BoolAttribute{Computed: true},
"sync_interval": schema.Int64Attribute{Computed: true},
"releases_remote": schema.StringAttribute{Computed: true},
"managed_by": schema.StringAttribute{Computed: true},
@@ -76,6 +79,9 @@ type remoteDataSourceModel struct {
QuarantineEnabled types.Bool `tfsdk:"quarantine_enabled"`
QuarantineDays types.Int64 `tfsdk:"quarantine_days"`
StaleOnError types.Bool `tfsdk:"stale_on_error"`
ServeStale types.Bool `tfsdk:"serve_stale"`
CacheRepodata types.Bool `tfsdk:"cache_repodata"`
SyncInterval types.Int64 `tfsdk:"sync_interval"`
ReleasesRemote types.String `tfsdk:"releases_remote"`
ManagedBy types.String `tfsdk:"managed_by"`
@@ -128,6 +134,9 @@ func (d *remoteDataSource) Read(ctx context.Context, req datasource.ReadRequest,
QuarantineEnabled: types.BoolValue(remote.QuarantineEnabled),
QuarantineDays: types.Int64Value(remote.QuarantineDays),
StaleOnError: types.BoolValue(remote.StaleOnError),
ServeStale: types.BoolValue(remote.ServeStale),
CacheRepodata: types.BoolValue(remote.CacheRepodata),
SyncInterval: types.Int64Value(remote.SyncInterval),
ReleasesRemote: types.StringValue(remote.ReleasesRemote),
ManagedBy: types.StringValue(remote.ManagedBy),
+18 -15
View File
@@ -29,21 +29,23 @@ func (d *virtualDataSource) Schema(_ context.Context, _ datasource.SchemaRequest
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},
"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},
"parallel_fetch": schema.BoolAttribute{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"`
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"`
ParallelFetch types.Bool `tfsdk:"parallel_fetch"`
}
func (d *virtualDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
@@ -72,11 +74,12 @@ func (d *virtualDataSource) Read(ctx context.Context, req datasource.ReadRequest
}
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),
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),
ParallelFetch: types.BoolValue(virt.ParallelFetch),
}
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}
+9 -5
View File
@@ -22,6 +22,9 @@ type remoteAPI struct {
QuarantineEnabled bool `json:"quarantine_enabled"`
QuarantineDays int64 `json:"quarantine_days"`
StaleOnError bool `json:"stale_on_error"`
ServeStale bool `json:"serve_stale"`
CacheRepodata bool `json:"cache_repodata"`
SyncInterval int64 `json:"sync_interval"`
ReleasesRemote string `json:"releases_remote,omitempty"`
ManagedBy string `json:"managed_by,omitempty"`
@@ -31,9 +34,10 @@ type remoteAPI struct {
}
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"`
Name string `json:"name"`
PackageType string `json:"package_type"`
Description string `json:"description,omitempty"`
Members []string `json:"members"`
ManagedBy string `json:"managed_by,omitempty"`
ParallelFetch bool `json:"parallel_fetch"`
}
+52
View File
@@ -33,6 +33,13 @@ const defaultMirrorStrategy = "round_robin"
// validMirrorStrategies are the accepted mirror_strategy values.
var validMirrorStrategies = map[string]bool{"round_robin": true, "least_conn": true}
// syncIntervalPackageTypes are the remote types with a background release
// syncer, the only ones accepting sync_interval.
var syncIntervalPackageTypes = map[string]bool{"github_rpm": true, "github_deb": true, "github_alpine": true}
// maxSyncInterval is the API's 30-day cap on sync_interval, in seconds.
const maxSyncInterval = 30 * 24 * 60 * 60
type remoteResource struct {
client *apiClient
packageType string
@@ -58,6 +65,9 @@ type remoteResourceModel struct {
QuarantineEnabled types.Bool `tfsdk:"quarantine_enabled"`
QuarantineDays types.Int64 `tfsdk:"quarantine_days"`
StaleOnError types.Bool `tfsdk:"stale_on_error"`
ServeStale types.Bool `tfsdk:"serve_stale"`
CacheRepodata types.Bool `tfsdk:"cache_repodata"`
SyncInterval types.Int64 `tfsdk:"sync_interval"`
ReleasesRemote types.String `tfsdk:"releases_remote"`
UpstreamDialTimeout types.Int64 `tfsdk:"upstream_dial_timeout"`
@@ -148,6 +158,18 @@ func (r *remoteResource) Schema(_ context.Context, _ resource.SchemaRequest, res
Description: "Serve stale cached content when upstream is unreachable.",
Optional: true, Computed: true, Default: booldefault.StaticBool(true),
},
"serve_stale": schema.BoolAttribute{
Description: "Let rpm/deb/alpine virtuals serve this member's last good index while it refreshes.",
Optional: true, Computed: true, Default: booldefault.StaticBool(false),
},
"cache_repodata": schema.BoolAttribute{
Description: "Cache generated repodata (github_rpm only).",
Optional: true, Computed: true, Default: booldefault.StaticBool(false),
},
"sync_interval": schema.Int64Attribute{
Description: "Release re-scan interval in seconds, 0..2592000 (0 = mutable_ttl); github_rpm/github_deb/github_alpine only.",
Optional: true, Computed: true, Default: int64default.StaticInt64(0),
},
"ban_tags_enabled": schema.BoolAttribute{
Description: "Enable tag banning (docker only).",
Optional: true, Computed: true, Default: booldefault.StaticBool(false),
@@ -304,6 +326,30 @@ func (r *remoteResource) ValidateConfig(ctx context.Context, req resource.Valida
)
}
}
if !config.CacheRepodata.IsNull() && !config.CacheRepodata.IsUnknown() && r.packageType != "github_rpm" {
resp.Diagnostics.AddAttributeError(
path.Root("cache_repodata"),
"cache_repodata not supported for this remote type",
fmt.Sprintf("cache_repodata is only valid on github_rpm remotes, not %q remotes.", r.packageType),
)
}
if !config.SyncInterval.IsNull() && !config.SyncInterval.IsUnknown() {
if !syncIntervalPackageTypes[r.packageType] {
resp.Diagnostics.AddAttributeError(
path.Root("sync_interval"),
"sync_interval not supported for this remote type",
fmt.Sprintf("sync_interval is only valid on github_rpm/github_deb/github_alpine remotes, not %q remotes.", r.packageType),
)
} else if v := config.SyncInterval.ValueInt64(); v < 0 || v > maxSyncInterval {
resp.Diagnostics.AddAttributeError(
path.Root("sync_interval"),
"invalid sync_interval",
fmt.Sprintf("sync_interval must be between 0 and %d seconds, got %d.", maxSyncInterval, v),
)
}
}
}
func reconcileOptionalLists(prior, current *remoteResourceModel) {
@@ -329,6 +375,9 @@ func (r *remoteResource) modelToAPI(ctx context.Context, m remoteResourceModel)
QuarantineEnabled: m.QuarantineEnabled.ValueBool(),
QuarantineDays: m.QuarantineDays.ValueInt64(),
StaleOnError: m.StaleOnError.ValueBool(),
ServeStale: m.ServeStale.ValueBool(),
CacheRepodata: m.CacheRepodata.ValueBool(),
SyncInterval: m.SyncInterval.ValueInt64(),
UpstreamDialTimeout: m.UpstreamDialTimeout.ValueInt64(),
UpstreamTLSTimeout: m.UpstreamTLSTimeout.ValueInt64(),
@@ -375,6 +424,9 @@ func (r *remoteResource) apiToModel(ctx context.Context, api remoteAPI) remoteRe
QuarantineEnabled: types.BoolValue(api.QuarantineEnabled),
QuarantineDays: types.Int64Value(api.QuarantineDays),
StaleOnError: types.BoolValue(api.StaleOnError),
ServeStale: types.BoolValue(api.ServeStale),
CacheRepodata: types.BoolValue(api.CacheRepodata),
SyncInterval: types.Int64Value(api.SyncInterval),
BanTagsEnabled: types.BoolValue(api.BanTagsEnabled),
BanTags: stringsToList(ctx, api.BanTags),
ReleasesRemote: types.StringValue(api.ReleasesRemote),
+138
View File
@@ -2,9 +2,14 @@ package provider
import (
"context"
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-go/tftypes"
@@ -770,3 +775,136 @@ func TestValidateConfig_MirrorStrategy(t *testing.T) {
}
})
}
func TestRemoteResource_SchemaCacheSyncFields(t *testing.T) {
r := &remoteResource{packageType: "github_rpm"}
var resp resource.SchemaResponse
r.Schema(context.Background(), resource.SchemaRequest{}, &resp)
for _, name := range []string{"serve_stale", "cache_repodata", "sync_interval"} {
attr, ok := resp.Schema.Attributes[name]
if !ok {
t.Fatalf("missing %s attribute in schema", name)
}
if !attr.IsOptional() || !attr.IsComputed() {
t.Errorf("%s must be Optional+Computed so the API default never churns", name)
}
}
ctx := context.Background()
for _, name := range []string{"serve_stale", "cache_repodata"} {
var d defaults.BoolResponse
resp.Schema.Attributes[name].(schema.BoolAttribute).Default.DefaultBool(ctx, defaults.BoolRequest{}, &d)
if d.PlanValue.ValueBool() {
t.Errorf("%s: expected default false", name)
}
}
var d defaults.Int64Response
resp.Schema.Attributes["sync_interval"].(schema.Int64Attribute).Default.DefaultInt64(ctx, defaults.Int64Request{}, &d)
if d.PlanValue.ValueInt64() != 0 {
t.Errorf("sync_interval: expected default 0, got %d", d.PlanValue.ValueInt64())
}
}
func TestRemoteCacheSyncFields_RoundTrip(t *testing.T) {
ctx := context.Background()
r := &remoteResource{packageType: "github_rpm"}
model := r.apiToModel(ctx, remoteAPI{
Name: "gh", PackageType: "github_rpm", BaseURL: "https://api.github.com/repos/acme/tools",
ServeStale: true, CacheRepodata: true, SyncInterval: 900,
})
if !model.ServeStale.ValueBool() || !model.CacheRepodata.ValueBool() || model.SyncInterval.ValueInt64() != 900 {
t.Errorf("apiToModel: got serve_stale=%v cache_repodata=%v sync_interval=%d",
model.ServeStale, model.CacheRepodata, model.SyncInterval)
}
api := r.modelToAPI(ctx, model)
if !api.ServeStale || !api.CacheRepodata || api.SyncInterval != 900 {
t.Errorf("modelToAPI: got serve_stale=%v cache_repodata=%v sync_interval=%d",
api.ServeStale, api.CacheRepodata, api.SyncInterval)
}
// The PUT is a full replace, so false/0 must be sent explicitly, not omitted.
body, err := json.Marshal(r.modelToAPI(ctx, r.apiToModel(ctx, remoteAPI{Name: "z", PackageType: "github_rpm"})))
if err != nil {
t.Fatal(err)
}
for _, key := range []string{`"serve_stale":false`, `"cache_repodata":false`, `"sync_interval":0`} {
if !strings.Contains(string(body), key) {
t.Errorf("request body missing %s: %s", key, body)
}
}
}
// remoteConfig builds a tfsdk.Config for the remote schema with every
// attribute null except those in set.
func remoteConfig(ctx context.Context, t *testing.T, r *remoteResource, set map[string]tftypes.Value) tfsdk.Config {
t.Helper()
var resp resource.SchemaResponse
r.Schema(ctx, resource.SchemaRequest{}, &resp)
objType := resp.Schema.Type().TerraformType(ctx).(tftypes.Object)
attrs := make(map[string]tftypes.Value, len(objType.AttributeTypes))
for name, at := range objType.AttributeTypes {
attrs[name] = tftypes.NewValue(at, nil)
}
for name, v := range set {
attrs[name] = v
}
return tfsdk.Config{Schema: resp.Schema, Raw: tftypes.NewValue(objType, attrs)}
}
func TestValidateConfig_CacheRepodata(t *testing.T) {
ctx := context.Background()
set := map[string]tftypes.Value{"cache_repodata": tftypes.NewValue(tftypes.Bool, true)}
cases := map[string]bool{"github_rpm": false, "github_deb": true, "github_alpine": true, "rpm": true, "docker": true}
for pkgType, wantErr := range cases {
t.Run(pkgType, func(t *testing.T) {
r := &remoteResource{packageType: pkgType}
var resp resource.ValidateConfigResponse
r.ValidateConfig(ctx, resource.ValidateConfigRequest{Config: remoteConfig(ctx, t, r, set)}, &resp)
if resp.Diagnostics.HasError() != wantErr {
t.Errorf("cache_repodata on %s: wantErr=%v, got %v", pkgType, wantErr, resp.Diagnostics.Errors())
}
})
}
}
func TestValidateConfig_SyncInterval(t *testing.T) {
ctx := context.Background()
cases := []struct {
pkgType string
value int64
wantErr bool
}{
{"github_rpm", 0, false},
{"github_deb", 3600, false},
{"github_alpine", maxSyncInterval, false},
{"github_rpm", -1, true},
{"github_rpm", maxSyncInterval + 1, true},
{"rpm", 3600, true},
{"docker", 0, true},
}
for _, tc := range cases {
t.Run(fmt.Sprintf("%s/%d", tc.pkgType, tc.value), func(t *testing.T) {
r := &remoteResource{packageType: tc.pkgType}
set := map[string]tftypes.Value{"sync_interval": tftypes.NewValue(tftypes.Number, tc.value)}
var resp resource.ValidateConfigResponse
r.ValidateConfig(ctx, resource.ValidateConfigRequest{Config: remoteConfig(ctx, t, r, set)}, &resp)
if resp.Diagnostics.HasError() != tc.wantErr {
t.Errorf("wantErr=%v, got %v", tc.wantErr, resp.Diagnostics.Errors())
}
})
}
t.Run("null-on-docker", func(t *testing.T) {
r := &remoteResource{packageType: "docker"}
var resp resource.ValidateConfigResponse
r.ValidateConfig(ctx, resource.ValidateConfigRequest{Config: remoteConfig(ctx, t, r, nil)}, &resp)
if resp.Diagnostics.HasError() {
t.Errorf("unexpected error for unset cache/sync fields on docker: %v", resp.Diagnostics.Errors())
}
})
}
+22 -12
View File
@@ -7,6 +7,7 @@ import (
"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/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
@@ -23,10 +24,11 @@ type virtualResource struct {
}
type virtualResourceModel struct {
Name types.String `tfsdk:"name"`
PackageType types.String `tfsdk:"package_type"`
Description types.String `tfsdk:"description"`
Members types.List `tfsdk:"members"`
Name types.String `tfsdk:"name"`
PackageType types.String `tfsdk:"package_type"`
Description types.String `tfsdk:"description"`
Members types.List `tfsdk:"members"`
ParallelFetch types.Bool `tfsdk:"parallel_fetch"`
}
func init() { registerResource(NewVirtualResource) }
@@ -65,6 +67,12 @@ func (r *virtualResource) Schema(_ context.Context, _ resource.SchemaRequest, re
Required: true,
ElementType: types.StringType,
},
"parallel_fetch": schema.BoolAttribute{
Description: "Fetch each member's index files concurrently.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
},
},
}
}
@@ -162,18 +170,20 @@ func (r *virtualResource) ImportState(ctx context.Context, req resource.ImportSt
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),
Name: m.Name.ValueString(),
PackageType: m.PackageType.ValueString(),
Description: m.Description.ValueString(),
Members: listToStrings(ctx, m.Members),
ParallelFetch: m.ParallelFetch.ValueBool(),
}
}
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),
Name: types.StringValue(api.Name),
PackageType: types.StringValue(api.PackageType),
Description: types.StringValue(api.Description),
Members: stringsToList(ctx, api.Members),
ParallelFetch: types.BoolValue(api.ParallelFetch),
}
}
+32 -1
View File
@@ -2,9 +2,13 @@ package provider
import (
"context"
"encoding/json"
"strings"
"testing"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults"
"github.com/hashicorp/terraform-plugin-framework/types"
)
@@ -110,11 +114,17 @@ func TestVirtualRoundTrip(t *testing.T) {
PackageType: "helm",
Description: "Round trip test",
Members: []string{"a", "b", "c"},
ParallelFetch: true,
}
model := virtualAPIToModel(ctx, original)
result := virtualModelToAPI(ctx, model)
if !model.ParallelFetch.ValueBool() || !result.ParallelFetch {
t.Errorf("ParallelFetch: expected true through the round trip, got model=%v api=%v", model.ParallelFetch, result.ParallelFetch)
}
if result.Name != original.Name {
t.Errorf("Name: expected %s, got %s", original.Name, result.Name)
}
@@ -150,12 +160,33 @@ func TestVirtualResource_Schema(t *testing.T) {
var resp resource.SchemaResponse
r.Schema(context.Background(), req, &resp)
expectedAttrs := []string{"name", "package_type", "description", "members"}
expectedAttrs := []string{"name", "package_type", "description", "members", "parallel_fetch"}
for _, attr := range expectedAttrs {
if _, ok := resp.Schema.Attributes[attr]; !ok {
t.Errorf("missing expected attribute: %s", attr)
}
}
pf := resp.Schema.Attributes["parallel_fetch"].(schema.BoolAttribute)
if !pf.IsOptional() || !pf.IsComputed() {
t.Error("parallel_fetch must be Optional+Computed")
}
var d defaults.BoolResponse
pf.Default.DefaultBool(context.Background(), defaults.BoolRequest{}, &d)
if d.PlanValue.ValueBool() {
t.Error("parallel_fetch: expected default false")
}
}
func TestVirtualModelToAPI_SendsParallelFetchFalse(t *testing.T) {
ctx := context.Background()
body, err := json.Marshal(virtualModelToAPI(ctx, virtualResourceModel{ParallelFetch: types.BoolValue(false)}))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(body), `"parallel_fetch":false`) {
t.Errorf("request body must carry parallel_fetch=false for the full-replace PUT: %s", body)
}
}
func TestNewVirtualResource_Type(t *testing.T) {