diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden
index aa318a5f857c7..2c00030489dbc 100644
--- a/cli/testdata/coder_server_--help.golden
+++ b/cli/testdata/coder_server_--help.golden
@@ -14,6 +14,12 @@ SUBCOMMANDS:
PostgreSQL deployment.
OPTIONS:
+ --agent-metadata-min-interval duration, $CODER_AGENT_METADATA_MIN_INTERVAL (default: 0s)
+ Minimum interval for agent metadata collection. Template-defined
+ intervals below this value will cause template import to fail.
+ Existing workspaces with lower intervals will be silently upgraded on
+ restart. Set to 0 to disable enforcement.
+
--allow-workspace-renames bool, $CODER_ALLOW_WORKSPACE_RENAMES (default: false)
DEPRECATED: Allow users to rename their workspaces. Use only for
temporary compatibility reasons, this will be removed in a future
diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden
index a9e6058a3eef2..eddc720fbcc26 100644
--- a/cli/testdata/server-config.yaml.golden
+++ b/cli/testdata/server-config.yaml.golden
@@ -485,6 +485,11 @@ sshKeygenAlgorithm: ed25519
# URL to use for agent troubleshooting when not set in the template.
# (default: https://coder.com/docs/admin/templates/troubleshooting, type: url)
agentFallbackTroubleshootingURL: https://coder.com/docs/admin/templates/troubleshooting
+# Minimum interval for agent metadata collection. Template-defined intervals below
+# this value will cause template import to fail. Existing workspaces with lower
+# intervals will be silently upgraded on restart. Set to 0 to disable enforcement.
+# (default: 0s, type: duration)
+agentMetadataMinInterval: 0s
# Disable workspace apps that are not served from subdomains. Path-based apps can
# make requests to the Coder API and pose a security risk when the workspace
# serves malicious JavaScript. This is recommended for security purposes if a
diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go
index b8e3331ecd1f2..ed73d0da32232 100644
--- a/coderd/apidoc/docs.go
+++ b/coderd/apidoc/docs.go
@@ -14172,6 +14172,9 @@ const docTemplate = `{
"agent_fallback_troubleshooting_url": {
"$ref": "#/definitions/serpent.URL"
},
+ "agent_metadata_min_interval": {
+ "type": "integer"
+ },
"agent_stat_refresh_interval": {
"type": "integer"
},
diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json
index 396a704a06119..29c599984d7b7 100644
--- a/coderd/apidoc/swagger.json
+++ b/coderd/apidoc/swagger.json
@@ -12756,6 +12756,9 @@
"agent_fallback_troubleshooting_url": {
"$ref": "#/definitions/serpent.URL"
},
+ "agent_metadata_min_interval": {
+ "type": "integer"
+ },
"agent_stat_refresh_interval": {
"type": "integer"
},
diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go
index c4598beaf8399..5090d1a75a4e3 100644
--- a/coderd/provisionerdserver/provisionerdserver.go
+++ b/coderd/provisionerdserver/provisionerdserver.go
@@ -1613,7 +1613,11 @@ func (s *server) completeTemplateImportJob(ctx context.Context, job database.Pro
slog.F("resource_type", resource.Type),
slog.F("transition", transition))
- if err := InsertWorkspaceResource(ctx, db, jobID, transition, resource, telemetrySnapshot); err != nil {
+ if err := InsertWorkspaceResource(ctx, db, jobID, transition, resource, telemetrySnapshot,
+ InsertWorkspaceResourceWithValidationMode(ValidationModeStrict),
+ InsertWorkspaceResourceWithDeploymentValues(s.DeploymentValues),
+ InsertWorkspaceResourceWithLogger(s.Logger),
+ ); err != nil {
return xerrors.Errorf("insert resource: %w", err)
}
}
@@ -2014,6 +2018,9 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro
// Ensure that the agent IDs we set previously
// are written to the database.
InsertWorkspaceResourceWithAgentIDsFromProto(),
+ InsertWorkspaceResourceWithValidationMode(ValidationModeUpgrade),
+ InsertWorkspaceResourceWithDeploymentValues(s.DeploymentValues),
+ InsertWorkspaceResourceWithLogger(s.Logger),
)
if err != nil {
return xerrors.Errorf("insert provisioner job: %w", err)
@@ -2623,8 +2630,23 @@ func InsertWorkspacePresetAndParameters(ctx context.Context, db database.Store,
return nil
}
+// ValidationMode determines how agent metadata interval validation is enforced.
+type ValidationMode int
+
+const (
+ // ValidationModeStrict fails the operation if metadata intervals are below the minimum.
+ // Used for template imports.
+ ValidationModeStrict ValidationMode = iota
+ // ValidationModeUpgrade silently upgrades metadata intervals to meet the minimum.
+ // Used for workspace builds.
+ ValidationModeUpgrade
+)
+
type insertWorkspaceResourceOptions struct {
useAgentIDsFromProto bool
+ validationMode ValidationMode
+ deploymentValues *codersdk.DeploymentValues
+ logger slog.Logger
}
// InsertWorkspaceResourceOption represents a functional option for
@@ -2639,6 +2661,27 @@ func InsertWorkspaceResourceWithAgentIDsFromProto() InsertWorkspaceResourceOptio
}
}
+// InsertWorkspaceResourceWithValidationMode sets the validation mode for agent metadata intervals.
+func InsertWorkspaceResourceWithValidationMode(mode ValidationMode) InsertWorkspaceResourceOption {
+ return func(opts *insertWorkspaceResourceOptions) {
+ opts.validationMode = mode
+ }
+}
+
+// InsertWorkspaceResourceWithDeploymentValues sets the deployment values for validation.
+func InsertWorkspaceResourceWithDeploymentValues(dv *codersdk.DeploymentValues) InsertWorkspaceResourceOption {
+ return func(opts *insertWorkspaceResourceOptions) {
+ opts.deploymentValues = dv
+ }
+}
+
+// InsertWorkspaceResourceWithLogger sets the logger for logging validation actions.
+func InsertWorkspaceResourceWithLogger(logger slog.Logger) InsertWorkspaceResourceOption {
+ return func(opts *insertWorkspaceResourceOptions) {
+ opts.logger = logger
+ }
+}
+
func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.UUID, transition database.WorkspaceTransition, protoResource *sdkproto.Resource, snapshot *telemetry.Snapshot, opt ...InsertWorkspaceResourceOption) error {
opts := &insertWorkspaceResourceOptions{}
for _, o := range opt {
@@ -2776,13 +2819,39 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
snapshot.WorkspaceAgents = append(snapshot.WorkspaceAgents, telemetry.ConvertWorkspaceAgent(dbAgent))
for _, md := range prAgent.Metadata {
+ interval := md.Interval
+
+ // Apply minimum interval validation if configured
+ if opts.deploymentValues != nil && opts.deploymentValues.AgentMetadataMinInterval.Value() > 0 {
+ minInterval := opts.deploymentValues.AgentMetadataMinInterval.Value()
+ minIntervalSeconds := int64(minInterval.Seconds())
+
+ if interval < minIntervalSeconds {
+ if opts.validationMode == ValidationModeStrict {
+ // Template import - fail the operation
+ return xerrors.Errorf(
+ "agent %q metadata %q interval %ds is below minimum required %ds",
+ prAgent.Name, md.Key, interval, minIntervalSeconds,
+ )
+ }
+ // Workspace build - upgrade silently
+ opts.logger.Info(ctx, "upgrading agent metadata interval to meet minimum",
+ slog.F("agent", prAgent.Name),
+ slog.F("metadata_key", md.Key),
+ slog.F("original_interval_seconds", interval),
+ slog.F("upgraded_interval_seconds", minIntervalSeconds),
+ )
+ interval = minIntervalSeconds
+ }
+ }
+
p := database.InsertWorkspaceAgentMetadataParams{
WorkspaceAgentID: agentID,
DisplayName: md.DisplayName,
Script: md.Script,
Key: md.Key,
Timeout: md.Timeout,
- Interval: md.Interval,
+ Interval: interval,
// #nosec G115 - Order represents a display order value that's always small and fits in int32
DisplayOrder: int32(md.Order),
}
diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go
index 4dc8621736b5c..09eb4530e4af5 100644
--- a/coderd/provisionerdserver/provisionerdserver_test.go
+++ b/coderd/provisionerdserver/provisionerdserver_test.go
@@ -20,11 +20,13 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace"
+ "go.uber.org/mock/gomock"
"golang.org/x/oauth2"
"golang.org/x/xerrors"
"google.golang.org/protobuf/types/known/timestamppb"
"storj.io/drpc"
+ "cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/coderd"
"github.com/coder/coder/v2/coderd/util/ptr"
@@ -37,6 +39,7 @@ import (
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbgen"
+ "github.com/coder/coder/v2/coderd/database/dbmock"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/database/pubsub"
@@ -4469,3 +4472,390 @@ func seedPreviousWorkspaceStartWithAITask(ctx context.Context, t testing.TB, db
})
return nil
}
+
+// setupAgentMetadataTest creates common test fixtures for agent metadata interval tests.
+func setupAgentMetadataTest(t *testing.T, minInterval time.Duration) (
+ ctx context.Context,
+ db *dbmock.MockStore,
+ logger slog.Logger,
+ deploymentValues *codersdk.DeploymentValues,
+ jobID, resourceID, agentID uuid.UUID,
+) {
+ t.Helper()
+ ctx = context.Background()
+ ctrl := gomock.NewController(t)
+ db = dbmock.NewMockStore(ctrl)
+ logger = slogtest.Make(t, nil).Leveled(slog.LevelDebug)
+
+ deploymentValues = &codersdk.DeploymentValues{}
+ deploymentValues.AgentMetadataMinInterval = serpent.Duration(minInterval)
+
+ jobID = uuid.New()
+ resourceID = uuid.New()
+ agentID = uuid.New()
+
+ // Mock successful resource insertion
+ db.EXPECT().
+ InsertWorkspaceResource(gomock.Any(), gomock.Any()).
+ Return(database.WorkspaceResource{
+ ID: resourceID,
+ JobID: jobID,
+ }, nil)
+
+ // Mock agent insertion
+ db.EXPECT().
+ InsertWorkspaceAgent(gomock.Any(), gomock.Any()).
+ Return(database.WorkspaceAgent{
+ ID: agentID,
+ ResourceID: resourceID,
+ Name: "main",
+ CreatedAt: dbtime.Now(),
+ }, nil)
+
+ return ctx, db, logger, deploymentValues, jobID, resourceID, agentID
+}
+
+// mockSuccessfulAgentInsertion mocks all the database calls needed for successful agent insertion.
+func mockSuccessfulAgentInsertion(db *dbmock.MockStore) {
+ // Mock log sources insertion
+ db.EXPECT().
+ InsertWorkspaceAgentLogSources(gomock.Any(), gomock.Any()).
+ Return([]database.WorkspaceAgentLogSource{}, nil)
+
+ // Mock scripts insertion
+ db.EXPECT().
+ InsertWorkspaceAgentScripts(gomock.Any(), gomock.Any()).
+ Return([]database.WorkspaceAgentScript{}, nil)
+
+ // Mock resource metadata insertion
+ db.EXPECT().
+ InsertWorkspaceResourceMetadata(gomock.Any(), gomock.Any()).
+ Return([]database.WorkspaceResourceMetadatum{}, nil)
+}
+
+// TestAgentMetadataMinInterval_TemplateImportStrict tests that InsertWorkspaceResource
+// fails when ValidationModeStrict is used and metadata intervals are below the minimum.
+func TestAgentMetadataMinInterval_TemplateImportStrict(t *testing.T) {
+ t.Parallel()
+
+ t.Run("FailsWhenBelowMinimum", func(t *testing.T) {
+ t.Parallel()
+ ctx, db, logger, deploymentValues, jobID, _, _ := setupAgentMetadataTest(t, 60*time.Second)
+
+ // Create resource with agent that has metadata interval below minimum
+ resource := &sdkproto.Resource{
+ Name: "example",
+ Type: "aws_instance",
+ Agents: []*sdkproto.Agent{
+ {
+ Name: "main",
+ Auth: &sdkproto.Agent_Token{},
+ Metadata: []*sdkproto.Agent_Metadata{
+ {
+ Key: "cpu",
+ DisplayName: "CPU Usage",
+ Script: "echo 50",
+ Interval: 30, // 30 seconds - below the 60s minimum
+ Timeout: 5,
+ },
+ },
+ },
+ },
+ }
+
+ // Should fail with ValidationModeStrict
+ err := provisionerdserver.InsertWorkspaceResource(
+ ctx,
+ db,
+ jobID,
+ database.WorkspaceTransitionStart,
+ resource,
+ &telemetry.Snapshot{},
+ provisionerdserver.InsertWorkspaceResourceWithValidationMode(provisionerdserver.ValidationModeStrict),
+ provisionerdserver.InsertWorkspaceResourceWithDeploymentValues(deploymentValues),
+ provisionerdserver.InsertWorkspaceResourceWithLogger(logger),
+ )
+
+ require.Error(t, err)
+ require.ErrorContains(t, err, `agent "main" metadata "cpu" interval 30s is below minimum required 60s`)
+ })
+
+ t.Run("SucceedsWhenAtOrAboveMinimum", func(t *testing.T) {
+ t.Parallel()
+ ctx, db, logger, deploymentValues, jobID, _, _ := setupAgentMetadataTest(t, 60*time.Second)
+ mockSuccessfulAgentInsertion(db)
+
+ // Mock metadata insertion - should be called twice
+ // Note: We use gomock.Any() for params because agent IDs are generated at runtime
+ db.EXPECT().
+ InsertWorkspaceAgentMetadata(gomock.Any(), gomock.Any()).
+ DoAndReturn(func(ctx context.Context, params database.InsertWorkspaceAgentMetadataParams) error {
+ // Verify the interval is correct (at minimum)
+ require.Equal(t, int64(60), params.Interval, "Expected interval to be at minimum (60s)")
+ require.Equal(t, "cpu", params.Key)
+ return nil
+ })
+
+ db.EXPECT().
+ InsertWorkspaceAgentMetadata(gomock.Any(), gomock.Any()).
+ DoAndReturn(func(ctx context.Context, params database.InsertWorkspaceAgentMetadataParams) error {
+ // Verify the interval is correct (above minimum)
+ require.Equal(t, int64(120), params.Interval, "Expected interval to be preserved (120s)")
+ require.Equal(t, "memory", params.Key)
+ return nil
+ })
+
+ // Create resource with agent that has metadata at and above minimum
+ resource := &sdkproto.Resource{
+ Name: "example",
+ Type: "aws_instance",
+ Agents: []*sdkproto.Agent{
+ {
+ Name: "main",
+ Auth: &sdkproto.Agent_Token{},
+ Metadata: []*sdkproto.Agent_Metadata{
+ {
+ Key: "cpu",
+ DisplayName: "CPU Usage",
+ Script: "echo 50",
+ Interval: 60, // At minimum
+ Timeout: 5,
+ },
+ {
+ Key: "memory",
+ DisplayName: "Memory Usage",
+ Script: "echo 2048",
+ Interval: 120, // Above minimum
+ Timeout: 5,
+ },
+ },
+ },
+ },
+ }
+
+ // Should succeed
+ err := provisionerdserver.InsertWorkspaceResource(
+ ctx,
+ db,
+ jobID,
+ database.WorkspaceTransitionStart,
+ resource,
+ &telemetry.Snapshot{},
+ provisionerdserver.InsertWorkspaceResourceWithValidationMode(provisionerdserver.ValidationModeStrict),
+ provisionerdserver.InsertWorkspaceResourceWithDeploymentValues(deploymentValues),
+ provisionerdserver.InsertWorkspaceResourceWithLogger(logger),
+ )
+
+ require.NoError(t, err)
+ })
+
+ t.Run("NoEnforcementWhenMinimumIsZero", func(t *testing.T) {
+ t.Parallel()
+ ctx, db, logger, deploymentValues, jobID, _, _ := setupAgentMetadataTest(t, 0)
+ mockSuccessfulAgentInsertion(db)
+
+ // Mock metadata insertion - original interval should be preserved
+ db.EXPECT().
+ InsertWorkspaceAgentMetadata(gomock.Any(), gomock.Any()).
+ DoAndReturn(func(ctx context.Context, params database.InsertWorkspaceAgentMetadataParams) error {
+ // Verify the interval is preserved (1s)
+ require.Equal(t, int64(1), params.Interval, "Expected interval to be preserved (1s)")
+ require.Equal(t, "cpu", params.Key)
+ return nil
+ })
+
+ // Create resource with very low interval
+ resource := &sdkproto.Resource{
+ Name: "example",
+ Type: "aws_instance",
+ Agents: []*sdkproto.Agent{
+ {
+ Name: "main",
+ Auth: &sdkproto.Agent_Token{},
+ Metadata: []*sdkproto.Agent_Metadata{
+ {
+ Key: "cpu",
+ DisplayName: "CPU Usage",
+ Script: "echo 50",
+ Interval: 1, // Very low
+ Timeout: 5,
+ },
+ },
+ },
+ },
+ }
+
+ // Should succeed since enforcement is disabled
+ err := provisionerdserver.InsertWorkspaceResource(
+ ctx,
+ db,
+ jobID,
+ database.WorkspaceTransitionStart,
+ resource,
+ &telemetry.Snapshot{},
+ provisionerdserver.InsertWorkspaceResourceWithValidationMode(provisionerdserver.ValidationModeStrict),
+ provisionerdserver.InsertWorkspaceResourceWithDeploymentValues(deploymentValues),
+ provisionerdserver.InsertWorkspaceResourceWithLogger(logger),
+ )
+
+ require.NoError(t, err)
+ })
+}
+
+// TestAgentMetadataMinInterval_WorkspaceBuildUpgrade tests that InsertWorkspaceResource
+// silently upgrades intervals when ValidationModeUpgrade is used.
+func TestAgentMetadataMinInterval_WorkspaceBuildUpgrade(t *testing.T) {
+ t.Parallel()
+
+ t.Run("SilentlyUpgradesWhenBelowMinimum", func(t *testing.T) {
+ t.Parallel()
+ ctx, db, logger, deploymentValues, jobID, _, _ := setupAgentMetadataTest(t, 60*time.Second)
+ mockSuccessfulAgentInsertion(db)
+
+ // Mock metadata insertion - both should be upgraded to 60s
+ db.EXPECT().
+ InsertWorkspaceAgentMetadata(gomock.Any(), gomock.Any()).
+ DoAndReturn(func(ctx context.Context, params database.InsertWorkspaceAgentMetadataParams) error {
+ // Verify interval is upgraded to minimum (60s)
+ require.Equal(t, int64(60), params.Interval, "Expected interval to be upgraded to 60s")
+ require.Equal(t, "cpu", params.Key)
+ return nil
+ })
+
+ db.EXPECT().
+ InsertWorkspaceAgentMetadata(gomock.Any(), gomock.Any()).
+ DoAndReturn(func(ctx context.Context, params database.InsertWorkspaceAgentMetadataParams) error {
+ // Verify interval is upgraded to minimum (60s)
+ require.Equal(t, int64(60), params.Interval, "Expected interval to be upgraded to 60s")
+ require.Equal(t, "memory", params.Key)
+ return nil
+ })
+
+ // Create resource with intervals below minimum
+ resource := &sdkproto.Resource{
+ Name: "example",
+ Type: "aws_instance",
+ Agents: []*sdkproto.Agent{
+ {
+ Name: "main",
+ Auth: &sdkproto.Agent_Token{},
+ Metadata: []*sdkproto.Agent_Metadata{
+ {
+ Key: "cpu",
+ DisplayName: "CPU Usage",
+ Script: "echo 50",
+ Interval: 30, // Below minimum
+ Timeout: 5,
+ },
+ {
+ Key: "memory",
+ DisplayName: "Memory Usage",
+ Script: "echo 2048",
+ Interval: 10, // Below minimum
+ Timeout: 5,
+ },
+ },
+ },
+ },
+ }
+
+ // Should succeed with ValidationModeUpgrade (not fail)
+ err := provisionerdserver.InsertWorkspaceResource(
+ ctx,
+ db,
+ jobID,
+ database.WorkspaceTransitionStart,
+ resource,
+ &telemetry.Snapshot{},
+ provisionerdserver.InsertWorkspaceResourceWithValidationMode(provisionerdserver.ValidationModeUpgrade),
+ provisionerdserver.InsertWorkspaceResourceWithDeploymentValues(deploymentValues),
+ provisionerdserver.InsertWorkspaceResourceWithLogger(logger),
+ )
+
+ require.NoError(t, err)
+ })
+
+ t.Run("PreservesIntervalsAtOrAboveMinimum", func(t *testing.T) {
+ t.Parallel()
+ ctx, db, logger, deploymentValues, jobID, _, _ := setupAgentMetadataTest(t, 30*time.Second)
+ mockSuccessfulAgentInsertion(db)
+
+ // Mock metadata insertions with expected intervals
+ db.EXPECT().
+ InsertWorkspaceAgentMetadata(gomock.Any(), gomock.Any()).
+ DoAndReturn(func(ctx context.Context, params database.InsertWorkspaceAgentMetadataParams) error {
+ // First call: verify "below" is upgraded to 30s
+ require.Equal(t, int64(30), params.Interval, "Expected 'below' interval to be upgraded to 30s")
+ require.Equal(t, "below", params.Key)
+ return nil
+ })
+
+ db.EXPECT().
+ InsertWorkspaceAgentMetadata(gomock.Any(), gomock.Any()).
+ DoAndReturn(func(ctx context.Context, params database.InsertWorkspaceAgentMetadataParams) error {
+ // Second call: verify "at" stays at 30s
+ require.Equal(t, int64(30), params.Interval, "Expected 'at' interval to stay at 30s")
+ require.Equal(t, "at", params.Key)
+ return nil
+ })
+
+ db.EXPECT().
+ InsertWorkspaceAgentMetadata(gomock.Any(), gomock.Any()).
+ DoAndReturn(func(ctx context.Context, params database.InsertWorkspaceAgentMetadataParams) error {
+ // Third call: verify "above" stays at 120s
+ require.Equal(t, int64(120), params.Interval, "Expected 'above' interval to stay at 120s")
+ require.Equal(t, "above", params.Key)
+ return nil
+ })
+
+ // Create resource with mixed intervals
+ resource := &sdkproto.Resource{
+ Name: "example",
+ Type: "aws_instance",
+ Agents: []*sdkproto.Agent{
+ {
+ Name: "main",
+ Auth: &sdkproto.Agent_Token{},
+ Metadata: []*sdkproto.Agent_Metadata{
+ {
+ Key: "below",
+ DisplayName: "Below Minimum",
+ Script: "echo below",
+ Interval: 10, // Below minimum
+ Timeout: 5,
+ },
+ {
+ Key: "at",
+ DisplayName: "At Minimum",
+ Script: "echo at",
+ Interval: 30, // At minimum
+ Timeout: 5,
+ },
+ {
+ Key: "above",
+ DisplayName: "Above Minimum",
+ Script: "echo above",
+ Interval: 120, // Above minimum
+ Timeout: 5,
+ },
+ },
+ },
+ },
+ }
+
+ // Should succeed and preserve/upgrade as expected
+ err := provisionerdserver.InsertWorkspaceResource(
+ ctx,
+ db,
+ jobID,
+ database.WorkspaceTransitionStart,
+ resource,
+ &telemetry.Snapshot{},
+ provisionerdserver.InsertWorkspaceResourceWithValidationMode(provisionerdserver.ValidationModeUpgrade),
+ provisionerdserver.InsertWorkspaceResourceWithDeploymentValues(deploymentValues),
+ provisionerdserver.InsertWorkspaceResourceWithLogger(logger),
+ )
+
+ require.NoError(t, err)
+ })
+}
diff --git a/codersdk/deployment.go b/codersdk/deployment.go
index 0dd082ab5eebc..97c6648a7efc2 100644
--- a/codersdk/deployment.go
+++ b/codersdk/deployment.go
@@ -476,6 +476,7 @@ type DeploymentValues struct {
MetricsCacheRefreshInterval serpent.Duration `json:"metrics_cache_refresh_interval,omitempty" typescript:",notnull"`
AgentStatRefreshInterval serpent.Duration `json:"agent_stat_refresh_interval,omitempty" typescript:",notnull"`
AgentFallbackTroubleshootingURL serpent.URL `json:"agent_fallback_troubleshooting_url,omitempty" typescript:",notnull"`
+ AgentMetadataMinInterval serpent.Duration `json:"agent_metadata_min_interval,omitempty" typescript:",notnull"`
BrowserOnly serpent.Bool `json:"browser_only,omitempty" typescript:",notnull"`
SCIMAPIKey serpent.String `json:"scim_api_key,omitempty" typescript:",notnull"`
ExternalTokenEncryptionKeys serpent.StringArray `json:"external_token_encryption_keys,omitempty" typescript:",notnull"`
@@ -2683,6 +2684,17 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
Value: &c.AgentFallbackTroubleshootingURL,
YAML: "agentFallbackTroubleshootingURL",
},
+ {
+ Name: "Agent Metadata Minimum Interval",
+ Description: `Minimum interval for agent metadata collection. Template-defined intervals below this value will cause template import to fail. Existing workspaces with lower intervals will be silently upgraded on restart. Set to 0 to disable enforcement.`,
+ Flag: "agent-metadata-min-interval",
+ Env: "CODER_AGENT_METADATA_MIN_INTERVAL",
+ YAML: "agentMetadataMinInterval",
+ Hidden: false,
+ Default: (0 * time.Second).String(),
+ Value: &c.AgentMetadataMinInterval,
+ Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"),
+ },
{
Name: "Browser Only",
Description: "Whether Coder only allows connections to workspaces via the browser.",
diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md
index 3ea0180ae1454..8916a6e551ed2 100644
--- a/docs/reference/api/general.md
+++ b/docs/reference/api/general.md
@@ -160,6 +160,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
"scheme": "string",
"user": {}
},
+ "agent_metadata_min_interval": 0,
"agent_stat_refresh_interval": 0,
"ai": {
"bridge": {
diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md
index bd00d79c4b40b..4954b7dab9a72 100644
--- a/docs/reference/api/schemas.md
+++ b/docs/reference/api/schemas.md
@@ -2844,6 +2844,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
"scheme": "string",
"user": {}
},
+ "agent_metadata_min_interval": 0,
"agent_stat_refresh_interval": 0,
"ai": {
"bridge": {
@@ -3367,6 +3368,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
"scheme": "string",
"user": {}
},
+ "agent_metadata_min_interval": 0,
"agent_stat_refresh_interval": 0,
"ai": {
"bridge": {
@@ -3781,6 +3783,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
| `additional_csp_policy` | array of string | false | | |
| `address` | [serpent.HostPort](#serpenthostport) | false | | Deprecated: Use HTTPAddress or TLS.Address instead. |
| `agent_fallback_troubleshooting_url` | [serpent.URL](#serpenturl) | false | | |
+| `agent_metadata_min_interval` | integer | false | | |
| `agent_stat_refresh_interval` | integer | false | | |
| `ai` | [codersdk.AIConfig](#codersdkaiconfig) | false | | |
| `allow_workspace_renames` | boolean | false | | |
diff --git a/docs/reference/cli/server.md b/docs/reference/cli/server.md
index 4ba8c026fb299..38f0f631bd0f4 100644
--- a/docs/reference/cli/server.md
+++ b/docs/reference/cli/server.md
@@ -1067,6 +1067,17 @@ Two optional fields can be set in the Strict-Transport-Security header; 'include
The algorithm to use for generating ssh keys. Accepted values are "ed25519", "ecdsa", or "rsa4096".
+### --agent-metadata-min-interval
+
+| | |
+|-------------|-------------------------------------------------|
+| Type | duration |
+| Environment | $CODER_AGENT_METADATA_MIN_INTERVAL |
+| YAML | agentMetadataMinInterval |
+| Default | 0s |
+
+Minimum interval for agent metadata collection. Template-defined intervals below this value will cause template import to fail. Existing workspaces with lower intervals will be silently upgraded on restart. Set to 0 to disable enforcement.
+
### --browser-only
| | |
diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden
index 32db725d93f77..b1dd9e755d8da 100644
--- a/enterprise/cli/testdata/coder_server_--help.golden
+++ b/enterprise/cli/testdata/coder_server_--help.golden
@@ -15,6 +15,12 @@ SUBCOMMANDS:
PostgreSQL deployment.
OPTIONS:
+ --agent-metadata-min-interval duration, $CODER_AGENT_METADATA_MIN_INTERVAL (default: 0s)
+ Minimum interval for agent metadata collection. Template-defined
+ intervals below this value will cause template import to fail.
+ Existing workspaces with lower intervals will be silently upgraded on
+ restart. Set to 0 to disable enforcement.
+
--allow-workspace-renames bool, $CODER_ALLOW_WORKSPACE_RENAMES (default: false)
DEPRECATED: Allow users to rename their workspaces. Use only for
temporary compatibility reasons, this will be removed in a future
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index 6cb14744035cc..bca79c20ca06e 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -1753,6 +1753,7 @@ export interface DeploymentValues {
readonly metrics_cache_refresh_interval?: number;
readonly agent_stat_refresh_interval?: number;
readonly agent_fallback_troubleshooting_url?: string;
+ readonly agent_metadata_min_interval?: number;
readonly browser_only?: boolean;
readonly scim_api_key?: string;
readonly external_token_encryption_keys?: string;