Add serve_stale, cache_repodata, sync_interval and parallel_fetch
The artifactapi PUT is a full replace, so unmodelled fields reset on every apply.
This commit is contained in:
@@ -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) |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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$",
|
||||
|
||||
@@ -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),
|
||||
|
||||
|
||||
@@ -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)...)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user