Skip to content

Commit df1818b

Browse files
davidfowlCopilotJamesNK
authored
Add resource command arguments and visibility (#16710)
* Add resource command argument inputs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Improve resource command arguments Allow resource command invocations to capture trailing arguments and map them through discovered command argument inputs. Keep --args-json as a hidden legacy alias for the new --arguments option. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use resource command positional arguments Remove the resource command JSON arguments option and rely on the variadic command arguments captured by System.CommandLine. A single trailing JSON object remains supported as the structured escape hatch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add resource command visibility Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Restore resource command annotation constructor compatibility Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update polyglot command option snapshots Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add Stress playground command argument sample Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Expand Stress command argument samples Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Expose Stress command argument UI sample Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use interaction inputs for command arguments Align resource command invocation arguments with the interaction service model by exposing populated InteractionInputCollection values to command handlers instead of raw JSON. Keep JSON/protobuf conversion in transport layers and let those adapters materialize the declared command inputs before executing commands. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Rename command argument options Use CommandOptions.Arguments for command argument declarations to match ExecuteCommandContext.Arguments. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Synchronize API docs test fetcher Protect requested URL tracking in the ApiDocs test fetcher because member container searches fetch pages concurrently. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add resource command argument validation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Forward resource command arguments by order Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update command argument code generation snapshots Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Support polyglot resource command arguments Expose typed command argument validation callbacks to polyglot AppHosts, add the TypeScript interaction input intrinsic, and keep CLI invocation positional. Also validates the TypeScript playground command path and fixes Dashboard argument dialog closing after successful validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix ATS object unmarshalling and TS input typing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Treat init-only ATS properties as read-only Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update polyglot validation samples for getter APIs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update reusable TypeScript E2E sample for getter API Use the generated callable getter for ExecuteCommandContext.resourceName so the helper package sample matches the current TypeScript SDK shape. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR feedback on command validation tests Forward validateOnly through the TestDashboardClient command delegate and update stale auxiliary backchannel capability comments. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address command argument PR feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address resource command execution feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Move resource command prompts to AppHost Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address resource command argument review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix rebase conflict fallout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refactor stress apphost to organize commands into nested resource groups * Fix dashboard command validation test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address resource command review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: James Newton-King <james@newtonking.com>
1 parent 8328fd9 commit df1818b

74 files changed

Lines changed: 6070 additions & 1540 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 3 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Text.Json;
54
using Microsoft.Extensions.DependencyInjection;
65
using Microsoft.Extensions.Diagnostics.HealthChecks;
76

@@ -72,29 +71,6 @@
7271
.WithEnvironment("HOST", $"{serviceBuilder.GetEndpoint("http").Property(EndpointProperty.Host)}")
7372
.WithEnvironment("PORT", $"{serviceBuilder.GetEndpoint("http").Property(EndpointProperty.Port)}")
7473
.WithEnvironment("URL", $"{serviceBuilder.GetEndpoint("http").Property(EndpointProperty.Url)}");
75-
serviceBuilder.WithCommand(
76-
name: "icon-test",
77-
displayName: "Icon test",
78-
executeCommand: (c) =>
79-
{
80-
return Task.FromResult(CommandResults.Success());
81-
},
82-
commandOptions: new CommandOptions
83-
{
84-
IconName = "CloudDatabase"
85-
});
86-
serviceBuilder.WithCommand(
87-
name: "icon-test-highlighted",
88-
displayName: "Icon test highlighted",
89-
executeCommand: (c) =>
90-
{
91-
return Task.FromResult(CommandResults.Success());
92-
},
93-
commandOptions: new CommandOptions
94-
{
95-
IconName = "CloudDatabase",
96-
IsHighlighted = true
97-
});
9874

9975
serviceBuilder.WithHttpEndpoint(5180, name: $"http");
10076
for (var i = 1; i <= 30; i++)
@@ -103,145 +79,12 @@
10379
serviceBuilder.WithHttpEndpoint(port, name: $"http-{port}");
10480
}
10581

106-
serviceBuilder.WithHttpCommand("/write-console", "Write to console", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
107-
serviceBuilder.WithHttpCommand("/write-console-large", "Write to console large", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
108-
serviceBuilder.WithHttpCommand("/increment-counter", "Increment counter", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
109-
serviceBuilder.WithHttpCommand("/big-trace", "Big trace", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
110-
serviceBuilder.WithHttpCommand("/trace-limit", "Trace limit", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
111-
serviceBuilder.WithHttpCommand("/log-message", "Log message", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
112-
serviceBuilder.WithHttpCommand("/log-message-limit", "Log message limit", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
113-
serviceBuilder.WithHttpCommand("/log-message-limit-large", "Log message limit large", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
114-
serviceBuilder.WithHttpCommand("/http-command-auto-result", "HTTP command auto result", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning", ResultMode = HttpCommandResultMode.Auto, Description = "Run an HTTP command and infer the result format from the response content type" });
115-
serviceBuilder.WithHttpCommand("/http-command-json-result", "HTTP command JSON result", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning", ResultMode = HttpCommandResultMode.Json, Description = "Run an HTTP command and flow the JSON response back to the caller" });
116-
serviceBuilder.WithHttpCommand("/http-command-text-result", "HTTP command text result", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning", ResultMode = HttpCommandResultMode.Text, Description = "Run an HTTP command and flow the plain-text response back to the caller" });
117-
serviceBuilder.WithHttpCommand("/multiple-traces-linked", "Multiple traces linked", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
118-
serviceBuilder.WithHttpCommand("/overflow-counter", "Overflow counter", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
119-
serviceBuilder.WithHttpCommand("/nested-trace-spans", "Out of order nested spans", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
120-
serviceBuilder.WithHttpCommand("/exemplars-no-span", "Examplars with no span", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
121-
serviceBuilder.WithHttpCommand("/genai-trace", "Gen AI trace", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
122-
serviceBuilder.WithHttpCommand("/genai-langchain-trace", "Gen AI LangChain trace", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
123-
serviceBuilder.WithHttpCommand("/genai-trace-display-error", "Gen AI trace display error", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
124-
serviceBuilder.WithHttpCommand("/genai-evaluations", "Gen AI evaluations", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
125-
serviceBuilder.WithHttpCommand("/log-formatting", "Log formatting", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
126-
serviceBuilder.WithHttpCommand("/big-nested-trace", "Big nested trace", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
127-
128-
builder.AddProject<Projects.Stress_TelemetryService>("stress-telemetryservice")
82+
var telemetryBuilder = builder.AddProject<Projects.Stress_TelemetryService>("stress-telemetryservice")
12983
.WithUrls(c => c.Urls.Add(new() { Url = "https://someplace.com", DisplayText = "Some place" }))
13084
.WithUrl("https://someotherplace.com/some-path", "Some other place")
131-
.WithUrl("https://extremely-long-url.com/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz//abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz/abcdefghijklmno")
132-
.AddInteractionCommands()
133-
.WithCommand(
134-
name: "long-command",
135-
displayName: "This is a custom command with a very long command display name",
136-
executeCommand: (c) =>
137-
{
138-
return Task.FromResult(CommandResults.Success());
139-
},
140-
commandOptions: new() { IconName = "CloudDatabase" })
141-
.WithCommand(
142-
name: "resource-stop-all",
143-
displayName: "Stop all resources",
144-
executeCommand: async (c) =>
145-
{
146-
await ExecuteCommandForAllResourcesAsync(c.ServiceProvider, KnownResourceCommands.StopCommand, c.CancellationToken);
147-
return CommandResults.Success();
148-
},
149-
commandOptions: new() { IconName = "Stop", IconVariant = IconVariant.Filled })
150-
.WithCommand(
151-
name: "resource-start-all",
152-
displayName: "Start all resources",
153-
executeCommand: async (c) =>
154-
{
155-
await ExecuteCommandForAllResourcesAsync(c.ServiceProvider, KnownResourceCommands.StartCommand, c.CancellationToken);
156-
return CommandResults.Success();
157-
},
158-
commandOptions: new() { IconName = "Play", IconVariant = IconVariant.Filled })
159-
.WithCommand(
160-
name: "generate-token",
161-
displayName: "Generate Token",
162-
executeCommand: (c) =>
163-
{
164-
var token = new
165-
{
166-
accessToken = Convert.ToBase64String(Guid.NewGuid().ToByteArray()) + Convert.ToBase64String(Guid.NewGuid().ToByteArray()),
167-
tokenType = "Bearer",
168-
expiresIn = 3600,
169-
scope = "api.read api.write",
170-
issuedAt = DateTime.UtcNow
171-
};
172-
var json = JsonSerializer.Serialize(token, new JsonSerializerOptions { WriteIndented = true });
173-
var resultData = new CommandResultData
174-
{
175-
Value = json,
176-
Format = CommandResultFormat.Json
177-
};
178-
return Task.FromResult(CommandResults.Success("Generated token.", resultData));
179-
},
180-
commandOptions: new() { IconName = "Key", Description = "Generate a temporary access token" })
181-
.WithCommand(
182-
name: "get-connection-string",
183-
displayName: "Get Connection String",
184-
executeCommand: (c) =>
185-
{
186-
var connectionString = $"Server=localhost,1433;Database=StressDb;User Id=sa;Password={Guid.NewGuid():N};TrustServerCertificate=true";
187-
var message = """
188-
Retrieved connection string. The database connection was established successfully
189-
after verifying TLS certificates and negotiating encryption parameters.
190-
191-
The server responded with protocol version 7.4 and confirmed support for multiple
192-
active result sets. Connection pooling is enabled with a maximum pool size of 100
193-
connections and a minimum of 10 idle connections maintained.
194-
195-
The login handshake completed in 42ms with SSPI authentication. All pre-login
196-
checks passed including network library validation and instance name resolution.
197-
""";
198-
return Task.FromResult(CommandResults.Success(message, new CommandResultData { Value = connectionString, DisplayImmediately = true }));
199-
},
200-
commandOptions: new() { IconName = "LinkMultiple", Description = "Get the connection string for this resource" })
201-
.WithCommand(
202-
name: "validate-config",
203-
displayName: "Validate Config",
204-
executeCommand: (c) =>
205-
{
206-
var errors = new { errors = new[] { new { field = "connectionString", message = "Invalid host" }, new { field = "timeout", message = "Must be positive" } } };
207-
var json = JsonSerializer.Serialize(errors, new JsonSerializerOptions { WriteIndented = true });
208-
return Task.FromResult(CommandResults.Failure("Validation failed", json, CommandResultFormat.Json));
209-
},
210-
commandOptions: new() { IconName = "Warning", Description = "Validate resource configuration (always fails with details)" })
211-
.WithCommand(
212-
name: "check-health",
213-
displayName: "Check Health",
214-
executeCommand: (c) =>
215-
{
216-
return Task.FromResult(CommandResults.Failure("Health check failed", "Connection refused: ECONNREFUSED 127.0.0.1:5432\nRetries exhausted after 3 attempts", CommandResultFormat.Text));
217-
},
218-
commandOptions: new() { IconName = "HeartBroken", Description = "Check resource health (always fails with details)" })
219-
.WithCommand(
220-
name: "migrate-database",
221-
displayName: "Migrate Database",
222-
executeCommand: (c) =>
223-
{
224-
var markdown = """
225-
# ⚙️ Database Migration Summary
85+
.WithUrl("https://extremely-long-url.com/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz//abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz/abcdefghijklmno");
22686

227-
| Table | Result |
228-
|------------|----------------------------|
229-
| Customers | ✅ 1,200 rows |
230-
| Products | ✅ 850 rows |
231-
| Orders | ✅ 3,400 rows |
232-
| OrderItems | ✅ 8,750 rows |
233-
| Categories | ✅ 45 rows |
234-
| Reviews | ❌ FK constraint violation |
235-
| Inventory | ✅ 850 rows |
236-
| Shipping | ✅ 3,400 rows |
237-
| Payments | ❌ Timeout after 30s |
238-
| Coupons | ✅ 120 rows |
239-
240-
**Summary:** 8 of 10 tables migrated successfully. 2 tables failed.
241-
""";
242-
return Task.FromResult(CommandResults.Success("Database migrated.", new CommandResultData { Value = markdown, Format = CommandResultFormat.Markdown }));
243-
},
244-
commandOptions: new() { IconName = "CloudDatabase", Description = "Migrate the database with sample store data" });
87+
builder.AddCommandResources(serviceBuilder, telemetryBuilder);
24588

24689
#if !SKIP_DASHBOARD_REFERENCE
24790
// This project is only added in playground projects to support development/debugging
@@ -279,21 +122,3 @@ checks passed including network library validation and instance name resolution.
279122
.WithArgs("arg_from_apphost");
280123

281124
builder.Build().Run();
282-
283-
static async Task ExecuteCommandForAllResourcesAsync(IServiceProvider serviceProvider, string commandName, CancellationToken cancellationToken)
284-
{
285-
var commandService = serviceProvider.GetRequiredService<ResourceCommandService>();
286-
var model = serviceProvider.GetRequiredService<DistributedApplicationModel>();
287-
288-
var resources = model.Resources
289-
.Where(r => r.IsContainer() || r is ProjectResource || r is ExecutableResource)
290-
.Where(r => r.Name != KnownResourceNames.AspireDashboard)
291-
.ToList();
292-
293-
var commandTasks = new List<Task>();
294-
foreach (var r in resources)
295-
{
296-
commandTasks.Add(commandService.ExecuteCommandAsync(r, commandName, cancellationToken));
297-
}
298-
await Task.WhenAll(commandTasks).ConfigureAwait(false);
299-
}

0 commit comments

Comments
 (0)