From 4dd290518d896452afff0ba374455e5897e200a8 Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Thu, 2 Jul 2026 22:19:39 +1000 Subject: [PATCH] feat: support per-remote upstream timeouts Add upstream_dial_timeout, upstream_tls_timeout and upstream_response_header_timeout (seconds; 0 = server default) to the remote resource and data source, matching the artifactapi server. Wire them through the API model, schema, create/read/update mapping, docs and unit tests. --- README.md | 3 +++ internal/provider/datasource_remote.go | 12 +++++++++ internal/provider/models.go | 4 +++ internal/provider/resource_remote.go | 24 +++++++++++++++++ internal/provider/resource_remote_test.go | 33 +++++++++++++++++++++++ 5 files changed, 76 insertions(+) diff --git a/README.md b/README.md index 61c3510..7f1e036 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,9 @@ Available resource types: | `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 | +| `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) | #### Docker-specific Attributes diff --git a/internal/provider/datasource_remote.go b/internal/provider/datasource_remote.go index 3540e2c..0fc2ff9 100644 --- a/internal/provider/datasource_remote.go +++ b/internal/provider/datasource_remote.go @@ -45,6 +45,10 @@ func (d *remoteDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, "stale_on_error": schema.BoolAttribute{Computed: true}, "releases_remote": schema.StringAttribute{Computed: true}, "managed_by": schema.StringAttribute{Computed: true}, + + "upstream_dial_timeout": schema.Int64Attribute{Computed: true}, + "upstream_tls_timeout": schema.Int64Attribute{Computed: true}, + "upstream_response_header_timeout": schema.Int64Attribute{Computed: true}, }, } } @@ -68,6 +72,10 @@ type remoteDataSourceModel struct { StaleOnError types.Bool `tfsdk:"stale_on_error"` ReleasesRemote types.String `tfsdk:"releases_remote"` ManagedBy types.String `tfsdk:"managed_by"` + + UpstreamDialTimeout types.Int64 `tfsdk:"upstream_dial_timeout"` + UpstreamTLSTimeout types.Int64 `tfsdk:"upstream_tls_timeout"` + UpstreamResponseHeaderTimeout types.Int64 `tfsdk:"upstream_response_header_timeout"` } func (d *remoteDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { @@ -114,6 +122,10 @@ func (d *remoteDataSource) Read(ctx context.Context, req datasource.ReadRequest, StaleOnError: types.BoolValue(remote.StaleOnError), ReleasesRemote: types.StringValue(remote.ReleasesRemote), ManagedBy: types.StringValue(remote.ManagedBy), + + UpstreamDialTimeout: types.Int64Value(remote.UpstreamDialTimeout), + UpstreamTLSTimeout: types.Int64Value(remote.UpstreamTLSTimeout), + UpstreamResponseHeaderTimeout: types.Int64Value(remote.UpstreamResponseHeaderTimeout), } resp.Diagnostics.Append(resp.State.Set(ctx, state)...) } diff --git a/internal/provider/models.go b/internal/provider/models.go index b1d1a51..fd6cc7f 100644 --- a/internal/provider/models.go +++ b/internal/provider/models.go @@ -22,6 +22,10 @@ type remoteAPI struct { StaleOnError bool `json:"stale_on_error"` ReleasesRemote string `json:"releases_remote,omitempty"` ManagedBy string `json:"managed_by,omitempty"` + + UpstreamDialTimeout int64 `json:"upstream_dial_timeout"` + UpstreamTLSTimeout int64 `json:"upstream_tls_timeout"` + UpstreamResponseHeaderTimeout int64 `json:"upstream_response_header_timeout"` } type virtualAPI struct { diff --git a/internal/provider/resource_remote.go b/internal/provider/resource_remote.go index 25587c2..5e53544 100644 --- a/internal/provider/resource_remote.go +++ b/internal/provider/resource_remote.go @@ -44,6 +44,10 @@ type remoteResourceModel struct { QuarantineDays types.Int64 `tfsdk:"quarantine_days"` StaleOnError types.Bool `tfsdk:"stale_on_error"` ReleasesRemote types.String `tfsdk:"releases_remote"` + + UpstreamDialTimeout types.Int64 `tfsdk:"upstream_dial_timeout"` + UpstreamTLSTimeout types.Int64 `tfsdk:"upstream_tls_timeout"` + UpstreamResponseHeaderTimeout types.Int64 `tfsdk:"upstream_response_header_timeout"` } func newRemoteResource(packageType string) func() resource.Resource { @@ -144,6 +148,18 @@ func (r *remoteResource) Schema(_ context.Context, _ resource.SchemaRequest, res Description: "Name of the CDN remote for download URL rewriting (terraform only).", Optional: true, Computed: true, Default: stringdefault.StaticString(""), }, + "upstream_dial_timeout": schema.Int64Attribute{ + Description: "Upstream TCP connect timeout in seconds (0 = server default).", + Optional: true, Computed: true, Default: int64default.StaticInt64(0), + }, + "upstream_tls_timeout": schema.Int64Attribute{ + Description: "Upstream TLS handshake timeout in seconds (0 = server default).", + Optional: true, Computed: true, Default: int64default.StaticInt64(0), + }, + "upstream_response_header_timeout": schema.Int64Attribute{ + Description: "Upstream response-header timeout in seconds (0 = server default).", + Optional: true, Computed: true, Default: int64default.StaticInt64(0), + }, } resp.Schema = schema.Schema{ @@ -268,6 +284,10 @@ func (r *remoteResource) modelToAPI(ctx context.Context, m remoteResourceModel) QuarantineEnabled: m.QuarantineEnabled.ValueBool(), QuarantineDays: m.QuarantineDays.ValueInt64(), StaleOnError: m.StaleOnError.ValueBool(), + + UpstreamDialTimeout: m.UpstreamDialTimeout.ValueInt64(), + UpstreamTLSTimeout: m.UpstreamTLSTimeout.ValueInt64(), + UpstreamResponseHeaderTimeout: m.UpstreamResponseHeaderTimeout.ValueInt64(), } api.Patterns = listToStrings(ctx, m.Patterns) api.Blocklist = listToStrings(ctx, m.Blocklist) @@ -299,6 +319,10 @@ func (r *remoteResource) apiToModel(ctx context.Context, api remoteAPI) remoteRe BanTagsEnabled: types.BoolValue(api.BanTagsEnabled), BanTags: stringsToList(ctx, api.BanTags), ReleasesRemote: types.StringValue(api.ReleasesRemote), + + UpstreamDialTimeout: types.Int64Value(api.UpstreamDialTimeout), + UpstreamTLSTimeout: types.Int64Value(api.UpstreamTLSTimeout), + UpstreamResponseHeaderTimeout: types.Int64Value(api.UpstreamResponseHeaderTimeout), } return m } diff --git a/internal/provider/resource_remote_test.go b/internal/provider/resource_remote_test.go index 17ad353..fd73364 100644 --- a/internal/provider/resource_remote_test.go +++ b/internal/provider/resource_remote_test.go @@ -31,10 +31,18 @@ func TestModelToAPI_FullFields(t *testing.T) { QuarantineDays: types.Int64Value(7), StaleOnError: types.BoolValue(false), ReleasesRemote: types.StringValue("cdn-remote"), + + UpstreamDialTimeout: types.Int64Value(3), + UpstreamTLSTimeout: types.Int64Value(4), + UpstreamResponseHeaderTimeout: types.Int64Value(5), } api := r.modelToAPI(ctx, model) + if api.UpstreamDialTimeout != 3 || api.UpstreamTLSTimeout != 4 || api.UpstreamResponseHeaderTimeout != 5 { + t.Errorf("upstream timeouts: got %d/%d/%d, want 3/4/5", + api.UpstreamDialTimeout, api.UpstreamTLSTimeout, api.UpstreamResponseHeaderTimeout) + } if api.Name != "my-remote" { t.Errorf("Name: expected my-remote, got %s", api.Name) } @@ -211,10 +219,22 @@ func TestAPIToModel_FullFields(t *testing.T) { StaleOnError: false, ReleasesRemote: "cdn-remote", ManagedBy: "terraform", + + UpstreamDialTimeout: 3, + UpstreamTLSTimeout: 4, + UpstreamResponseHeaderTimeout: 5, } model := r.apiToModel(ctx, api) + if model.UpstreamDialTimeout.ValueInt64() != 3 || + model.UpstreamTLSTimeout.ValueInt64() != 4 || + model.UpstreamResponseHeaderTimeout.ValueInt64() != 5 { + t.Errorf("upstream timeouts: got %d/%d/%d, want 3/4/5", + model.UpstreamDialTimeout.ValueInt64(), + model.UpstreamTLSTimeout.ValueInt64(), + model.UpstreamResponseHeaderTimeout.ValueInt64()) + } if model.Name.ValueString() != "my-remote" { t.Errorf("Name: expected my-remote, got %s", model.Name.ValueString()) } @@ -324,12 +344,24 @@ func TestModelToAPI_RoundTrip(t *testing.T) { QuarantineDays: 3, StaleOnError: true, ReleasesRemote: "", + + UpstreamDialTimeout: 5, + UpstreamTLSTimeout: 0, + UpstreamResponseHeaderTimeout: 45, } // API -> Model -> API round-trip model := r.apiToModel(ctx, original) result := r.modelToAPI(ctx, model) + if result.UpstreamDialTimeout != original.UpstreamDialTimeout || + result.UpstreamTLSTimeout != original.UpstreamTLSTimeout || + result.UpstreamResponseHeaderTimeout != original.UpstreamResponseHeaderTimeout { + t.Errorf("upstream timeouts round-trip: got %d/%d/%d, want %d/%d/%d", + result.UpstreamDialTimeout, result.UpstreamTLSTimeout, result.UpstreamResponseHeaderTimeout, + original.UpstreamDialTimeout, original.UpstreamTLSTimeout, original.UpstreamResponseHeaderTimeout) + } + if result.Name != original.Name { t.Errorf("Name: expected %s, got %s", original.Name, result.Name) } @@ -405,6 +437,7 @@ func TestRemoteResource_Schema(t *testing.T) { "ban_tags_enabled", "ban_tags", "quarantine_enabled", "quarantine_days", "stale_on_error", "releases_remote", + "upstream_dial_timeout", "upstream_tls_timeout", "upstream_response_header_timeout", } for _, attr := range expectedAttrs {